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:
2025-11-02 19:04:03 +03:00
parent c84a372f98
commit 6c8af5ab2c
120 changed files with 9035 additions and 3036 deletions

View File

@@ -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('Нет товаров для обновления'))