- 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
181 lines
6.6 KiB
Python
181 lines
6.6 KiB
Python
from django.db import models
|
||
from accounts.models import CustomUser
|
||
from decimal import Decimal
|
||
from django.db import transaction
|
||
from django.core.exceptions import ValidationError
|
||
|
||
|
||
class PaymentMethod(models.Model):
|
||
"""
|
||
Способ оплаты заказа.
|
||
Справочник для управления доступными методами оплаты.
|
||
"""
|
||
|
||
# Код для программного доступа
|
||
code = models.SlugField(
|
||
unique=True,
|
||
max_length=50,
|
||
verbose_name="Код способа оплаты",
|
||
help_text="Уникальный идентификатор (например: 'cash_to_courier', 'card_to_courier')"
|
||
)
|
||
|
||
# Отображаемое название
|
||
name = models.CharField(
|
||
max_length=100,
|
||
verbose_name="Название способа оплаты"
|
||
)
|
||
|
||
# Описание
|
||
description = models.TextField(
|
||
blank=True,
|
||
verbose_name="Описание",
|
||
help_text="Дополнительная информация о способе оплаты"
|
||
)
|
||
|
||
# Активность
|
||
is_active = models.BooleanField(
|
||
default=True,
|
||
verbose_name="Активен",
|
||
help_text="Отключенные способы оплаты не отображаются при создании заказа"
|
||
)
|
||
|
||
# Порядок отображения
|
||
order = models.PositiveIntegerField(
|
||
default=0,
|
||
verbose_name="Порядок отображения"
|
||
)
|
||
|
||
# Системный флаг
|
||
is_system = models.BooleanField(
|
||
default=False,
|
||
verbose_name="Системный",
|
||
help_text="Системные способы оплаты нельзя удалить через интерфейс"
|
||
)
|
||
|
||
# Аудит
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
updated_at = models.DateTimeField(auto_now=True)
|
||
|
||
created_by = models.ForeignKey(
|
||
CustomUser,
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='created_payment_methods',
|
||
verbose_name="Создано"
|
||
)
|
||
|
||
class Meta:
|
||
verbose_name = "Способ оплаты"
|
||
verbose_name_plural = "Способы оплаты"
|
||
ordering = ['order', 'name']
|
||
indexes = [
|
||
models.Index(fields=['code']),
|
||
models.Index(fields=['is_active']),
|
||
models.Index(fields=['order']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
|
||
class Payment(models.Model):
|
||
"""
|
||
Платеж по заказу.
|
||
Хранит историю всех платежей, включая частичные оплаты.
|
||
Поддерживает смешанную оплату (несколько платежей разными способами на один заказ).
|
||
"""
|
||
order = models.ForeignKey(
|
||
'Order',
|
||
on_delete=models.CASCADE,
|
||
related_name='payments',
|
||
verbose_name="Заказ"
|
||
)
|
||
|
||
amount = models.DecimalField(
|
||
max_digits=10,
|
||
decimal_places=2,
|
||
verbose_name="Сумма платежа"
|
||
)
|
||
|
||
payment_method = models.ForeignKey(
|
||
'PaymentMethod',
|
||
on_delete=models.PROTECT,
|
||
related_name='payments',
|
||
verbose_name="Способ оплаты",
|
||
help_text="Способ оплаты данного платежа"
|
||
)
|
||
|
||
payment_date = models.DateTimeField(
|
||
auto_now_add=True,
|
||
verbose_name="Дата и время платежа"
|
||
)
|
||
|
||
created_by = models.ForeignKey(
|
||
CustomUser,
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='payments_created',
|
||
verbose_name="Принял платеж"
|
||
)
|
||
|
||
notes = models.TextField(
|
||
blank=True,
|
||
null=True,
|
||
verbose_name="Примечания",
|
||
help_text="Дополнительная информация о платеже"
|
||
)
|
||
|
||
class Meta:
|
||
verbose_name = "Платеж"
|
||
verbose_name_plural = "Платежи"
|
||
ordering = ['-payment_date']
|
||
indexes = [
|
||
models.Index(fields=['order']),
|
||
models.Index(fields=['payment_date']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return f"Платеж {self.amount} руб. по заказу #{self.order.order_number}"
|
||
|
||
def save(self, *args, **kwargs):
|
||
"""При сохранении платежа обновляем сумму оплаты в заказе и обрабатываем кошелёк/переплаты"""
|
||
is_new = self.pk is None
|
||
with transaction.atomic():
|
||
super().save(*args, **kwargs)
|
||
|
||
# Пересчитываем общую сумму оплаты в заказе
|
||
self.order.amount_paid = sum(p.amount for p in self.order.payments.all())
|
||
self.order.update_payment_status()
|
||
|
||
# Списание из кошелька при новом платеже методом 'account_balance'
|
||
if is_new and self.payment_method.code == 'account_balance':
|
||
from customers.models import Customer, WalletTransaction
|
||
# Блокируем запись клиента
|
||
customer = Customer.objects.select_for_update().get(pk=self.order.customer_id)
|
||
if customer.wallet_balance < self.amount:
|
||
raise ValidationError(f'Недостаточно средств в кошельке (доступно {customer.wallet_balance} руб.)')
|
||
|
||
# Списываем и округляем до 2 знаков
|
||
customer.wallet_balance = (customer.wallet_balance - self.amount).quantize(Decimal('0.01'))
|
||
customer.save(update_fields=['wallet_balance'])
|
||
|
||
# Пишем историю
|
||
WalletTransaction.objects.create(
|
||
customer=customer,
|
||
amount=self.amount,
|
||
transaction_type='spend',
|
||
order=self.order,
|
||
description=f'Оплата из кошелька по заказу #{self.order.order_number}',
|
||
created_by=self.created_by
|
||
)
|
||
|
||
# Нормализация переплаты: лишнее в кошелёк, amount_paid = total_amount
|
||
try:
|
||
from customers.services.wallet_service import WalletService
|
||
WalletService.add_overpayment(self.order, self.created_by)
|
||
except Exception:
|
||
# Продолжаем, даже если нормализация переплаты не удалась
|
||
pass
|