Commit Graph

743 Commits

Author SHA1 Message Date
c4e83fd535 Улучшен layout страницы клиента: две колонки вверху
- Информация о клиенте — левая колонка (col-md-6)
- Операции с кошельком — правая колонка (col-md-6)
- Баланс кошелька перенесён из таблицы в заголовок блока операций
- История кошелька и история заказов остаются в полную ширину
- Компактный вертикальный layout форм в правой колонке
2025-11-29 18:48:43 +03:00
4b7241bcfc Добавлены операции с кошельком клиента: пополнение и возврат
- Добавлены view wallet_deposit и wallet_withdraw с защитой (login_required, is_staff, CSRF)
- Новые маршруты: /customers/<pk>/wallet/deposit/ и /customers/<pk>/wallet/withdraw/
- UI на странице клиента: две симметричные формы для пополнения и списания баланса
- Пополнение: произвольная сумма с обязательным описанием (подарки, компенсации)
- Возврат/списание: с ограничением макс. суммы = текущий баланс, обязательное описание
- Все операции логируются в WalletTransaction с типом 'adjustment'
- Защита от операций с системным клиентом
- Компактный симметричный дизайн форм с фиксированной высотой подсказок
2025-11-29 18:09:40 +03:00
3f22677573 Защита от переплаты кошельком и улучшение отображения транзакций
Изменения в 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 автоматически зачислится в кошелёк как переплата

Цель:
- Исключить путаницу в истории транзакций при оплате кошельком
- Разграничить поведение: кошелёк строго ограничен, внешние методы допускают переплату
- Обеспечить прозрачность движения средств для операторов
2025-11-29 16:54:24 +03:00
312cd808e6 Уточнение UI возвратов: пометка метода кошелька и корректный текст предупреждения
Изменения:
- Добавлена пометка '(кошелёк клиента)' к методу оплаты с кодом account_balance в селектах платежа и возврата
- Обновлён текст предупреждения о возврате: теперь явно указано, что зачисление в кошелёк происходит только при выборе метода 'кошелёк клиента'
- Для всех остальных методов (наличные, карта и т.п.) возврат — это информационная метка для истории, без фактического движения средств

Цель:
- Устранить путаницу операторов относительно поведения возвратов
- Чётко разделить возврат клиенту (внешними способами) и зачисление в кошелёк (только для account_balance)
- UI теперь соответствует фактической серверной логике в Transaction.save()

Защита от переплаты:
- Серверная валидация в TransactionService.create_refund проверяет amount <= order.amount_paid
- UI ограничение max на поле ввода суммы возврата
- ValidationError с понятным сообщением при попытке превысить лимит
2025-11-29 15:47:47 +03:00
2ec6d1935d Refactor order edit page layout and payment UI
- 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.
2025-11-29 15:12:07 +03:00
c1351e1f49 Исправлена форма заказа: две колонки и корректная работа кнопки сохранения
- Разделен экран на две колонки: заказ слева, оплата справа
- Форма оплаты вынесена за пределы основной формы заказа (устранена проблема вложенных форм)
- Исправлен метод calculate_total() для сохранения итоговой суммы в БД
- Добавлена модель Transaction для учета платежей и возвратов
- Добавлена модель PaymentMethod для методов оплаты
- Удалена старая модель Payment, заменена на Transaction
- Добавлен TransactionService для управления транзакциями
- Обновлен интерфейс форм оплаты для правой колонки
- Кнопка 'Сохранить изменения' теперь работает корректно
2025-11-29 14:33:23 +03:00
438ca5d515 Move payment form back to Payment section (outside main form)\n\nPayment add form is now positioned right after the Payment card section,\nmaking it more logical - all payment-related UI is together.\n\nThe form remains OUTSIDE the main order form to avoid nested forms issue. 2025-11-29 02:34:08 +03:00
84ed3a0c7d Рефакторинг: отдельные endpoints для управления платежами (Django best practices)
ПРОБЛЕМА:
Использование 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: чище и понятнее
2025-11-29 02:27:50 +03:00
ee002d5fed Исправить: кнопка сохранения заказа не работала (вложенная форма)
ПРОБЛЕМА:
После предыдущего коммита кнопка сохранения заказа перестала работать.
Клик на кнопку не приводил к отправке формы - ноль реакции.

