комит
This commit is contained in:
@@ -441,4 +441,47 @@ def api_create_customer(request):
|
|||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': f'Ошибка сервера: {str(e)}'
|
'error': f'Ошибка сервера: {str(e)}'
|
||||||
}, status=500)
|
}, 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,
|
||||||
|
})
|
||||||
@@ -934,9 +934,160 @@ document.getElementById('createTempKitModal').addEventListener('hidden.bs.modal'
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Заглушки для функционала (будет реализовано позже)
|
// Открытие модалки "Продажа" и рендер сводки корзины
|
||||||
document.getElementById('checkoutNow').onclick = async () => {
|
document.getElementById('checkoutNow').onclick = () => {
|
||||||
alert('Функционал будет подключен позже: создание заказа и списание со склада.');
|
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 = '<i class="bi bi-box-seam text-info me-1" title="Комплект"></i>';
|
||||||
|
} else {
|
||||||
|
typeIcon = '<i class="bi bi-box text-success me-1" title="Товар"></i>';
|
||||||
|
}
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<div>
|
||||||
|
<div class="fw-semibold">${typeIcon}${item.name}</div>
|
||||||
|
<small class="text-muted">${item.qty} шт × ${formatMoney(item.price)} руб.</small>
|
||||||
|
</div>
|
||||||
|
<div class="fw-bold text-end">${formatMoney(item.qty * item.price)} руб.</div>
|
||||||
|
`;
|
||||||
|
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 () => {
|
document.getElementById('scheduleLater').onclick = async () => {
|
||||||
|
|||||||
@@ -252,6 +252,113 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Модалка: Продажа -->
|
||||||
|
<div class="modal fade" id="checkoutModal" tabindex="-1" aria-labelledby="checkoutModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="checkoutModalLabel">
|
||||||
|
<i class="bi bi-cash-stack"></i> Продажа
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Левая колонка: состав заказа -->
|
||||||
|
<div class="col-md-7">
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>Состав заказа</strong>
|
||||||
|
<div class="border rounded p-3 mt-2" id="checkoutItems" style="max-height: 280px; overflow-y: auto; background: #f8f9fa;">
|
||||||
|
<!-- Заполняется из JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Опции оплаты и комментарий -->
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="paymentMethod" class="form-label">Способ оплаты</label>
|
||||||
|
<select class="form-select" id="paymentMethod">
|
||||||
|
<option value="cash">Наличные</option>
|
||||||
|
<option value="card">Карта</option>
|
||||||
|
<option value="mixed">Смешанная оплата</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="orderNote" class="form-label">Комментарий</label>
|
||||||
|
<input type="text" class="form-control" id="orderNote" placeholder="Примечание к заказу">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Правая колонка: ценообразование -->
|
||||||
|
<div class="col-md-5">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<strong>Ценообразование</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Базовая сумма -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<small class="text-muted">Базовая сумма (корзина):</small>
|
||||||
|
<div class="fw-bold" id="checkoutBasePrice">0.00 руб.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Скидка -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="discountType" class="form-label small">Скидка</label>
|
||||||
|
<select class="form-select form-select-sm" id="discountType">
|
||||||
|
<option value="none">Без скидки</option>
|
||||||
|
<option value="percent">Процент (%)</option>
|
||||||
|
<option value="amount">Сумма (руб.)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2" id="discountValueBlock" style="display: none;">
|
||||||
|
<label for="discountValue" class="form-label small">Значение скидки</label>
|
||||||
|
<input type="number" class="form-control form-control-sm" id="discountValue"
|
||||||
|
min="0" step="0.01" value="0" placeholder="0.00">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Расчётная сумма со скидкой -->
|
||||||
|
<div class="mb-2 pb-2 border-bottom">
|
||||||
|
<small class="text-muted">Сумма со скидкой:</small>
|
||||||
|
<div class="fw-bold text-success" id="checkoutDiscountedPrice">0.00 руб.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ручная финальная сумма -->
|
||||||
|
<div class="mb-0">
|
||||||
|
<div class="form-check form-switch mb-2">
|
||||||
|
<input class="form-check-input" type="checkbox" id="useManualPrice">
|
||||||
|
<label class="form-check-label small" for="useManualPrice">
|
||||||
|
Установить свою сумму (приоритет)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="manualPriceBlock" style="display: none;">
|
||||||
|
<input type="number" class="form-control form-control-sm" id="manualPrice"
|
||||||
|
min="0" step="0.01" placeholder="Введите сумму">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Итоговая сумма продажи -->
|
||||||
|
<div class="alert alert-success mt-3 mb-0">
|
||||||
|
<strong>Итого к оплате:</strong><br>
|
||||||
|
<span class="fs-3" id="checkoutFinalPrice">0.00</span> руб.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||||
|
<button type="button" class="btn btn-success btn-lg" id="confirmCheckoutBtn">
|
||||||
|
<i class="bi bi-check2-circle"></i> Подтвердить продажу
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Модалка: Выбор склада -->
|
<!-- Модалка: Выбор склада -->
|
||||||
<div class="modal fade" id="selectWarehouseModal" tabindex="-1" aria-labelledby="selectWarehouseModalLabel" aria-hidden="true">
|
<div class="modal fade" id="selectWarehouseModal" tabindex="-1" aria-labelledby="selectWarehouseModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
|
|||||||
@@ -332,6 +332,8 @@ class SubscriptionAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
def days_left_display(self, obj):
|
def days_left_display(self, obj):
|
||||||
"""Отображение оставшихся дней"""
|
"""Отображение оставшихся дней"""
|
||||||
|
if not obj or not obj.expires_at:
|
||||||
|
return "—"
|
||||||
days = obj.days_left()
|
days = obj.days_left()
|
||||||
if days == 0:
|
if days == 0:
|
||||||
return format_html('<span style="color: red;">Истекла</span>')
|
return format_html('<span style="color: red;">Истекла</span>')
|
||||||
@@ -344,6 +346,8 @@ class SubscriptionAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
def is_expired_display(self, obj):
|
def is_expired_display(self, obj):
|
||||||
"""Отображение статуса истечения"""
|
"""Отображение статуса истечения"""
|
||||||
|
if not obj or not obj.expires_at:
|
||||||
|
return "—"
|
||||||
if obj.is_expired():
|
if obj.is_expired():
|
||||||
return format_html('<span style="color: red;">Да</span>')
|
return format_html('<span style="color: red;">Да</span>')
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -262,14 +262,18 @@ class Subscription(models.Model):
|
|||||||
|
|
||||||
def is_expired(self):
|
def is_expired(self):
|
||||||
"""Проверка истечения подписки"""
|
"""Проверка истечения подписки"""
|
||||||
|
if not self.expires_at:
|
||||||
|
return False
|
||||||
return timezone.now() > self.expires_at
|
return timezone.now() > self.expires_at
|
||||||
|
|
||||||
def days_left(self):
|
def days_left(self):
|
||||||
"""Количество дней до окончания подписки"""
|
"""Количество дней до окончания подписки"""
|
||||||
|
if not self.expires_at:
|
||||||
|
return 0
|
||||||
if self.is_expired():
|
if self.is_expired():
|
||||||
return 0
|
return 0
|
||||||
delta = self.expires_at - timezone.now()
|
delta = self.expires_at - timezone.now()
|
||||||
return delta.days
|
return max(delta.days, 0)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_trial(client):
|
def create_trial(client):
|
||||||
|
|||||||
Reference in New Issue
Block a user