При выборе способа оплаты сумма теперь автоматически выделяется,
позволяя пользователю сразу начать вводить новое значение без
необходимости вручную выделять текст.
Улучшение скорости ввода для оператора POS.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Полностью переработан интерфейс смешанной оплаты:
НОВЫЙ UI:
- Построчное добавление платежей с галочкой подтверждения
- Каждая строка: [Способ ▼] [Сумма] [✓] [🗑️]
- Зафиксированные платежи показываются отдельно с зеленой галочкой
- Кнопка "+ Добавить еще часть платежа" (скрывается при остатке = 0)
- Индикатор "Осталось оплатить" / "Оплачено полностью"
ФУНКЦИОНАЛЬНОСТЬ:
✅ Автоподстановка остаточной суммы при выборе способа оплаты
✅ Автофокус на поле суммы после выбора способа
✅ Inline валидация с красными подсказками под полем
✅ Проверка превышения остатка и баланса кошелька
✅ Удаление любого платежа (зафиксированного или нет)
✅ Автодобавление пустой строки при удалении всех платежей
ИЗМЕНЕНИЯ В КОДЕ:
+ Новые методы: addPaymentRow(), renderPaymentRows(), attachPaymentRowEvents()
+ Новые методы: confirmPaymentRow(), removePaymentRow(), updateRemainingHint()
~ Обновлены: render(), getTotalPayments(), validate(), submit(), attachEvents()
~ getTotalPayments() теперь учитывает только зафиксированные платежи (fixed: true)
- Удалены старые методы: addPayment(), removePayment(), updatePaymentsList(), renderPaymentsList()
SINGLE MODE остался без изменений.
Проблема решена: больше невозможно "забыть" добавить последний платеж,
так как каждый платеж фиксируется галочкой явно.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Убрано лишнее поле "Примечание" из PaymentWidget:
- Удалено HTML-поле для ввода примечания
- Убраны все обращения к notesInput в коде
- Примечания теперь передаются как пустая строка
Для POS-терминала это поле избыточно и только замедляет процесс оплаты.
Интерфейс стал проще и быстрее.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
1. Убран параметр версии ?v=4 из подключения terminal.js (для разработки не нужен)
2. Исправлена логика автоподстановки суммы в PaymentWidget:
- Теперь при выборе способа оплаты подставляется ОСТАТОЧНАЯ сумма
- Остаток = amount_due - уже добавленные платежи
- Добавлена подсказка "Осталось оплатить: X руб."
Пример:
- Заказ на 30 руб.
- Добавили платеж 10 руб. наличными
- Выбираем картой → автоматически подставится 20 руб. (а не 30!)
Это предотвращает ошибки и переплаты при смешанной оплате.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Убрана кнопка "Оплатить" внутри PaymentWidget, чтобы избежать путаницы.
Теперь есть только одна кнопка "Подтвердить продажу" внизу модального окна.
При смешанной оплате:
- Кнопка "Добавить платеж" для добавления способов оплаты
- Кнопка "Подтвердить продажу" для завершения (внизу модалки)
Это удобнее и безопаснее - пользователи не будут случайно нажимать
не ту кнопку при добавлении нескольких платежей.
Версия JS: v4
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Добавлена интеграция оплаты в 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>
Заблокировал изменение полей is_positive_end и is_negative_end для
системных статусов заказов, так как эти флаги используются в сигналах
inventory для управления резервированием и списанием товаров со склада.
Что изменено:
- OrderStatusForm: Добавлена блокировка (disabled=True) для полей
is_positive_end и is_negative_end при редактировании системных статусов
- status_form.html: Заменено информационное предупреждение на красное
с детальным описанием заблокированных полей и их влияния на систему
Почему это критично:
Эти флаги проверяются в 3 сигналах inventory/signals.py:
1. rollback_sale_on_status_change - откатывает продажи при уходе от 'completed'
2. release_reservations_on_cancellation - освобождает резервы при отмене
3. reserve_stock_on_uncancellation - резервирует при восстановлении заказа
Случайное изменение флагов может привести к:
- Неправильному освобождению резервов товара
- Двойному резервированию
- Блокировке товара навсегда
- Списанию товара для отмененных заказов
Разрешено редактировать для системных статусов: name, label, color, description
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The cleanup_draft_orders command was no longer needed in the project.
Also updated test output file with latest test results.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Создан файл orders/tests/test_payment_methods.py с комплексными тестами:
1. PaymentMethodCreationTest (6 тестов)
- Проверка создания всех 5 способов оплаты через команду
- Проверка системных флагов и активности
- Проверка правильности порядка сортировки
- Проверка идемпотентности команды
- Критический тест наличия account_balance
2. PaymentMethodMultiTenantTest (2 теста)
- Проверка изоляции данных между тенантами
- Проверка кастомных способов оплаты в разных тенантах
3. PaymentMethodTransactionTest (7 тестов)
- Проверка связи PaymentMethod.transactions
- Проверка создания транзакций
- Проверка изоляции транзакций по способам оплаты
- Проверка защиты от удаления (PROTECT)
- Критический тест использования account_balance
- Исправление бага obj.payments → obj.transactions
4. PaymentMethodOrderingTest (2 теста)
- Проверка сортировки по полю order
- Проверка что account_balance первый (order=0)
Особенности тестирования:
- Использование TenantTestCase для изоляции тенантов
- Использование TransactionTestCase для мультитенантных тестов
- Ручное создание/удаление схем для безопасности
- Проверка изоляции данных между схемами
Результат: 15 тестов, все прошли успешно ✓
Проблема #1: Дублирование кода способов оплаты
- В tenants/admin.py был полный список способов оплаты (45 строк)
- В orders/management/commands/create_payment_methods.py был другой список
- При создании тенанта отсутствовал способ 'account_balance'
- Нарушение DRY принципа
Решение: Single Source of Truth
- Единственный источник истины: команда create_payment_methods
- В tenants/admin.py заменено дублирование на call_command()
- Удалено 45 строк дублирующего кода
- Теперь все тенанты получают одинаковый полный список
Проблема #2: AttributeError в админке PaymentMethod
- obj.payments.count() вызывал ошибку
- В модели Transaction связь называется 'transactions', а не 'payments'
Решение: Исправлено в orders/admin.py
- obj.payments → obj.transactions (2 места)
- Админка PaymentMethod теперь работает корректно
Для тенанта buba:
- Создан скрипт add_payment_methods_to_buba.py
- Добавлен недостающий способ оплаты 'С баланса счёта'
Изменённые файлы:
- myproject/tenants/admin.py - вызов команды вместо дублирования
- myproject/orders/admin.py - исправлено на transactions
- add_payment_methods_to_buba.py - скрипт для существующих тенантов
Проблема:
- При изменении статуса заказа на 'Выполнен' товар списывался дважды
- Заказ на 10 шт создавал Sale на 10 шт, но со склада уходило 20 шт
Найдено ДВЕ причины:
1. Повторное обновление резервов через .save() (inventory/signals.py)
- Резервы обновлялись через res.save() каждый раз при сохранении заказа
- Это вызывало сигнал update_stock_on_reservation_change
- При повторном сохранении заказа происходило двойное срабатывание
Решение:
- Проверка дубликатов ПЕРЕД обновлением резервов
- Замена .save() на .update() для массового обновления без вызова сигналов
- Ручное обновление Stock после .update()
2. Двойное FIFO-списание (inventory/services/sale_processor.py)
- Sale создавалась с processed=False
- Сигнал process_sale_fifo срабатывал и списывал товар (1-й раз)
- Затем SaleProcessor.create_sale() тоже списывал товар (2-й раз)
Решение:
- Sale создаётся сразу с processed=True
- Сигнал не срабатывает, списание только в сервисе
Дополнительно:
- Ограничен выбор статусов при создании заказа только промежуточными
- Статус 'Черновик' установлен по умолчанию
- Убран пустой выбор '-------' из поля статуса
Изменённые файлы:
- myproject/orders/forms.py - настройки статусов для формы заказа
- myproject/inventory/signals.py - исправление сигнала create_sale_on_order_completion
- myproject/inventory/services/sale_processor.py - исправление create_sale
- myproject/test_order_status_default.py - обновлён тест
- DOUBLE_SALE_FIX.md - документация по исправлению
Проблема: Сигнал post_save срабатывает несколько раз,
создавая дубликаты Sale для одного заказа.
Решения:
1. Добавлена проверка Sale.objects.filter(order=instance).exists()
перед созданием продаж (inventory/signals.py:74-75)
2. Создана management команда fix_duplicate_sales для исправления
существующих дубликатов
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Проблема:
- calculate_total() пытался автоматически обрабатывать переплату
- Это приводило к дублированию логики и сложной отладке
- Нарушался принцип единственной ответственности
Решение:
- Удалена автоматическая обработка переплаты из Order.calculate_total()
- Теперь calculate_total() ТОЛЬКО считает сумму - всё
- Переплата обрабатывается ТОЛЬКО в TransactionService при создании платежей/возвратов
- Добавлено предупреждение в UI о переплате с инструкцией
Как работает теперь:
1. При оплате - TransactionService автоматически вызывает add_overpayment()
2. При изменении товаров - calculate_total() только пересчитывает сумму
3. Если появилась переплата - оператор видит предупреждение
4. Оператор вручную создаёт возврат в кошелёк через форму
Преимущества:
- Одно место ответственности за переплаты
- Прозрачность для оператора
- Нет скрытых автоматизмов
- Легко обслуживать и отлаживать
- Стандартный подход для e-commerce
Проблема:
- add_overpayment вызывался дважды:
1. При оплате 300 руб (заказ 150) → +150 в кошелёк
2. При изменении суммы до 100 → +200 в кошелёк
- Итого: 350 руб вместо правильных 200 руб
Причина:
- calculate_total() вызывал add_overpayment при любой переплате
- Не учитывалось, что переплата уже была обработана при оплате
Решение:
- Сохраняем old_total перед пересчётом
- Вызываем add_overpayment ТОЛЬКО если:
- old_total > 0 (заказ уже существовал)
- total_amount < old_total (сумма УМЕНЬШИЛАСЬ)
- amount_paid > total_amount (есть переплата)
- Это предотвращает двойную обработку при первичной оплате
Теперь переплата обрабатывается корректно только при изменении суммы заказа
Проблема:
- Создан заказ на 150 руб, оплачено 300 руб → 150 руб в кошелёк
- Изменены товары, сумма стала 100 руб
- amount_paid остался 300, total_amount стал 100
- Новая переплата 200 руб НЕ переносилась в кошелёк автоматически
Решение:
- В Order.calculate_total() добавлена проверка переплаты после пересчёта суммы
- Если amount_paid > total_amount, вызывается WalletService.add_overpayment()
- Излишек автоматически переносится в кошелёк, amount_paid нормализуется до total_amount
- Создаётся WalletTransaction для аудита
Теперь при уменьшении суммы заказа переплата корректно возвращается клиенту
Изменения в UI (order_form.html):
- Добавлен data-code к опциям способов оплаты для идентификации метода кошелька
- ID для селекта способа оплаты (payment-method-select) и поля суммы (payment-amount-input)
- Динамическое ограничение max на поле суммы платежа при выборе кошелька
- Подсказка 'Макс: X руб.' отображается только для оплаты кошельком
- Для внешних методов (карта, наличные) ограничение отсутствует — переплата допустима
Логика JS:
- При выборе метода с code == 'account_balance' устанавливается max = order.amount_due
- Для остальных методов max удаляется — оператор может внести сумму больше остатка
- Переплата по внешним методам корректно зачисляется в кошелёк через WalletService.add_overpayment
Серверная защита (transaction_service.py):
- В TransactionService.create_payment добавлена проверка:
если payment_method.code == 'account_balance' и amount > order.amount_due — ValidationError
- Сообщение: 'Сумма оплаты из кошелька (X руб.) не может превышать остаток к оплате (Y руб.)'
- Защита от обхода UI через API или прямой вызов
Улучшение отображения (order_form.html, order_detail.html):
- Для возврата в кошелёк (transaction_type == 'refund' и code == 'account_balance') показываем 'на баланс счёта' вместо названия метода
- История становится понятнее: '−750,00 руб. Возврат 29.11.2025 на баланс счёта'
Сценарий:
- Кошелёк клиента 500 руб., заказ 65 руб.
- Оператор выбирает оплату из кошелька — поле суммы ограничено 65 руб.
- Попытка ввести 500 заблокирована UI и серверной валидацией
- Для внешней оплаты (карта онлайн) можно внести 500 руб. — остаток 435 автоматически зачислится в кошелёк как переплата
Цель:
- Исключить путаницу в истории транзакций при оплате кошельком
- Разграничить поведение: кошелёк строго ограничен, внешние методы допускают переплату
- Обеспечить прозрачность движения средств для операторов
Изменения:
- Добавлена пометка '(кошелёк клиента)' к методу оплаты с кодом account_balance в селектах платежа и возврата
- Обновлён текст предупреждения о возврате: теперь явно указано, что зачисление в кошелёк происходит только при выборе метода 'кошелёк клиента'
- Для всех остальных методов (наличные, карта и т.п.) возврат — это информационная метка для истории, без фактического движения средств
Цель:
- Устранить путаницу операторов относительно поведения возвратов
- Чётко разделить возврат клиенту (внешними способами) и зачисление в кошелёк (только для account_balance)
- UI теперь соответствует фактической серверной логике в Transaction.save()
Защита от переплаты:
- Серверная валидация в TransactionService.create_refund проверяет amount <= order.amount_paid
- UI ограничение max на поле ввода суммы возврата
- ValidationError с понятным сообщением при попытке превысить лимит
- Move order form scope to left column only to avoid nested forms
- Place payment/refund forms in right column within same grid row
- Remove transaction delete button - use refunds instead for audit trail
- Simplify transaction history table: show only Date, Payment Method, Amount, and Created By
- Fix form submit buttons to use form attribute for proper association
- Improve visual alignment of two-column layout without empty gaps
This ensures valid HTML (no nested forms), clean financial audit history, and better UX with aligned columns.
- Разделен экран на две колонки: заказ слева, оплата справа
- Форма оплаты вынесена за пределы основной формы заказа (устранена проблема вложенных форм)
- Исправлен метод calculate_total() для сохранения итоговой суммы в БД
- Добавлена модель Transaction для учета платежей и возвратов
- Добавлена модель PaymentMethod для методов оплаты
- Удалена старая модель Payment, заменена на Transaction
- Добавлен TransactionService для управления транзакциями
- Обновлен интерфейс форм оплаты для правой колонки
- Кнопка 'Сохранить изменения' теперь работает корректно
ПРОБЛЕМА:
Использование PaymentFormSet для платежей было НЕПРАВИЛЬНЫМ подходом:
1. Платежи = финансовые транзакции (не должны редактироваться inline)
2. Формы валидировали существующие платежи как новые
3. Сложная логика с formset management forms
4. Конфликты валидации кошелька
РЕШЕНИЕ (Django Best Practices):
Разделили управление платежами на отдельные операции:
АРХИТЕКТУРА:
`
POST /orders/111/payments/add/ # Добавить платеж
POST /orders/111/payments/123/delete/ # Удалить платеж
`
ПРЕИМУЩЕСТВА:
✅ Чистая архитектура (separation of concerns)
✅ Платежи = неизменяемые транзакции
✅ Простая валидация (только для новых)
✅ Легко тестировать
✅ API-ready структура
ИЗМЕНЕНИЯ:
1. orders/views.py:
- Убран PaymentFormSet из order_create и order_update
- Добавлен payment_add(request, order_number)
- Добавлен payment_delete(request, order_number, payment_id)
- Используется простой PaymentForm вместо formset
- Payment.save() автоматически обрабатывает:
* Списание из кошелька
* Обработку переплаты
* Обновление amount_paid
2. orders/urls.py:
- Добавлены URL patterns для payment-add и payment-delete
- Структура: /orders/<number>/payments/add|<id>/delete/
3. orders/templates/orders/order_form.html:
- Убран PaymentFormSet и все его скрипты (~265 строк)
- Простая HTML форма для добавления платежа
- Существующие платежи: read-only список с кнопками удаления
- Каждое удаление = отдельный POST запрос
- Для создания: показываем предупреждение вместо формы
4. orders/templatetags/orders_tags.py (NEW):
- Template tag get_payment_methods
- Загружает активные способы оплаты
- Использование: {% get_payment_methods as payment_methods %}
РЕЗУЛЬТАТ:
- Код: -191 строка
- Логика: простая и понятная
- Архитектура: правильная (как в учебнике)
- Платежи: только add/delete (без edit)
- Валидация: работает корректно
- UX: чище и понятнее