Compare commits
3 Commits
1b749ebe63
...
2778796118
| Author | SHA1 | Date | |
|---|---|---|---|
| 2778796118 | |||
| 392471ff06 | |||
| b188f5c2df |
@@ -1801,8 +1801,13 @@ def update_kit_prices_on_product_change(sender, instance, created, **kwargs):
|
|||||||
if created:
|
if created:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Находим все KitItem с этим товаром
|
# Находим все KitItem с этим товаром, исключая временные (витринные) комплекты
|
||||||
kit_items = KitItem.objects.filter(product=instance)
|
# Витринные комплекты имеют зафиксированную цену и не должны обновляться автоматически
|
||||||
|
kit_items = KitItem.objects.filter(
|
||||||
|
product=instance
|
||||||
|
).select_related('kit').exclude(
|
||||||
|
kit__is_temporary=True
|
||||||
|
)
|
||||||
|
|
||||||
if not kit_items.exists():
|
if not kit_items.exists():
|
||||||
return # Товар не используется в комплектах
|
return # Товар не используется в комплектах
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ body {
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 3 колонки для товаров и категорий на экранах от 400px */
|
||||||
|
@media (min-width: 400px) {
|
||||||
|
.col-custom-3 {
|
||||||
|
flex: 0 0 33.333%;
|
||||||
|
max-width: 33.333%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 5 колонок для товаров и категорий на экранах от 1100px */
|
/* 5 колонок для товаров и категорий на экранах от 1100px */
|
||||||
@media (min-width: 1100px) {
|
@media (min-width: 1100px) {
|
||||||
.col-lg-custom-5 {
|
.col-lg-custom-5 {
|
||||||
|
|||||||
@@ -701,7 +701,7 @@ function renderCategories() {
|
|||||||
|
|
||||||
// Кнопка "Витрина" - первая в ряду
|
// Кнопка "Витрина" - первая в ряду
|
||||||
const showcaseCol = document.createElement('div');
|
const showcaseCol = document.createElement('div');
|
||||||
showcaseCol.className = 'col-6 col-sm-4 col-md-3 col-lg-2';
|
showcaseCol.className = 'col-6 col-custom-3 col-md-3 col-lg-2';
|
||||||
const showcaseCard = document.createElement('div');
|
const showcaseCard = document.createElement('div');
|
||||||
showcaseCard.className = 'card category-card showcase-card' + (isShowcaseView ? ' active' : '');
|
showcaseCard.className = 'card category-card showcase-card' + (isShowcaseView ? ' active' : '');
|
||||||
showcaseCard.style.backgroundColor = '#fff3cd';
|
showcaseCard.style.backgroundColor = '#fff3cd';
|
||||||
@@ -725,7 +725,7 @@ function renderCategories() {
|
|||||||
|
|
||||||
// Кнопка "Все"
|
// Кнопка "Все"
|
||||||
const allCol = document.createElement('div');
|
const allCol = document.createElement('div');
|
||||||
allCol.className = 'col-6 col-sm-4 col-md-3 col-lg-2';
|
allCol.className = 'col-6 col-custom-3 col-md-3 col-lg-2';
|
||||||
const allCard = document.createElement('div');
|
const allCard = document.createElement('div');
|
||||||
allCard.className = 'card category-card' + (currentCategoryId === null && !isShowcaseView ? ' active' : '');
|
allCard.className = 'card category-card' + (currentCategoryId === null && !isShowcaseView ? ' active' : '');
|
||||||
allCard.onclick = async () => {
|
allCard.onclick = async () => {
|
||||||
@@ -749,7 +749,7 @@ function renderCategories() {
|
|||||||
// Категории
|
// Категории
|
||||||
CATEGORIES.forEach(cat => {
|
CATEGORIES.forEach(cat => {
|
||||||
const col = document.createElement('div');
|
const col = document.createElement('div');
|
||||||
col.className = 'col-6 col-sm-4 col-md-3 col-lg-custom-5';
|
col.className = 'col-6 col-custom-3 col-md-3 col-lg-custom-5';
|
||||||
|
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'card category-card' + (currentCategoryId === cat.id && !isShowcaseView ? ' active' : '');
|
card.className = 'card category-card' + (currentCategoryId === cat.id && !isShowcaseView ? ' active' : '');
|
||||||
@@ -804,7 +804,7 @@ function renderProducts() {
|
|||||||
|
|
||||||
filtered.forEach(item => {
|
filtered.forEach(item => {
|
||||||
const col = document.createElement('div');
|
const col = document.createElement('div');
|
||||||
col.className = 'col-6 col-sm-4 col-md-3 col-lg-custom-5';
|
col.className = 'col-6 col-custom-3 col-md-3 col-lg-custom-5';
|
||||||
|
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'card product-card';
|
card.className = 'card product-card';
|
||||||
@@ -859,6 +859,28 @@ function renderProducts() {
|
|||||||
openEditKitModal(item.id);
|
openEditKitModal(item.id);
|
||||||
};
|
};
|
||||||
card.appendChild(editBtn);
|
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,
|
id: item.product_id,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
price: Number(item.price),
|
price: Number(item.price),
|
||||||
|
actual_catalog_price: item.actual_catalog_price ? Number(item.actual_catalog_price) : Number(item.price),
|
||||||
qty: Number(item.qty),
|
qty: Number(item.qty),
|
||||||
type: 'product'
|
type: 'product'
|
||||||
});
|
});
|
||||||
@@ -1889,6 +1912,9 @@ async function openEditKitModal(kitId) {
|
|||||||
// Открываем модальное окно
|
// Открываем модальное окно
|
||||||
const modal = new bootstrap.Modal(document.getElementById('createTempKitModal'));
|
const modal = new bootstrap.Modal(document.getElementById('createTempKitModal'));
|
||||||
modal.show();
|
modal.show();
|
||||||
|
|
||||||
|
// Проверяем актуальность цен (сразу после открытия)
|
||||||
|
checkPricesActual();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading kit for edit:', 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 = `
|
||||||
|
<i class="bi bi-exclamation-triangle-fill flex-shrink-0 me-2 mt-1"></i>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<strong>Цена неактуальна!</strong><br>
|
||||||
|
<small class="text-muted">При сохранении комплекта было: <strong>${formatMoney(oldTotalPrice)} руб.</strong></small><br>
|
||||||
|
<small class="text-muted">Актуальная цена сейчас: <strong>${formatMoney(newTotalPrice)} руб.</strong></small>
|
||||||
|
<button type="button" class="btn btn-sm btn-warning mt-2" onclick="actualizeKitPrices()">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i> Пересчитать по актуальным ценам
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close flex-shrink-0" data-bs-dismiss="alert"></button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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() {
|
async function loadShowcaseKits() {
|
||||||
try {
|
try {
|
||||||
@@ -2205,6 +2308,14 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
|||||||
// Получаем количество букетов для создания
|
// Получаем количество букетов для создания
|
||||||
const showcaseKitQuantity = parseInt(document.getElementById('showcaseKitQuantity').value, 10) || 1;
|
const showcaseKitQuantity = parseInt(document.getElementById('showcaseKitQuantity').value, 10) || 1;
|
||||||
|
|
||||||
|
// Вычисляем итоговую цену комплекта на основе изменённых цен в корзине
|
||||||
|
let calculatedPrice = 0;
|
||||||
|
tempCart.forEach((item) => {
|
||||||
|
if (item.type === 'product') {
|
||||||
|
calculatedPrice += item.qty * item.price;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Формируем FormData для отправки с файлом
|
// Формируем FormData для отправки с файлом
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('kit_name', kitName);
|
formData.append('kit_name', kitName);
|
||||||
@@ -2216,8 +2327,10 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
|||||||
formData.append('items', JSON.stringify(items));
|
formData.append('items', JSON.stringify(items));
|
||||||
formData.append('price_adjustment_type', priceAdjustmentType);
|
formData.append('price_adjustment_type', priceAdjustmentType);
|
||||||
formData.append('price_adjustment_value', priceAdjustmentValue);
|
formData.append('price_adjustment_value', priceAdjustmentValue);
|
||||||
if (useSalePrice && salePrice > 0) {
|
// Если пользователь не задал свою цену, используем вычисленную
|
||||||
formData.append('sale_price', salePrice);
|
const finalSalePrice = useSalePrice ? salePrice : calculatedPrice;
|
||||||
|
if (finalSalePrice > 0) {
|
||||||
|
formData.append('sale_price', finalSalePrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Фото: для редактирования проверяем, удалено ли оно
|
// Фото: для редактирования проверяем, удалено ли оно
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ def get_showcase_kits_for_pos():
|
|||||||
'product_kit__sku',
|
'product_kit__sku',
|
||||||
'product_kit__price',
|
'product_kit__price',
|
||||||
'product_kit__sale_price',
|
'product_kit__sale_price',
|
||||||
|
'product_kit__base_price',
|
||||||
'showcase_id',
|
'showcase_id',
|
||||||
'showcase__name'
|
'showcase__name'
|
||||||
).annotate(
|
).annotate(
|
||||||
@@ -109,6 +110,19 @@ def get_showcase_kits_for_pos():
|
|||||||
thumbnail_url = None
|
thumbnail_url = None
|
||||||
kit_photos[photo.kit_id] = thumbnail_url
|
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 = []
|
showcase_kits = []
|
||||||
for item in all_items:
|
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']
|
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({
|
showcase_kits.append({
|
||||||
'id': kit_id,
|
'id': kit_id,
|
||||||
'name': item['product_kit__name'],
|
'name': item['product_kit__name'],
|
||||||
@@ -139,7 +158,9 @@ def get_showcase_kits_for_pos():
|
|||||||
# Количества
|
# Количества
|
||||||
'available_count': item['available_count'], # Сколько можно добавить
|
'available_count': item['available_count'], # Сколько можно добавить
|
||||||
'total_count': item['total_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
|
return showcase_kits
|
||||||
@@ -954,15 +975,24 @@ def get_product_kit_details(request, kit_id):
|
|||||||
).select_related('showcase').first()
|
).select_related('showcase').first()
|
||||||
|
|
||||||
showcase_id = showcase_reservation.showcase.id if showcase_reservation else None
|
showcase_id = showcase_reservation.showcase.id if showcase_reservation else None
|
||||||
|
|
||||||
# Собираем данные о составе
|
# Собираем данные о составе
|
||||||
items = [{
|
# Используем unit_price если есть (зафиксированная цена), иначе актуальную цену товара
|
||||||
'product_id': ki.product.id,
|
items = []
|
||||||
'name': ki.product.name,
|
for ki in kit.kit_items.all():
|
||||||
'qty': str(ki.quantity),
|
# Зафиксированная цена или актуальная цена товара
|
||||||
'price': str(ki.product.actual_price)
|
item_price = ki.unit_price if ki.unit_price is not None else ki.product.actual_price
|
||||||
} for ki in kit.kit_items.all()]
|
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
|
photo_url = None
|
||||||
if kit.photos.exists():
|
if kit.photos.exists():
|
||||||
@@ -1100,10 +1130,12 @@ def create_temp_kit_to_showcase(request):
|
|||||||
|
|
||||||
# 2. Создаём KitItem для каждого товара из корзины
|
# 2. Создаём KitItem для каждого товара из корзины
|
||||||
for product_id, quantity in aggregated_items.items():
|
for product_id, quantity in aggregated_items.items():
|
||||||
|
product = products[product_id]
|
||||||
KitItem.objects.create(
|
KitItem.objects.create(
|
||||||
kit=kit,
|
kit=kit,
|
||||||
product=products[product_id],
|
product=product,
|
||||||
quantity=quantity
|
quantity=quantity,
|
||||||
|
unit_price=product.actual_price # Фиксируем цену для временного комплекта
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3. Пересчитываем цену комплекта
|
# 3. Пересчитываем цену комплекта
|
||||||
@@ -1318,10 +1350,12 @@ def update_product_kit(request, kit_id):
|
|||||||
# Обновляем состав
|
# Обновляем состав
|
||||||
kit.kit_items.all().delete()
|
kit.kit_items.all().delete()
|
||||||
for product_id, quantity in aggregated_items.items():
|
for product_id, quantity in aggregated_items.items():
|
||||||
|
product = products[product_id]
|
||||||
KitItem.objects.create(
|
KitItem.objects.create(
|
||||||
kit=kit,
|
kit=kit,
|
||||||
product=products[product_id],
|
product=product,
|
||||||
quantity=quantity
|
quantity=quantity,
|
||||||
|
unit_price=product.actual_price # Фиксируем актуальную цену
|
||||||
)
|
)
|
||||||
|
|
||||||
kit.recalculate_base_price()
|
kit.recalculate_base_price()
|
||||||
|
|||||||
@@ -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='Цена за единицу (зафиксированная)'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -162,13 +162,17 @@ 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')
|
||||||
if item.product:
|
if item.product:
|
||||||
actual_price = item.product.actual_price or Decimal('0')
|
# Используем зафиксированную цену если есть, иначе актуальную цену товара
|
||||||
qty = item.quantity or Decimal('1')
|
if item.unit_price is not None:
|
||||||
total += actual_price * qty
|
unit_price = item.unit_price
|
||||||
|
else:
|
||||||
|
unit_price = item.product.actual_price or Decimal('0')
|
||||||
|
total += unit_price * qty
|
||||||
elif item.variant_group:
|
elif item.variant_group:
|
||||||
|
# Для variant_group unit_price не используется (только для продуктов)
|
||||||
actual_price = item.variant_group.price or Decimal('0')
|
actual_price = item.variant_group.price or Decimal('0')
|
||||||
qty = item.quantity or Decimal('1')
|
|
||||||
total += actual_price * qty
|
total += actual_price * qty
|
||||||
|
|
||||||
self.base_price = total
|
self.base_price = total
|
||||||
@@ -395,6 +399,14 @@ class KitItem(models.Model):
|
|||||||
verbose_name="Группа вариантов"
|
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(
|
||||||
|
max_digits=10,
|
||||||
|
decimal_places=2,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Цена за единицу (зафиксированная)",
|
||||||
|
help_text="Если задана, используется эта цена вместо актуальной цены товара. Применяется для временных витринных комплектов."
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Компонент комплекта"
|
verbose_name = "Компонент комплекта"
|
||||||
|
|||||||
@@ -111,6 +111,10 @@ def make_kit_permanent(kit: ProductKit) -> bool:
|
|||||||
kit.is_temporary = False
|
kit.is_temporary = False
|
||||||
kit.order = None # Отвязываем от заказа
|
kit.order = None # Отвязываем от заказа
|
||||||
kit.save()
|
kit.save()
|
||||||
|
|
||||||
|
# Очищаем зафиксированные цены - теперь будет использоваться актуальная цена товаров
|
||||||
|
kit.kit_items.update(unit_price=None)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,14 +20,14 @@
|
|||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
{% comment %}Показываем меню tenant приложений только если мы не на странице setup-password (public схема){% endcomment %}
|
{% comment %}Показываем меню tenant приложений только если мы не на странице setup-password (public схема){% endcomment %}
|
||||||
{% if 'setup-password' not in request.path %}
|
{% if 'setup-password' not in request.path %}
|
||||||
<!-- 📦 Товары -->
|
<!-- Товары -->
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle {% if request.resolver_match.namespace == 'products' %}active{% endif %}" href="#" id="productsDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<a class="nav-link dropdown-toggle {% if request.resolver_match.namespace == 'products' %}active{% endif %}" href="#" id="productsDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
📦 Товары
|
Товары
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu" aria-labelledby="productsDropdown">
|
<ul class="dropdown-menu" aria-labelledby="productsDropdown">
|
||||||
<li><a class="dropdown-item" href="{% url 'products:products-list' %}">Все товары</a></li>
|
<li><a class="dropdown-item" href="{% url 'products:products-list' %}">Все товары</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'products:catalog' %}"><i class="bi bi-grid-3x3-gap"></i> Каталог</a></li>
|
<li><a class="dropdown-item" href="{% url 'products:catalog' %}">Каталог</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'products:configurableproduct-list' %}">Вариативные товары</a></li>
|
<li><a class="dropdown-item" href="{% url 'products:configurableproduct-list' %}">Вариативные товары</a></li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'products:category-list' %}">Категории</a></li>
|
<li><a class="dropdown-item" href="{% url 'products:category-list' %}">Категории</a></li>
|
||||||
@@ -35,15 +35,15 @@
|
|||||||
<li><a class="dropdown-item" href="{% url 'products:variantgroup-list' %}">Варианты (группы)</a></li>
|
<li><a class="dropdown-item" href="{% url 'products:variantgroup-list' %}">Варианты (группы)</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'products:attribute-list' %}">Атрибуты</a></li>
|
<li><a class="dropdown-item" href="{% url 'products:attribute-list' %}">Атрибуты</a></li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'products:unit-list' %}"><i class="bi bi-rulers"></i> Единицы измерения</a></li>
|
<li><a class="dropdown-item" href="{% url 'products:unit-list' %}">Единицы измерения</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'products:sales-unit-list' %}"><i class="bi bi-box-seam"></i> Единицы продажи</a></li>
|
<li><a class="dropdown-item" href="{% url 'products:sales-unit-list' %}">Единицы продажи</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- 📋 Заказы -->
|
<!-- Заказы -->
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle {% if request.resolver_match.namespace == 'orders' %}active{% endif %}" href="{% url 'orders:order-list' %}" id="ordersDropdown">
|
<a class="nav-link dropdown-toggle {% if request.resolver_match.namespace == 'orders' %}active{% endif %}" href="{% url 'orders:order-list' %}" id="ordersDropdown">
|
||||||
📋 Заказы
|
Заказы
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu" aria-labelledby="ordersDropdown">
|
<ul class="dropdown-menu" aria-labelledby="ordersDropdown">
|
||||||
<li><a class="dropdown-item" href="{% url 'orders:order-list' %}">Список заказов</a></li>
|
<li><a class="dropdown-item" href="{% url 'orders:order-list' %}">Список заказов</a></li>
|
||||||
@@ -52,17 +52,17 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- 👥 Клиенты -->
|
<!-- Клиенты -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.resolver_match.namespace == 'customers' %}active{% endif %}" href="{% url 'customers:customer-list' %}">
|
<a class="nav-link {% if request.resolver_match.namespace == 'customers' %}active{% endif %}" href="{% url 'customers:customer-list' %}">
|
||||||
👥 Клиенты
|
Клиенты
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- 📦 Склад -->
|
<!-- Склад -->
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle {% if request.resolver_match.namespace == 'inventory' %}active{% endif %}" href="#" id="inventoryDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<a class="nav-link dropdown-toggle {% if request.resolver_match.namespace == 'inventory' %}active{% endif %}" href="#" id="inventoryDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
🏭 Склад
|
Склад
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu" aria-labelledby="inventoryDropdown">
|
<ul class="dropdown-menu" aria-labelledby="inventoryDropdown">
|
||||||
<li><a class="dropdown-item" href="{% url 'inventory:inventory-home' %}">Управление складом</a></li>
|
<li><a class="dropdown-item" href="{% url 'inventory:inventory-home' %}">Управление складом</a></li>
|
||||||
@@ -70,37 +70,37 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- 💰 Касса -->
|
<!-- Касса -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.resolver_match.namespace == 'pos' %}active{% endif %}" href="{% url 'pos:terminal' %}">
|
<a class="nav-link {% if request.resolver_match.namespace == 'pos' %}active{% endif %}" href="{% url 'pos:terminal' %}">
|
||||||
💰 Касса
|
Касса
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- ⚙️ Настройки (только для owner/superuser) -->
|
<!-- Настройки (только для owner/superuser) -->
|
||||||
{% if request.user.is_owner or request.user.is_superuser %}
|
{% if request.user.is_owner or request.user.is_superuser %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
{% if request.tenant %}
|
{% if request.tenant %}
|
||||||
<a class="nav-link {% if request.resolver_match.namespace == 'system_settings' or 'user_roles' in request.resolver_match.app_names %}active{% endif %}"
|
<a class="nav-link {% if request.resolver_match.namespace == 'system_settings' or 'user_roles' in request.resolver_match.app_names %}active{% endif %}"
|
||||||
href="{% url 'system_settings:settings' %}">
|
href="{% url 'system_settings:settings' %}">
|
||||||
⚙️ Настройки
|
Настройки
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="nav-link" href="/platform/dashboard">
|
<a class="nav-link" href="/platform/dashboard">
|
||||||
⚙️ Настройки
|
Настройки
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- 🔧 Debug (для owner или manager) -->
|
<!-- Debug (для owner или manager) -->
|
||||||
{% if user.is_owner or user.is_manager %}
|
{% if user.is_owner or user.is_manager %}
|
||||||
{% url 'inventory:debug_page' as debug_url %}
|
{% url 'inventory:debug_page' as debug_url %}
|
||||||
{% if debug_url %}
|
{% if debug_url %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ debug_url }}" style="color: #dc3545; font-weight: bold;">
|
<a class="nav-link" href="{{ debug_url }}" style="color: #dc3545; font-weight: bold;">
|
||||||
🔧 Debug
|
Debug
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user