diff --git a/myproject/products/forms.py b/myproject/products/forms.py
index 6b6a335..4a5276a 100644
--- a/myproject/products/forms.py
+++ b/myproject/products/forms.py
@@ -313,15 +313,17 @@ class KitItemForm(forms.ModelForm):
"""
class Meta:
model = KitItem
- fields = ['product', 'variant_group', 'quantity']
+ fields = ['product', 'variant_group', 'sales_unit', 'quantity']
labels = {
'product': 'Конкретный товар',
'variant_group': 'Группа вариантов',
+ 'sales_unit': 'Единица продажи',
'quantity': 'Количество'
}
widgets = {
'product': forms.Select(attrs={'class': 'form-control'}),
'variant_group': forms.Select(attrs={'class': 'form-control'}),
+ 'sales_unit': forms.Select(attrs={'class': 'form-control'}),
'quantity': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001', 'min': '0'}),
}
@@ -335,24 +337,35 @@ class KitItemForm(forms.ModelForm):
cleaned_data = super().clean()
product = cleaned_data.get('product')
variant_group = cleaned_data.get('variant_group')
+ sales_unit = cleaned_data.get('sales_unit')
quantity = cleaned_data.get('quantity')
- # Если оба поля пусты - это пустая форма (не валидируем, она будет удалена)
- if not product and not variant_group:
+ # Подсчитываем, сколько полей заполнено
+ filled_fields = sum([bool(product), bool(variant_group), bool(sales_unit)])
+
+ # Если все поля пусты - это пустая форма (не валидируем, она будет удалена)
+ if filled_fields == 0:
# Для пустых форм обнуляем количество
cleaned_data['quantity'] = None
return cleaned_data
- # Валидация: должен быть указан либо product, либо variant_group (но не оба)
- if product and variant_group:
+ # Валидация несовместимых полей
+ if variant_group and (product or sales_unit):
raise forms.ValidationError(
- "Нельзя указывать одновременно товар и группу вариантов. Выберите что-то одно."
+ "Нельзя указывать группу вариантов одновременно с товаром или единицей продажи."
)
+
+ # Если выбрана единица продажи, товар обязателен
+ if sales_unit and not product:
+ raise forms.ValidationError("Для единицы продажи должен быть выбран товар.")
- # Валидация: если выбран товар/группа, количество обязательно и должно быть > 0
- if (product or variant_group):
- if not quantity or quantity <= 0:
- raise forms.ValidationError('Необходимо указать количество больше 0')
+ # Валидация: если выбран товар/группа/единица продажи, количество обязательно и должно быть > 0
+ if not quantity or quantity <= 0:
+ raise forms.ValidationError('Необходимо указать количество больше 0')
+
+ # Валидация: если выбрана единица продажи, проверяем, что она принадлежит выбранному продукту
+ if sales_unit and product and sales_unit.product != product:
+ raise forms.ValidationError('Выбранная единица продажи не принадлежит указанному товару.')
return cleaned_data
@@ -367,6 +380,7 @@ class BaseKitItemFormSet(forms.BaseInlineFormSet):
products = []
variant_groups = []
+ sales_units = []
for form in self.forms:
if self.can_delete and self._should_delete_form(form):
@@ -374,6 +388,7 @@ class BaseKitItemFormSet(forms.BaseInlineFormSet):
product = form.cleaned_data.get('product')
variant_group = form.cleaned_data.get('variant_group')
+ sales_unit = form.cleaned_data.get('sales_unit')
# Проверка дубликатов товаров
if product:
@@ -393,13 +408,22 @@ class BaseKitItemFormSet(forms.BaseInlineFormSet):
)
variant_groups.append(variant_group)
+ # Проверка дубликатов единиц продажи
+ if sales_unit:
+ if sales_unit in sales_units:
+ raise forms.ValidationError(
+ f'Единица продажи "{sales_unit.name}" добавлена в комплект более одного раза. '
+ f'Каждая единица продажи может быть добавлена только один раз.'
+ )
+ sales_units.append(sales_unit)
+
# Формсет для создания комплектов (с пустой формой для удобства)
KitItemFormSetCreate = inlineformset_factory(
ProductKit,
KitItem,
form=KitItemForm,
formset=BaseKitItemFormSet,
- fields=['product', 'variant_group', 'quantity'],
+ fields=['product', 'variant_group', 'sales_unit', 'quantity'],
extra=1, # Показать 1 пустую форму для первого компонента
can_delete=True, # Разрешить удаление компонентов
min_num=0, # Минимум 0 компонентов (можно создать пустой комплект)
@@ -413,7 +437,7 @@ KitItemFormSetUpdate = inlineformset_factory(
KitItem,
form=KitItemForm,
formset=BaseKitItemFormSet,
- fields=['product', 'variant_group', 'quantity'],
+ fields=['product', 'variant_group', 'sales_unit', 'quantity'],
extra=0, # НЕ показывать пустые формы при редактировании
can_delete=True, # Разрешить удаление компонентов
min_num=0, # Минимум 0 компонентов
diff --git a/myproject/products/migrations/0001_add_sales_unit_to_kititem.py b/myproject/products/migrations/0001_add_sales_unit_to_kititem.py
new file mode 100644
index 0000000..12664b8
--- /dev/null
+++ b/myproject/products/migrations/0001_add_sales_unit_to_kititem.py
@@ -0,0 +1,25 @@
+# Generated migration file for adding sales_unit field to KitItem model
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('products', '0005_base_unit_nullable'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='kititem',
+ name='sales_unit',
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='kit_items',
+ to='products.productsalesunit',
+ verbose_name='Единица продажи'
+ ),
+ ),
+ ]
\ No newline at end of file
diff --git a/myproject/products/models/kits.py b/myproject/products/models/kits.py
index 76faf4f..76eb1d4 100644
--- a/myproject/products/models/kits.py
+++ b/myproject/products/models/kits.py
@@ -163,7 +163,11 @@ class ProductKit(BaseProductEntity):
total = Decimal('0')
for item in self.kit_items.all():
qty = item.quantity or Decimal('1')
- if item.product:
+ if item.sales_unit:
+ # Для sales_unit используем цену единицы продажи
+ unit_price = item.sales_unit.actual_price or Decimal('0')
+ total += unit_price * qty
+ elif item.product:
# Используем зафиксированную цену если есть, иначе актуальную цену товара
if item.unit_price is not None:
unit_price = item.unit_price
@@ -213,7 +217,11 @@ class ProductKit(BaseProductEntity):
# Пересчитаем базовую цену из компонентов
total = Decimal('0')
for item in self.kit_items.all():
- if item.product:
+ if item.sales_unit:
+ actual_price = item.sales_unit.actual_price or Decimal('0')
+ qty = item.quantity or Decimal('1')
+ total += actual_price * qty
+ elif item.product:
actual_price = item.product.actual_price or Decimal('0')
qty = item.quantity or Decimal('1')
total += actual_price * qty
@@ -382,8 +390,8 @@ class ProductKit(BaseProductEntity):
class KitItem(models.Model):
"""
- Состав комплекта: связь между ProductKit и Product или ProductVariantGroup.
- Позиция может быть либо конкретным товаром, либо группой вариантов.
+ Состав комплекта: связь между ProductKit и Product, ProductVariantGroup или ProductSalesUnit.
+ Позиция может быть либо конкретным товаром, либо группой вариантов, либо конкретной единицей продажи.
"""
kit = models.ForeignKey(ProductKit, on_delete=models.CASCADE, related_name='kit_items',
verbose_name="Комплект")
@@ -403,6 +411,14 @@ class KitItem(models.Model):
related_name='kit_items',
verbose_name="Группа вариантов"
)
+ sales_unit = models.ForeignKey(
+ 'ProductSalesUnit',
+ on_delete=models.CASCADE,
+ null=True,
+ blank=True,
+ related_name='kit_items',
+ verbose_name="Единица продажи"
+ )
quantity = models.DecimalField(max_digits=10, decimal_places=3, null=True, blank=True, verbose_name="Количество")
unit_price = models.DecimalField(
max_digits=10,
@@ -428,21 +444,46 @@ class KitItem(models.Model):
return f"{self.kit.name} - {self.get_display_name()}"
def clean(self):
- """Валидация: должен быть указан либо product, либо variant_group (но не оба)"""
- if self.product and self.variant_group:
- raise ValidationError(
- "Нельзя указывать одновременно товар и группу вариантов. Выберите что-то одно."
- )
- if not self.product and not self.variant_group:
+ """Валидация: должна быть указана группа вариантов ИЛИ (товар [плюс опционально единица продажи])"""
+
+ has_variant = bool(self.variant_group)
+ has_product = bool(self.product)
+ has_sales_unit = bool(self.sales_unit)
+
+ # 1. Проверка на пустоту
+ if not (has_variant or has_product or has_sales_unit):
raise ValidationError(
"Необходимо указать либо товар, либо группу вариантов."
)
+ # 2. Несовместимость: Группа вариантов VS Товар/Единица
+ if has_variant and (has_product or has_sales_unit):
+ raise ValidationError(
+ "Нельзя указывать группу вариантов одновременно с товаром или единицей продажи."
+ )
+
+ # 3. Зависимость: Если есть sales_unit, должен быть product
+ if has_sales_unit and not has_product:
+ raise ValidationError(
+ "Если указана единица продажи, должен быть выбран соответствующий товар."
+ )
+
+ # 4. Проверка принадлежности
+ if has_sales_unit and has_product and self.sales_unit.product != self.product:
+ raise ValidationError(
+ "Выбранная единица продажи не принадлежит указанному товару."
+ )
+
def get_display_name(self):
"""Возвращает строку для отображения названия компонента"""
- if self.variant_group:
+ # Приоритет: сначала единица продажи, затем товар, затем группа вариантов
+ if self.sales_unit:
+ return f"[Единица продажи] {self.sales_unit.name}"
+ elif self.product:
+ return self.product.name
+ elif self.variant_group:
return f"[Варианты] {self.variant_group.name}"
- return self.product.name if self.product else "Не указан"
+ return "Не указан"
def has_priorities_set(self):
"""Проверяет, настроены ли приоритеты замены для данного компонента"""
@@ -452,10 +493,16 @@ class KitItem(models.Model):
"""
Возвращает список доступных товаров для этого компонента.
+ Если указана единица продажи - возвращает товар, к которому она относится.
Если указан конкретный товар - возвращает его.
Если указаны приоритеты - возвращает товары в порядке приоритета.
Если не указаны приоритеты - возвращает все активные товары из группы вариантов.
"""
+ # Приоритет: сначала единица продажи, затем товар, затем группа вариантов
+ if self.sales_unit:
+ # Если указана единица продажи, возвращаем товар, к которому она относится
+ return [self.sales_unit.product]
+
if self.product:
# Если указан конкретный товар, возвращаем только его
return [self.product]
diff --git a/myproject/products/models/products.py b/myproject/products/models/products.py
index 1d9c585..c6b8bae 100644
--- a/myproject/products/models/products.py
+++ b/myproject/products/models/products.py
@@ -141,6 +141,14 @@ class Product(BaseProductEntity):
from ..services.cost_calculator import ProductCostCalculator
return ProductCostCalculator.get_cost_details(self)
+ @property
+ def kit_items_using_as_sales_unit(self):
+ """
+ Возвращает QuerySet KitItem, где этот товар используется как единица продажи.
+ """
+ from .kits import KitItem
+ return KitItem.objects.filter(sales_unit__product=self)
+
def save(self, *args, **kwargs):
# Используем сервис для подготовки к сохранению
ProductSaveService.prepare_product_for_save(self)
diff --git a/myproject/products/templates/products/includes/kititem_formset.html b/myproject/products/templates/products/includes/kititem_formset.html
index b053974..dfcb453 100644
--- a/myproject/products/templates/products/includes/kititem_formset.html
+++ b/myproject/products/templates/products/includes/kititem_formset.html
@@ -8,25 +8,33 @@
{% for kititem_form in kititem_formset %}
-
+
{{ kititem_form.id }}
{% if kititem_form.non_field_errors %}
-
- {% for error in kititem_form.non_field_errors %}{{ error }}{% endfor %}
-
+
+ {% for error in kititem_form.non_field_errors %}{{ error }}{% endfor %}
+
{% endif %}
-
+
{{ kititem_form.product }}
{% if kititem_form.product.errors %}
-
Комплекты, содержащие этот товар как часть группы вариантов
+
+
+
+
+
+
+
Комплект
+
Группа вариантов
+
Количество в комплекте
+
+
+
+ {% for variant_group_item in variant_group_kit_items %}
+ {% for kit_item in variant_group_item.variant_group.kit_items.all %}
+ {% if kit_item.product == product %}
+