diff --git a/myproject/inventory/services/showcase_manager.py b/myproject/inventory/services/showcase_manager.py
index 7de836d..c2f8e5f 100644
--- a/myproject/inventory/services/showcase_manager.py
+++ b/myproject/inventory/services/showcase_manager.py
@@ -274,12 +274,25 @@ class ShowcaseManager:
# Создаём заказ
order = Order.objects.create(
customer=customer,
- is_delivery=False,
- pickup_warehouse=warehouse,
status=completed_status,
is_paid=True,
modified_by=user
)
+
+ # Создаём доставку (самовывоз)
+ from orders.models import Delivery
+ from django.utils import timezone as tz
+ now_local = tz.localtime(tz.now())
+
+ Delivery.objects.create(
+ order=order,
+ delivery_type=Delivery.DELIVERY_TYPE_PICKUP,
+ pickup_warehouse=warehouse,
+ delivery_date=now_local.date(),
+ time_from=now_local.time(),
+ time_to=now_local.time(),
+ cost=0
+ )
# Определяем цену
price = custom_price if custom_price else product_kit.actual_price
diff --git a/myproject/orders/management/commands/create_demo_orders.py b/myproject/orders/management/commands/create_demo_orders.py
deleted file mode 100644
index 9af66e0..0000000
--- a/myproject/orders/management/commands/create_demo_orders.py
+++ /dev/null
@@ -1,207 +0,0 @@
-"""
-Management команда для создания демо-заказов на разные даты
-ВАЖНО: Создает заказы через Django ORM, что автоматически активирует
-сигналы резервирования товаров!
-"""
-from django.core.management.base import BaseCommand
-from django.utils import timezone
-from django.db import connection
-from datetime import datetime, timedelta
-import random
-from decimal import Decimal
-
-from orders.models import Order, OrderItem, Address, Recipient
-from customers.models import Customer
-from inventory.models import Warehouse
-from products.models import Product
-
-
-class Command(BaseCommand):
- help = 'Создает демо-заказы через ORM (с автоматическим резервированием товаров)'
-
- def add_arguments(self, parser):
- parser.add_argument(
- '--count',
- type=int,
- default=25,
- help='Количество заказов для создания (по умолчанию: 25)'
- )
- parser.add_argument(
- '--schema',
- type=str,
- default='grach',
- help='Схема базы данных (tenant) для создания заказов'
- )
-
- def handle(self, *args, **options):
- count = options['count']
- schema_name = options['schema']
-
- # Устанавливаем схему для работы с tenant
- with connection.cursor() as cursor:
- cursor.execute(f'SET search_path TO {schema_name}')
-
- self.stdout.write(f'[НАЧАЛО] Создание {count} демо-заказов в схеме {schema_name}...')
- self.stdout.write('[INFO] Заказы создаются через ORM - резервы товаров будут созданы автоматически!')
-
- # Проверяем наличие необходимых данных
- customers = list(Customer.objects.all())
- if not customers:
- self.stdout.write(self.style.ERROR('Нет клиентов в базе! Создайте хотя бы одного клиента.'))
- return
-
- products = list(Product.objects.all())
- if not products:
- self.stdout.write(self.style.ERROR('Нет товаров в базе! Создайте хотя бы один товар.'))
- return
-
- addresses = list(Address.objects.all())
- warehouses = list(Warehouse.objects.filter(is_pickup_point=True))
-
- if not addresses and not warehouses:
- self.stdout.write(self.style.ERROR('Нет ни адресов, ни складов для самовывоза! Создайте хотя бы что-то одно.'))
- return
-
- # Статусы и их вероятности
- statuses = [
- ('new', 0.15),
- ('confirmed', 0.25),
- ('in_assembly', 0.20),
- ('in_delivery', 0.15),
- ('delivered', 0.20),
- ('cancelled', 0.05),
- ]
-
- payment_statuses = [
- ('unpaid', 0.30),
- ('partial', 0.20),
- ('paid', 0.50),
- ]
-
- payment_methods = [
- 'cash_to_courier',
- 'card_to_courier',
- 'online',
- 'bank_transfer',
- ]
-
- # Генерируем даты в диапазоне ±15 дней от сегодня
- today = datetime.now().date()
-
- created_count = 0
- for i in range(count):
- try:
- # Случайная дата доставки
- days_offset = random.randint(-15, 15)
- delivery_date = today + timedelta(days=days_offset)
-
- # Выбираем клиента
- customer = random.choice(customers)
-
- # Выбираем тип доставки
- is_delivery = random.choice([True, False]) if addresses and shops else bool(addresses)
-
- # Создаем заказ
- order = Order()
- order.customer = customer
- order.is_delivery = is_delivery
-
- # Устанавливаем адрес или магазин
- if is_delivery and addresses:
- # Для доставки выбираем случайный адрес (адреса теперь привязаны к заказам)
- order.delivery_address = random.choice(addresses)
- order.delivery_cost = Decimal(random.randint(200, 500))
- elif warehouses:
- order.pickup_warehouse = random.choice(warehouses)
- order.delivery_cost = Decimal(0)
-
- # Дата и время
- order.delivery_date = delivery_date
- if random.random() > 0.3: # 70% заказов с указанным временем
- start_hour = random.randint(9, 18)
- order.delivery_time_start = f"{start_hour:02d}:00:00"
- order.delivery_time_end = f"{start_hour + 2:02d}:00:00"
-
- # Статус
- status_choices = [s[0] for s in statuses]
- status_weights = [s[1] for s in statuses]
- order.status = random.choices(status_choices, weights=status_weights)[0]
-
- # Способ оплаты
- order.payment_method = random.choice(payment_methods)
-
- # Дополнительная информация
- if random.random() > 0.7: # 30% - подарок другому человеку
- # Создаем получателя
- recipient_name = f"Получатель {i+1}"
- recipient_phone = f"+7{random.randint(9000000000, 9999999999)}"
- recipient, created = Recipient.objects.get_or_create(
- name=recipient_name,
- phone=recipient_phone
- )
- order.recipient = recipient
-
- if random.random() > 0.8: # 20% анонимных
- order.is_anonymous = True
-
- if random.random() > 0.5: # 50% с комментариями
- comments = [
- "Позвонить за час до доставки",
- "Доставить точно в указанное время",
- "Не звонить в дверь, только по телефону",
- "Упаковать покрасивее",
- "Приложить открытку",
- ]
- order.special_instructions = random.choice(comments)
-
- # Сохраняем заказ (чтобы получить ID)
- order.save()
-
- # Добавляем товары в заказ
- items_count = random.randint(1, 4)
- order_products = random.sample(products, min(items_count, len(products)))
-
- items_total = Decimal(0)
- for product in order_products:
- item = OrderItem()
- item.order = order
- item.product = product
- item.quantity = random.randint(1, 3)
- item.price = product.price
- item.save()
- items_total += item.get_total_price()
-
- # Рассчитываем итоговую сумму
- order.total_amount = items_total + order.delivery_cost
-
- # Скидка (20% заказов)
- if random.random() > 0.8:
- order.discount_amount = Decimal(random.randint(100, 500))
- order.total_amount -= order.discount_amount
-
- # Статус оплаты
- payment_status_choices = [s[0] for s in payment_statuses]
- payment_status_weights = [s[1] for s in payment_statuses]
- order.payment_status = random.choices(payment_status_choices, weights=payment_status_weights)[0]
-
- if order.payment_status == 'paid':
- order.amount_paid = order.total_amount
- order.is_paid = True
- elif order.payment_status == 'partial':
- order.amount_paid = order.total_amount * Decimal(random.uniform(0.2, 0.8))
- order.is_paid = False
- else:
- order.amount_paid = Decimal(0)
- order.is_paid = False
-
- order.save()
-
- created_count += 1
- self.stdout.write(f' [OK] Заказ #{order.order_number} на {delivery_date} (товаров: {len(order_products)})')
-
- except Exception as e:
- self.stdout.write(self.style.ERROR(f'[ОШИБКА] Заказ {i+1}: {str(e)}'))
-
- self.stdout.write(self.style.SUCCESS(f'\n[ЗАВЕРШЕНО] Успешно создано {created_count} заказов!'))
- self.stdout.write(f'Даты доставки: от {today - timedelta(days=15)} до {today + timedelta(days=15)}')
- self.stdout.write(self.style.SUCCESS('\n[ВАЖНО] Резервы товаров созданы автоматически через Django сигналы!'))
diff --git a/myproject/pos/static/pos/css/terminal.css b/myproject/pos/static/pos/css/terminal.css
index e486859..bdfacd8 100644
--- a/myproject/pos/static/pos/css/terminal.css
+++ b/myproject/pos/static/pos/css/terminal.css
@@ -356,3 +356,70 @@ body {
font-size: 0.8rem;
}
}
+
+/* Стили для модального окна выбора единицы продажи */
+.unit-selection-card {
+ border: 2px solid #dee2e6;
+ border-radius: 8px;
+ padding: 12px;
+ cursor: pointer;
+ transition: all 0.2s;
+ background: white;
+}
+
+.unit-selection-card:hover {
+ border-color: #0d6efd;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ transform: translateY(-1px);
+}
+
+.unit-selection-card.selected {
+ border-color: #0d6efd;
+ background: #e7f3ff;
+ box-shadow: 0 2px 12px rgba(13,110,253,0.3);
+}
+
+.unit-selection-card .unit-name {
+ font-weight: 600;
+ font-size: 1rem;
+ color: #212529;
+ margin-bottom: 4px;
+}
+
+.unit-selection-card .unit-price {
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: #0d6efd;
+ margin-bottom: 4px;
+}
+
+.unit-selection-card .unit-availability {
+ font-size: 0.85rem;
+ color: #6c757d;
+}
+
+.unit-selection-card .unit-code {
+ font-size: 0.8rem;
+ color: #adb5bd;
+}
+
+.unit-selection-card .badge {
+ font-size: 0.75rem;
+ padding: 4px 8px;
+}
+
+/* Индикаторы наличия */
+.stock-badge-good {
+ background-color: #d4edda;
+ color: #155724;
+}
+
+.stock-badge-low {
+ background-color: #fff3cd;
+ color: #856404;
+}
+
+.stock-badge-none {
+ background-color: #f8d7da;
+ color: #721c24;
+}
diff --git a/myproject/pos/static/pos/js/terminal.js b/myproject/pos/static/pos/js/terminal.js
index a8aa483..bd902a0 100644
--- a/myproject/pos/static/pos/js/terminal.js
+++ b/myproject/pos/static/pos/js/terminal.js
@@ -34,6 +34,12 @@ let editingKitId = null;
// Временная корзина для модального окна создания/редактирования комплекта
const tempCart = new Map();
+// ===== ПЕРЕМЕННЫЕ ДЛЯ МОДАЛЬНОГО ОКНА ВЫБОРА ЕДИНИЦЫ ПРОДАЖИ =====
+let unitModalProduct = null; // Текущий товар для модального окна
+let unitModalSalesUnits = []; // Список единиц продажи
+let selectedSalesUnit = null; // Выбранная единица продажи
+let unitModalInstance = null; // Bootstrap Modal instance
+
// ===== СОХРАНЕНИЕ КОРЗИНЫ В REDIS =====
let saveCartTimeout = null;
@@ -390,6 +396,255 @@ async function createNewCustomer() {
}
}
+// ===== ФУНКЦИИ ДЛЯ МОДАЛЬНОГО ОКНА ВЫБОРА ЕДИНИЦЫ ПРОДАЖИ =====
+
+/**
+ * Открывает модальное окно выбора единицы продажи
+ * @param {object} product - Объект товара с информацией о единицах продажи
+ */
+async function openProductUnitModal(product) {
+ unitModalProduct = product;
+
+ // Устанавливаем название товара
+ document.getElementById('unitModalProductName').textContent =
+ `${product.name}${product.sku ? ' (' + product.sku + ')' : ''}`;
+
+ // Загружаем единицы продажи
+ try {
+ const response = await fetch(
+ `/products/api/products/${product.id}/sales-units/?warehouse=${currentWarehouse.id}`
+ );
+ const data = await response.json();
+
+ if (!data.success || !data.sales_units || data.sales_units.length === 0) {
+ alert('Не удалось загрузить единицы продажи');
+ return;
+ }
+
+ unitModalSalesUnits = data.sales_units;
+
+ // Отрисовываем список единиц
+ renderUnitSelectionList();
+
+ // Выбираем единицу по умолчанию или первую
+ const defaultUnit = unitModalSalesUnits.find(u => u.is_default) || unitModalSalesUnits[0];
+ if (defaultUnit) {
+ selectUnit(defaultUnit);
+ }
+
+ // Открываем модальное окно
+ if (!unitModalInstance) {
+ unitModalInstance = new bootstrap.Modal(document.getElementById('selectProductUnitModal'));
+ }
+ unitModalInstance.show();
+
+ } catch (error) {
+ console.error('Ошибка загрузки единиц продажи:', error);
+ alert('Ошибка загрузки данных. Попробуйте ещё раз.');
+ }
+}
+
+/**
+ * Отрисовывает список единиц продажи
+ */
+function renderUnitSelectionList() {
+ const listContainer = document.getElementById('unitSelectionList');
+ listContainer.innerHTML = '';
+
+ unitModalSalesUnits.forEach(unit => {
+ const card = document.createElement('div');
+ card.className = 'unit-selection-card';
+ card.dataset.unitId = unit.id;
+ card.onclick = () => selectUnit(unit);
+
+ // Доступное количество
+ const availableQty = parseFloat(unit.available_quantity || 0);
+ let stockBadgeClass = 'stock-badge-none';
+ let stockText = 'Нет на складе';
+
+ if (availableQty > 10) {
+ stockBadgeClass = 'stock-badge-good';
+ stockText = `${availableQty} ${unit.unit_short_name} доступно`;
+ } else if (availableQty > 0) {
+ stockBadgeClass = 'stock-badge-low';
+ stockText = `${availableQty} ${unit.unit_short_name} доступно`;
+ }
+
+ // Бейдж "По умолчанию"
+ const defaultBadge = unit.is_default ?
+ 'По умолчанию' : '';
+
+ card.innerHTML = `
+
+
+
${unit.name}${defaultBadge}
+
${unit.unit_code} (${unit.unit_short_name})
+
+
${formatMoney(unit.actual_price)} руб
+
+
+ ${stockText}
+
+ `;
+
+ listContainer.appendChild(card);
+ });
+}
+
+/**
+ * Выбирает единицу продажи
+ * @param {object} unit - Объект единицы продажи
+ */
+function selectUnit(unit) {
+ selectedSalesUnit = unit;
+
+ // Обновляем визуальное выделение
+ document.querySelectorAll('.unit-selection-card').forEach(card => {
+ if (card.dataset.unitId === String(unit.id)) {
+ card.classList.add('selected');
+ } else {
+ card.classList.remove('selected');
+ }
+ });
+
+ // Обновляем отображение выбранной единицы
+ document.getElementById('selectedUnitDisplay').textContent =
+ `${unit.name} (${unit.unit_short_name})`;
+
+ // Устанавливаем минимальное количество и шаг
+ const qtyInput = document.getElementById('unitModalQuantity');
+ qtyInput.value = roundQuantity(unit.min_quantity, 3);
+ qtyInput.min = unit.min_quantity;
+ qtyInput.step = unit.quantity_step;
+
+ // Устанавливаем цену
+ document.getElementById('unitModalPrice').value = unit.actual_price;
+
+ // Обновляем подсказку
+ const hintEl = document.getElementById('unitQtyHint');
+ hintEl.textContent = `Мин. ${unit.min_quantity}, шаг ${unit.quantity_step}`;
+
+ // Сбрасываем индикатор изменения цены
+ document.getElementById('priceOverrideIndicator').style.display = 'none';
+
+ // Пересчитываем итого
+ calculateUnitModalSubtotal();
+
+ // Валидируем количество
+ validateUnitQuantity();
+}
+
+/**
+ * Проверяет количество на соответствие ограничениям
+ * @returns {boolean} - true если валидно
+ */
+function validateUnitQuantity() {
+ if (!selectedSalesUnit) return false;
+
+ const qtyInput = document.getElementById('unitModalQuantity');
+ const qty = parseFloat(qtyInput.value);
+ const errorEl = document.getElementById('unitQtyError');
+ const confirmBtn = document.getElementById('confirmAddUnitToCart');
+
+ // Проверка минимального количества
+ if (qty < parseFloat(selectedSalesUnit.min_quantity)) {
+ errorEl.textContent = `Минимальное количество: ${selectedSalesUnit.min_quantity}`;
+ errorEl.style.display = 'block';
+ confirmBtn.disabled = true;
+ return false;
+ }
+
+ // Проверка шага (с учётом погрешности)
+ const step = parseFloat(selectedSalesUnit.quantity_step);
+ const minQty = parseFloat(selectedSalesUnit.min_quantity);
+ const diff = qty - minQty;
+ const remainder = diff % step;
+ const epsilon = 0.0001;
+
+ if (remainder > epsilon && (step - remainder) > epsilon) {
+ errorEl.textContent = `Количество должно быть кратно ${step}`;
+ errorEl.style.display = 'block';
+ confirmBtn.disabled = true;
+ return false;
+ }
+
+ // Всё ок, скрываем ошибку
+ errorEl.style.display = 'none';
+ confirmBtn.disabled = false;
+ return true;
+}
+
+/**
+ * Рассчитывает итоговую сумму
+ */
+function calculateUnitModalSubtotal() {
+ const qtyRaw = parseFloat(document.getElementById('unitModalQuantity').value) || 0;
+ const qty = roundQuantity(qtyRaw, 3); // Округляем количество
+ const price = parseFloat(document.getElementById('unitModalPrice').value) || 0;
+ // Округляем до 2 знаков после запятой для корректного отображения
+ const subtotal = Math.round(qty * price * 100) / 100;
+
+ document.getElementById('unitModalSubtotal').textContent = `${formatMoney(subtotal)} руб`;
+
+ // Проверяем изменение цены
+ if (selectedSalesUnit && Math.abs(price - parseFloat(selectedSalesUnit.actual_price)) > 0.01) {
+ document.getElementById('priceOverrideIndicator').style.display = 'block';
+ } else {
+ document.getElementById('priceOverrideIndicator').style.display = 'none';
+ }
+}
+
+/**
+ * Добавляет товар с выбранной единицей в корзину
+ */
+function addToCartFromModal() {
+ if (!validateUnitQuantity()) {
+ return;
+ }
+
+ const qtyRaw = parseFloat(document.getElementById('unitModalQuantity').value);
+ const qty = roundQuantity(qtyRaw, 3); // Округляем количество
+ const price = parseFloat(document.getElementById('unitModalPrice').value);
+ const priceOverridden = Math.abs(price - parseFloat(selectedSalesUnit.actual_price)) > 0.01;
+
+ // Формируем ключ корзины: product-{id}-{sales_unit_id}
+ const cartKey = `product-${unitModalProduct.id}-${selectedSalesUnit.id}`;
+
+ // Добавляем или обновляем в корзине
+ if (cart.has(cartKey)) {
+ const existing = cart.get(cartKey);
+ existing.qty = roundQuantity(existing.qty + qty, 3); // Округляем сумму
+ existing.price = price; // Обновляем цену
+ existing.quantity_step = parseFloat(selectedSalesUnit.quantity_step) || 1; // Обновляем шаг
+ existing.price_overridden = priceOverridden;
+ } else {
+ cart.set(cartKey, {
+ id: unitModalProduct.id,
+ name: unitModalProduct.name,
+ price: price,
+ qty: qty,
+ type: 'product',
+ sales_unit_id: selectedSalesUnit.id,
+ unit_name: selectedSalesUnit.name,
+ unit_short_name: selectedSalesUnit.unit_short_name,
+ quantity_step: parseFloat(selectedSalesUnit.quantity_step) || 1, // Сохраняем шаг количества
+ price_overridden: priceOverridden
+ });
+ }
+
+ // Обновляем корзину
+ renderCart();
+ saveCartToRedis();
+
+ // Перерисовываем товары для обновления визуального остатка
+ if (!isShowcaseView) {
+ renderProducts();
+ }
+
+ // Закрываем модальное окно
+ unitModalInstance.hide();
+}
+
function renderCategories() {
const grid = document.getElementById('categoryGrid');
grid.innerHTML = '';
@@ -755,6 +1010,13 @@ function setupInfiniteScroll() {
}
async function addToCart(item) {
+ // ПРОВЕРКА НА НАЛИЧИЕ НЕСКОЛЬКИХ ЕДИНИЦ ПРОДАЖИ
+ if (item.type === 'product' && item.sales_units_count > 1) {
+ // Открываем модальное окно выбора единицы
+ await openProductUnitModal(item);
+ return;
+ }
+
const cartKey = `${item.type}-${item.id}`; // Уникальный ключ: "product-1" или "kit-1"
// СПЕЦИАЛЬНАЯ ЛОГИКА ДЛЯ ВИТРИННЫХ КОМПЛЕКТОВ (Soft Lock)
@@ -819,7 +1081,8 @@ async function addToCart(item) {
if (!cart.has(cartKey)) {
cart.set(cartKey, { id: item.id, name: item.name, price: Number(item.price), qty: 1, type: item.type });
} else {
- cart.get(cartKey).qty += 1;
+ const cartItem = cart.get(cartKey);
+ cartItem.qty = roundQuantity(cartItem.qty + 1, 3);
}
}
@@ -850,10 +1113,13 @@ async function updateCartItemQty(cartKey, newQty) {
const item = cart.get(cartKey);
if (!item) return;
- if (newQty <= 0) {
+ // Округляем новое количество
+ const roundedQty = roundQuantity(newQty, 3);
+
+ if (roundedQty <= 0) {
await removeFromCart(cartKey);
} else {
- item.qty = newQty;
+ item.qty = roundedQty;
renderCart();
saveCartToRedis();
@@ -898,10 +1164,16 @@ function renderCart() {
if (item.type === 'kit' || item.type === 'showcase_kit') {
typeIcon = ' ';
}
+
+ // Единица продажи (если есть)
+ let unitInfo = '';
+ if (item.sales_unit_id && item.unit_name) {
+ unitInfo = ` ${item.unit_name}`;
+ }
namePrice.innerHTML = `
- ${typeIcon}${item.name}
- ${formatMoney(item.price)} / шт
+ ${typeIcon}${item.name}${unitInfo}
+ ${formatMoney(item.price)} / ${item.unit_short_name || 'шт'}
`;
// Знак умножения
@@ -932,7 +1204,7 @@ function renderCart() {
qtyInput.style.width = '60px';
qtyInput.style.textAlign = 'center';
qtyInput.style.padding = '0.375rem 0.25rem';
- qtyInput.value = item.qty;
+ qtyInput.value = roundQuantity(item.qty, 3);
qtyInput.min = 1;
qtyInput.readOnly = true; // Только чтение - изменяем только через +/-
qtyInput.style.backgroundColor = '#fff3cd'; // Желтый фон как у витринных
@@ -960,7 +1232,8 @@ function renderCart() {
minusBtn.onclick = async (e) => {
e.preventDefault();
const currentQty = cart.get(cartKey).qty;
- await updateCartItemQty(cartKey, currentQty - 1);
+ const step = item.quantity_step || 1; // Используем шаг единицы или 1 по умолчанию
+ await updateCartItemQty(cartKey, roundQuantity(currentQty - step, 3));
};
// Поле ввода количества
@@ -970,12 +1243,18 @@ function renderCart() {
qtyInput.style.width = '60px';
qtyInput.style.textAlign = 'center';
qtyInput.style.padding = '0.375rem 0.25rem';
- qtyInput.value = item.qty;
+ qtyInput.value = roundQuantity(item.qty, 3);
qtyInput.min = 1;
+ qtyInput.step = item.quantity_step || 0.001; // Устанавливаем шаг единицы продажи
qtyInput.onchange = async (e) => {
- const newQty = parseInt(e.target.value) || 1;
+ const newQty = parseFloat(e.target.value) || 1;
await updateCartItemQty(cartKey, newQty);
};
+ // Округление при потере фокуса
+ qtyInput.onblur = (e) => {
+ const rawValue = parseFloat(e.target.value) || 1;
+ e.target.value = roundQuantity(rawValue, 3);
+ };
// Кнопка плюс
const plusBtn = document.createElement('button');
@@ -984,7 +1263,8 @@ function renderCart() {
plusBtn.onclick = async (e) => {
e.preventDefault();
const currentQty = cart.get(cartKey).qty;
- await updateCartItemQty(cartKey, currentQty + 1);
+ const step = item.quantity_step || 1; // Используем шаг единицы или 1 по умолчанию
+ await updateCartItemQty(cartKey, roundQuantity(currentQty + step, 3));
};
// Собираем контейнер
@@ -2103,6 +2383,10 @@ async function handleCheckoutSubmit(paymentsData) {
if (item.type === 'showcase_kit' && item.showcase_item_ids) {
itemData.showcase_item_ids = item.showcase_item_ids;
}
+ // Для товаров с единицами продажи
+ if (item.sales_unit_id) {
+ itemData.sales_unit_id = item.sales_unit_id;
+ }
return itemData;
}),
payments: paymentsData,
@@ -2222,6 +2506,51 @@ document.addEventListener('DOMContentLoaded', () => {
});
renderCart(); // Отрисовываем восстановленную корзину
}
+
+ // ===== ОБРАБОТЧИКИ ДЛЯ МОДАЛКИ ВЫБОРА ЕДИНИЦЫ ПРОДАЖИ =====
+
+ // Кнопки изменения количества
+ document.getElementById('unitQtyDecrement').addEventListener('click', () => {
+ const input = document.getElementById('unitModalQuantity');
+ const step = parseFloat(input.step) || 1;
+ const newValue = Math.max(parseFloat(input.min), parseFloat(input.value) - step);
+ input.value = roundQuantity(newValue, 3);
+ calculateUnitModalSubtotal();
+ validateUnitQuantity();
+ });
+
+ document.getElementById('unitQtyIncrement').addEventListener('click', () => {
+ const input = document.getElementById('unitModalQuantity');
+ const step = parseFloat(input.step) || 1;
+ const newValue = parseFloat(input.value) + step;
+ input.value = roundQuantity(newValue, 3);
+ calculateUnitModalSubtotal();
+ validateUnitQuantity();
+ });
+
+ // Изменение количества вручную
+ document.getElementById('unitModalQuantity').addEventListener('input', () => {
+ calculateUnitModalSubtotal();
+ validateUnitQuantity();
+ });
+
+ // Округление количества при потере фокуса
+ document.getElementById('unitModalQuantity').addEventListener('blur', (e) => {
+ const rawValue = parseFloat(e.target.value) || 0;
+ e.target.value = roundQuantity(rawValue, 3);
+ calculateUnitModalSubtotal();
+ validateUnitQuantity();
+ });
+
+ // Изменение цены
+ document.getElementById('unitModalPrice').addEventListener('input', () => {
+ calculateUnitModalSubtotal();
+ });
+
+ // Кнопка подтверждения добавления в корзину
+ document.getElementById('confirmAddUnitToCart').addEventListener('click', () => {
+ addToCartFromModal();
+ });
});
// Смена склада
diff --git a/myproject/pos/templates/pos/terminal.html b/myproject/pos/templates/pos/terminal.html
index 4fffa00..093fd40 100644
--- a/myproject/pos/templates/pos/terminal.html
+++ b/myproject/pos/templates/pos/terminal.html
@@ -482,6 +482,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ₽
+
+
+
+ Цена изменена
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{% endblock %}
{% block extra_js %}
diff --git a/myproject/pos/views.py b/myproject/pos/views.py
index 230ac46..199ca12 100644
--- a/myproject/pos/views.py
+++ b/myproject/pos/views.py
@@ -792,6 +792,10 @@ def get_items_api(request):
reserved = p.reserved_qty
free_qty = available - reserved
+ # Подсчитываем активные единицы продажи
+ sales_units_count = p.sales_units.filter(is_active=True).count()
+ has_sales_units = sales_units_count > 0
+
products.append({
'id': p.id,
'name': p.name,
@@ -804,7 +808,9 @@ def get_items_api(request):
'available_qty': str(available),
'reserved_qty': str(reserved),
'free_qty': str(free_qty), # Передаём как строку для сохранения точности
- 'free_qty_sort': float(free_qty) # Для сортировки отдельное поле
+ 'free_qty_sort': float(free_qty), # Для сортировки отдельное поле
+ 'sales_units_count': sales_units_count,
+ 'has_sales_units': has_sales_units
})
# Prefetch для первого фото комплектов
@@ -1434,21 +1440,28 @@ def pos_checkout(request):
with db_transaction.atomic():
# 1. Создаём заказ с текущей датой и временем в локальном часовом поясе (Europe/Minsk)
from django.utils import timezone as tz
+ from orders.models import Delivery
now_utc = tz.now() # Текущее время в UTC
now_local = tz.localtime(now_utc) # Конвертируем в локальный часовой пояс (Europe/Minsk)
current_time = now_local.time() # Извлекаем время в минском часовом поясе
order = Order.objects.create(
customer=customer,
- is_delivery=False, # POS - всегда самовывоз
- pickup_warehouse=warehouse,
status=completed_status, # Сразу "Выполнен"
- delivery_date=now_local.date(), # Текущая дата в минском часовом поясе
- delivery_time_start=current_time, # Текущее время (Минск)
- delivery_time_end=current_time, # То же время (точное время)
special_instructions=order_notes,
modified_by=request.user
)
+
+ # Создаём связанную доставку (самовывоз для POS)
+ Delivery.objects.create(
+ order=order,
+ delivery_type=Delivery.DELIVERY_TYPE_PICKUP, # POS - всегда самовывоз
+ pickup_warehouse=warehouse,
+ delivery_date=now_local.date(), # Текущая дата в минском часовом поясе
+ time_from=current_time, # Текущее время (Минск)
+ time_to=current_time, # То же время (точное время)
+ cost=0 # Самовывоз бесплатный
+ )
# 2. Добавляем товары
from inventory.models import ShowcaseItem
diff --git a/myproject/products/views/api_views.py b/myproject/products/views/api_views.py
index fa1cb89..80adc98 100644
--- a/myproject/products/views/api_views.py
+++ b/myproject/products/views/api_views.py
@@ -5,9 +5,10 @@ from django.http import JsonResponse
from django.db import models
from django.core.cache import cache
from django.core.exceptions import ValidationError
+from django.contrib.auth.decorators import login_required
import logging
-from ..models import Product, ProductVariantGroup, ProductKit, ProductCategory, ProductPhoto
+from ..models import Product, ProductVariantGroup, ProductKit, ProductCategory, ProductPhoto, ProductSalesUnit
logger = logging.getLogger(__name__)
@@ -1360,7 +1361,7 @@ def get_payment_methods(request):
'error': f'Ошибка при загрузке способов оплаты: {str(e)}'
}, status=500)
-
+@login_required
def get_product_sales_units_api(request, product_id):
"""
API для получения единиц продажи товара с остатками.