ПРИЧИНА:
Вложенная форма удаления платежа внутри основной формы order-form.
Вложенные формы недопустимы в HTML и браузер неправильно обрабатывает
submit-события.

РЕШЕНИЕ:
Заменил вложенную форму на JavaScript обработчик:
- Кнопка удаления теперь type=button (не submit)
- Добавлены data-атрибуты: payment-id, payment-name, payment-amount
- JavaScript создает временную форму для POST-запроса с delete_payment_id
- Форма отправляется программно через form.submit()

ИЗМЕНЕНИЯ:
- Заменена форма на button для удаления платежей
- Добавлен JavaScript обработчик .delete-existing-payment-btn
- Подтверждение удаления с именем и суммой платежа

РЕЗУЛЬТАТ:
 Кнопка сохранения заказа работает
 Удаление существующих платежей работает
 Нет вложенных форм (валидный HTML)
2025-11-29 02:18:05 +03:00
f9e086fd89 Исправить: показывать существующие платежи информационно при редактировании заказа
ПРОБЛЕМА:
При редактировании заказа с уже существующими платежами из кошелька,
formset пытался валидировать ВСЕ платежи как новые, включая уже
проведенные. Это вызывало ошибки валидации кошелька, даже когда
пользователь просто хотел добавить новый платеж другим методом.

РЕШЕНИЕ:
Разделили отображение платежей на две части:

1. УЖЕ ПРОВЕДЕННЫЕ ПЛАТЕЖИ (информационный блок):
   - Показываются в виде read-only карточек (bg-light)
   - Не проходят через formset валидацию
   - Можно удалить через отдельную форму с POST-запросом
   - Содержат: способ оплаты, сумму, примечания, кнопку удаления

