diff --git a/myproject/inventory/signals.py b/myproject/inventory/signals.py
index 6225674..0ee4c7a 100644
--- a/myproject/inventory/signals.py
+++ b/myproject/inventory/signals.py
@@ -1801,8 +1801,13 @@ def update_kit_prices_on_product_change(sender, instance, created, **kwargs):
if created:
return
- # Находим все KitItem с этим товаром
- kit_items = KitItem.objects.filter(product=instance)
+ # Находим все KitItem с этим товаром, исключая временные (витринные) комплекты
+ # Витринные комплекты имеют зафиксированную цену и не должны обновляться автоматически
+ kit_items = KitItem.objects.filter(
+ product=instance
+ ).select_related('kit').exclude(
+ kit__is_temporary=True
+ )
if not kit_items.exists():
return # Товар не используется в комплектах
diff --git a/myproject/pos/static/pos/js/terminal.js b/myproject/pos/static/pos/js/terminal.js
index b90bfb1..d7963fa 100644
--- a/myproject/pos/static/pos/js/terminal.js
+++ b/myproject/pos/static/pos/js/terminal.js
@@ -859,6 +859,28 @@ function renderProducts() {
openEditKitModal(item.id);
};
card.appendChild(editBtn);
+
+ // Индикатор неактуальной цены (красный кружок)
+ if (item.price_outdated) {
+ const outdatedBadge = document.createElement('div');
+ outdatedBadge.className = 'badge bg-danger';
+ outdatedBadge.style.position = 'absolute';
+ outdatedBadge.style.top = '5px';
+ outdatedBadge.style.right = '45px';
+ outdatedBadge.style.zIndex = '10';
+ outdatedBadge.style.width = '18px';
+ outdatedBadge.style.height = '18px';
+ outdatedBadge.style.padding = '0';
+ outdatedBadge.style.borderRadius = '50%';
+ outdatedBadge.style.display = 'flex';
+ outdatedBadge.style.alignItems = 'center';
+ outdatedBadge.style.justifyContent = 'center';
+ outdatedBadge.style.fontSize = '10px';
+ outdatedBadge.style.minWidth = '18px';
+ outdatedBadge.title = 'Цена неактуальна';
+ outdatedBadge.innerHTML = '!';
+ card.appendChild(outdatedBadge);
+ }
}
}
@@ -1805,6 +1827,7 @@ async function openEditKitModal(kitId) {
id: item.product_id,
name: item.name,
price: Number(item.price),
+ actual_catalog_price: item.actual_catalog_price ? Number(item.actual_catalog_price) : Number(item.price),
qty: Number(item.qty),
type: 'product'
});
@@ -1889,6 +1912,9 @@ async function openEditKitModal(kitId) {
// Открываем модальное окно
const modal = new bootstrap.Modal(document.getElementById('createTempKitModal'));
modal.show();
+
+ // Проверяем актуальность цен (сразу после открытия)
+ checkPricesActual();
} catch (error) {
console.error('Error loading kit for edit:', error);
@@ -1896,6 +1922,83 @@ async function openEditKitModal(kitId) {
}
}
+// Проверка актуальности цен в витринном комплекте
+function checkPricesActual() {
+ // Удаляем старый warning если есть
+ const existingWarning = document.getElementById('priceOutdatedWarning');
+ if (existingWarning) existingWarning.remove();
+
+ // Проверяем цены используя actual_catalog_price из tempCart (уже загружен с бэкенда)
+ const outdatedItems = [];
+ let oldTotalPrice = 0;
+ let newTotalPrice = 0;
+
+ tempCart.forEach((item, cartKey) => {
+ if (item.type === 'product' && item.actual_catalog_price !== undefined) {
+ const savedPrice = parseFloat(item.price);
+ const actualPrice = parseFloat(item.actual_catalog_price);
+ const qty = parseFloat(item.qty) || 1;
+
+ if (Math.abs(savedPrice - actualPrice) > 0.01) {
+ oldTotalPrice += savedPrice * qty;
+ newTotalPrice += actualPrice * qty;
+ outdatedItems.push({
+ name: item.name,
+ old: savedPrice,
+ new: actualPrice,
+ qty: qty
+ });
+ }
+ }
+ });
+
+ if (outdatedItems.length > 0) {
+ showPriceOutdatedWarning(oldTotalPrice, newTotalPrice);
+ }
+}
+
+// Показать warning о неактуальных ценах
+function showPriceOutdatedWarning(oldTotalPrice, newTotalPrice) {
+ const modalBody = document.querySelector('#createTempKitModal .modal-body');
+
+ const warning = document.createElement('div');
+ warning.id = 'priceOutdatedWarning';
+ warning.className = 'alert alert-warning alert-dismissible fade show d-flex align-items-start';
+ warning.innerHTML = `
+
+
+ Цена неактуальна!
+ При сохранении комплекта было: ${formatMoney(oldTotalPrice)} руб.
+ Актуальная цена сейчас: ${formatMoney(newTotalPrice)} руб.
+
+
+
+ `;
+
+ modalBody.insertBefore(warning, modalBody.firstChild);
+}
+
+// Актуализировать цены в комплекте
+function actualizeKitPrices() {
+ tempCart.forEach((item) => {
+ if (item.type === 'product' && item.actual_catalog_price !== undefined) {
+ item.price = item.actual_catalog_price;
+ // Удаляем actual_catalog_price чтобы не показывался warning снова
+ delete item.actual_catalog_price;
+ }
+ });
+
+ // Перерисовываем товары и пересчитываем цену
+ renderTempKitItems();
+ updatePriceCalculations();
+
+ // Убираем warning
+ const warning = document.getElementById('priceOutdatedWarning');
+ if (warning) warning.remove();
+}
+
// Обновление списка витринных комплектов
async function loadShowcaseKits() {
try {
diff --git a/myproject/pos/views.py b/myproject/pos/views.py
index 31a2bba..e8b2535 100644
--- a/myproject/pos/views.py
+++ b/myproject/pos/views.py
@@ -81,6 +81,7 @@ def get_showcase_kits_for_pos():
'product_kit__sku',
'product_kit__price',
'product_kit__sale_price',
+ 'product_kit__base_price',
'showcase_id',
'showcase__name'
).annotate(
@@ -109,6 +110,19 @@ def get_showcase_kits_for_pos():
thumbnail_url = None
kit_photos[photo.kit_id] = thumbnail_url
+ # Загружаем состав комплектов для проверки актуальности цен
+ kit_items_data = {}
+ for ki in KitItem.objects.filter(kit_id__in=kit_ids).select_related('product'):
+ if ki.kit_id not in kit_items_data:
+ kit_items_data[ki.kit_id] = []
+ kit_items_data[ki.kit_id].append(ki)
+
+ # Считаем актуальные цены для каждого комплекта
+ kit_actual_prices = {}
+ for kit_id, items in kit_items_data.items():
+ actual_price = sum((ki.product.actual_price or 0) * (ki.quantity or 0) for ki in items)
+ kit_actual_prices[kit_id] = actual_price
+
# Формируем результат
showcase_kits = []
for item in all_items:
@@ -125,6 +139,11 @@ def get_showcase_kits_for_pos():
# Определяем актуальную цену
price = item['product_kit__sale_price'] or item['product_kit__price']
+ # Проверяем актуальность цены (сравниваем сохранённую цену с актуальной ценой товаров)
+ actual_price = kit_actual_prices.get(kit_id, Decimal('0'))
+ base_price = item['product_kit__base_price']
+ price_outdated = base_price and abs(float(base_price) - float(actual_price)) > 0.01
+
showcase_kits.append({
'id': kit_id,
'name': item['product_kit__name'],
@@ -139,7 +158,9 @@ def get_showcase_kits_for_pos():
# Количества
'available_count': item['available_count'], # Сколько можно добавить
'total_count': item['total_count'], # Всего на витрине (включая в корзине)
- 'showcase_item_ids': available_item_ids # IDs только доступных
+ 'showcase_item_ids': available_item_ids, # IDs только доступных
+ # Флаг неактуальной цены
+ 'price_outdated': price_outdated
})
return showcase_kits
@@ -954,15 +975,24 @@ def get_product_kit_details(request, kit_id):
).select_related('showcase').first()
showcase_id = showcase_reservation.showcase.id if showcase_reservation else None
-
+
# Собираем данные о составе
- items = [{
- 'product_id': ki.product.id,
- 'name': ki.product.name,
- 'qty': str(ki.quantity),
- 'price': str(ki.product.actual_price)
- } for ki in kit.kit_items.all()]
-
+ # Используем unit_price если есть (зафиксированная цена), иначе актуальную цену товара
+ items = []
+ for ki in kit.kit_items.all():
+ # Зафиксированная цена или актуальная цена товара
+ item_price = ki.unit_price if ki.unit_price is not None else ki.product.actual_price
+ item_data = {
+ 'product_id': ki.product.id,
+ 'name': ki.product.name,
+ 'qty': str(ki.quantity),
+ 'price': str(item_price)
+ }
+ # Для временных комплектов добавляем актуальную цену из каталога для сравнения
+ if kit.is_temporary and ki.unit_price is not None:
+ item_data['actual_catalog_price'] = str(ki.product.actual_price)
+ items.append(item_data)
+
# Фото (используем миниатюру для быстрой загрузки)
photo_url = None
if kit.photos.exists():
@@ -1100,10 +1130,12 @@ def create_temp_kit_to_showcase(request):
# 2. Создаём KitItem для каждого товара из корзины
for product_id, quantity in aggregated_items.items():
+ product = products[product_id]
KitItem.objects.create(
kit=kit,
- product=products[product_id],
- quantity=quantity
+ product=product,
+ quantity=quantity,
+ unit_price=product.actual_price # Фиксируем цену для временного комплекта
)
# 3. Пересчитываем цену комплекта
@@ -1318,10 +1350,12 @@ def update_product_kit(request, kit_id):
# Обновляем состав
kit.kit_items.all().delete()
for product_id, quantity in aggregated_items.items():
+ product = products[product_id]
KitItem.objects.create(
kit=kit,
- product=products[product_id],
- quantity=quantity
+ product=product,
+ quantity=quantity,
+ unit_price=product.actual_price # Фиксируем актуальную цену
)
kit.recalculate_base_price()
diff --git a/myproject/products/migrations/0004_add_unit_price_to_kit_item.py b/myproject/products/migrations/0004_add_unit_price_to_kit_item.py
new file mode 100644
index 0000000..c4e9472
--- /dev/null
+++ b/myproject/products/migrations/0004_add_unit_price_to_kit_item.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.10 on 2026-01-19 12:05
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('products', '0003_remove_unit_from_sales_unit'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='kititem',
+ name='unit_price',
+ field=models.DecimalField(blank=True, decimal_places=2, help_text='Если задана, используется эта цена вместо актуальной цены товара. Применяется для временных витринных комплектов.', max_digits=10, null=True, verbose_name='Цена за единицу (зафиксированная)'),
+ ),
+ ]
diff --git a/myproject/products/models/kits.py b/myproject/products/models/kits.py
index 70ce706..ea3a086 100644
--- a/myproject/products/models/kits.py
+++ b/myproject/products/models/kits.py
@@ -162,13 +162,17 @@ class ProductKit(BaseProductEntity):
total = Decimal('0')
for item in self.kit_items.all():
+ qty = item.quantity or Decimal('1')
if item.product:
- actual_price = item.product.actual_price or Decimal('0')
- qty = item.quantity or Decimal('1')
- total += actual_price * qty
+ # Используем зафиксированную цену если есть, иначе актуальную цену товара
+ if item.unit_price is not None:
+ unit_price = item.unit_price
+ else:
+ unit_price = item.product.actual_price or Decimal('0')
+ total += unit_price * qty
elif item.variant_group:
+ # Для variant_group unit_price не используется (только для продуктов)
actual_price = item.variant_group.price or Decimal('0')
- qty = item.quantity or Decimal('1')
total += actual_price * qty
self.base_price = total
@@ -395,6 +399,14 @@ class KitItem(models.Model):
verbose_name="Группа вариантов"
)
quantity = models.DecimalField(max_digits=10, decimal_places=3, null=True, blank=True, verbose_name="Количество")
+ unit_price = models.DecimalField(
+ max_digits=10,
+ decimal_places=2,
+ null=True,
+ blank=True,
+ verbose_name="Цена за единицу (зафиксированная)",
+ help_text="Если задана, используется эта цена вместо актуальной цены товара. Применяется для временных витринных комплектов."
+ )
class Meta:
verbose_name = "Компонент комплекта"
diff --git a/myproject/products/services/kit_service.py b/myproject/products/services/kit_service.py
index bce3a5c..99a9f96 100644
--- a/myproject/products/services/kit_service.py
+++ b/myproject/products/services/kit_service.py
@@ -111,6 +111,10 @@ def make_kit_permanent(kit: ProductKit) -> bool:
kit.is_temporary = False
kit.order = None # Отвязываем от заказа
kit.save()
+
+ # Очищаем зафиксированные цены - теперь будет использоваться актуальная цена товаров
+ kit.kit_items.update(unit_price=None)
+
return True