Рефакторинг системы кошелька клиентов
Основные изменения: - Переход от денормализованного поля wallet_balance к вычисляемому балансу - Баланс теперь вычисляется как SUM(signed_amount) транзакций - Добавлено кеширование баланса для производительности (5 минут) - Новая модель WalletTransaction с полем signed_amount (может быть +/-) - WalletService для всех операций с кошельком (deposit, spend, adjustment) - Защита от отрицательного баланса и race conditions через select_for_update - Добавлен balance_after в каждую транзакцию для аудита - Обновлены миграции для переноса данных из старой схемы Улучшения безопасности: - Атомарные транзакции для всех операций с балансом - Блокировка строк при модификации баланса - Валидация недостаточности средств - Обязательное описание для корректировок баланса UI/UX изменения: - Обновлён вывод баланса кошелька в деталях клиента - Добавлена история транзакций с типами и описаниями - Цветовая индикация положительных транзакций (зелёный) Техническая документация: - Добавлены docstrings для всех методов WalletService - Комментарии к критичным участкам кода - Примеры использования в docstrings
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from django.contrib import admin
|
||||
from django.db import models
|
||||
from django.utils.html import format_html
|
||||
from .models import Customer, WalletTransaction
|
||||
from .models import Customer, WalletTransaction, ContactChannel
|
||||
|
||||
|
||||
class IsSystemCustomerFilter(admin.SimpleListFilter):
|
||||
@@ -44,14 +44,14 @@ class CustomerAdmin(admin.ModelAdmin):
|
||||
)
|
||||
date_hierarchy = 'created_at'
|
||||
ordering = ('-created_at',)
|
||||
readonly_fields = ('created_at', 'updated_at', 'is_system_customer', 'wallet_balance')
|
||||
readonly_fields = ('created_at', 'updated_at', 'is_system_customer', 'wallet_balance_display')
|
||||
|
||||
fieldsets = (
|
||||
('Основная информация', {
|
||||
'fields': ('name', 'email', 'phone', 'is_system_customer')
|
||||
}),
|
||||
('Кошелёк', {
|
||||
'fields': ('wallet_balance',),
|
||||
'fields': ('wallet_balance_display',),
|
||||
}),
|
||||
('Заметки', {
|
||||
'fields': ('notes',)
|
||||
@@ -64,20 +64,20 @@ class CustomerAdmin(admin.ModelAdmin):
|
||||
|
||||
def wallet_balance_display(self, obj):
|
||||
"""Отображение баланса кошелька с цветом"""
|
||||
if obj.wallet_balance > 0:
|
||||
balance = obj.wallet_balance
|
||||
if balance > 0:
|
||||
return format_html(
|
||||
'<span style="color: green; font-weight: bold;">{} руб.</span>',
|
||||
obj.wallet_balance
|
||||
balance
|
||||
)
|
||||
return f'{obj.wallet_balance} руб.'
|
||||
return f'{balance} руб.'
|
||||
wallet_balance_display.short_description = 'Баланс кошелька'
|
||||
wallet_balance_display.admin_order_field = 'wallet_balance'
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""Делаем все поля read-only для системного клиента"""
|
||||
if obj and obj.is_system_customer:
|
||||
# Для системного клиента все поля только для чтения
|
||||
return ['name', 'email', 'phone', 'is_system_customer', 'wallet_balance', 'notes', 'created_at', 'updated_at']
|
||||
return ['name', 'email', 'phone', 'is_system_customer', 'wallet_balance_display', 'notes', 'created_at', 'updated_at']
|
||||
return self.readonly_fields
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
@@ -98,14 +98,20 @@ class CustomerAdmin(admin.ModelAdmin):
|
||||
return super().changeform_view(request, object_id, form_url, extra_context)
|
||||
|
||||
|
||||
class ContactChannelInline(admin.TabularInline):
|
||||
"""Inline для управления каналами связи клиента"""
|
||||
model = ContactChannel
|
||||
extra = 1
|
||||
fields = ('channel_type', 'value', 'is_primary', 'notes')
|
||||
|
||||
|
||||
class WalletTransactionInline(admin.TabularInline):
|
||||
"""
|
||||
line для отображения транзакций кошелька"""
|
||||
"""Inline для отображения транзакций кошелька"""
|
||||
model = WalletTransaction
|
||||
extra = 0
|
||||
can_delete = False
|
||||
readonly_fields = ('transaction_type', 'amount', 'order', 'description', 'created_at', 'created_by')
|
||||
fields = ('created_at', 'transaction_type', 'amount', 'order', 'description', 'created_by')
|
||||
readonly_fields = ('created_at', 'transaction_type', 'signed_amount', 'balance_after', 'order', 'description', 'created_by')
|
||||
fields = ('created_at', 'transaction_type', 'signed_amount', 'balance_after', 'order', 'description', 'created_by')
|
||||
ordering = ('-created_at',)
|
||||
|
||||
def has_add_permission(self, request, obj=None):
|
||||
@@ -114,32 +120,33 @@ line для отображения транзакций кошелька"""
|
||||
|
||||
|
||||
# Добавляем inline в CustomerAdmin
|
||||
CustomerAdmin.inlines = [WalletTransactionInline]
|
||||
CustomerAdmin.inlines = [ContactChannelInline, WalletTransactionInline]
|
||||
|
||||
|
||||
@admin.register(WalletTransaction)
|
||||
class WalletTransactionAdmin(admin.ModelAdmin):
|
||||
"""Админка для просмотра всех транзакций кошелька"""
|
||||
list_display = ('created_at', 'customer', 'transaction_type', 'amount_display', 'order', 'created_by')
|
||||
list_filter = ('transaction_type', 'created_at')
|
||||
list_display = ('created_at', 'customer', 'transaction_type', 'amount_display', 'balance_after', 'order', 'created_by')
|
||||
list_filter = ('transaction_type', 'balance_category', 'created_at')
|
||||
search_fields = ('customer__name', 'customer__email', 'customer__phone', 'description')
|
||||
readonly_fields = ('customer', 'transaction_type', 'amount', 'order', 'description', 'created_at', 'created_by')
|
||||
readonly_fields = ('customer', 'transaction_type', 'signed_amount', 'balance_category', 'balance_after', 'order', 'description', 'created_at', 'created_by')
|
||||
date_hierarchy = 'created_at'
|
||||
ordering = ('-created_at',)
|
||||
|
||||
def amount_display(self, obj):
|
||||
"""Отображение суммы с цветом"""
|
||||
if obj.transaction_type == 'deposit':
|
||||
amount = obj.signed_amount
|
||||
if amount > 0:
|
||||
return format_html(
|
||||
'<span style="color: green; font-weight: bold;">+{} руб.</span>',
|
||||
obj.amount
|
||||
amount
|
||||
)
|
||||
elif obj.transaction_type == 'spend':
|
||||
elif amount < 0:
|
||||
return format_html(
|
||||
'<span style="color: red; font-weight: bold;">-{} руб.</span>',
|
||||
obj.amount
|
||||
'<span style="color: red; font-weight: bold;">{} руб.</span>',
|
||||
amount
|
||||
)
|
||||
return f'{obj.amount} руб.'
|
||||
return f'{amount} руб.'
|
||||
amount_display.short_description = 'Сумма'
|
||||
|
||||
def has_add_permission(self, request):
|
||||
|
||||
Reference in New Issue
Block a user