Исправление ошибок в редактировании комплектов: валидация, верстка, расчет цены
This commit is contained in:
@@ -313,15 +313,17 @@ class KitItemForm(forms.ModelForm):
|
|||||||
"""
|
"""
|
||||||
class Meta:
|
class Meta:
|
||||||
model = KitItem
|
model = KitItem
|
||||||
fields = ['product', 'variant_group', 'quantity']
|
fields = ['product', 'variant_group', 'sales_unit', 'quantity']
|
||||||
labels = {
|
labels = {
|
||||||
'product': 'Конкретный товар',
|
'product': 'Конкретный товар',
|
||||||
'variant_group': 'Группа вариантов',
|
'variant_group': 'Группа вариантов',
|
||||||
|
'sales_unit': 'Единица продажи',
|
||||||
'quantity': 'Количество'
|
'quantity': 'Количество'
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'product': forms.Select(attrs={'class': 'form-control'}),
|
'product': forms.Select(attrs={'class': 'form-control'}),
|
||||||
'variant_group': 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'}),
|
'quantity': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001', 'min': '0'}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,25 +337,36 @@ class KitItemForm(forms.ModelForm):
|
|||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
product = cleaned_data.get('product')
|
product = cleaned_data.get('product')
|
||||||
variant_group = cleaned_data.get('variant_group')
|
variant_group = cleaned_data.get('variant_group')
|
||||||
|
sales_unit = cleaned_data.get('sales_unit')
|
||||||
quantity = cleaned_data.get('quantity')
|
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
|
cleaned_data['quantity'] = None
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
# Валидация: должен быть указан либо product, либо variant_group (но не оба)
|
# Валидация несовместимых полей
|
||||||
if product and variant_group:
|
if variant_group and (product or sales_unit):
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
"Нельзя указывать одновременно товар и группу вариантов. Выберите что-то одно."
|
"Нельзя указывать группу вариантов одновременно с товаром или единицей продажи."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Валидация: если выбран товар/группа, количество обязательно и должно быть > 0
|
# Если выбрана единица продажи, товар обязателен
|
||||||
if (product or variant_group):
|
if sales_unit and not product:
|
||||||
|
raise forms.ValidationError("Для единицы продажи должен быть выбран товар.")
|
||||||
|
|
||||||
|
# Валидация: если выбран товар/группа/единица продажи, количество обязательно и должно быть > 0
|
||||||
if not quantity or quantity <= 0:
|
if not quantity or quantity <= 0:
|
||||||
raise forms.ValidationError('Необходимо указать количество больше 0')
|
raise forms.ValidationError('Необходимо указать количество больше 0')
|
||||||
|
|
||||||
|
# Валидация: если выбрана единица продажи, проверяем, что она принадлежит выбранному продукту
|
||||||
|
if sales_unit and product and sales_unit.product != product:
|
||||||
|
raise forms.ValidationError('Выбранная единица продажи не принадлежит указанному товару.')
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
@@ -367,6 +380,7 @@ class BaseKitItemFormSet(forms.BaseInlineFormSet):
|
|||||||
|
|
||||||
products = []
|
products = []
|
||||||
variant_groups = []
|
variant_groups = []
|
||||||
|
sales_units = []
|
||||||
|
|
||||||
for form in self.forms:
|
for form in self.forms:
|
||||||
if self.can_delete and self._should_delete_form(form):
|
if self.can_delete and self._should_delete_form(form):
|
||||||
@@ -374,6 +388,7 @@ class BaseKitItemFormSet(forms.BaseInlineFormSet):
|
|||||||
|
|
||||||
product = form.cleaned_data.get('product')
|
product = form.cleaned_data.get('product')
|
||||||
variant_group = form.cleaned_data.get('variant_group')
|
variant_group = form.cleaned_data.get('variant_group')
|
||||||
|
sales_unit = form.cleaned_data.get('sales_unit')
|
||||||
|
|
||||||
# Проверка дубликатов товаров
|
# Проверка дубликатов товаров
|
||||||
if product:
|
if product:
|
||||||
@@ -393,13 +408,22 @@ class BaseKitItemFormSet(forms.BaseInlineFormSet):
|
|||||||
)
|
)
|
||||||
variant_groups.append(variant_group)
|
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(
|
KitItemFormSetCreate = inlineformset_factory(
|
||||||
ProductKit,
|
ProductKit,
|
||||||
KitItem,
|
KitItem,
|
||||||
form=KitItemForm,
|
form=KitItemForm,
|
||||||
formset=BaseKitItemFormSet,
|
formset=BaseKitItemFormSet,
|
||||||
fields=['product', 'variant_group', 'quantity'],
|
fields=['product', 'variant_group', 'sales_unit', 'quantity'],
|
||||||
extra=1, # Показать 1 пустую форму для первого компонента
|
extra=1, # Показать 1 пустую форму для первого компонента
|
||||||
can_delete=True, # Разрешить удаление компонентов
|
can_delete=True, # Разрешить удаление компонентов
|
||||||
min_num=0, # Минимум 0 компонентов (можно создать пустой комплект)
|
min_num=0, # Минимум 0 компонентов (можно создать пустой комплект)
|
||||||
@@ -413,7 +437,7 @@ KitItemFormSetUpdate = inlineformset_factory(
|
|||||||
KitItem,
|
KitItem,
|
||||||
form=KitItemForm,
|
form=KitItemForm,
|
||||||
formset=BaseKitItemFormSet,
|
formset=BaseKitItemFormSet,
|
||||||
fields=['product', 'variant_group', 'quantity'],
|
fields=['product', 'variant_group', 'sales_unit', 'quantity'],
|
||||||
extra=0, # НЕ показывать пустые формы при редактировании
|
extra=0, # НЕ показывать пустые формы при редактировании
|
||||||
can_delete=True, # Разрешить удаление компонентов
|
can_delete=True, # Разрешить удаление компонентов
|
||||||
min_num=0, # Минимум 0 компонентов
|
min_num=0, # Минимум 0 компонентов
|
||||||
|
|||||||
@@ -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='Единица продажи'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -163,7 +163,11 @@ class ProductKit(BaseProductEntity):
|
|||||||
total = Decimal('0')
|
total = Decimal('0')
|
||||||
for item in self.kit_items.all():
|
for item in self.kit_items.all():
|
||||||
qty = item.quantity or Decimal('1')
|
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:
|
if item.unit_price is not None:
|
||||||
unit_price = item.unit_price
|
unit_price = item.unit_price
|
||||||
@@ -213,7 +217,11 @@ class ProductKit(BaseProductEntity):
|
|||||||
# Пересчитаем базовую цену из компонентов
|
# Пересчитаем базовую цену из компонентов
|
||||||
total = Decimal('0')
|
total = Decimal('0')
|
||||||
for item in self.kit_items.all():
|
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')
|
actual_price = item.product.actual_price or Decimal('0')
|
||||||
qty = item.quantity or Decimal('1')
|
qty = item.quantity or Decimal('1')
|
||||||
total += actual_price * qty
|
total += actual_price * qty
|
||||||
@@ -382,8 +390,8 @@ class ProductKit(BaseProductEntity):
|
|||||||
|
|
||||||
class KitItem(models.Model):
|
class KitItem(models.Model):
|
||||||
"""
|
"""
|
||||||
Состав комплекта: связь между ProductKit и Product или ProductVariantGroup.
|
Состав комплекта: связь между ProductKit и Product, ProductVariantGroup или ProductSalesUnit.
|
||||||
Позиция может быть либо конкретным товаром, либо группой вариантов.
|
Позиция может быть либо конкретным товаром, либо группой вариантов, либо конкретной единицей продажи.
|
||||||
"""
|
"""
|
||||||
kit = models.ForeignKey(ProductKit, on_delete=models.CASCADE, related_name='kit_items',
|
kit = models.ForeignKey(ProductKit, on_delete=models.CASCADE, related_name='kit_items',
|
||||||
verbose_name="Комплект")
|
verbose_name="Комплект")
|
||||||
@@ -403,6 +411,14 @@ class KitItem(models.Model):
|
|||||||
related_name='kit_items',
|
related_name='kit_items',
|
||||||
verbose_name="Группа вариантов"
|
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="Количество")
|
quantity = models.DecimalField(max_digits=10, decimal_places=3, null=True, blank=True, verbose_name="Количество")
|
||||||
unit_price = models.DecimalField(
|
unit_price = models.DecimalField(
|
||||||
max_digits=10,
|
max_digits=10,
|
||||||
@@ -428,21 +444,46 @@ class KitItem(models.Model):
|
|||||||
return f"{self.kit.name} - {self.get_display_name()}"
|
return f"{self.kit.name} - {self.get_display_name()}"
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Валидация: должен быть указан либо product, либо variant_group (но не оба)"""
|
"""Валидация: должна быть указана группа вариантов ИЛИ (товар [плюс опционально единица продажи])"""
|
||||||
if self.product and self.variant_group:
|
|
||||||
raise ValidationError(
|
has_variant = bool(self.variant_group)
|
||||||
"Нельзя указывать одновременно товар и группу вариантов. Выберите что-то одно."
|
has_product = bool(self.product)
|
||||||
)
|
has_sales_unit = bool(self.sales_unit)
|
||||||
if not self.product and not self.variant_group:
|
|
||||||
|
# 1. Проверка на пустоту
|
||||||
|
if not (has_variant or has_product or has_sales_unit):
|
||||||
raise ValidationError(
|
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):
|
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 f"[Варианты] {self.variant_group.name}"
|
||||||
return self.product.name if self.product else "Не указан"
|
return "Не указан"
|
||||||
|
|
||||||
def has_priorities_set(self):
|
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:
|
if self.product:
|
||||||
# Если указан конкретный товар, возвращаем только его
|
# Если указан конкретный товар, возвращаем только его
|
||||||
return [self.product]
|
return [self.product]
|
||||||
|
|||||||
@@ -141,6 +141,14 @@ class Product(BaseProductEntity):
|
|||||||
from ..services.cost_calculator import ProductCostCalculator
|
from ..services.cost_calculator import ProductCostCalculator
|
||||||
return ProductCostCalculator.get_cost_details(self)
|
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):
|
def save(self, *args, **kwargs):
|
||||||
# Используем сервис для подготовки к сохранению
|
# Используем сервис для подготовки к сохранению
|
||||||
ProductSaveService.prepare_product_for_save(self)
|
ProductSaveService.prepare_product_for_save(self)
|
||||||
|
|||||||
@@ -8,8 +8,7 @@
|
|||||||
|
|
||||||
<div id="kititem-forms">
|
<div id="kititem-forms">
|
||||||
{% for kititem_form in kititem_formset %}
|
{% for kititem_form in kititem_formset %}
|
||||||
<div class="card mb-2 kititem-form border"
|
<div class="card mb-2 kititem-form border" data-form-index="{{ forloop.counter0 }}"
|
||||||
data-form-index="{{ forloop.counter0 }}"
|
|
||||||
data-product-id="{% if kititem_form.instance.product %}{{ kititem_form.instance.product.id }}{% endif %}"
|
data-product-id="{% if kititem_form.instance.product %}{{ kititem_form.instance.product.id }}{% endif %}"
|
||||||
data-product-price="{% if kititem_form.instance.product %}{{ kititem_form.instance.product.actual_price|default:0 }}{% else %}0{% endif %}">
|
data-product-price="{% if kititem_form.instance.product %}{{ kititem_form.instance.product.actual_price|default:0 }}{% else %}0{% endif %}">
|
||||||
{{ kititem_form.id }}
|
{{ kititem_form.id }}
|
||||||
@@ -22,7 +21,7 @@
|
|||||||
|
|
||||||
<div class="row g-2 align-items-end">
|
<div class="row g-2 align-items-end">
|
||||||
<!-- ТОВАР -->
|
<!-- ТОВАР -->
|
||||||
<div class="col-md-4">
|
<div class="col-md-3">
|
||||||
<label class="form-label small text-muted mb-1">Товар</label>
|
<label class="form-label small text-muted mb-1">Товар</label>
|
||||||
{{ kititem_form.product }}
|
{{ kititem_form.product }}
|
||||||
{% if kititem_form.product.errors %}
|
{% if kititem_form.product.errors %}
|
||||||
@@ -30,19 +29,27 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ЕДИНИЦА ПРОДАЖИ -->
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label small text-muted mb-1">Единица продажи</label>
|
||||||
|
{{ kititem_form.sales_unit }}
|
||||||
|
{% if kititem_form.sales_unit.errors %}
|
||||||
|
<div class="text-danger small">{{ kititem_form.sales_unit.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- РАЗДЕЛИТЕЛЬ ИЛИ -->
|
<!-- РАЗДЕЛИТЕЛЬ ИЛИ -->
|
||||||
<div class="col-md-1 d-flex justify-content-center align-items-center">
|
<div class="col-md-1 d-flex justify-content-center align-items-center">
|
||||||
<div class="kit-item-separator">
|
<div class="kit-item-separator">
|
||||||
<span class="separator-text">ИЛИ</span>
|
<span class="separator-text">ИЛИ</span>
|
||||||
<i class="bi bi-info-circle separator-help"
|
<i class="bi bi-info-circle separator-help" data-bs-toggle="tooltip"
|
||||||
data-bs-toggle="tooltip"
|
|
||||||
data-bs-placement="top"
|
data-bs-placement="top"
|
||||||
title="Вы можете выбрать что-то одно: либо товар, либо группу вариантов"></i>
|
title="Вы можете выбрать что-то одно: либо товар, либо группу вариантов"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ГРУППА ВАРИАНТОВ -->
|
<!-- ГРУППА ВАРИАНТОВ -->
|
||||||
<div class="col-md-4">
|
<div class="col-md-3">
|
||||||
<label class="form-label small text-muted mb-1">Группа вариантов</label>
|
<label class="form-label small text-muted mb-1">Группа вариантов</label>
|
||||||
{{ kititem_form.variant_group }}
|
{{ kititem_form.variant_group }}
|
||||||
{% if kititem_form.variant_group.errors %}
|
{% if kititem_form.variant_group.errors %}
|
||||||
@@ -62,7 +69,9 @@
|
|||||||
<!-- УДАЛЕНИЕ -->
|
<!-- УДАЛЕНИЕ -->
|
||||||
<div class="col-md-1 text-end">
|
<div class="col-md-1 text-end">
|
||||||
{% if kititem_form.DELETE %}
|
{% if kititem_form.DELETE %}
|
||||||
<button type="button" class="btn btn-sm btn-link text-danger p-0" onclick="this.nextElementSibling.checked = true; this.closest('.kititem-form').style.display='none'; if(typeof calculateFinalPrice === 'function') calculateFinalPrice();" title="Удалить">
|
<button type="button" class="btn btn-sm btn-link text-danger p-0"
|
||||||
|
onclick="this.nextElementSibling.checked = true; this.closest('.kititem-form').style.display='none'; if(typeof calculateFinalPrice === 'function') calculateFinalPrice();"
|
||||||
|
title="Удалить">
|
||||||
<i class="bi bi-x-lg"></i>
|
<i class="bi bi-x-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
{{ kititem_form.DELETE }}
|
{{ kititem_form.DELETE }}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
/**
|
/**
|
||||||
* Инициализирует Select2 для элемента с AJAX поиском товаров
|
* Инициализирует Select2 для элемента с AJAX поиском товаров
|
||||||
* @param {Element} element - DOM элемент select
|
* @param {Element} element - DOM элемент select
|
||||||
* @param {string} type - Тип поиска ('product' или 'variant')
|
* @param {string} type - Тип поиска ('product', 'variant' или 'sales_unit')
|
||||||
* @param {string} apiUrl - URL API для поиска
|
* @param {string} apiUrl - URL API для поиска
|
||||||
* @returns {boolean} - true если инициализация прошла успешно, false иначе
|
* @returns {boolean} - true если инициализация прошла успешно, false иначе
|
||||||
*/
|
*/
|
||||||
@@ -70,9 +70,34 @@
|
|||||||
|
|
||||||
var placeholders = {
|
var placeholders = {
|
||||||
'product': 'Начните вводить название товара...',
|
'product': 'Начните вводить название товара...',
|
||||||
'variant': 'Начните вводить название группы...'
|
'variant': 'Начните вводить название группы...',
|
||||||
|
'sales_unit': 'Выберите единицу продажи...'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Для единиц продажи используем другой подход - не AJAX, а загрузка при выборе товара
|
||||||
|
if (type === 'sales_unit') {
|
||||||
|
try {
|
||||||
|
$element.select2({
|
||||||
|
theme: 'bootstrap-5',
|
||||||
|
placeholder: placeholders[type] || 'Выберите...',
|
||||||
|
allowClear: true,
|
||||||
|
width: '100%',
|
||||||
|
language: 'ru',
|
||||||
|
minimumInputLength: 0,
|
||||||
|
dropdownAutoWidth: false,
|
||||||
|
// Для единиц продажи не используем AJAX, т.к. они загружаются при выборе товара
|
||||||
|
disabled: true, // Изначально отключен до выбора товара
|
||||||
|
templateResult: formatSelectResult,
|
||||||
|
templateSelection: formatSelectSelection
|
||||||
|
});
|
||||||
|
console.log('initProductSelect2: successfully initialized sales_unit for', element.name);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('initProductSelect2: initialization error for sales_unit', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Для товаров и вариантов используем AJAX
|
||||||
try {
|
try {
|
||||||
$element.select2({
|
$element.select2({
|
||||||
theme: 'bootstrap-5',
|
theme: 'bootstrap-5',
|
||||||
@@ -112,18 +137,25 @@
|
|||||||
console.error('initProductSelect2: initialization error', error);
|
console.error('initProductSelect2: initialization error', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Инициализирует Select2 для всех селектов, совпадающих с паттерном
|
* Инициализирует Select2 для всех селектов, совпадающих с паттерном
|
||||||
* @param {string} fieldPattern - Паттерн name атрибута (например: 'items-', 'kititem-')
|
* @param {string} fieldPattern - Паттерн name атрибута (например: 'items-', 'kititem-')
|
||||||
* @param {string} type - Тип поиска ('product' или 'variant')
|
* @param {string} type - Тип поиска ('product', 'variant' или 'sales_unit')
|
||||||
* @param {string} apiUrl - URL API для поиска
|
* @param {string} apiUrl - URL API для поиска
|
||||||
*/
|
*/
|
||||||
window.initAllProductSelect2 = function(fieldPattern, type, apiUrl) {
|
window.initAllProductSelect2 = function(fieldPattern, type, apiUrl) {
|
||||||
|
if (type === 'sales_unit') {
|
||||||
|
document.querySelectorAll('[name*="' + fieldPattern + '"][name*="-sales_unit"]').forEach(function(element) {
|
||||||
|
window.initProductSelect2(element, type, apiUrl);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
document.querySelectorAll('[name*="' + fieldPattern + '"][name*="-product"]').forEach(function(element) {
|
document.querySelectorAll('[name*="' + fieldPattern + '"][name*="-product"]').forEach(function(element) {
|
||||||
window.initProductSelect2(element, type, apiUrl);
|
window.initProductSelect2(element, type, apiUrl);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -362,6 +362,113 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Комплекты, содержащие этот товар как единицу продажи -->
|
||||||
|
{% if kit_items_using_sales_units %}
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Комплекты, содержащие этот товар как единицу продажи</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Комплект</th>
|
||||||
|
<th>Количество в комплекте</th>
|
||||||
|
<th>Цена за единицу</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for kit_item in kit_items_using_sales_units %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'products:productkit-detail' kit_item.kit.pk %}">
|
||||||
|
{{ kit_item.kit.name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ kit_item.quantity|default:"1" }}</td>
|
||||||
|
<td>{{ kit_item.sales_unit.actual_price|default:"0.00" }} руб.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Комплекты, содержащие этот товар напрямую -->
|
||||||
|
{% if kit_items_using_products %}
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Комплекты, содержащие этот товар напрямую</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Комплект</th>
|
||||||
|
<th>Количество в комплекте</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for kit_item in kit_items_using_products %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'products:productkit-detail' kit_item.kit.pk %}">
|
||||||
|
{{ kit_item.kit.name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ kit_item.quantity|default:"1" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Комплекты, содержащие этот товар как часть группы вариантов -->
|
||||||
|
{% if variant_group_kit_items %}
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Комплекты, содержащие этот товар как часть группы вариантов</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Комплект</th>
|
||||||
|
<th>Группа вариантов</th>
|
||||||
|
<th>Количество в комплекте</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% 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 %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'products:productkit-detail' kit_item.kit.pk %}">
|
||||||
|
{{ kit_item.kit.name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ variant_group_item.variant_group.name }}</td>
|
||||||
|
<td>{{ kit_item.quantity|default:"1" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
|
|||||||
@@ -539,6 +539,135 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Функция для получения цены единицы продажи
|
||||||
|
async function getSalesUnitPrice(selectElement) {
|
||||||
|
if (!selectElement) {
|
||||||
|
console.warn('getSalesUnitPrice: selectElement is null or undefined');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawValue = selectElement.value;
|
||||||
|
if (!rawValue) {
|
||||||
|
console.warn('getSalesUnitPrice: no value');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Извлекаем числовой ID из значения (может быть "sales_unit_123" или "123")
|
||||||
|
let salesUnitId;
|
||||||
|
if (rawValue.includes('_')) {
|
||||||
|
const parts = rawValue.split('_');
|
||||||
|
salesUnitId = parseInt(parts[1]);
|
||||||
|
} else {
|
||||||
|
salesUnitId = parseInt(rawValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(salesUnitId) || salesUnitId <= 0) {
|
||||||
|
console.warn('getSalesUnitPrice: invalid sales unit id', rawValue);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если уже загружена в кэш - возвращаем
|
||||||
|
const cacheKey = `sales_unit_${salesUnitId}`;
|
||||||
|
if (priceCache[cacheKey] !== undefined) {
|
||||||
|
const cachedPrice = parseFloat(priceCache[cacheKey]) || 0;
|
||||||
|
console.log('getSalesUnitPrice: from cache', salesUnitId, cachedPrice);
|
||||||
|
return cachedPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пытаемся получить из Select2 data (приоритет: actual_price > price)
|
||||||
|
const $select = $(selectElement);
|
||||||
|
const selectedData = $select.select2('data');
|
||||||
|
if (selectedData && selectedData.length > 0) {
|
||||||
|
const itemData = selectedData[0];
|
||||||
|
const priceData = itemData.actual_price || itemData.price;
|
||||||
|
if (priceData) {
|
||||||
|
const price = parseFloat(priceData) || 0;
|
||||||
|
if (price > 0) {
|
||||||
|
priceCache[cacheKey] = price;
|
||||||
|
console.log('getSalesUnitPrice: from select2 data', salesUnitId, price);
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загружаем информацию о единице продажи через API
|
||||||
|
try {
|
||||||
|
console.log('getSalesUnitPrice: fetching from API', salesUnitId);
|
||||||
|
const response = await fetch(
|
||||||
|
`{% url "products:api-product-sales-units" product_id=0 %}`.replace('/0/', `/${salesUnitId}/`),
|
||||||
|
{ method: 'GET', headers: { 'Accept': 'application/json' } }
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.sales_units && data.sales_units.length > 0) {
|
||||||
|
const salesUnitData = data.sales_units.find(su => su.id == salesUnitId);
|
||||||
|
if (salesUnitData) {
|
||||||
|
const price = parseFloat(salesUnitData.actual_price || salesUnitData.price || 0);
|
||||||
|
if (price > 0) {
|
||||||
|
priceCache[cacheKey] = price;
|
||||||
|
console.log('getSalesUnitPrice: from API', salesUnitId, price);
|
||||||
|
}
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching sales unit price:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn('getSalesUnitPrice: returning 0 for sales unit', salesUnitId);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для обновления списка единиц продажи при выборе товара
|
||||||
|
async function updateSalesUnitsOptions(salesUnitSelect, productValue) {
|
||||||
|
// Очищаем текущие опции
|
||||||
|
salesUnitSelect.innerHTML = '<option value="">---------</option>';
|
||||||
|
salesUnitSelect.disabled = true;
|
||||||
|
|
||||||
|
if (!productValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Извлекаем ID товара
|
||||||
|
let productId;
|
||||||
|
if (productValue.includes('_')) {
|
||||||
|
const parts = productValue.split('_');
|
||||||
|
productId = parseInt(parts[1]);
|
||||||
|
} else {
|
||||||
|
productId = parseInt(productValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(productId) || productId <= 0) {
|
||||||
|
console.warn('updateSalesUnitsOptions: invalid product id', productValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Загружаем единицы продажи для выбранного товара
|
||||||
|
const response = await fetch(
|
||||||
|
`{% url "products:api-product-sales-units" product_id=0 %}`.replace('/0/', `/${productId}/`),
|
||||||
|
{ method: 'GET', headers: { 'Accept': 'application/json' } }
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.sales_units && data.sales_units.length > 0) {
|
||||||
|
data.sales_units.forEach(su => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = su.id;
|
||||||
|
option.textContent = `${su.name} (${su.actual_price || su.price} руб.)`;
|
||||||
|
option.dataset.price = su.actual_price || su.price;
|
||||||
|
option.dataset.actual_price = su.actual_price;
|
||||||
|
salesUnitSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
salesUnitSelect.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching sales units:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Обновляем data-product-id и загружаем цену при выборе товара
|
// Обновляем data-product-id и загружаем цену при выборе товара
|
||||||
$('[name$="-product"]').on('select2:select', async function() {
|
$('[name$="-product"]').on('select2:select', async function() {
|
||||||
const form = $(this).closest('.kititem-form');
|
const form = $(this).closest('.kititem-form');
|
||||||
@@ -637,14 +766,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Пропускаем если количество не валидно
|
// Пропускаем если количество не валидно
|
||||||
const validQuantity = quantity > 0 ? quantity : 1;
|
const validQuantity = quantity > 0 ? quantity : 1;
|
||||||
|
|
||||||
// Проверяем товар
|
// Проверяем единицу продажи (имеет наивысший приоритет)
|
||||||
if (productSelect && productSelect.value) {
|
if (salesUnitSelect && salesUnitSelect.value) {
|
||||||
|
const salesUnitPrice = await getSalesUnitPrice(salesUnitSelect);
|
||||||
|
if (salesUnitPrice > 0) {
|
||||||
|
newBasePrice += (salesUnitPrice * validQuantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Проверяем товар (если нет единицы продажи)
|
||||||
|
else if (productSelect && productSelect.value) {
|
||||||
const productPrice = await getProductPrice(productSelect);
|
const productPrice = await getProductPrice(productSelect);
|
||||||
if (productPrice > 0) {
|
if (productPrice > 0) {
|
||||||
newBasePrice += (productPrice * validQuantity);
|
newBasePrice += (productPrice * validQuantity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Проверяем группу вариантов
|
// Проверяем группу вариантов (если нет ни единицы продажи, ни товара)
|
||||||
else if (variantGroupSelect && variantGroupSelect.value) {
|
else if (variantGroupSelect && variantGroupSelect.value) {
|
||||||
const variantPrice = await getVariantGroupPrice(variantGroupSelect);
|
const variantPrice = await getVariantGroupPrice(variantGroupSelect);
|
||||||
if (variantPrice > 0) {
|
if (variantPrice > 0) {
|
||||||
@@ -801,6 +937,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
const selectedProducts = {{ selected_products|default:"{}"|safe }};
|
const selectedProducts = {{ selected_products|default:"{}"|safe }};
|
||||||
const selectedVariants = {{ selected_variants|default:"{}"|safe }};
|
const selectedVariants = {{ selected_variants|default:"{}"|safe }};
|
||||||
|
const selectedSalesUnits = {{ selected_sales_units|default:"{}"|safe }};
|
||||||
|
|
||||||
$('[name$="-product"]').each(function() {
|
$('[name$="-product"]').each(function() {
|
||||||
const fieldName = $(this).attr('name');
|
const fieldName = $(this).attr('name');
|
||||||
@@ -817,34 +954,72 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
$(this).on('select2:select select2:unselect', calculateFinalPrice);
|
$(this).on('select2:select select2:unselect', calculateFinalPrice);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('[name$="-sales_unit"]').each(function() {
|
||||||
|
const fieldName = $(this).attr('name');
|
||||||
|
const preloadedData = selectedSalesUnits[fieldName] || null;
|
||||||
|
initSelect2(this, 'sales_unit', preloadedData);
|
||||||
|
$(this).on('select2:select select2:unselect', calculateFinalPrice);
|
||||||
|
});
|
||||||
|
|
||||||
// ========== УПРАВЛЕНИЕ КОМПОНЕНТАМИ ==========
|
// ========== УПРАВЛЕНИЕ КОМПОНЕНТАМИ ==========
|
||||||
function updateFieldStatus(form) {
|
function updateFieldStatus(form) {
|
||||||
const productSelect = form.querySelector('[name$="-product"]');
|
const productSelect = form.querySelector('[name$="-product"]');
|
||||||
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
||||||
|
const salesUnitSelect = form.querySelector('[name$="-sales_unit"]');
|
||||||
|
|
||||||
if (!productSelect || !variantGroupSelect) return;
|
if (!productSelect || !variantGroupSelect || !salesUnitSelect) return;
|
||||||
|
|
||||||
const hasProduct = productSelect.value;
|
const hasProduct = productSelect.value;
|
||||||
const hasVariant = variantGroupSelect.value;
|
const hasVariant = variantGroupSelect.value;
|
||||||
|
const hasSalesUnit = salesUnitSelect.value;
|
||||||
|
|
||||||
variantGroupSelect.disabled = !!hasProduct;
|
// Если выбрана группа вариантов, блокируем товар и единицу продажи
|
||||||
productSelect.disabled = !!hasVariant;
|
if (hasVariant) {
|
||||||
|
productSelect.disabled = true;
|
||||||
|
salesUnitSelect.disabled = true;
|
||||||
|
}
|
||||||
|
// Если выбран товар, разблокируем единицу продажи и блокируем группу вариантов
|
||||||
|
else if (hasProduct) {
|
||||||
|
salesUnitSelect.disabled = false;
|
||||||
|
variantGroupSelect.disabled = true;
|
||||||
|
}
|
||||||
|
// Если выбрана только единица продажи, но не товар - блокируем все остальные
|
||||||
|
else if (hasSalesUnit) {
|
||||||
|
productSelect.disabled = true;
|
||||||
|
variantGroupSelect.disabled = true;
|
||||||
|
}
|
||||||
|
// Если ничего не выбрано - разблокируем товар и группу вариантов, блокируем единицу продажи
|
||||||
|
else {
|
||||||
|
productSelect.disabled = false;
|
||||||
|
variantGroupSelect.disabled = false;
|
||||||
|
salesUnitSelect.disabled = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeForm(form) {
|
function initializeForm(form) {
|
||||||
updateFieldStatus(form);
|
updateFieldStatus(form);
|
||||||
const productSelect = form.querySelector('[name$="-product"]');
|
const productSelect = form.querySelector('[name$="-product"]');
|
||||||
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
||||||
|
const salesUnitSelect = form.querySelector('[name$="-sales_unit"]');
|
||||||
|
|
||||||
[productSelect, variantGroupSelect].forEach(field => {
|
[productSelect, variantGroupSelect, salesUnitSelect].forEach(field => {
|
||||||
if (field) {
|
if (field) {
|
||||||
field.addEventListener('change', () => {
|
field.addEventListener('change', () => {
|
||||||
updateFieldStatus(form);
|
updateFieldStatus(form);
|
||||||
|
// Обновляем список единиц продажи при изменении товара
|
||||||
|
if (field === productSelect) {
|
||||||
|
updateSalesUnitsOptions(salesUnitSelect, productSelect.value);
|
||||||
|
}
|
||||||
calculateFinalPrice();
|
calculateFinalPrice();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Инициализируем список единиц продажи, если товар уже выбран
|
||||||
|
if (productSelect && productSelect.value) {
|
||||||
|
updateSalesUnitsOptions(salesUnitSelect, productSelect.value);
|
||||||
|
}
|
||||||
|
|
||||||
const quantityInput = form.querySelector('[name$="-quantity"]');
|
const quantityInput = form.querySelector('[name$="-quantity"]');
|
||||||
if (quantityInput) {
|
if (quantityInput) {
|
||||||
quantityInput.addEventListener('change', calculateFinalPrice);
|
quantityInput.addEventListener('change', calculateFinalPrice);
|
||||||
@@ -874,12 +1049,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
<option value="">---------</option>
|
<option value="">---------</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-1 d-flex justify-content-center">
|
<div class="col-md-3">
|
||||||
<div class="kit-item-separator">
|
<label class="form-label small text-muted mb-1">Единица продажи</label>
|
||||||
<span class="separator-text">ИЛИ</span>
|
<select class="form-control form-control-sm" name="kititem-${newFormId}-sales_unit">
|
||||||
|
<option value="">---------</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-md-3">
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label small text-muted mb-1">Группа вариантов</label>
|
<label class="form-label small text-muted mb-1">Группа вариантов</label>
|
||||||
<select class="form-control form-control-sm" name="kititem-${newFormId}-variant_group">
|
<select class="form-control form-control-sm" name="kititem-${newFormId}-variant_group">
|
||||||
<option value="">---------</option>
|
<option value="">---------</option>
|
||||||
@@ -911,12 +1087,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const productSelect = newForm.querySelector('[name$="-product"]');
|
const productSelect = newForm.querySelector('[name$="-product"]');
|
||||||
const variantSelect = newForm.querySelector('[name$="-variant_group"]');
|
const variantSelect = newForm.querySelector('[name$="-variant_group"]');
|
||||||
|
|
||||||
|
const salesUnitSelect = newForm.querySelector('[name$="-sales_unit"]');
|
||||||
|
|
||||||
initSelect2(productSelect, 'product');
|
initSelect2(productSelect, 'product');
|
||||||
initSelect2(variantSelect, 'variant');
|
initSelect2(variantSelect, 'variant');
|
||||||
|
initSelect2(salesUnitSelect, 'sales_unit');
|
||||||
|
|
||||||
// Добавляем обработчики для новой формы
|
// Добавляем обработчики для новой формы
|
||||||
$(productSelect).on('select2:select select2:unselect', calculateFinalPrice);
|
$(productSelect).on('select2:select select2:unselect', calculateFinalPrice);
|
||||||
$(variantSelect).on('select2:select select2:unselect', calculateFinalPrice);
|
$(variantSelect).on('select2:select select2:unselect', calculateFinalPrice);
|
||||||
|
$(salesUnitSelect).on('select2:select select2:unselect', calculateFinalPrice);
|
||||||
|
|
||||||
initializeForm(newForm);
|
initializeForm(newForm);
|
||||||
|
|
||||||
@@ -987,9 +1167,20 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
allForms.forEach(form => {
|
allForms.forEach(form => {
|
||||||
const productSelect = form.querySelector('[name$="-product"]');
|
const productSelect = form.querySelector('[name$="-product"]');
|
||||||
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
||||||
|
const salesUnitSelect = form.querySelector('[name$="-sales_unit"]');
|
||||||
const deleteCheckbox = form.querySelector('[name$="-DELETE"]');
|
const deleteCheckbox = form.querySelector('[name$="-DELETE"]');
|
||||||
|
|
||||||
if (!productSelect.value && !variantGroupSelect.value && deleteCheckbox) {
|
// Проверяем, что выбран хотя бы один компонент (товар, группа вариантов или единица продажи)
|
||||||
|
// Если выбрана единица продажи, товар должен быть выбран
|
||||||
|
if (salesUnitSelect && salesUnitSelect.value && (!productSelect || !productSelect.value)) {
|
||||||
|
// Если выбрана единица продажи, но не выбран товар - это ошибка
|
||||||
|
alert('Если выбрана единица продажи, должен быть выбран соответствующий товар.');
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если ничего не выбрано, отмечаем для удаления
|
||||||
|
if (!productSelect.value && !variantGroupSelect.value && !salesUnitSelect.value && deleteCheckbox) {
|
||||||
deleteCheckbox.checked = true;
|
deleteCheckbox.checked = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -136,7 +136,13 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{ forloop.counter }}</td>
|
<td>{{ forloop.counter }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if item.product %}
|
{% if item.sales_unit %}
|
||||||
|
<a href="{% url 'products:product-detail' item.sales_unit.product.pk %}">
|
||||||
|
{{ item.sales_unit.name }}
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">Единица продажи: {{ item.sales_unit.product.name }}</small>
|
||||||
|
{% elif item.product %}
|
||||||
<a href="{% url 'products:product-detail' item.product.pk %}">
|
<a href="{% url 'products:product-detail' item.product.pk %}">
|
||||||
{{ item.product.name }}
|
{{ item.product.name }}
|
||||||
</a>
|
</a>
|
||||||
@@ -149,7 +155,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if item.product %}
|
{% if item.sales_unit %}
|
||||||
|
<span class="badge bg-info">Единица продажи</span>
|
||||||
|
{% elif item.product %}
|
||||||
<span class="badge bg-success">Товар</span>
|
<span class="badge bg-success">Товар</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-primary">Варианты</span>
|
<span class="badge bg-primary">Варианты</span>
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
<nav aria-label="breadcrumb" class="mb-2">
|
<nav aria-label="breadcrumb" class="mb-2">
|
||||||
<ol class="breadcrumb breadcrumb-sm mb-0">
|
<ol class="breadcrumb breadcrumb-sm mb-0">
|
||||||
<li class="breadcrumb-item"><a href="{% url 'products:products-list' %}">Комплекты</a></li>
|
<li class="breadcrumb-item"><a href="{% url 'products:products-list' %}">Комплекты</a></li>
|
||||||
<li class="breadcrumb-item"><a href="{% url 'products:productkit-detail' object.pk %}">{{ object.name }}</a></li>
|
<li class="breadcrumb-item"><a href="{% url 'products:productkit-detail' object.pk %}">{{ object.name }}</a>
|
||||||
|
</li>
|
||||||
<li class="breadcrumb-item active">Редактирование</li>
|
<li class="breadcrumb-item active">Редактирование</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<!-- ЛЕВАЯ КОЛОНКА: Основная информация -->
|
<!-- ЛЕВАЯ КОЛОНКА: Основная информация -->
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-9">
|
||||||
<!-- Название -->
|
<!-- Название -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{{ form.name }}
|
{{ form.name }}
|
||||||
@@ -66,7 +67,8 @@
|
|||||||
<div class="card border-0 shadow-sm mb-3">
|
<div class="card border-0 shadow-sm mb-3">
|
||||||
<div class="card-body p-3">
|
<div class="card-body p-3">
|
||||||
<h6 class="mb-2 text-muted"><i class="bi bi-images me-1"></i>Фотографии</h6>
|
<h6 class="mb-2 text-muted"><i class="bi bi-images me-1"></i>Фотографии</h6>
|
||||||
<input type="file" name="photos" accept="image/*" multiple class="form-control form-control-sm" id="id_photos">
|
<input type="file" name="photos" accept="image/*" multiple class="form-control form-control-sm"
|
||||||
|
id="id_photos">
|
||||||
<div id="photoPreviewContainer" class="mt-2" style="display: none;">
|
<div id="photoPreviewContainer" class="mt-2" style="display: none;">
|
||||||
<div id="photoPreview" class="row g-1"></div>
|
<div id="photoPreview" class="row g-1"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +77,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ПРАВАЯ КОЛОНКА: Настройки -->
|
<!-- ПРАВАЯ КОЛОНКА: Настройки -->
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-3">
|
||||||
<!-- Расчет цены -->
|
<!-- Расчет цены -->
|
||||||
<div class="card border-0 shadow-sm mb-3">
|
<div class="card border-0 shadow-sm mb-3">
|
||||||
<div class="card-body p-3">
|
<div class="card-body p-3">
|
||||||
@@ -91,8 +93,10 @@
|
|||||||
<!-- Базовая цена (отображение) -->
|
<!-- Базовая цена (отображение) -->
|
||||||
<div class="mb-3 p-2 rounded" style="background: #f8f9fa; border-left: 3px solid #6c757d;">
|
<div class="mb-3 p-2 rounded" style="background: #f8f9fa; border-left: 3px solid #6c757d;">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<span class="text-muted small"><i class="bi bi-calculator"></i> Сумма цен компонентов:</span>
|
<span class="text-muted small"><i class="bi bi-calculator"></i> Сумма цен
|
||||||
<span id="basePriceDisplay" class="fw-semibold" style="font-size: 1.1rem;">0.00 руб.</span>
|
компонентов:</span>
|
||||||
|
<span id="basePriceDisplay" class="fw-semibold" style="font-size: 1.1rem;">0.00
|
||||||
|
руб.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -106,13 +110,15 @@
|
|||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input type="number" id="id_increase_percent" class="form-control" placeholder="%" step="0.01" min="0">
|
<input type="number" id="id_increase_percent" class="form-control"
|
||||||
|
placeholder="%" step="0.01" min="0">
|
||||||
<span class="input-group-text">%</span>
|
<span class="input-group-text">%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input type="number" id="id_increase_amount" class="form-control" placeholder="руб" step="0.01" min="0">
|
<input type="number" id="id_increase_amount" class="form-control"
|
||||||
|
placeholder="руб" step="0.01" min="0">
|
||||||
<span class="input-group-text">руб</span>
|
<span class="input-group-text">руб</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,13 +134,15 @@
|
|||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input type="number" id="id_decrease_percent" class="form-control" placeholder="%" step="0.01" min="0">
|
<input type="number" id="id_decrease_percent" class="form-control"
|
||||||
|
placeholder="%" step="0.01" min="0">
|
||||||
<span class="input-group-text">%</span>
|
<span class="input-group-text">%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input type="number" id="id_decrease_amount" class="form-control" placeholder="руб" step="0.01" min="0">
|
<input type="number" id="id_decrease_amount" class="form-control"
|
||||||
|
placeholder="руб" step="0.01" min="0">
|
||||||
<span class="input-group-text">руб</span>
|
<span class="input-group-text">руб</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,14 +155,18 @@
|
|||||||
<!-- Итоговая цена -->
|
<!-- Итоговая цена -->
|
||||||
<div class="p-2 rounded" style="background: #e7f5e7; border-left: 3px solid #198754;">
|
<div class="p-2 rounded" style="background: #e7f5e7; border-left: 3px solid #198754;">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<span class="text-dark small"><i class="bi bi-check-circle me-1"></i><strong>Итоговая цена:</strong></span>
|
<span class="text-dark small"><i class="bi bi-check-circle me-1"></i><strong>Итоговая
|
||||||
<span id="finalPriceDisplay" class="fw-bold" style="font-size: 1.3rem; color: #198754;">0.00 руб.</span>
|
цена:</strong></span>
|
||||||
|
<span id="finalPriceDisplay" class="fw-bold"
|
||||||
|
style="font-size: 1.3rem; color: #198754;">0.00 руб.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Скрытые поля для формы (автоматически заполняются JavaScript) -->
|
<!-- Скрытые поля для формы (автоматически заполняются JavaScript) -->
|
||||||
<input type="hidden" id="id_price_adjustment_type" name="price_adjustment_type" value="{{ object.price_adjustment_type|default:'none' }}">
|
<input type="hidden" id="id_price_adjustment_type" name="price_adjustment_type"
|
||||||
<input type="hidden" id="id_price_adjustment_value" name="price_adjustment_value" value="{{ object.price_adjustment_value|default:0 }}">
|
value="{{ object.price_adjustment_type|default:'none' }}">
|
||||||
|
<input type="hidden" id="id_price_adjustment_value" name="price_adjustment_value"
|
||||||
|
value="{{ object.price_adjustment_value|default:0 }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -164,7 +176,8 @@
|
|||||||
<h6 class="mb-2 text-muted"><i class="bi bi-tag-discount"></i> Цена со скидкой</h6>
|
<h6 class="mb-2 text-muted"><i class="bi bi-tag-discount"></i> Цена со скидкой</h6>
|
||||||
<label class="form-label small mb-1">{{ form.sale_price.label }}</label>
|
<label class="form-label small mb-1">{{ form.sale_price.label }}</label>
|
||||||
{{ form.sale_price }}
|
{{ form.sale_price }}
|
||||||
<small class="form-text text-muted">Если указана, будет использоваться вместо расчетной цены</small>
|
<small class="form-text text-muted">Если указана, будет использоваться вместо расчетной
|
||||||
|
цены</small>
|
||||||
{% if form.sale_price.errors %}
|
{% if form.sale_price.errors %}
|
||||||
<div class="text-danger small mt-1">{{ form.sale_price.errors }}</div>
|
<div class="text-danger small mt-1">{{ form.sale_price.errors }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -233,7 +246,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sticky Footer -->
|
<!-- Sticky Footer -->
|
||||||
<div class="sticky-bottom bg-white border-top mt-4 p-3 d-flex justify-content-between align-items-center shadow-sm">
|
<div
|
||||||
|
class="sticky-bottom bg-white border-top mt-4 p-3 d-flex justify-content-between align-items-center shadow-sm">
|
||||||
<a href="{% url 'products:productkit-detail' object.pk %}" class="btn btn-outline-secondary">
|
<a href="{% url 'products:productkit-detail' object.pk %}" class="btn btn-outline-secondary">
|
||||||
Отмена
|
Отмена
|
||||||
</a>
|
</a>
|
||||||
@@ -245,14 +259,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Breadcrumbs */
|
/* Breadcrumbs */
|
||||||
.breadcrumb-sm {
|
.breadcrumb-sm {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Крупное поле названия */
|
/* Крупное поле названия */
|
||||||
#id_name {
|
#id_name {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border: 3px solid #dee2e6;
|
border: 3px solid #dee2e6;
|
||||||
@@ -260,178 +274,181 @@
|
|||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
#id_name:focus {
|
#id_name:focus {
|
||||||
border-color: #198754;
|
border-color: #198754;
|
||||||
box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.15);
|
box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.15);
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Описание */
|
/* Описание */
|
||||||
#id_description {
|
#id_description {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Компактные чекбоксы */
|
/* Компактные чекбоксы */
|
||||||
.compact-checkboxes {
|
.compact-checkboxes {
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-checkboxes ul {
|
.compact-checkboxes ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-checkboxes li {
|
.compact-checkboxes li {
|
||||||
padding: 0.25rem 0;
|
padding: 0.25rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-checkboxes label {
|
.compact-checkboxes label {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Компонент комплекта */
|
/* Компонент комплекта */
|
||||||
.kititem-form {
|
.kititem-form {
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kititem-form:hover {
|
.kititem-form:hover {
|
||||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.075) !important;
|
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kititem-form .card-body {
|
.kititem-form .card-body {
|
||||||
background: #fafbfc;
|
background: #fafbfc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kititem-form input[type="checkbox"][name$="-DELETE"] {
|
.kititem-form input[type="checkbox"][name$="-DELETE"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sticky footer */
|
/* Sticky footer */
|
||||||
.sticky-bottom {
|
.sticky-bottom {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 1020;
|
z-index: 1020;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Карточки */
|
/* Карточки */
|
||||||
.card.border-0 {
|
.card.border-0 {
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Лейблы */
|
/* Лейблы */
|
||||||
.form-label.small {
|
.form-label.small {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Фото превью */
|
/* Фото превью */
|
||||||
#photoPreview .col-4,
|
#photoPreview .col-4,
|
||||||
#photoPreview .col-md-3,
|
#photoPreview .col-md-3,
|
||||||
#photoPreview .col-lg-2 {
|
#photoPreview .col-lg-2 {
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#photoPreview .card {
|
#photoPreview .card {
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#photoPreview img {
|
#photoPreview img {
|
||||||
height: 100px;
|
height: 100px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Alert компактный */
|
/* Alert компактный */
|
||||||
.alert-sm {
|
.alert-sm {
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Анимация */
|
/* Анимация */
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-10px);
|
transform: translateY(-10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.kititem-form.new-item {
|
.kititem-form.new-item {
|
||||||
animation: slideIn 0.3s ease-out;
|
animation: slideIn 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Разделитель ИЛИ */
|
/* Разделитель ИЛИ */
|
||||||
.kit-item-separator {
|
.kit-item-separator {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kit-item-separator .separator-text {
|
.kit-item-separator .separator-text {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #adb5bd;
|
color: #adb5bd;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kit-item-separator .separator-help {
|
.kit-item-separator .separator-help {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kit-item-separator .separator-help:hover {
|
.kit-item-separator .separator-help:hover {
|
||||||
color: #0d6efd;
|
color: #0d6efd;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Стили для полей корректировки цены */
|
/* Стили для полей корректировки цены */
|
||||||
#id_increase_percent:disabled,
|
#id_increase_percent:disabled,
|
||||||
#id_increase_amount:disabled,
|
#id_increase_amount:disabled,
|
||||||
#id_decrease_percent:disabled,
|
#id_decrease_percent:disabled,
|
||||||
#id_decrease_amount:disabled {
|
#id_decrease_amount:disabled {
|
||||||
background-color: #e9ecef;
|
background-color: #e9ecef;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
#id_increase_percent.is-invalid,
|
#id_increase_percent.is-invalid,
|
||||||
#id_increase_amount.is-invalid,
|
#id_increase_amount.is-invalid,
|
||||||
#id_decrease_percent.is-invalid,
|
#id_decrease_percent.is-invalid,
|
||||||
#id_decrease_amount.is-invalid {
|
#id_decrease_amount.is-invalid {
|
||||||
border-color: #dc3545;
|
border-color: #dc3545;
|
||||||
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Адаптивность */
|
/* Адаптивность */
|
||||||
@media (max-width: 991px) {
|
@media (max-width: 991px) {
|
||||||
.col-lg-8, .col-lg-4 {
|
|
||||||
|
.col-lg-8,
|
||||||
|
.col-lg-4 {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- Select2 инициализация -->
|
<!-- Select2 инициализация -->
|
||||||
{% include 'products/includes/select2-product-init.html' %}
|
{% include 'products/includes/select2-product-init.html' %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// ========== УПРАВЛЕНИЕ ЦЕНОЙ КОМПЛЕКТА ==========
|
// ========== УПРАВЛЕНИЕ ЦЕНОЙ КОМПЛЕКТА ==========
|
||||||
const increasePercentInput = document.getElementById('id_increase_percent');
|
const increasePercentInput = document.getElementById('id_increase_percent');
|
||||||
const increaseAmountInput = document.getElementById('id_increase_amount');
|
const increaseAmountInput = document.getElementById('id_increase_amount');
|
||||||
@@ -526,15 +543,29 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем data-product-id и загружаем цену при выборе товара
|
// Обновляем data-product-id и загружаем цену при выборе товара
|
||||||
$('[name$="-product"]').on('select2:select', async function() {
|
$('[name$="-product"]').on('select2:select', async function () {
|
||||||
const form = $(this).closest('.kititem-form');
|
const form = $(this).closest('.kititem-form');
|
||||||
if (this.value) {
|
if (this.value) {
|
||||||
form.attr('data-product-id', this.value);
|
form.attr('data-product-id', this.value);
|
||||||
|
|
||||||
|
// Обновляем список единиц продажи
|
||||||
|
const salesUnitSelect = form.find('[name$="-sales_unit"]').get(0);
|
||||||
|
if (salesUnitSelect) {
|
||||||
|
await updateSalesUnitsOptions(salesUnitSelect, this.value);
|
||||||
|
}
|
||||||
|
|
||||||
// Загружаем цену и пересчитываем
|
// Загружаем цену и пересчитываем
|
||||||
await getProductPrice(this);
|
await getProductPrice(this);
|
||||||
calculateFinalPrice();
|
calculateFinalPrice();
|
||||||
}
|
}
|
||||||
}).on('select2:unselect', function() {
|
}).on('select2:unselect', function () {
|
||||||
|
// Очищаем список единиц продажи
|
||||||
|
const form = $(this).closest('.kititem-form');
|
||||||
|
const salesUnitSelect = form.find('[name$="-sales_unit"]').get(0);
|
||||||
|
if (salesUnitSelect) {
|
||||||
|
salesUnitSelect.innerHTML = '<option value="">---------</option>';
|
||||||
|
salesUnitSelect.disabled = true;
|
||||||
|
}
|
||||||
calculateFinalPrice();
|
calculateFinalPrice();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -599,6 +630,148 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Функция для получения цены единицы продажи
|
||||||
|
async function getSalesUnitPrice(selectElement) {
|
||||||
|
if (!selectElement) {
|
||||||
|
console.warn('getSalesUnitPrice: selectElement is null or undefined');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawValue = selectElement.value;
|
||||||
|
if (!rawValue) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Извлекаем числовой ID из значения
|
||||||
|
let salesUnitId;
|
||||||
|
if (typeof rawValue === 'string' && rawValue.includes('_')) {
|
||||||
|
salesUnitId = parseInt(rawValue.split('_')[1]);
|
||||||
|
} else {
|
||||||
|
salesUnitId = parseInt(rawValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(salesUnitId) || salesUnitId <= 0) {
|
||||||
|
console.warn('getSalesUnitPrice: invalid sales unit id', rawValue);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если уже загружена в кэш - возвращаем
|
||||||
|
const cacheKey = `sales_unit_${salesUnitId}`;
|
||||||
|
if (priceCache[cacheKey] !== undefined) {
|
||||||
|
const cachedPrice = parseFloat(priceCache[cacheKey]) || 0;
|
||||||
|
return cachedPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пытаемся получить из option element (для стандартного select)
|
||||||
|
const selectedOption = selectElement.selectedOptions ? selectElement.selectedOptions[0] : null;
|
||||||
|
if (selectedOption) {
|
||||||
|
let priceData = selectedOption.dataset.actual_price || selectedOption.dataset.price;
|
||||||
|
if (priceData) {
|
||||||
|
const price = parseFloat(priceData) || 0;
|
||||||
|
if (price > 0) {
|
||||||
|
priceCache[cacheKey] = price;
|
||||||
|
console.log('getSalesUnitPrice: from standard select option data', salesUnitId, price);
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пытаемся получить из Select2 data
|
||||||
|
const $select = $(selectElement);
|
||||||
|
if ($select.data('select2')) { // Check if Select2 is initialized on the element
|
||||||
|
const selectedData = $select.select2('data');
|
||||||
|
if (selectedData && selectedData.length > 0) {
|
||||||
|
const itemData = selectedData[0];
|
||||||
|
const priceData = itemData.actual_price || itemData.price;
|
||||||
|
if (priceData) {
|
||||||
|
const price = parseFloat(priceData) || 0;
|
||||||
|
if (price > 0) {
|
||||||
|
priceCache[cacheKey] = price;
|
||||||
|
console.log('getSalesUnitPrice: from select2 data', salesUnitId, price);
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загружаем информацию о единице продажи через API
|
||||||
|
try {
|
||||||
|
console.log('getSalesUnitPrice: fetching from API', salesUnitId);
|
||||||
|
const response = await fetch(
|
||||||
|
`{% url "products:api-product-sales-units" product_id=0 %}`.replace('/0/', `/${salesUnitId}/`),
|
||||||
|
{ method: 'GET', headers: { 'Accept': 'application/json' } }
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.sales_units && data.sales_units.length > 0) {
|
||||||
|
const salesUnitData = data.sales_units.find(su => su.id == salesUnitId);
|
||||||
|
if (salesUnitData) {
|
||||||
|
const price = parseFloat(salesUnitData.actual_price || salesUnitData.price || 0);
|
||||||
|
if (price > 0) {
|
||||||
|
priceCache[cacheKey] = price;
|
||||||
|
console.log('getSalesUnitPrice: from API', salesUnitId, price);
|
||||||
|
}
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching sales unit price:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn('getSalesUnitPrice: returning 0 for sales unit', salesUnitId);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для обновления списка единиц продажи при выборе товара
|
||||||
|
async function updateSalesUnitsOptions(salesUnitSelect, productValue) {
|
||||||
|
// Очищаем текущие опции
|
||||||
|
salesUnitSelect.innerHTML = '<option value="">---------</option>';
|
||||||
|
salesUnitSelect.disabled = true;
|
||||||
|
|
||||||
|
if (!productValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Извлекаем ID товара
|
||||||
|
let productId;
|
||||||
|
if (productValue.includes('_')) {
|
||||||
|
const parts = productValue.split('_');
|
||||||
|
productId = parseInt(parts[1]);
|
||||||
|
} else {
|
||||||
|
productId = parseInt(productValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(productId) || productId <= 0) {
|
||||||
|
console.warn('updateSalesUnitsOptions: invalid product id', productValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Загружаем единицы продажи для выбранного товара
|
||||||
|
const response = await fetch(
|
||||||
|
`{% url "products:api-product-sales-units" product_id=0 %}`.replace('/0/', `/${productId}/`),
|
||||||
|
{ method: 'GET', headers: { 'Accept': 'application/json' } }
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.sales_units && data.sales_units.length > 0) {
|
||||||
|
data.sales_units.forEach(su => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = su.id;
|
||||||
|
option.textContent = `${su.name} (${su.actual_price || su.price} руб.)`;
|
||||||
|
option.dataset.price = su.actual_price || su.price;
|
||||||
|
option.dataset.actual_price = su.actual_price;
|
||||||
|
salesUnitSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
salesUnitSelect.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching sales units:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Функция для расчета финальной цены
|
// Функция для расчета финальной цены
|
||||||
async function calculateFinalPrice() {
|
async function calculateFinalPrice() {
|
||||||
// Получаем базовую цену (сумма всех компонентов)
|
// Получаем базовую цену (сумма всех компонентов)
|
||||||
@@ -612,20 +785,28 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
||||||
const quantity = parseFloat(form.querySelector('[name$="-quantity"]')?.value || '1');
|
const quantity = parseFloat(form.querySelector('[name$="-quantity"]')?.value || '1');
|
||||||
const deleteCheckbox = form.querySelector('[name$="-DELETE"]');
|
const deleteCheckbox = form.querySelector('[name$="-DELETE"]');
|
||||||
|
const salesUnitSelect = form.querySelector('[name$="-sales_unit"]');
|
||||||
|
|
||||||
if (deleteCheckbox && deleteCheckbox.checked) continue;
|
if (deleteCheckbox && deleteCheckbox.checked) continue;
|
||||||
|
|
||||||
// Пропускаем если количество не валидно
|
// Пропускаем если количество не валидно
|
||||||
const validQuantity = quantity > 0 ? quantity : 1;
|
const validQuantity = quantity > 0 ? quantity : 1;
|
||||||
|
|
||||||
// Проверяем товар
|
// Проверяем единицу продажи (имеет наивысший приоритет)
|
||||||
if (productSelect && productSelect.value) {
|
if (salesUnitSelect && salesUnitSelect.value) {
|
||||||
|
const salesUnitPrice = await getSalesUnitPrice(salesUnitSelect);
|
||||||
|
if (salesUnitPrice > 0) {
|
||||||
|
newBasePrice += (salesUnitPrice * validQuantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Проверяем товар (если нет единицы продажи)
|
||||||
|
else if (productSelect && productSelect.value) {
|
||||||
const productPrice = await getProductPrice(productSelect);
|
const productPrice = await getProductPrice(productSelect);
|
||||||
if (productPrice > 0) {
|
if (productPrice > 0) {
|
||||||
newBasePrice += (productPrice * validQuantity);
|
newBasePrice += (productPrice * validQuantity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Проверяем группу вариантов
|
// Проверяем группу вариантов (если нет ни единицы продажи, ни товара)
|
||||||
else if (variantGroupSelect && variantGroupSelect.value) {
|
else if (variantGroupSelect && variantGroupSelect.value) {
|
||||||
const variantPrice = await getVariantGroupPrice(variantGroupSelect);
|
const variantPrice = await getVariantGroupPrice(variantGroupSelect);
|
||||||
if (variantPrice > 0) {
|
if (variantPrice > 0) {
|
||||||
@@ -795,8 +976,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
const selectedProducts = {{ selected_products|default:"{}"|safe }};
|
const selectedProducts = {{ selected_products|default:"{}"|safe }};
|
||||||
const selectedVariants = {{ selected_variants|default:"{}"|safe }};
|
const selectedVariants = {{ selected_variants|default:"{}"|safe }};
|
||||||
|
const selectedSalesUnits = {{ selected_sales_units|default:"{}"|safe }};
|
||||||
|
|
||||||
$('[name$="-product"]').each(function() {
|
$('[name$="-product"]').each(function () {
|
||||||
const fieldName = $(this).attr('name');
|
const fieldName = $(this).attr('name');
|
||||||
const preloadedData = selectedProducts[fieldName] || null;
|
const preloadedData = selectedProducts[fieldName] || null;
|
||||||
initSelect2(this, 'product', preloadedData);
|
initSelect2(this, 'product', preloadedData);
|
||||||
@@ -804,46 +986,84 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
$(this).on('select2:select select2:unselect', calculateFinalPrice);
|
$(this).on('select2:select select2:unselect', calculateFinalPrice);
|
||||||
});
|
});
|
||||||
|
|
||||||
$('[name$="-variant_group"]').each(function() {
|
$('[name$="-variant_group"]').each(function () {
|
||||||
const fieldName = $(this).attr('name');
|
const fieldName = $(this).attr('name');
|
||||||
const preloadedData = selectedVariants[fieldName] || null;
|
const preloadedData = selectedVariants[fieldName] || null;
|
||||||
initSelect2(this, 'variant', preloadedData);
|
initSelect2(this, 'variant', preloadedData);
|
||||||
$(this).on('select2:select select2:unselect', calculateFinalPrice);
|
$(this).on('select2:select select2:unselect', calculateFinalPrice);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('[name$="-sales_unit"]').each(function () {
|
||||||
|
const fieldName = $(this).attr('name');
|
||||||
|
const preloadedData = selectedSalesUnits[fieldName] || null;
|
||||||
|
initSelect2(this, 'sales_unit', preloadedData);
|
||||||
|
$(this).on('select2:select select2:unselect', calculateFinalPrice);
|
||||||
|
});
|
||||||
|
|
||||||
// ========== УПРАВЛЕНИЕ КОМПОНЕНТАМИ ==========
|
// ========== УПРАВЛЕНИЕ КОМПОНЕНТАМИ ==========
|
||||||
function updateFieldStatus(form) {
|
function updateFieldStatus(form) {
|
||||||
const productSelect = form.querySelector('[name$="-product"]');
|
const productSelect = form.querySelector('[name$="-product"]');
|
||||||
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
||||||
|
const salesUnitSelect = form.querySelector('[name$="-sales_unit"]');
|
||||||
|
|
||||||
if (!productSelect || !variantGroupSelect) return;
|
if (!productSelect || !variantGroupSelect || !salesUnitSelect) return;
|
||||||
|
|
||||||
const hasProduct = productSelect.value;
|
const hasProduct = productSelect.value;
|
||||||
const hasVariant = variantGroupSelect.value;
|
const hasVariant = variantGroupSelect.value;
|
||||||
|
const hasSalesUnit = salesUnitSelect.value;
|
||||||
|
|
||||||
variantGroupSelect.disabled = !!hasProduct;
|
// Если выбрана группа вариантов, блокируем товар и единицу продажи
|
||||||
productSelect.disabled = !!hasVariant;
|
if (hasVariant) {
|
||||||
|
productSelect.disabled = true;
|
||||||
|
salesUnitSelect.disabled = true;
|
||||||
|
}
|
||||||
|
// Если выбран товар, разблокируем единицу продажи и блокируем группу вариантов
|
||||||
|
else if (hasProduct) {
|
||||||
|
salesUnitSelect.disabled = false;
|
||||||
|
variantGroupSelect.disabled = true;
|
||||||
|
}
|
||||||
|
// Если выбрана только единица продажи, но не товар - блокируем все остальные
|
||||||
|
else if (hasSalesUnit) {
|
||||||
|
productSelect.disabled = true;
|
||||||
|
variantGroupSelect.disabled = true;
|
||||||
|
}
|
||||||
|
// Если ничего не выбрано - разблокируем товар и группу вариантов, блокируем единицу продажи
|
||||||
|
else {
|
||||||
|
productSelect.disabled = false;
|
||||||
|
variantGroupSelect.disabled = false;
|
||||||
|
salesUnitSelect.disabled = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeForm(form) {
|
function initializeForm(form) {
|
||||||
updateFieldStatus(form);
|
updateFieldStatus(form);
|
||||||
const productSelect = form.querySelector('[name$="-product"]');
|
const productSelect = form.querySelector('[name$="-product"]');
|
||||||
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
||||||
|
const salesUnitSelect = form.querySelector('[name$="-sales_unit"]');
|
||||||
|
|
||||||
[productSelect, variantGroupSelect].forEach(field => {
|
[productSelect, variantGroupSelect, salesUnitSelect].forEach(field => {
|
||||||
if (field) {
|
if (field) {
|
||||||
field.addEventListener('change', () => {
|
field.addEventListener('change', () => {
|
||||||
updateFieldStatus(form);
|
updateFieldStatus(form);
|
||||||
|
// Обновляем список единиц продажи при изменении товара
|
||||||
|
if (field === productSelect) {
|
||||||
|
updateSalesUnitsOptions(salesUnitSelect, productSelect.value);
|
||||||
|
}
|
||||||
calculateFinalPrice();
|
calculateFinalPrice();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Инициализируем список единиц продажи, если товар уже выбран
|
||||||
|
if (productSelect && productSelect.value) {
|
||||||
|
updateSalesUnitsOptions(salesUnitSelect, productSelect.value);
|
||||||
|
}
|
||||||
|
|
||||||
const quantityInput = form.querySelector('[name$="-quantity"]');
|
const quantityInput = form.querySelector('[name$="-quantity"]');
|
||||||
if (quantityInput) {
|
if (quantityInput) {
|
||||||
quantityInput.addEventListener('change', calculateFinalPrice);
|
quantityInput.addEventListener('change', calculateFinalPrice);
|
||||||
// Выделяем весь текст при фокусе на поле количества
|
// Выделяем весь текст при фокусе на поле количества
|
||||||
quantityInput.addEventListener('focus', function() {
|
quantityInput.addEventListener('focus', function () {
|
||||||
this.select();
|
this.select();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -862,18 +1082,28 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
<div class="card mb-2 kititem-form border new-item">
|
<div class="card mb-2 kititem-form border new-item">
|
||||||
<div class="card-body p-2">
|
<div class="card-body p-2">
|
||||||
<div class="row g-2 align-items-end">
|
<div class="row g-2 align-items-end">
|
||||||
<div class="col-md-4">
|
<div class="col-md-3">
|
||||||
<label class="form-label small text-muted mb-1">Товар</label>
|
<label class="form-label small text-muted mb-1">Товар</label>
|
||||||
<select class="form-control form-control-sm" name="kititem-${newFormId}-product">
|
<select class="form-control form-control-sm" name="kititem-${newFormId}-product">
|
||||||
<option value="">---------</option>
|
<option value="">---------</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-1 d-flex justify-content-center">
|
<div class="col-md-2">
|
||||||
|
<label class="form-label small text-muted mb-1">Единица продажи</label>
|
||||||
|
<select class="form-control form-control-sm" name="kititem-${newFormId}-sales_unit">
|
||||||
|
<option value="">---------</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1 d-flex justify-content-center align-items-center">
|
||||||
<div class="kit-item-separator">
|
<div class="kit-item-separator">
|
||||||
<span class="separator-text">ИЛИ</span>
|
<span class="separator-text">ИЛИ</span>
|
||||||
|
<i class="bi bi-info-circle separator-help"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-placement="top"
|
||||||
|
title="Вы можете выбрать что-то одно: либо товар, либо группу вариантов"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-3">
|
||||||
<label class="form-label small text-muted mb-1">Группа вариантов</label>
|
<label class="form-label small text-muted mb-1">Группа вариантов</label>
|
||||||
<select class="form-control form-control-sm" name="kititem-${newFormId}-variant_group">
|
<select class="form-control form-control-sm" name="kititem-${newFormId}-variant_group">
|
||||||
<option value="">---------</option>
|
<option value="">---------</option>
|
||||||
@@ -904,13 +1134,36 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
const productSelect = newForm.querySelector('[name$="-product"]');
|
const productSelect = newForm.querySelector('[name$="-product"]');
|
||||||
const variantSelect = newForm.querySelector('[name$="-variant_group"]');
|
const variantSelect = newForm.querySelector('[name$="-variant_group"]');
|
||||||
|
const salesUnitSelect = newForm.querySelector('[name$="-sales_unit"]');
|
||||||
|
|
||||||
initSelect2(productSelect, 'product');
|
initSelect2(productSelect, 'product');
|
||||||
initSelect2(variantSelect, 'variant');
|
initSelect2(variantSelect, 'variant');
|
||||||
|
initSelect2(salesUnitSelect, 'sales_unit');
|
||||||
|
|
||||||
// Добавляем обработчики для новой формы
|
// Добавляем обработчики для новой формы
|
||||||
$(productSelect).on('select2:select select2:unselect', calculateFinalPrice);
|
$(productSelect).on('select2:select', async function () {
|
||||||
|
const form = $(this).closest('.kititem-form');
|
||||||
|
if (this.value) {
|
||||||
|
form.attr('data-product-id', this.value);
|
||||||
|
// Обновляем список единиц продажи
|
||||||
|
if (salesUnitSelect) {
|
||||||
|
await updateSalesUnitsOptions(salesUnitSelect, this.value);
|
||||||
|
}
|
||||||
|
// Загружаем цену и пересчитываем
|
||||||
|
await getProductPrice(this);
|
||||||
|
calculateFinalPrice();
|
||||||
|
}
|
||||||
|
}).on('select2:unselect', function () {
|
||||||
|
// Очищаем список единиц продажи
|
||||||
|
if (salesUnitSelect) {
|
||||||
|
salesUnitSelect.innerHTML = '<option value="">---------</option>';
|
||||||
|
salesUnitSelect.disabled = true;
|
||||||
|
}
|
||||||
|
calculateFinalPrice();
|
||||||
|
});
|
||||||
|
|
||||||
$(variantSelect).on('select2:select select2:unselect', calculateFinalPrice);
|
$(variantSelect).on('select2:select select2:unselect', calculateFinalPrice);
|
||||||
|
$(salesUnitSelect).on('select2:select select2:unselect', calculateFinalPrice);
|
||||||
|
|
||||||
initializeForm(newForm);
|
initializeForm(newForm);
|
||||||
|
|
||||||
@@ -932,7 +1185,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
let selectedFiles = [];
|
let selectedFiles = [];
|
||||||
|
|
||||||
if (photoInput) {
|
if (photoInput) {
|
||||||
photoInput.addEventListener('change', function(e) {
|
photoInput.addEventListener('change', function (e) {
|
||||||
selectedFiles = Array.from(e.target.files);
|
selectedFiles = Array.from(e.target.files);
|
||||||
|
|
||||||
if (selectedFiles.length > 0) {
|
if (selectedFiles.length > 0) {
|
||||||
@@ -941,7 +1194,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
selectedFiles.forEach((file, index) => {
|
selectedFiles.forEach((file, index) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function(event) {
|
reader.onload = function (event) {
|
||||||
const col = document.createElement('div');
|
const col = document.createElement('div');
|
||||||
col.className = 'col-4 col-md-3 col-lg-2';
|
col.className = 'col-4 col-md-3 col-lg-2';
|
||||||
col.innerHTML = `
|
col.innerHTML = `
|
||||||
@@ -963,7 +1216,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
window.removePhoto = function(index) {
|
window.removePhoto = function (index) {
|
||||||
selectedFiles.splice(index, 1);
|
selectedFiles.splice(index, 1);
|
||||||
const dataTransfer = new DataTransfer();
|
const dataTransfer = new DataTransfer();
|
||||||
selectedFiles.forEach(file => dataTransfer.items.add(file));
|
selectedFiles.forEach(file => dataTransfer.items.add(file));
|
||||||
@@ -1025,16 +1278,27 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// ========== ВАЛИДАЦИЯ ПЕРЕД ОТПРАВКОЙ ==========
|
// ========== ВАЛИДАЦИЯ ПЕРЕД ОТПРАВКОЙ ==========
|
||||||
const kitForm = document.querySelector('form[method="post"]');
|
const kitForm = document.querySelector('form[method="post"]');
|
||||||
if (kitForm) {
|
if (kitForm) {
|
||||||
kitForm.addEventListener('submit', function(e) {
|
kitForm.addEventListener('submit', function (e) {
|
||||||
const formsContainer = document.getElementById('kititem-forms');
|
const formsContainer = document.getElementById('kititem-forms');
|
||||||
if (formsContainer) {
|
if (formsContainer) {
|
||||||
const allForms = formsContainer.querySelectorAll('.kititem-form');
|
const allForms = formsContainer.querySelectorAll('.kititem-form');
|
||||||
allForms.forEach(form => {
|
allForms.forEach(form => {
|
||||||
const productSelect = form.querySelector('[name$="-product"]');
|
const productSelect = form.querySelector('[name$="-product"]');
|
||||||
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
||||||
|
const salesUnitSelect = form.querySelector('[name$="-sales_unit"]');
|
||||||
const deleteCheckbox = form.querySelector('[name$="-DELETE"]');
|
const deleteCheckbox = form.querySelector('[name$="-DELETE"]');
|
||||||
|
|
||||||
if (!productSelect.value && !variantGroupSelect.value && deleteCheckbox) {
|
// Проверяем, что выбран хотя бы один компонент (товар, группа вариантов или единица продажи)
|
||||||
|
// Если выбрана единица продажи, товар должен быть выбран
|
||||||
|
if (salesUnitSelect && salesUnitSelect.value && (!productSelect || !productSelect.value)) {
|
||||||
|
// Если выбрана единица продажи, но не выбран товар - это ошибка
|
||||||
|
alert('Если выбрана единица продажи, должен быть выбран соответствующий товар.');
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если ничего не выбрано, отмечаем для удаления
|
||||||
|
if (!productSelect.value && !variantGroupSelect.value && !salesUnitSelect.value && deleteCheckbox) {
|
||||||
deleteCheckbox.checked = true;
|
deleteCheckbox.checked = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -208,6 +208,15 @@ class ProductDetailView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DetailVie
|
|||||||
# Единицы продажи (активные, отсортированные)
|
# Единицы продажи (активные, отсортированные)
|
||||||
context['sales_units'] = self.object.sales_units.filter(is_active=True).order_by('position', 'name')
|
context['sales_units'] = self.object.sales_units.filter(is_active=True).order_by('position', 'name')
|
||||||
|
|
||||||
|
# Комплекты, в которых этот товар используется как единица продажи
|
||||||
|
context['kit_items_using_sales_units'] = self.object.kit_items_using_as_sales_unit.select_related('kit', 'sales_unit').prefetch_related('kit__photos')
|
||||||
|
|
||||||
|
# Комплекты, в которых этот товар используется напрямую
|
||||||
|
context['kit_items_using_products'] = self.object.kit_items_direct.select_related('kit').prefetch_related('kit__photos')
|
||||||
|
|
||||||
|
# Комплекты, в которых этот товар используется как часть группы вариантов
|
||||||
|
context['variant_group_kit_items'] = self.object.variant_group_items.select_related('variant_group').prefetch_related('variant_group__kit_items__kit__photos')
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,12 @@ class ProductKitCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Create
|
|||||||
# Извлекаем числовой ID из "product_123"
|
# Извлекаем числовой ID из "product_123"
|
||||||
numeric_id = value.split('_')[1]
|
numeric_id = value.split('_')[1]
|
||||||
post_data[key] = numeric_id
|
post_data[key] = numeric_id
|
||||||
|
elif key.endswith('-sales_unit') and post_data[key]:
|
||||||
|
value = post_data[key]
|
||||||
|
if '_' in value:
|
||||||
|
# Извлекаем числовой ID из "sales_unit_123"
|
||||||
|
numeric_id = value.split('_')[1]
|
||||||
|
post_data[key] = numeric_id
|
||||||
|
|
||||||
# Заменяем request.POST на очищенные данные
|
# Заменяем request.POST на очищенные данные
|
||||||
request.POST = post_data
|
request.POST = post_data
|
||||||
@@ -126,9 +132,10 @@ class ProductKitCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Create
|
|||||||
context['kititem_formset'] = KitItemFormSetCreate(self.request.POST, prefix='kititem')
|
context['kititem_formset'] = KitItemFormSetCreate(self.request.POST, prefix='kititem')
|
||||||
|
|
||||||
# При ошибке валидации: извлекаем выбранные товары для предзагрузки в Select2
|
# При ошибке валидации: извлекаем выбранные товары для предзагрузки в Select2
|
||||||
from ..models import Product, ProductVariantGroup
|
from ..models import Product, ProductVariantGroup, ProductSalesUnit
|
||||||
selected_products = {}
|
selected_products = {}
|
||||||
selected_variants = {}
|
selected_variants = {}
|
||||||
|
selected_sales_units = {}
|
||||||
|
|
||||||
for key, value in self.request.POST.items():
|
for key, value in self.request.POST.items():
|
||||||
if '-product' in key and value:
|
if '-product' in key and value:
|
||||||
@@ -168,8 +175,25 @@ class ProductKitCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Create
|
|||||||
except ProductVariantGroup.DoesNotExist:
|
except ProductVariantGroup.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if '-sales_unit' in key and value:
|
||||||
|
try:
|
||||||
|
sales_unit = ProductSalesUnit.objects.select_related('product').get(id=value)
|
||||||
|
|
||||||
|
text = f"{sales_unit.name} ({sales_unit.product.name})"
|
||||||
|
# Получаем actual_price: приоритет sale_price > price
|
||||||
|
actual_price = sales_unit.sale_price if sales_unit.sale_price else sales_unit.price
|
||||||
|
selected_sales_units[key] = {
|
||||||
|
'id': sales_unit.id,
|
||||||
|
'text': text,
|
||||||
|
'price': str(sales_unit.price) if sales_unit.price else None,
|
||||||
|
'actual_price': str(actual_price) if actual_price else '0'
|
||||||
|
}
|
||||||
|
except ProductSalesUnit.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
context['selected_products'] = selected_products
|
context['selected_products'] = selected_products
|
||||||
context['selected_variants'] = selected_variants
|
context['selected_variants'] = selected_variants
|
||||||
|
context['selected_sales_units'] = selected_sales_units
|
||||||
else:
|
else:
|
||||||
context['kititem_formset'] = KitItemFormSetCreate(prefix='kititem')
|
context['kititem_formset'] = KitItemFormSetCreate(prefix='kititem')
|
||||||
|
|
||||||
@@ -271,6 +295,12 @@ class ProductKitUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Update
|
|||||||
# Извлекаем числовой ID из "product_123"
|
# Извлекаем числовой ID из "product_123"
|
||||||
numeric_id = value.split('_')[1]
|
numeric_id = value.split('_')[1]
|
||||||
post_data[key] = numeric_id
|
post_data[key] = numeric_id
|
||||||
|
elif key.endswith('-sales_unit') and post_data[key]:
|
||||||
|
value = post_data[key]
|
||||||
|
if '_' in value:
|
||||||
|
# Извлекаем числовой ID из "sales_unit_123"
|
||||||
|
numeric_id = value.split('_')[1]
|
||||||
|
post_data[key] = numeric_id
|
||||||
|
|
||||||
# Заменяем request.POST на очищенные данные
|
# Заменяем request.POST на очищенные данные
|
||||||
request.POST = post_data
|
request.POST = post_data
|
||||||
@@ -284,8 +314,10 @@ class ProductKitUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Update
|
|||||||
context['kititem_formset'] = KitItemFormSetUpdate(self.request.POST, instance=self.object, prefix='kititem')
|
context['kititem_formset'] = KitItemFormSetUpdate(self.request.POST, instance=self.object, prefix='kititem')
|
||||||
|
|
||||||
# При ошибке валидации - подготавливаем данные для Select2
|
# При ошибке валидации - подготавливаем данные для Select2
|
||||||
|
from ..models import Product, ProductVariantGroup, ProductSalesUnit
|
||||||
selected_products = {}
|
selected_products = {}
|
||||||
selected_variants = {}
|
selected_variants = {}
|
||||||
|
selected_sales_units = {}
|
||||||
|
|
||||||
for key, value in self.request.POST.items():
|
for key, value in self.request.POST.items():
|
||||||
if '-product' in key and value:
|
if '-product' in key and value:
|
||||||
@@ -328,14 +360,35 @@ class ProductKitUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Update
|
|||||||
except ProductVariantGroup.DoesNotExist:
|
except ProductVariantGroup.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if '-sales_unit' in key and value:
|
||||||
|
try:
|
||||||
|
# Очищаем ID от префикса если есть
|
||||||
|
numeric_value = value.split('_')[1] if '_' in value else value
|
||||||
|
sales_unit = ProductSalesUnit.objects.select_related('product').get(id=numeric_value)
|
||||||
|
|
||||||
|
text = f"{sales_unit.name} ({sales_unit.product.name})"
|
||||||
|
# Получаем actual_price: приоритет sale_price > price
|
||||||
|
actual_price = sales_unit.sale_price if sales_unit.sale_price else sales_unit.price
|
||||||
|
selected_sales_units[key] = {
|
||||||
|
'id': sales_unit.id,
|
||||||
|
'text': text,
|
||||||
|
'price': str(sales_unit.price) if sales_unit.price else None,
|
||||||
|
'actual_price': str(actual_price) if actual_price else '0'
|
||||||
|
}
|
||||||
|
except ProductSalesUnit.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
context['selected_products'] = selected_products
|
context['selected_products'] = selected_products
|
||||||
context['selected_variants'] = selected_variants
|
context['selected_variants'] = selected_variants
|
||||||
|
context['selected_sales_units'] = selected_sales_units
|
||||||
else:
|
else:
|
||||||
context['kititem_formset'] = KitItemFormSetUpdate(instance=self.object, prefix='kititem')
|
context['kititem_formset'] = KitItemFormSetUpdate(instance=self.object, prefix='kititem')
|
||||||
|
|
||||||
# Подготавливаем данные для предзагрузки в Select2
|
# Подготавливаем данные для предзагрузки в Select2
|
||||||
|
from ..models import Product, ProductVariantGroup, ProductSalesUnit
|
||||||
selected_products = {}
|
selected_products = {}
|
||||||
selected_variants = {}
|
selected_variants = {}
|
||||||
|
selected_sales_units = {}
|
||||||
|
|
||||||
for item in self.object.kit_items.all():
|
for item in self.object.kit_items.all():
|
||||||
form_prefix = f"kititem-{item.id}"
|
form_prefix = f"kititem-{item.id}"
|
||||||
@@ -354,6 +407,17 @@ class ProductKitUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Update
|
|||||||
'actual_price': str(actual_price) if actual_price else '0'
|
'actual_price': str(actual_price) if actual_price else '0'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if item.sales_unit:
|
||||||
|
sales_unit = item.sales_unit
|
||||||
|
text = f"{sales_unit.name} ({sales_unit.product.name})"
|
||||||
|
actual_price = sales_unit.sale_price if sales_unit.sale_price else sales_unit.price
|
||||||
|
selected_sales_units[f"{form_prefix}-sales_unit"] = {
|
||||||
|
'id': sales_unit.id,
|
||||||
|
'text': text,
|
||||||
|
'price': str(sales_unit.price) if sales_unit.price else None,
|
||||||
|
'actual_price': str(actual_price) if actual_price else '0'
|
||||||
|
}
|
||||||
|
|
||||||
if item.variant_group:
|
if item.variant_group:
|
||||||
variant_group = ProductVariantGroup.objects.prefetch_related(
|
variant_group = ProductVariantGroup.objects.prefetch_related(
|
||||||
'items__product'
|
'items__product'
|
||||||
@@ -373,6 +437,7 @@ class ProductKitUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Update
|
|||||||
|
|
||||||
context['selected_products'] = selected_products
|
context['selected_products'] = selected_products
|
||||||
context['selected_variants'] = selected_variants
|
context['selected_variants'] = selected_variants
|
||||||
|
context['selected_sales_units'] = selected_sales_units
|
||||||
|
|
||||||
context['productkit_photos'] = self.object.photos.all().order_by('order', 'created_at')
|
context['productkit_photos'] = self.object.photos.all().order_by('order', 'created_at')
|
||||||
context['photos_count'] = self.object.photos.count()
|
context['photos_count'] = self.object.photos.count()
|
||||||
|
|||||||
Reference in New Issue
Block a user