Files
octopus/myproject/orders/services/transaction_service.py
Andrey Smakotin c1351e1f49 Исправлена форма заказа: две колонки и корректная работа кнопки сохранения
- Разделен экран на две колонки: заказ слева, оплата справа
- Форма оплаты вынесена за пределы основной формы заказа (устранена проблема вложенных форм)
- Исправлен метод calculate_total() для сохранения итоговой суммы в БД
- Добавлена модель Transaction для учета платежей и возвратов
- Добавлена модель PaymentMethod для методов оплаты
- Удалена старая модель Payment, заменена на Transaction
- Добавлен TransactionService для управления транзакциями
- Обновлен интерфейс форм оплаты для правой колонки
- Кнопка 'Сохранить изменения' теперь работает корректно
2025-11-29 14:33:23 +03:00

231 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Сервис для работы с финансовыми транзакциями заказа.
Обрабатывает платежи и возвраты.
"""
from decimal import Decimal, ROUND_HALF_UP
from django.db import transaction
from django.core.exceptions import ValidationError
# Константа для округления до 2 знаков
QUANTIZE_2D = Decimal('0.01')
def _quantize(value):
"""Округление до 2 знаков после запятой"""
if isinstance(value, (int, float)):
value = Decimal(str(value))
return value.quantize(QUANTIZE_2D, rounding=ROUND_HALF_UP)
class TransactionService:
"""
Сервис для управления финансовыми транзакциями заказа.
Все операции атомарны.
"""
@staticmethod
@transaction.atomic
def create_payment(order, amount, payment_method, user, notes=None):
"""
Создать платёж по заказу.
Args:
order: Заказ для оплаты
amount: Сумма платежа
payment_method: Способ оплаты (PaymentMethod или code)
user: Пользователь, создающий платёж
notes: Примечания
Returns:
Transaction: Созданная транзакция платежа
"""
from orders.models import Transaction, PaymentMethod
# Округляем сумму
amount = _quantize(amount)
if amount <= 0:
raise ValueError('Сумма платежа должна быть положительной')
# Получаем PaymentMethod если передан code
if isinstance(payment_method, str):
try:
payment_method = PaymentMethod.objects.get(code=payment_method)
except PaymentMethod.DoesNotExist:
raise ValueError(f'Способ оплаты "{payment_method}" не найден')
# Создаём транзакцию
txn = Transaction.objects.create(
order=order,
transaction_type='payment',
amount=amount,
payment_method=payment_method,
created_by=user,
notes=notes or f'Платёж {payment_method.name}'
)
return txn
@staticmethod
@transaction.atomic
def create_refund(order, amount, payment_method, user, reason=None, notes=None, related_payment=None):
"""
Создать возврат по заказу.
Args:
order: Заказ для возврата
amount: Сумма возврата
payment_method: Способ возврата (PaymentMethod или code)
user: Пользователь, создающий возврат
reason: Причина возврата
notes: Примечания
related_payment: Связанный платёж (Transaction ID или объект)
Returns:
Transaction: Созданная транзакция возврата
"""
from orders.models import Transaction, PaymentMethod
# Округляем сумму
amount = _quantize(amount)
if amount <= 0:
raise ValueError('Сумма возврата должна быть положительной')
# Проверяем, что не возвращаем больше чем оплачено
if amount > order.amount_paid:
raise ValidationError(
f'Сумма возврата ({amount}) превышает оплаченную сумму ({order.amount_paid})'
)
# Получаем PaymentMethod если передан code
if isinstance(payment_method, str):
try:
payment_method = PaymentMethod.objects.get(code=payment_method)
except PaymentMethod.DoesNotExist:
raise ValueError(f'Способ оплаты "{payment_method}" не найден')
# Получаем related_payment если передан ID
if isinstance(related_payment, int):
try:
related_payment = Transaction.objects.get(pk=related_payment, transaction_type='payment')
except Transaction.DoesNotExist:
raise ValueError(f'Платёж #{related_payment} не найден')
# Создаём транзакцию возврата
txn = Transaction.objects.create(
order=order,
transaction_type='refund',
amount=amount,
payment_method=payment_method,
related_payment=related_payment,
reason=reason or 'Возврат средств',
notes=notes,
created_by=user
)
return txn
@staticmethod
@transaction.atomic
def create_full_refund(order, user, reason=None):
"""
Полный возврат всей оплаченной суммы заказа.
Возвращает средства пропорционально платежам (LIFO - последний вошёл, первый вышел).
Args:
order: Заказ для возврата
user: Пользователь, создающий возврат
reason: Причина возврата
Returns:
list[Transaction]: Список созданных транзакций возврата
"""
from orders.models import Transaction
if order.amount_paid <= 0:
raise ValidationError('Нет средств для возврата')
refunds = []
remaining_to_refund = order.amount_paid
# Берём платежи в обратном порядке (LIFO)
payments = order.transactions.filter(transaction_type='payment').order_by('-transaction_date')
for payment in payments:
if remaining_to_refund <= 0:
break
# Сколько уже вернули по этому платежу
from django.db.models import Sum
already_refunded = payment.refunds.aggregate(
total=Sum('amount')
)['total'] or Decimal('0')
# Сколько можно вернуть по этому платежу
available_for_refund = payment.amount - already_refunded
if available_for_refund <= 0:
continue
# Возвращаем минимум из доступного и оставшегося
refund_amount = min(available_for_refund, remaining_to_refund)
# Создаём возврат
refund = TransactionService.create_refund(
order=order,
amount=refund_amount,
payment_method=payment.payment_method,
user=user,
reason=reason or 'Полный возврат заказа',
related_payment=payment
)
refunds.append(refund)
remaining_to_refund -= refund_amount
return refunds
@staticmethod
def get_payment_summary(order):
"""
Получить сводку по платежам и возвратам заказа.
Args:
order: Заказ
Returns:
dict: Сводка с суммами платежей, возвратов и балансом
"""
from django.db.models import Sum
from decimal import Decimal
payments_sum = order.transactions.filter(
transaction_type='payment'
).aggregate(total=Sum('amount'))['total'] or Decimal('0')
refunds_sum = order.transactions.filter(
transaction_type='refund'
).aggregate(total=Sum('amount'))['total'] or Decimal('0')
return {
'payments_total': payments_sum,
'refunds_total': refunds_sum,
'amount_paid': payments_sum - refunds_sum,
'amount_due': max(order.total_amount - (payments_sum - refunds_sum), Decimal('0')),
'payments_count': order.transactions.filter(transaction_type='payment').count(),
'refunds_count': order.transactions.filter(transaction_type='refund').count(),
}
@staticmethod
def get_refundable_amount(order):
"""
Получить сумму, доступную для возврата.
Args:
order: Заказ
Returns:
Decimal: Сумма, которую можно вернуть
"""
return max(order.amount_paid, Decimal('0'))