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