fix: Улучшения системы ценообразования комплектов
Исправлены 4 проблемы: 1. Расчёт цены первого товара - улучшена валидация в getProductPrice и calculateFinalPrice 2. Отображение actual_price в Select2 вместо обычной цены 3. Количество по умолчанию = 1 для новых форм компонентов 4. Auto-select текста при клике на поле количества для удобства редактирования Изменённые файлы: - products/forms.py: добавлен __init__ в KitItemForm для quantity.initial = 1 - products/templates/includes/select2-product-init.html: обновлена formatSelectResult - products/templates/productkit_create.html: добавлен focus handler для auto-select - products/templates/productkit_edit.html: добавлен focus handler для auto-select 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Команда управления для пересчёта себестоимости (cost_price) всех товаров.
|
||||
|
||||
Использование (для multi-tenant проекта):
|
||||
python manage.py recalculate_product_costs --schema=grach
|
||||
python manage.py recalculate_product_costs --schema=grach --verbose
|
||||
python manage.py recalculate_product_costs --schema=grach --dry-run
|
||||
|
||||
Описание:
|
||||
Пересчитывает Product.cost_price на основе активных партий (StockBatch).
|
||||
Использует средневзвешенную стоимость по FIFO принципу.
|
||||
Товар без партий получает cost_price = 0.00.
|
||||
"""
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import connection
|
||||
from django_tenants.management.commands import InteractiveTenantOption
|
||||
from decimal import Decimal
|
||||
from products.models import Product
|
||||
from products.services.cost_calculator import ProductCostCalculator
|
||||
|
||||
|
||||
class Command(InteractiveTenantOption, BaseCommand):
|
||||
help = 'Пересчитать себестоимость (cost_price) для всех товаров на основе партий StockBatch'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
# Добавляем --schema из InteractiveTenantOption
|
||||
super().add_arguments(parser)
|
||||
|
||||
parser.add_argument(
|
||||
'--verbose',
|
||||
action='store_true',
|
||||
help='Выводить подробную информацию о каждом товаре',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='Показать изменения без сохранения в БД',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--only-changed',
|
||||
action='store_true',
|
||||
help='Показывать только товары с изменившейся стоимостью',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Получаем тенанта из опций или интерактивно
|
||||
tenant = self.get_tenant_from_options_or_interactive(**options)
|
||||
|
||||
# Устанавливаем схему тенанта
|
||||
connection.set_tenant(tenant)
|
||||
verbose = options.get('verbose', False)
|
||||
dry_run = options.get('dry_run', False)
|
||||
only_changed = options.get('only_changed', False)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('\n' + '='*80))
|
||||
self.stdout.write(self.style.SUCCESS('ПЕРЕСЧЁТ СЕБЕСТОИМОСТИ ТОВАРОВ'))
|
||||
self.stdout.write(self.style.SUCCESS(f'ТЕНАНТ: {tenant.schema_name} ({tenant.name})'))
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING('РЕЖИМ: DRY-RUN (БЕЗ СОХРАНЕНИЯ)'))
|
||||
self.stdout.write(self.style.SUCCESS('='*80 + '\n'))
|
||||
|
||||
# Получаем все активные товары
|
||||
all_products = Product.objects.filter(is_active=True)
|
||||
total = all_products.count()
|
||||
updated_count = 0
|
||||
unchanged_count = 0
|
||||
with_batches_count = 0
|
||||
without_batches_count = 0
|
||||
|
||||
self.stdout.write(f'Всего товаров для обработки: {total}\n')
|
||||
|
||||
for product in all_products:
|
||||
# Получаем старую стоимость
|
||||
old_cost = product.cost_price
|
||||
|
||||
# Рассчитываем новую стоимость
|
||||
old_cost_result, new_cost, was_updated = ProductCostCalculator.update_product_cost(
|
||||
product,
|
||||
save=not dry_run # Сохраняем только если НЕ dry-run
|
||||
)
|
||||
|
||||
# Подсчитываем статистику
|
||||
if was_updated:
|
||||
updated_count += 1
|
||||
else:
|
||||
unchanged_count += 1
|
||||
|
||||
if new_cost > 0:
|
||||
with_batches_count += 1
|
||||
else:
|
||||
without_batches_count += 1
|
||||
|
||||
# Выводим информацию
|
||||
if verbose or (only_changed and was_updated):
|
||||
status_symbol = '✓' if was_updated else '='
|
||||
style = self.style.SUCCESS if was_updated else self.style.WARNING
|
||||
|
||||
# Получаем детали для вывода
|
||||
details = ProductCostCalculator.get_cost_details(product)
|
||||
batches_count = len(details['batches'])
|
||||
total_qty = details['total_quantity']
|
||||
|
||||
self.stdout.write(
|
||||
style(
|
||||
f'{status_symbol} {product.sku:15} {product.name[:40]:40} | '
|
||||
f'Старая: {old_cost:8.2f} → Новая: {new_cost:8.2f} | '
|
||||
f'Партий: {batches_count:3}, Кол-во: {total_qty:8.2f}'
|
||||
)
|
||||
)
|
||||
|
||||
# Финальный отчет
|
||||
self.stdout.write(self.style.SUCCESS('\n' + '='*80))
|
||||
self.stdout.write(self.style.SUCCESS('РЕЗУЛЬТАТЫ:'))
|
||||
self.stdout.write(self.style.SUCCESS(f' Всего обработано: {total}'))
|
||||
self.stdout.write(self.style.SUCCESS(f' Обновлено: {updated_count}'))
|
||||
self.stdout.write(self.style.SUCCESS(f' Без изменений: {unchanged_count}'))
|
||||
self.stdout.write(self.style.SUCCESS(f' С партиями (>0): {with_batches_count}'))
|
||||
self.stdout.write(self.style.SUCCESS(f' Без партий (=0): {without_batches_count}'))
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING('\n ⚠ Изменения НЕ сохранены (dry-run режим)'))
|
||||
self.stdout.write(self.style.SUCCESS('='*80 + '\n'))
|
||||
|
||||
if updated_count > 0:
|
||||
if dry_run:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f'✓ Будет обновлено {updated_count} товаров при реальном запуске')
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f'✓ Успешно обновлено {updated_count} товаров')
|
||||
)
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING('Нет товаров для обновления'))
|
||||
Reference in New Issue
Block a user