Реализована полноценная система оплаты для POS-терминала

Добавлена интеграция оплаты в POS с поддержкой одиночной и смешанной оплаты,
работой с кошельком клиента и автоматическим созданием заказов.

Backend изменения:
- TransactionService: добавлены методы get_available_payment_methods() и create_multiple_payments()
  для фильтрации способов оплаты и атомарного создания нескольких платежей
- POS API: новый endpoint pos_checkout() для создания заказов со статусом "Выполнен"
  с обработкой платежей, освобождением блокировок и очисткой корзины
- Template tags: payment_tags.py для получения способов оплаты в шаблонах

Frontend изменения:
- PaymentWidget: переиспользуемый ES6 класс с поддержкой single/mixed режимов,
  автоматической валидацией и интеграцией с кошельком клиента
- terminal.html: компактное модальное окно (70vw) с оптимизированной компоновкой,
  удален функционал скидок, добавлен показ баланса кошелька
- terminal.js: динамическая загрузка PaymentWidget, интеграция с backend API,
  обработка успешной оплаты и ошибок

Поддерживаемые способы оплаты: наличные, карта, онлайн, баланс счёта.
Смешанная оплата позволяет комбинировать несколько способов в одной транзакции.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-03 15:38:35 +03:00
parent 9dab280def
commit 1cda9086d0
7 changed files with 865 additions and 193 deletions

View File

@@ -274,121 +274,96 @@
<!-- Модалка: Продажа -->
<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">
<div class="modal-dialog modal-dialog-centered modal-lg" style="max-width: 70vw;">
<div class="modal-content" style="max-height: 90vh; overflow: hidden;">
<div class="modal-header py-2">
<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="modal-body py-2" style="overflow-y: auto;">
<div class="row g-3">
<!-- Левая колонка: состав заказа -->
<div class="col-md-7">
<!-- Информация о клиенте -->
<div class="mb-3">
<label class="form-label fw-semibold">Клиент</label>
<div class="d-flex gap-1 align-items-center">
<button class="btn btn-outline-primary btn-sm d-flex align-items-center" id="checkoutCustomerSelectBtn">
<i class="bi bi-person me-1"></i>
<div class="d-flex flex-column align-items-start lh-1">
<small class="text-muted" style="font-size: 0.65rem;">Клиент</small>
<span id="checkoutCustomerSelectBtnText" class="fw-semibold">Выбрать</span>
</div>
</button>
<button class="btn btn-sm btn-outline-danger" id="checkoutResetCustomerBtn" title="Сброс на системного клиента" style="display: none;">
<i class="bi bi-x-lg"></i>
</button>
<!-- Информация о клиенте и баланс в одной строке -->
<div class="row g-2 mb-2">
<div class="col-auto">
<label class="form-label fw-semibold small mb-1">Клиент</label>
<div class="d-flex gap-1">
<button class="btn btn-outline-primary btn-sm" id="checkoutCustomerSelectBtn" style="font-size: 0.85rem; padding: 0.25rem 0.5rem;">
<i class="bi bi-person me-1"></i>
<span id="checkoutCustomerSelectBtnText">Выбрать</span>
</button>
<button class="btn btn-sm btn-outline-danger" id="checkoutResetCustomerBtn" title="Сброс" style="display: none; font-size: 0.85rem; padding: 0.25rem 0.5rem;">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
<div class="col" id="checkoutWalletBalance" style="display: none;">
<label class="form-label fw-semibold small mb-1">Баланс кошелька</label>
<div class="alert alert-info py-1 px-2 mb-0" style="font-size: 0.9rem;">
<i class="bi bi-wallet2"></i>
<span id="checkoutWalletBalanceAmount">0.00</span> руб.
</div>
</div>
</div>
<div class="mb-3">
<strong>Состав заказа</strong>
<div class="border rounded p-3 mt-2" id="checkoutItems" style="max-height: 240px; overflow-y: auto; background: #f8f9fa;">
<!-- Состав заказа -->
<div class="mb-2">
<strong class="small">Состав заказа</strong>
<div class="border rounded p-2 mt-1" id="checkoutItems" style="max-height: 180px; overflow-y: auto; background: #f8f9fa; font-size: 0.9rem;">
<!-- Заполняется из 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 class="mb-2">
<label for="orderNote" class="form-label small mb-1">Комментарий</label>
<input type="text" class="form-control form-control-sm" id="orderNote" placeholder="Примечание к заказу">
</div>
</div>
<!-- Правая колонка: ценообразование -->
<!-- Правая колонка: оплата -->
<div class="col-md-5">
<div class="card">
<div class="card-header bg-light">
<strong>Ценообразование</strong>
<div class="card mb-0">
<div class="card-header bg-light py-2">
<strong class="small">Оплата</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="card-body p-2">
<!-- Итого к оплате -->
<div class="mb-2 pb-2 border-bottom">
<small class="text-muted">Сумма со скидкой:</small>
<div class="fw-bold text-success" id="checkoutDiscountedPrice">0.00 руб.</div>
<small class="text-muted">Итого к оплате:</small>
<div class="fw-bold text-success fs-5" id="checkoutFinalPrice">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 class="mb-2">
<label class="form-label fw-semibold small mb-1">Режим оплаты</label>
<div class="btn-group w-100" role="group">
<button type="button"
class="btn btn-sm btn-outline-primary active"
id="singlePaymentMode">
Одним способом
</button>
<button type="button"
class="btn btn-sm btn-outline-primary"
id="mixedPaymentMode">
Смешанная
</button>
</div>
</div>
<!-- Итоговая сумма продажи -->
<div class="alert alert-success mt-3 mb-0">
<strong>Итого к оплате:</strong><br>
<span class="fs-3" id="checkoutFinalPrice">0.00</span> руб.
</div>
<!-- Контейнер для PaymentWidget -->
<div id="paymentWidgetContainer"></div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<div class="modal-footer py-2">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-success btn-lg" id="confirmCheckoutBtn">
<button type="button" class="btn btn-success" id="confirmCheckoutBtn">
<i class="bi bi-check2-circle"></i> Подтвердить продажу
</button>
</div>