feat(products): обеспечить уникальность единицы продажи по умолчанию и улучшить UI формы
Добавлено ограничение на уровне базы данных и валидация форм для обеспечения, что у товара может быть только одна единица продажи с флагом "по умолчанию". Переработан интерфейс маркетинговых флагов и единиц продажи для улучшения UX. Основные изменения: - Добавлен UniqueConstraint в модель ProductSalesUnit для валидации на уровне БД - Создан BaseProductSalesUnitFormSet с кастомной валидацией формы - Обновлен метод save() для корректной обработки новых и существующих записей - Добавлена транзакционная обертка в представлениях ProductCreateView и ProductUpdateView - Переработан блок маркетинговых флагов с карточным дизайном и интерактивными переключателями - Переработан блок единиц продажи в табличный вид с улучшенным UX - Добавлена клиентская логика для взаимного исключения чекбоксов "По умолчанию
This commit is contained in:
144
myproject/products/management/commands/check_default_units.py
Normal file
144
myproject/products/management/commands/check_default_units.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
Команда для проверки дубликатов is_default в ProductSalesUnit.
|
||||
|
||||
Проверяет, есть ли товары с несколькими единицами продажи, у которых is_default=True.
|
||||
Если дубликаты найдены, выводит детальную информацию и предлагает варианты исправления.
|
||||
|
||||
Использование:
|
||||
python manage.py check_default_units
|
||||
python manage.py check_default_units --fix # автоматически исправить дубликаты
|
||||
"""
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction, models
|
||||
from django.db.models import Count
|
||||
from products.models.units import ProductSalesUnit
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Проверить и исправить дубликаты is_default в ProductSalesUnit'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--fix',
|
||||
action='store_true',
|
||||
dest='fix',
|
||||
help='Автоматически исправить найденные дубликаты',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
dest='dry_run',
|
||||
help='Показать, что будет изменено, но не применять изменения',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
fix = options.get('fix')
|
||||
dry_run = options.get('dry_run')
|
||||
|
||||
self.stdout.write(self.style.WARNING('Проверка дубликатов is_default в ProductSalesUnit...'))
|
||||
self.stdout.write('')
|
||||
|
||||
# Находим все товары с несколькими is_default=True
|
||||
products_with_duplicates = (
|
||||
ProductSalesUnit.objects
|
||||
.filter(is_default=True)
|
||||
.values('product_id')
|
||||
.annotate(default_count=models.Count('id'))
|
||||
.filter(default_count__gt=1)
|
||||
)
|
||||
|
||||
if not products_with_duplicates.exists():
|
||||
self.stdout.write(self.style.SUCCESS('✓ Дубликаты не найдены. Все в порядке!'))
|
||||
return
|
||||
|
||||
# Собираем детальную информацию о дубликатах
|
||||
duplicates_info = []
|
||||
for item in products_with_duplicates:
|
||||
product_id = item['product_id']
|
||||
default_units = list(
|
||||
ProductSalesUnit.objects.filter(
|
||||
product_id=product_id,
|
||||
is_default=True
|
||||
).order_by('id')
|
||||
)
|
||||
duplicates_info.append({
|
||||
'product_id': product_id,
|
||||
'product_name': default_units[0].product.name if default_units else f'ID: {product_id}',
|
||||
'units': default_units
|
||||
})
|
||||
|
||||
# Выводим информацию о найденных дубликатах
|
||||
self.stdout.write(
|
||||
self.style.ERROR(f'Найдено {len(duplicates_info)} товаров с дубликатами is_default:')
|
||||
)
|
||||
self.stdout.write('')
|
||||
|
||||
for info in duplicates_info:
|
||||
self.stdout.write(f' Товар: {info["product_name"]} (ID: {info["product_id"]})')
|
||||
self.stdout.write(f' Единиц с is_default=True: {len(info["units"])}')
|
||||
for unit in info['units']:
|
||||
self.stdout.write(f' - ID: {unit.id}, Название: "{unit.name}", Цена: {unit.price}₽')
|
||||
self.stdout.write('')
|
||||
|
||||
if not fix:
|
||||
self.stdout.write('Для исправления дубликатов запустите:')
|
||||
self.stdout.write(' python manage.py check_default_units --fix')
|
||||
self.stdout.write('')
|
||||
self.stdout.write('Для проверки что будет изменено (без применения):')
|
||||
self.stdout.write(' python manage.py check_default_units --fix --dry-run')
|
||||
return
|
||||
|
||||
# Исправляем дубликаты
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING('РЕЖИМ DRY-RUN - изменения не будут применены'))
|
||||
self.stdout.write('')
|
||||
|
||||
self.stdout.write(self.style.WARNING('Исправление дубликатов...'))
|
||||
self.stdout.write('')
|
||||
|
||||
with transaction.atomic():
|
||||
if dry_run:
|
||||
# В режиме dry-run просто показываем что будет сделано
|
||||
for info in duplicates_info:
|
||||
units = info['units']
|
||||
if len(units) > 1:
|
||||
# Оставляем первую, остальные снимаем флаг
|
||||
keep_unit = units[0]
|
||||
remove_units = units[1:]
|
||||
self.stdout.write(
|
||||
f' Товар: {info["product_name"]} (ID: {info["product_id"]})'
|
||||
)
|
||||
self.stdout.write(f' Оставить: ID {keep_unit.id}, "{keep_unit.name}"')
|
||||
for unit in remove_units:
|
||||
self.stdout.write(
|
||||
f' Снять is_default: ID {unit.id}, "{unit.name}"'
|
||||
)
|
||||
else:
|
||||
# Реальное исправление
|
||||
for info in duplicates_info:
|
||||
units = info['units']
|
||||
if len(units) > 1:
|
||||
keep_unit = units[0]
|
||||
remove_units = units[1:]
|
||||
|
||||
# Снимаем флаг is_default со всех кроме первой
|
||||
removed_ids = [unit.id for unit in remove_units]
|
||||
ProductSalesUnit.objects.filter(id__in=removed_ids).update(is_default=False)
|
||||
|
||||
self.stdout.write(
|
||||
f' ✓ Товар: {info["product_name"]} (ID: {info["product_id"]})'
|
||||
)
|
||||
self.stdout.write(
|
||||
f' Оставлен: ID {keep_unit.id}, "{keep_unit.name}"'
|
||||
)
|
||||
self.stdout.write(
|
||||
f' Снято is_default с {len(remove_units)} записей: {removed_ids}'
|
||||
)
|
||||
|
||||
if not dry_run:
|
||||
self.stdout.write('')
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'✓ Исправлено {len(duplicates_info)} товаров с дубликатами!'
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user