feat: Добавить трёхуровневую защиту от дубликатов имён товаров, категорий, тегов и комплектов

Реализована полная система обеспечения уникальности названий:

1. **Уровень БД (Model Constraints)** - добавлены UniqueConstraint для:
   - Product: уникальность имени среди активных товаров
   - ProductCategory: уникальность имени среди активных категорий
   - ProductTag: уникальность имени только для активных тегов (неактивные могут повторяться)
   - ProductKit: уникальность имени среди активных, непроизвременных комплектов

2. **Уровень формы (Form Validation)** - добавлены clean() методы для:
   - ProductForm, ProductKitForm, ProductCategoryForm, ProductTagForm
   - Валидация до попытки сохранения в БД
   - Сохранение введённых данных при ошибке валидации

3. **Уровень представления (IntegrityError Handling)** - добавлена обработка в views:
   - ProductCategoryCreateView, ProductCategoryUpdateView
   - ProductTagCreateView, ProductTagUpdateView
   - ProductKitCreateView, ProductKitUpdateView
   - create_tag_api: защита от race conditions с fallback поиском

Три уровня защиты гарантируют:
- Профилактика ошибок на уровне формы
- Обработка исключительных ситуаций в views
- Защита БД от одновременных запросов (race conditions)
- Пользователь видит понятное сообщение об ошибке вместо 500 ошибки

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-15 13:49:52 +03:00
parent 0b41c6815c
commit 079bd23829
9 changed files with 386 additions and 45 deletions

View File

@@ -4,6 +4,7 @@
"""
from decimal import Decimal
from django.db import models
from django.db.models import Q
from django.utils import timezone
from django.core.exceptions import ValidationError
@@ -110,6 +111,15 @@ class ProductKit(BaseProductEntity):
models.Index(fields=['is_temporary']),
models.Index(fields=['order']),
]
constraints = [
# Уникальное имя для активных комплектов (исключаем удалённые)
# Примечание: временные комплекты могут иметь дубли имён (создаются для заказов)
models.UniqueConstraint(
fields=['name'],
condition=Q(is_deleted=False, is_temporary=False),
name='unique_active_kit_name'
),
]
@property
def actual_price(self):