diff --git a/myproject/customers/views.py b/myproject/customers/views.py
index 505b422..c2e6ced 100644
--- a/myproject/customers/views.py
+++ b/myproject/customers/views.py
@@ -441,4 +441,47 @@ def api_create_customer(request):
return JsonResponse({
'success': False,
'error': f'Ошибка сервера: {str(e)}'
- }, status=500)
\ No newline at end of file
+ }, status=500)
+
+
+@require_http_methods(["POST"])
+def api_create_system_customer(request):
+ """
+ Создать или получить системного анонимного клиента для POS.
+
+ Идентификаторы системного клиента:
+ - email: system@pos.customer
+ - name: АНОНИМНЫЙ ПОКУПАТЕЛЬ (POS)
+ - loyalty_tier: 'no_discount'
+ - notes: 'SYSTEM_CUSTOMER'
+
+ Поведение:
+ - Если клиент уже существует (по уникальному email), новый не создаётся.
+ - Если не существует — создаётся с указанными полями.
+ - Возвращает JSON с признаком, был ли создан новый клиент.
+
+ Возвращаемый JSON:
+ {
+ "success": true,
+ "created": false, # или true, если впервые создан
+ "id": 123,
+ "name": "АНОНИМНЫЙ ПОКУПАТЕЛЬ (POS)",
+ "email": "system@pos.customer"
+ }
+ """
+ customer, created = Customer.objects.get_or_create(
+ email="system@pos.customer",
+ defaults={
+ "name": "АНОНИМНЫЙ ПОКУПАТЕЛЬ (POS)",
+ "loyalty_tier": "no_discount",
+ "notes": "SYSTEM_CUSTOMER",
+ },
+ )
+
+ return JsonResponse({
+ "success": True,
+ "created": created,
+ "id": customer.pk,
+ "name": customer.name,
+ "email": customer.email,
+ })
\ No newline at end of file
diff --git a/myproject/pos/static/pos/js/terminal.js b/myproject/pos/static/pos/js/terminal.js
index 3137985..69a1efe 100644
--- a/myproject/pos/static/pos/js/terminal.js
+++ b/myproject/pos/static/pos/js/terminal.js
@@ -934,9 +934,160 @@ document.getElementById('createTempKitModal').addEventListener('hidden.bs.modal'
}
});
-// Заглушки для функционала (будет реализовано позже)
-document.getElementById('checkoutNow').onclick = async () => {
- alert('Функционал будет подключен позже: создание заказа и списание со склада.');
+// Открытие модалки "Продажа" и рендер сводки корзины
+document.getElementById('checkoutNow').onclick = () => {
+ if (cart.size === 0) {
+ alert('Корзина пуста. Добавьте товары перед продажей.');
+ return;
+ }
+ renderCheckoutModal();
+ const modal = new bootstrap.Modal(document.getElementById('checkoutModal'));
+ modal.show();
+};
+
+// Рендер позиций корзины и итога в модалке продажи
+function renderCheckoutModal() {
+ const container = document.getElementById('checkoutItems');
+ container.innerHTML = '';
+ let total = 0;
+
+ cart.forEach((item) => {
+ const row = document.createElement('div');
+ row.className = 'd-flex justify-content-between align-items-center mb-2 pb-2 border-bottom';
+
+ // Иконка для комплектов
+ let typeIcon = '';
+ if (item.type === 'kit' || item.type === 'showcase_kit') {
+ typeIcon = '';
+ } else {
+ typeIcon = '';
+ }
+
+ row.innerHTML = `
+
+
${typeIcon}${item.name}
+
${item.qty} шт × ${formatMoney(item.price)} руб.
+
+ ${formatMoney(item.qty * item.price)} руб.
+ `;
+ container.appendChild(row);
+ total += item.qty * item.price;
+ });
+
+ // Обновляем базовую цену и пересчитываем
+ updateCheckoutPricing(total);
+}
+
+// Пересчёт цен в модалке продажи
+function updateCheckoutPricing(basePrice = null) {
+ // Если basePrice не передан, пересчитываем из корзины
+ if (basePrice === null) {
+ basePrice = 0;
+ cart.forEach((item) => {
+ basePrice += item.qty * item.price;
+ });
+ }
+
+ // Базовая сумма
+ document.getElementById('checkoutBasePrice').textContent = formatMoney(basePrice) + ' руб.';
+
+ // Скидка
+ const discountType = document.getElementById('discountType').value;
+ const discountValue = parseFloat(document.getElementById('discountValue').value) || 0;
+
+ let discountedPrice = basePrice;
+ if (discountType !== 'none' && discountValue > 0) {
+ if (discountType === 'percent') {
+ discountedPrice = basePrice - (basePrice * discountValue / 100);
+ } else if (discountType === 'amount') {
+ discountedPrice = Math.max(0, basePrice - discountValue);
+ }
+ }
+
+ document.getElementById('checkoutDiscountedPrice').textContent = formatMoney(discountedPrice) + ' руб.';
+
+ // Финальная цена (с учётом ручной суммы если задана)
+ const useManualPrice = document.getElementById('useManualPrice').checked;
+ const manualPrice = parseFloat(document.getElementById('manualPrice').value) || 0;
+
+ let finalPrice = discountedPrice;
+ if (useManualPrice && manualPrice > 0) {
+ finalPrice = manualPrice;
+ }
+
+ document.getElementById('checkoutFinalPrice').textContent = formatMoney(finalPrice);
+}
+
+// Обработчики для полей скидки и цены
+document.getElementById('discountType').addEventListener('change', function() {
+ const discountBlock = document.getElementById('discountValueBlock');
+ if (this.value === 'none') {
+ discountBlock.style.display = 'none';
+ document.getElementById('discountValue').value = '0';
+ } else {
+ discountBlock.style.display = 'block';
+ }
+ updateCheckoutPricing();
+});
+
+document.getElementById('discountValue').addEventListener('input', function() {
+ updateCheckoutPricing();
+});
+
+document.getElementById('useManualPrice').addEventListener('change', function() {
+ const manualPriceBlock = document.getElementById('manualPriceBlock');
+ if (this.checked) {
+ manualPriceBlock.style.display = 'block';
+ } else {
+ manualPriceBlock.style.display = 'none';
+ document.getElementById('manualPrice').value = '';
+ }
+ updateCheckoutPricing();
+});
+
+document.getElementById('manualPrice').addEventListener('input', function() {
+ updateCheckoutPricing();
+});
+
+// Подтверждение продажи (пока заглушка)
+document.getElementById('confirmCheckoutBtn').onclick = () => {
+ const payment = document.getElementById('paymentMethod').value;
+ const note = document.getElementById('orderNote').value.trim();
+
+ const paymentText = {
+ 'cash': 'Наличные',
+ 'card': 'Карта',
+ 'mixed': 'Смешанная оплата'
+ }[payment] || payment;
+
+ // Получаем данные о ценах и скидке
+ const basePrice = document.getElementById('checkoutBasePrice').textContent;
+ const discountType = document.getElementById('discountType').value;
+ const discountValue = document.getElementById('discountValue').value;
+ const finalPrice = document.getElementById('checkoutFinalPrice').textContent;
+
+ let discountInfo = 'нет скидки';
+ if (discountType === 'percent' && discountValue > 0) {
+ discountInfo = `скидка ${discountValue}%`;
+ } else if (discountType === 'amount' && discountValue > 0) {
+ discountInfo = `скидка ${discountValue} руб.`;
+ }
+
+ const useManual = document.getElementById('useManualPrice').checked;
+ if (useManual) {
+ discountInfo += ' (установлена своя сумма)';
+ }
+
+ alert(`Функция проведения продажи будет подключена позже.
+
+Базовая сумма: ${basePrice}
+Скидка: ${discountInfo}
+Итого к оплате: ${finalPrice} руб.
+Оплата: ${paymentText}
+Комментарий: ${note || '—'}`);
+
+ const modal = bootstrap.Modal.getInstance(document.getElementById('checkoutModal'));
+ modal.hide();
};
document.getElementById('scheduleLater').onclick = async () => {
diff --git a/myproject/pos/templates/pos/terminal.html b/myproject/pos/templates/pos/terminal.html
index e4dba33..cebf626 100644
--- a/myproject/pos/templates/pos/terminal.html
+++ b/myproject/pos/templates/pos/terminal.html
@@ -252,6 +252,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Базовая сумма (корзина):
+
0.00 руб.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Сумма со скидкой:
+
0.00 руб.
+
+
+
+
+
+
+
+ Итого к оплате:
+ 0.00 руб.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/myproject/tenants/admin.py b/myproject/tenants/admin.py
index b763a8d..3722110 100644
--- a/myproject/tenants/admin.py
+++ b/myproject/tenants/admin.py
@@ -332,6 +332,8 @@ class SubscriptionAdmin(admin.ModelAdmin):
def days_left_display(self, obj):
"""Отображение оставшихся дней"""
+ if not obj or not obj.expires_at:
+ return "—"
days = obj.days_left()
if days == 0:
return format_html('Истекла')
@@ -344,6 +346,8 @@ class SubscriptionAdmin(admin.ModelAdmin):
def is_expired_display(self, obj):
"""Отображение статуса истечения"""
+ if not obj or not obj.expires_at:
+ return "—"
if obj.is_expired():
return format_html('Да')
else:
diff --git a/myproject/tenants/models.py b/myproject/tenants/models.py
index d492f2b..eb2460e 100644
--- a/myproject/tenants/models.py
+++ b/myproject/tenants/models.py
@@ -262,14 +262,18 @@ class Subscription(models.Model):
def is_expired(self):
"""Проверка истечения подписки"""
+ if not self.expires_at:
+ return False
return timezone.now() > self.expires_at
def days_left(self):
"""Количество дней до окончания подписки"""
+ if not self.expires_at:
+ return 0
if self.is_expired():
return 0
delta = self.expires_at - timezone.now()
- return delta.days
+ return max(delta.days, 0)
@staticmethod
def create_trial(client):