2. НОВЫЕ ПЛАТЕЖИ (formset):
   - Добавляются через кнопку 'Добавить платеж'
   - Проходят валидацию только для новых записей
   - Контейнер изначально пустой (#payments-container)

ИЗМЕНЕНИЯ:

orders/templates/orders/order_form.html:
- Добавлен блок 'Проведенные платежи' с информационным отображением
- Каждый существующий платеж с формой удаления (delete_payment_id)
- Контейнер для новых платежей теперь пустой при загрузке
- Обновлен calculatePaymentsTotal(): считает существующие + новые
- Убраны обработчики для несуществующих элементов formset
- Итоговая сумма инициализируется из order.amount_paid

orders/views.py (order_update):
- Добавлена обработка delete_payment_id из POST
- При удалении платежа из кошелька - возврат средств через WalletService
- Пересчет amount_paid после удаления
- Редирект обратно в форму после удаления

РЕЗУЛЬТАТ:
 Существующие платежи не валидируются повторно
 Можно свободно добавлять новые платежи любым методом
 Удаление существующих платежей работает корректно
 Возврат в кошелек при удалении платежа 'account_balance'
 Правильный подсчет итоговой суммы (существующие + новые)
2025-11-29 02:14:54 +03:00
65ab153f9e Исправить: дублирование обработки переплаты (баг с двойным возвратом в кошелек)
ПРОБЛЕМА:
При оплате заказа с переплатой (например, 15000 руб за заказ 7770 руб),
сдача возвращалась в кошелек клиента дважды:
- 1 раз: 7230 руб (правильно)
- 2 раз: 7230 руб (дубль!)
- ИТОГО: 14460 руб вместо 7230 руб

ПРИЧИНА:
Обработка переплаты вызывалась в двух местах:
1. Payment.save() → вызывал WalletService.add_overpayment() ✓
2. order_create/order_update в views.py → еще раз вызывал add_overpayment() ✗

РЕШЕНИЕ:
Убраны дублирующие вызовы WalletService.add_overpayment() из views.py.
Теперь переплата обрабатывается ТОЛЬКО в Payment.save() - это правильное
место, т.к. переплата появляется именно при сохранении нового платежа.

ИЗМЕНЕНИЯ:
- orders/views.py (order_create): убран вызов add_overpayment
- orders/views.py (order_update): убран вызов add_overpayment

Теперь при переплате сдача возвращается ровно 1 раз.
2025-11-29 02:07:36 +03:00
fa845ada29 миграции 2025-11-29 02:01:33 +03:00
cf1dce2621 Удалить поле discount_amount из модели Order
Убрано поле скидки из системы для последующей реализации полноценной системы скидок.

Изменения:
- Удалено поле discount_amount из модели Order
- Убрано из формы OrderForm
- Удалено из шаблонов order_form.html и order_detail.html
- Убрано из админки OrderAdmin
- Обновлен метод calculate_total() (без вычитания скидки)

В будущем будет создана отдельная модель Discount с промокодами, процентными скидками и автоматическими акциями.

ВАЖНО: После этого коммита нужно создать и применить миграцию:
  python manage.py makemigrations orders -n remove_discount_amount
  python manage.py migrate orders
2025-11-29 02:00:23 +03:00
a97fc39a2c Рефакторинг: убрана финализация черновиков и улучшены шаблоны заказов
- Убран черновик как отдельная сущность с процессом финализации
- Черновик теперь просто обычный OrderStatus
- Удалены кнопки 'Сохранить как черновик' и 'Финализировать черновик'
- Унифицирована логика сохранения/обновления заказов для всех статусов

Улучшения шаблонов:
- Стандартизировано форматирование валюты через floatformat:2
- Исправлено отображение статуса (используется OrderStatus.label и color)
- Исправлено отображение способа оплаты (корректное использование ForeignKey)
- Добавлены иконки к заголовкам секций для лучшего UX
- Удалены избыточные console.log (~160 строк)
- Очищены комментарии и улучшена читаемость кода
- Убрано использование переменной is_draft в контексте
- Добавлена визуальная согласованность между шаблонами заказов
2025-11-29 01:51:19 +03:00
9415aca63d Исправлена проблема с сохранением платежей и автоматический пересчёт статуса оплаты
- Добавлен префикс 'payments' для PaymentFormSet во всех представлениях
- Добавлен атрибут form='order-form' для динамически создаваемых полей платежей
- Убрано переопределение has_changed() в PaymentForm (использует стандартную логику Django)
- Автоматическая установка created_by для новых платежей
- Автоматический пересчёт payment_status при изменении суммы заказа
- Автоматическая обработка переплаты с возвратом в кошелёк клиента
- Убран весь отладочный код
2025-11-29 00:48:04 +03:00
a101d2919c fix: Payment formset not saving - fixed template replacement and has_changed()
Проблема: Платежи не сохранялись при создании/редактировании заказа.

Причины:
1. JavaScript функция addNewPayment() использовала неправильный метод
   замены __prefix__. При clone().innerHTML.replace() атрибуты name
   оставались с буквальным "__prefix__" вместо номера формы.

2. PaymentForm не переопределял has_changed(), из-за чего Django formset
   считал заполненные формы "пустыми" и не сохранял их.

Исправления:
- order_form.html: Переписана addNewPayment() - теперь клонирует
  template.content, конвертирует в HTML строку, делает replace,
  и только потом парсит обратно в DOM элемент

- forms.py: Добавлен метод PaymentForm.has_changed() который правильно
  определяет что форма заполнена если указан payment_method ИЛИ amount

- views.py: Добавлена отладочная информация для диагностики проблем
  с formset (TODO: удалить после тестирования)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 23:58:39 +03:00
ffdab80698 fix: Allow payment saving during new order creation
Fixed PaymentForm.clean() validation that was preventing payments from
being saved on new orders. The validation required order to exist, but
during creation self.instance.order is None until formset is saved.

Changes:
- Removed hard requirement for order in PaymentForm.clean()
- Wallet balance checks now only run when order exists
- Empty payment forms still allowed (for deletion in formset)
- Basic amount validation maintained

This fixes the issue where payments wouldn't persist when creating
a new order, even though no validation errors were shown to user.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 23:47:50 +03:00
39ab474a3c Fix: Order creation error - avoid calling reset_delivery_cost before items are saved
The error occurred because:
1. OrderForm.save(commit=False) was calling reset_delivery_cost()
2. reset_delivery_cost() uses DeliveryCostCalculator which accesses order.items
3. But items don't exist yet when order is not saved to DB

Solution:
- OrderForm.save() now only calls reset_delivery_cost() when commit=True
- order_create() explicitly calls reset_delivery_cost() AFTER saving items
- This ensures items exist in DB before delivery cost calculation

Error was: 'Order' instance needs to have a primary key value before this relationship can be used.
2025-11-28 23:34:53 +03:00
9a44c98e6e Simplify order creation and editing - remove autosave
- Removed autosave.js (665 lines) and draft-creator.js (441 lines)
- Removed draft_service.py (~500 lines) and DraftOrderService
- Removed AJAX endpoints: autosave and create-draft
- Updated order_create() to add is_create_page flag
- Updated order_update() to finalize drafts without DraftOrderService
- Added get_new_status() method to OrderStatusService
- Updated order_form.html:
  - Removed old JS includes
  - Added beforeunload warning for unsaved data
  - Updated buttons: separate buttons for create/draft/finalize
- Total code reduction: ~1600 lines (92% removed)

New workflow:
- /orders/create/ - user fills form, chooses button
- /orders/<id>/edit/ - simple editing without autosave
- beforeunload warning when leaving page (except on submit)
2025-11-28 23:29:19 +03:00
f911a57640 Before simplifying order creation and editing 2025-11-28 23:11:34 +03:00
94ddb0424b Добавлены методы-обёртки для работы с кошельком в модель Customer 2025-11-27 21:24:33 +03:00
82ed5a409e Добавлена функциональность редактирования заказов с обновлением резервов товаров 2025-11-27 21:13:42 +03:00
da5d4001b5 feat: Add adaptive multi-column layout for categories and tags checkboxes
Implemented responsive grid system for product form checkboxes to improve UX
when dealing with multiple categories and tags.

Changes:
- Added CSS Grid layout with adaptive columns (1-4 based on screen width)
- Mobile (< 768px): 1 column
- Tablet (≥ 768px): 2 columns
- Desktop (≥ 1200px): 3 columns
- Wide screens (≥ 1600px): 4 columns
- Compact spacing (0.35rem gap) for better density
- Enhanced checkbox styling with hover effects and selected state
- JavaScript fallback for forced grid application and responsive resizing
- Improved form container width (col-12 col-xl-10) for better space usage

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 01:11:01 +03:00
c62cdb0298 feat: Add customer prefill from URL parameter in order creation
- Modified order_create view to read customer from GET parameter
- Pass preselected_customer to template context
- Template renders select with preselected option for Select2
- Fixed draft creation timing with callback after Select2 initialization
- Auto-create draft when customer is preselected from URL
- Graceful handling if customer not found or invalid ID
2025-11-27 00:17:02 +03:00
5ead7fdd2e Реализация системы кошелька клиента для переплат
- Добавлено поле wallet_balance в модель Customer
- Создана модель WalletTransaction для истории операций
- Реализован сервис WalletService с методами:
  * add_overpayment - автоматическое зачисление переплаты
  * pay_with_wallet - оплата заказа из кошелька
  * adjust_balance - ручная корректировка баланса
- Интеграция с Payment.save() для автоматической обработки переплат
- UI для оплаты из кошелька в деталях заказа
- Отображение баланса и долга на странице клиента
- Админка с inline транзакций и запретом ручного создания
- Добавлен способ оплаты account_balance
- Миграция 0004 для customers приложения
2025-11-26 14:47:11 +03:00
0653ec0545 Рефакторинг моделей заказов и добавление методов оплаты 2025-11-26 13:38:02 +03:00
08e8409a66 Fix: Restore checkbox values on page reload for recipient fields
Adds proper initialization for checkbox fields on page load:
- address_confirm_with_recipient (Уточнить адрес у получателя)
- customer_is_recipient (Покупатель является получателем)

These fields are already tracked by autosave.js and properly saved
by backend, but were not displaying saved values after page reload.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 06:34:44 +03:00
3b4785e2ad Fix: Implement proper deletion of order items with confirmation dialog
Fixes deletion functionality for order items across frontend and backend:
- Remove restriction preventing deletion of last item
- Add confirmation dialog before deletion
- Properly track and send deleted item IDs to backend via autosave
- Update backend to handle item deletion by ID instead of index
- Fix visual feedback: deleted items are hidden immediately
- Auto-recalculate total sum after deletion

Technical changes:
- order_form.html: Add confirmation dialog, trigger autosave on delete
- autosave.js: Collect deleted item IDs, send to backend
- draft_service.py: Process deleted_item_ids, update items by ID

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 06:27:27 +03:00
5df182e030 Fix: Auto-fill product prices when empty or zero in autosave
Исправлена проблема с пропаданием цен товаров при автосохранении и
перезагрузке страницы.

ПРОБЛЕМА:
- При автосохранении пустые поля цен отправлялись как '0'
- Backend сохранял 0 в базу данных
- При перезагрузке страницы поля цен оставались пустыми
- Итоговая сумма товаров показывала 0.00 руб.

РЕШЕНИЕ 1 - Backend (draft_service.py):
- Изменена логика обработки цен в update_draft()
- Если цена пустая или равна 0, используется actual_price из каталога
- Добавлена корректная обработка price_raw перед конвертацией в Decimal
- Улучшена логика определения is_custom_price

Логика обработки цены:
1. Получаем price_raw из items_data
2. Если price_raw пустой или 0 → используем original_price из каталога
3. Если price_raw заполнен → используем его и сравниваем с original_price
4. is_custom_price = True только если разница больше 0.01

РЕШЕНИЕ 2 - Frontend (order_form.html):
- Добавлена fallback логика в шаблоне для отображения цены
- Если item_form.instance.price пустой/None/0 → показываем actual_price
- Используется inline условие {% if %} для проверки наличия цены
- Отдельная логика для product и product_kit

Теперь работает корректно:
 При выборе товара цена автоматически заполняется из каталога
 Автосохранение сохраняет правильную цену (из каталога или изменённую)
 При перезагрузке страницы цены отображаются корректно
 Итоговая сумма товаров рассчитывается правильно
 Бейдж "Изменена" показывается только для реально изменённых цен

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 20:15:50 +03:00
80260c8a34 Fix: Restore delivery address and order items data on page reload
Исправлены две критические проблемы с автосохранением и восстановлением
данных при перезагрузке страницы редактирования заказа:

1. ПРОБЛЕМА: Адрес доставки не восстанавливался после перезагрузки
   РЕШЕНИЕ (forms.py):
   - Добавлена инициализация полей адреса в OrderForm.__init__()
   - Поля заполняются из order.delivery_address при редактировании
   - Инициализируются все поля: улица, дом, квартира, подъезд, этаж, домофон, инструкции

2. ПРОБЛЕМА: Цены и количество товаров не сохранялись через автосохранение
   РЕШЕНИЕ (draft_service.py):
   - Добавлена обработка items в DraftOrderService.update_draft()
   - Автосохранение теперь обновляет/создаёт/удаляет позиции заказа
   - Сохраняются: product/product_kit, quantity, price, is_custom_price
   - Корректно определяется is_custom_price через сравнение с оригинальной ценой

Логика обработки items:
- Существующие позиции обновляются (product, quantity, price)
- Новые позиции создаются
- Лишние позиции удаляются
- Поддержка как товаров (product_id), так и комплектов (product_kit_id)

Теперь при перезагрузке страницы:
 Адрес доставки полностью восстанавливается во всех полях
 Товары сохраняются с правильными ценами и количествами
 Изменённые цены корректно отмечаются бейджем "Изменена"
 Все данные синхронизируются между автосохранением и базой данных

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 19:57:32 +03:00
a3f5ac4c08 Fix: Enable autosave for delivery address fields with partial data
Исправлена проблема с автосохранением полей адреса доставки. Теперь адрес
сохраняется при заполнении любого поля, а не только при наличии улицы и
номера дома.

Изменения в Backend (address_service.py):
- Изменена логика валидации в process_address_from_form()
- Теперь адрес сохраняется если заполнено хотя бы одно поле
- Удалено жёсткое требование заполнения street и building_number
- Проверяем наличие данных во всех полях адреса

Изменения в Frontend (autosave.js):
- Переработана логика сбора данных адреса
- address_mode='new' устанавливается при заполнении любого поля адреса
- Все непустые поля адреса отправляются на сервер
- Используется более элегантный подход с Object.values() и .some()

Теперь автосохранение работает корректно:
- Заполнение поля "Улица" → автосохранение срабатывает
- Заполнение "Номер дома" → автосохранение срабатывает
- Заполнение любого другого поля адреса → автосохранение срабатывает
- После перезагрузки страницы все данные восстанавливаются

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 19:49:58 +03:00
fbc9d86a62 Fix: Set original price for existing order items on page load
Исправлена проблема с отображением бейджа "Изменена" для существующих
товаров в заказе. Теперь при загрузке страницы для всех существующих
позиций устанавливается атрибут data-original-price с актуальной ценой
из каталога, что позволяет корректно отслеживать изменения цены.

Изменения:
- Добавлена условная логика рендеринга поля price с атрибутом data-original-price
- Для товаров используется item_form.instance.product.actual_price
- Для комплектов используется item_form.instance.product_kit.actual_price
- Бейдж "Изменена" теперь работает одинаково для всех форм

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 19:42:18 +03:00
436cac28ed Feat: Add order items total sum display with real-time calculation
Добавлено отображение общей суммы всех товаров в заказе с автоматическим
обновлением в реальном времени при изменении количества, цены, добавлении
или удалении товаров.

Изменения:
- HTML: Добавлен блок отображения итоговой суммы товаров под списком позиций
- JavaScript: Реализованы функции calculateOrderItemsTotal() и updateOrderItemsTotal()
- Интеграция: Подключены слушатели событий на поля quantity и price
- Обновление суммы происходит при:
  * Изменении количества или цены товара
  * Добавлении новой позиции
  * Удалении позиции
  * Загрузке страницы

Сумма автоматически исключает удалённые формы и корректно обрабатывает
пустые значения. Форматирование: 2 знака после запятой.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 19:39:03 +03:00
22bf7e137d Feat: Add inline price editing for products in catalog
Implemented inline editing functionality for product prices directly in the catalog view with support for both regular and sale prices.

Features:
- Click-to-edit price fields with visual hover indicators
- Separate editing for price and sale_price fields
- Add/remove sale price with validation
- Real-time UI updates without page reload
- Permission-based access control
- Server-side validation for price constraints

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 01:23:46 +03:00
0f212bda69 модерн 2025-11-25 01:14:38 +03:00
b9e02af74a Improve: Auto-expand parent category and clarify expand/collapse buttons
Improvements to category tree UX:
- Auto-expand parent category after creating subcategory at any nesting level
- Recursive ancestor expansion using elegant DOM traversal with .closest()
- Smooth scroll to newly created category with visual feedback
- Replace icon buttons with text: "Развернуть все" / "Свернуть все"
- Eliminates confusion between "+" for adding category vs expanding tree

Technical implementation:
- URL parameter ?expand=<parent_id> to preserve state after page reload
- Recursive expandAncestors() function traverses up the DOM tree
- Uses :scope selector for precise parent-child relationship queries
- Auto-cleans URL parameter after expansion using history.replaceState()

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 00:37:41 +03:00
d566819367 Feat: Add inline category creation in catalog with clickable product names
Added inline category creation functionality to catalog page with user-friendly interface:
- Inline input fields for creating root and nested categories
- '+' button in category tree header for root categories
- '+' icon on hover for each category node to create subcategories
- Clickable product/kit names in catalog grid and list views
- AJAX API endpoint for category creation with validation
- User-friendly error messages for duplicate names and constraints
- Clean implementation using clearTimeout pattern to prevent duplicate requests

Technical details:
- New API endpoint: POST /products/api/categories/create/
- Auto-generates slug and SKU for new categories
- Validates uniqueness, parent existence, and circular references
- Single-request submission with proper race condition handling
- Removes debug logging for production-ready code

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 00:24:05 +03:00
03990292a5 Refactor: Show rename icon on hover instead of click on name
Changed category rename behavior - clicking name now expands/collapses,
pencil icon appears on hover for renaming.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 01:00:51 +03:00
6517643e0d Feat: Add catalog page with inline category renaming
- Create catalog view with category tree and product grid
- Add grid/list view toggle for products
- Implement inline category name editing (click to rename)
- Add API endpoint for category rename
- Extract JS to separate catalog.js file
- Remove unused partial templates

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 00:54:34 +03:00
4549b2c2c2 Feat: Add catalog page with category tree and product grid
- Create catalog view with recursive category tree building
- Add left-side category tree with expand/collapse functionality
- Add right-side product/kit grid with filtering and search
- Include category navigation with product/kit counts

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 00:31:37 +03:00
157bd50082 Refactor: Compact UI for product tags templates
Redesigned tag_form.html and tag_list.html with modern, compact layout:
- Narrower card width, inline form elements, simplified controls
- List-group layout instead of table for tag list
- Streamlined JavaScript with auto-hiding messages

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 23:54:58 +03:00
addc5e0962 Feat: Автоматическая себестоимость товара (read-only)
- Удалено ручное редактирование себестоимости из формы товара
- Себестоимость теперь рассчитывается автоматически из партий (FIFO)
- Добавлена модель CostPriceHistory для логирования изменений
- Добавлен signal для автоматического логирования изменений cost_price
- Админ-панель: себестоимость read-only с детальной информацией о партиях
- Фронтенд: цены перемещены под название, теги под категории
- Поле cost_price сделано опциональным (default=0) для создания товаров

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 23:22:45 +03:00
493b6c212d Add bulk photo deletion feature for products.
Allows selecting and deleting multiple photos at once via checkboxes
and an AJAX endpoint. Key features:
- Checkboxes next to each photo in edit form
- Delete button that shows only when photos are selected
- AJAX request with JSON payload and success confirmation
- DOM removal and counter update after deletion
- Uses existing ImageProcessor cleanup logic

Files changed:
- product_form.html: Added checkboxes and delete button with JS handler
- photo_management.py: Added product_photos_delete_bulk AJAX view
- urls.py: Added /product/photos/delete-bulk/ endpoint
- views/__init__.py: Exported new function

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 21:49:50 +03:00
26146ac639 Add automatic cleanup of empty photo directories after deletion.
When photos are deleted, now automatically removes empty photo_id
directories after all image versions are removed. Uses safe empty
directory checks and handles tenant-aware file storage correctly.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 21:42:21 +03:00
8079abe939 Feat: Add cleanup_tenant management command for improved tenant deletion
- Create new cleanup_tenant command with better tenant deletion workflow
- Handle TenantRegistration automatic processing (keep history or delete)
- Add --purge-registration flag to remove TenantRegistration records
- Add --delete-files flag to remove physical tenant files from /media/tenants/
- Provide detailed deletion summary with confirmation step
- Update УДАЛЕНИЕ_ТЕНАНТОВ.md documentation with:
  * Complete guide for new cleanup_tenant command
  * Explanation of TenantRegistration problem and solutions
  * Recommendations for different use cases (test vs production)
  * Examples of all command variants

Tested:
- Successfully deleted grach tenant (no registration)
- Successfully deleted bingo tenant with --purge-registration flag
- Verified registration records are properly managed
- All database and file operations work correctly

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 21:32:32 +03:00
30f21989d6 Fix: Use storage.url() for tenant-aware image URLs in frontend
Updated ImageService to use default_storage.url() instead of manually
constructing URLs. This ensures images displayed on the frontend correctly
include the tenant_id in the path, enabling proper file access within
multi-tenant environment.

Changes:
- ImageService.get_url() now delegates to default_storage.url()
- All image URLs now include /media/tenants/{tenant_id}/ path
- Ensures consistent behavior with TenantAwareFileSystemStorage
- Frontend photos now display correctly with tenant isolation

Result:
- Thumbnail URLs: /media/tenants/papa/products/4/28/thumb.webp
- Medium URLs: /media/tenants/papa/products/4/28/medium.webp
- Large URLs: /media/tenants/papa/products/4/28/large.webp
- Original URLs: /media/tenants/papa/products/4/28/original.jpg

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 20:55:02 +03:00
87cba63c47 Fix: Add _open() and path() methods to TenantAwareFileSystemStorage
Critical fix for Celery photo processing. The storage class now correctly
handles file reading operations by automatically adding tenant_id prefix
when opening files.

Problems fixed:
- Celery tasks could not open image files from storage
- PIL/Pillow couldn't locate files in tenant-specific directories
- temp file deletion was failing due to path validation

Changes:
- Added _open() method to add tenant_id prefix when opening files
- Added path() method to convert relative paths to full filesystem paths
- Updated delete() method to handle paths with or without tenant prefix
- All methods include security checks to prevent cross-tenant access

Testing:
- All 5 existing tests pass
- Verified photo processing task works end-to-end:
  * Reads temp image file from disk
  * Processes and creates all image versions
  * Saves processed files to tenant-specific directory
  * Cleans up temporary files correctly
- Files correctly stored in: media/tenants/{tenant_id}/products/{product_id}/{photo_id}/

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 20:30:52 +03:00
ff40a9c1f0 Fix: Implement tenant-aware file storage for photo isolation
Resolves critical bug where photos of products with the same ID in different
tenants were overwriting each other. Implemented complete isolation of media
files between tenants using custom Django storage backend.

## Changes

### New Files
- products/utils/storage.py: TenantAwareFileSystemStorage backend
  * Automatically adds tenant_id to file paths on disk
  * Prevents cross-tenant file access with security checks
  * Stores clean paths in DB for portability

- products/tests/test_multi_tenant_photos.py: Comprehensive tests
  * 5 tests covering isolation, security, and configuration
  * All tests passing 

- MULTITENANT_PHOTO_FIX.md: Complete documentation

### Modified Files
- settings.py: Configured DEFAULT_FILE_STORAGE to use TenantAwareFileSystemStorage
- products/models/photos.py:
  * Converted upload_to from strings to callable functions
  * Updated ProductPhoto, ProductKitPhoto, ProductCategoryPhoto
  * Added tenant isolation documentation

- products/tasks.py: Added documentation about file structure
- products/utils/image_processor.py: Added documentation
- products/utils/image_service.py: Added documentation

## Architecture

**On disk:** media/tenants/{tenant_id}/products/{entity_id}/{photo_id}/{size}.ext
**In DB:** products/{entity_id}/{photo_id}/{size}.ext

Tenant ID is automatically added/removed during file operations.

## Security
- Storage rejects cross-tenant file access
- Proper tenant context validation
- Integration with django-tenants schema system

## Testing
- All 5 multi-tenant photo tests pass
- Verified photo paths are isolated per tenant
- Verified storage rejects cross-tenant access
- Verified configuration is correct

## Future-proof
- Ready for S3 migration (just change storage backend)
- No breaking changes to existing code
- Clean separation of concerns

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 20:05:20 +03:00
122ea807d2 Fix: Remove unused Product import from Customer model
The import was left over from the removed loyalty system and could
cause issues during tenant creation when products tables don't exist yet.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 18:06:21 +03:00
4a9df60f91 Auto-create order statuses on tenant activation
Add OrderStatusService.create_default_statuses() call during tenant
registration approval. Creates 8 system order statuses (draft, new,
confirmed, in_assembly, in_delivery, completed, return, cancelled).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 01:14:28 +03:00