django-cfg 1.3.7__py3-none-any.whl â 1.3.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/accounts/admin/__init__.py +24 -8
- django_cfg/apps/accounts/admin/activity_admin.py +146 -0
- django_cfg/apps/accounts/admin/filters.py +98 -22
- django_cfg/apps/accounts/admin/group_admin.py +86 -0
- django_cfg/apps/accounts/admin/inlines.py +42 -13
- django_cfg/apps/accounts/admin/otp_admin.py +115 -0
- django_cfg/apps/accounts/admin/registration_admin.py +173 -0
- django_cfg/apps/accounts/admin/resources.py +123 -19
- django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
- django_cfg/apps/accounts/admin/user_admin.py +362 -0
- django_cfg/apps/agents/admin/__init__.py +17 -4
- django_cfg/apps/agents/admin/execution_admin.py +204 -183
- django_cfg/apps/agents/admin/registry_admin.py +230 -255
- django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
- django_cfg/apps/agents/core/__init__.py +1 -1
- django_cfg/apps/agents/core/django_agent.py +221 -0
- django_cfg/apps/agents/core/exceptions.py +14 -0
- django_cfg/apps/agents/core/orchestrator.py +18 -3
- django_cfg/apps/knowbase/admin/__init__.py +1 -1
- django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
- django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
- django_cfg/apps/knowbase/admin/document_admin.py +269 -262
- django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
- django_cfg/apps/knowbase/config/settings.py +21 -4
- django_cfg/apps/knowbase/views/chat_views.py +3 -0
- django_cfg/apps/leads/admin/__init__.py +3 -1
- django_cfg/apps/leads/admin/leads_admin.py +235 -35
- django_cfg/apps/maintenance/admin/__init__.py +2 -2
- django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
- django_cfg/apps/maintenance/admin/log_admin.py +143 -61
- django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
- django_cfg/apps/maintenance/admin/site_admin.py +213 -352
- django_cfg/apps/newsletter/admin/__init__.py +29 -2
- django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
- django_cfg/apps/payments/admin/__init__.py +18 -27
- django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
- django_cfg/apps/payments/admin/balance_admin.py +166 -632
- django_cfg/apps/payments/admin/currencies_admin.py +235 -607
- django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
- django_cfg/apps/payments/admin/filters.py +83 -3
- django_cfg/apps/payments/admin/networks_admin.py +258 -0
- django_cfg/apps/payments/admin/payments_admin.py +171 -461
- django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
- django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
- django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +105 -34
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +12 -16
- django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +13 -18
- django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
- django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
- django_cfg/apps/payments/middleware/api_access.py +32 -6
- django_cfg/apps/payments/migrations/0002_currency_usd_rate_currency_usd_rate_updated_at.py +26 -0
- django_cfg/apps/payments/migrations/0003_remove_provider_currency_fields.py +28 -0
- django_cfg/apps/payments/migrations/0004_add_reserved_usd_field.py +30 -0
- django_cfg/apps/payments/models/balance.py +12 -0
- django_cfg/apps/payments/models/currencies.py +106 -32
- django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
- django_cfg/apps/payments/services/core/currency_service.py +35 -28
- django_cfg/apps/payments/services/core/payment_service.py +1 -1
- django_cfg/apps/payments/services/providers/__init__.py +3 -0
- django_cfg/apps/payments/services/providers/base.py +95 -39
- django_cfg/apps/payments/services/providers/models/__init__.py +40 -0
- django_cfg/apps/payments/services/providers/models/base.py +122 -0
- django_cfg/apps/payments/services/providers/models/providers.py +87 -0
- django_cfg/apps/payments/services/providers/models/universal.py +48 -0
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
- django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
- django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
- django_cfg/apps/payments/services/providers/{nowpayments.py â nowpayments/provider.py} +240 -209
- django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
- django_cfg/apps/payments/services/providers/registry.py +4 -32
- django_cfg/apps/payments/services/providers/sync_service.py +277 -0
- django_cfg/apps/payments/static/payments/js/api-client.js +23 -5
- django_cfg/apps/payments/static/payments/js/payment-form.js +65 -8
- django_cfg/apps/payments/tasks/__init__.py +39 -0
- django_cfg/apps/payments/tasks/types.py +73 -0
- django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
- django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
- django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
- django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
- django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
- django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
- django_cfg/apps/payments/urls_admin.py +1 -1
- django_cfg/apps/payments/views/api/currencies.py +5 -5
- django_cfg/apps/payments/views/overview/services.py +2 -2
- django_cfg/apps/payments/views/serializers/currencies.py +4 -3
- django_cfg/apps/support/admin/__init__.py +10 -1
- django_cfg/apps/support/admin/support_admin.py +338 -141
- django_cfg/apps/tasks/admin/__init__.py +11 -0
- django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +10 -5
- django_cfg/core/generation.py +1 -1
- django_cfg/management/commands/__init__.py +13 -1
- django_cfg/management/commands/app_agent_diagnose.py +470 -0
- django_cfg/management/commands/app_agent_generate.py +342 -0
- django_cfg/management/commands/app_agent_info.py +308 -0
- django_cfg/management/commands/migrate_all.py +9 -3
- django_cfg/management/commands/migrator.py +11 -6
- django_cfg/management/commands/rundramatiq.py +3 -2
- django_cfg/middleware/__init__.py +0 -2
- django_cfg/models/api_keys.py +115 -0
- django_cfg/modules/django_admin/__init__.py +64 -0
- django_cfg/modules/django_admin/decorators/__init__.py +13 -0
- django_cfg/modules/django_admin/decorators/actions.py +106 -0
- django_cfg/modules/django_admin/decorators/display.py +106 -0
- django_cfg/modules/django_admin/mixins/__init__.py +14 -0
- django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
- django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
- django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
- django_cfg/modules/django_admin/models/__init__.py +20 -0
- django_cfg/modules/django_admin/models/action_models.py +33 -0
- django_cfg/modules/django_admin/models/badge_models.py +20 -0
- django_cfg/modules/django_admin/models/base.py +26 -0
- django_cfg/modules/django_admin/models/display_models.py +31 -0
- django_cfg/modules/django_admin/utils/badges.py +159 -0
- django_cfg/modules/django_admin/utils/displays.py +247 -0
- django_cfg/modules/django_app_agent/__init__.py +87 -0
- django_cfg/modules/django_app_agent/agents/__init__.py +40 -0
- django_cfg/modules/django_app_agent/agents/base/__init__.py +24 -0
- django_cfg/modules/django_app_agent/agents/base/agent.py +354 -0
- django_cfg/modules/django_app_agent/agents/base/context.py +236 -0
- django_cfg/modules/django_app_agent/agents/base/executor.py +430 -0
- django_cfg/modules/django_app_agent/agents/generation/__init__.py +12 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/__init__.py +15 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/config_validator.py +147 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/main.py +99 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/models.py +32 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/prompt_manager.py +290 -0
- django_cfg/modules/django_app_agent/agents/interfaces.py +376 -0
- django_cfg/modules/django_app_agent/core/__init__.py +33 -0
- django_cfg/modules/django_app_agent/core/config.py +300 -0
- django_cfg/modules/django_app_agent/core/exceptions.py +359 -0
- django_cfg/modules/django_app_agent/models/__init__.py +71 -0
- django_cfg/modules/django_app_agent/models/base.py +283 -0
- django_cfg/modules/django_app_agent/models/context.py +496 -0
- django_cfg/modules/django_app_agent/models/enums.py +481 -0
- django_cfg/modules/django_app_agent/models/requests.py +500 -0
- django_cfg/modules/django_app_agent/models/responses.py +585 -0
- django_cfg/modules/django_app_agent/pytest.ini +6 -0
- django_cfg/modules/django_app_agent/services/__init__.py +42 -0
- django_cfg/modules/django_app_agent/services/app_generator/__init__.py +30 -0
- django_cfg/modules/django_app_agent/services/app_generator/ai_integration.py +133 -0
- django_cfg/modules/django_app_agent/services/app_generator/context.py +40 -0
- django_cfg/modules/django_app_agent/services/app_generator/main.py +202 -0
- django_cfg/modules/django_app_agent/services/app_generator/structure.py +316 -0
- django_cfg/modules/django_app_agent/services/app_generator/validation.py +125 -0
- django_cfg/modules/django_app_agent/services/base.py +437 -0
- django_cfg/modules/django_app_agent/services/context_builder/__init__.py +34 -0
- django_cfg/modules/django_app_agent/services/context_builder/code_extractor.py +141 -0
- django_cfg/modules/django_app_agent/services/context_builder/context_generator.py +276 -0
- django_cfg/modules/django_app_agent/services/context_builder/main.py +272 -0
- django_cfg/modules/django_app_agent/services/context_builder/models.py +40 -0
- django_cfg/modules/django_app_agent/services/context_builder/pattern_analyzer.py +85 -0
- django_cfg/modules/django_app_agent/services/project_scanner/__init__.py +31 -0
- django_cfg/modules/django_app_agent/services/project_scanner/app_discovery.py +311 -0
- django_cfg/modules/django_app_agent/services/project_scanner/main.py +221 -0
- django_cfg/modules/django_app_agent/services/project_scanner/models.py +59 -0
- django_cfg/modules/django_app_agent/services/project_scanner/pattern_detection.py +94 -0
- django_cfg/modules/django_app_agent/services/questioning_service/__init__.py +28 -0
- django_cfg/modules/django_app_agent/services/questioning_service/main.py +273 -0
- django_cfg/modules/django_app_agent/services/questioning_service/models.py +111 -0
- django_cfg/modules/django_app_agent/services/questioning_service/question_generator.py +251 -0
- django_cfg/modules/django_app_agent/services/questioning_service/response_processor.py +347 -0
- django_cfg/modules/django_app_agent/services/questioning_service/session_manager.py +356 -0
- django_cfg/modules/django_app_agent/services/report_service.py +332 -0
- django_cfg/modules/django_app_agent/services/template_manager/__init__.py +18 -0
- django_cfg/modules/django_app_agent/services/template_manager/jinja_engine.py +236 -0
- django_cfg/modules/django_app_agent/services/template_manager/main.py +159 -0
- django_cfg/modules/django_app_agent/services/template_manager/models.py +36 -0
- django_cfg/modules/django_app_agent/services/template_manager/template_loader.py +100 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/admin.py.j2 +105 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/apps.py.j2 +31 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_config.py.j2 +44 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_module.py.j2 +81 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/forms.py.j2 +107 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/models.py.j2 +139 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/serializers.py.j2 +91 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/tests.py.j2 +195 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/urls.py.j2 +35 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/views.py.j2 +211 -0
- django_cfg/modules/django_app_agent/services/template_manager/variable_processor.py +200 -0
- django_cfg/modules/django_app_agent/services/validation_service/__init__.py +25 -0
- django_cfg/modules/django_app_agent/services/validation_service/django_validator.py +333 -0
- django_cfg/modules/django_app_agent/services/validation_service/main.py +242 -0
- django_cfg/modules/django_app_agent/services/validation_service/models.py +66 -0
- django_cfg/modules/django_app_agent/services/validation_service/quality_validator.py +352 -0
- django_cfg/modules/django_app_agent/services/validation_service/security_validator.py +272 -0
- django_cfg/modules/django_app_agent/services/validation_service/syntax_validator.py +203 -0
- django_cfg/modules/django_app_agent/ui/__init__.py +25 -0
- django_cfg/modules/django_app_agent/ui/cli.py +419 -0
- django_cfg/modules/django_app_agent/ui/rich_components.py +622 -0
- django_cfg/modules/django_app_agent/utils/__init__.py +38 -0
- django_cfg/modules/django_app_agent/utils/logging.py +360 -0
- django_cfg/modules/django_app_agent/utils/validation.py +417 -0
- django_cfg/modules/django_currency/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
- django_cfg/modules/django_currency/core/converter.py +12 -12
- django_cfg/modules/django_currency/database/__init__.py +2 -2
- django_cfg/modules/django_currency/database/database_loader.py +93 -42
- django_cfg/modules/django_llm/llm/client.py +10 -2
- django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
- django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
- django_cfg/modules/django_unfold/dashboard.py +14 -13
- django_cfg/modules/django_unfold/models/config.py +1 -1
- django_cfg/registry/core.py +3 -0
- django_cfg/registry/third_party.py +2 -2
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.3.7.dist-info â django_cfg-1.3.9.dist-info}/METADATA +2 -1
- {django_cfg-1.3.7.dist-info â django_cfg-1.3.9.dist-info}/RECORD +223 -117
- django_cfg/apps/accounts/admin/activity.py +0 -96
- django_cfg/apps/accounts/admin/group.py +0 -17
- django_cfg/apps/accounts/admin/otp.py +0 -59
- django_cfg/apps/accounts/admin/registration_source.py +0 -97
- django_cfg/apps/accounts/admin/twilio_response.py +0 -227
- django_cfg/apps/accounts/admin/user.py +0 -300
- django_cfg/apps/agents/core/agent.py +0 -281
- django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
- django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
- django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
- django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
- django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
- django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
- django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
- django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
- django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
- django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
- django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
- django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
- django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
- django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
- django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
- django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
- django_cfg/apps/tasks/admin.py +0 -320
- django_cfg/middleware/static_nocache.py +0 -55
- django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
- /django_cfg/modules/{django_unfold â django_admin}/icons/README.md +0 -0
- /django_cfg/modules/{django_unfold â django_admin}/icons/__init__.py +0 -0
- /django_cfg/modules/{django_unfold â django_admin}/icons/constants.py +0 -0
- /django_cfg/modules/{django_unfold â django_admin}/icons/generate_icons.py +0 -0
- {django_cfg-1.3.7.dist-info â django_cfg-1.3.9.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.7.dist-info â django_cfg-1.3.9.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.3.7.dist-info â django_cfg-1.3.9.dist-info}/licenses/LICENSE +0 -0
@@ -1,590 +1,201 @@
|
|
1
1
|
"""
|
2
|
-
Balance Admin interfaces
|
2
|
+
Balance Admin interfaces using Django Admin Utilities.
|
3
3
|
|
4
|
-
|
4
|
+
Clean, modern admin interfaces with no HTML duplication.
|
5
5
|
"""
|
6
6
|
|
7
7
|
from django.contrib import admin
|
8
|
-
from django.utils.html import format_html
|
9
|
-
from django.contrib.humanize.templatetags.humanize import naturaltime, intcomma
|
10
|
-
from django.contrib import messages
|
11
|
-
from django.shortcuts import redirect
|
12
|
-
from django.utils.safestring import mark_safe
|
13
8
|
from django.db.models import Count, Sum, Q, Avg
|
14
9
|
from django.utils import timezone
|
15
10
|
from datetime import timedelta
|
16
11
|
from decimal import Decimal
|
17
|
-
from typing import Optional
|
18
12
|
|
19
13
|
from unfold.admin import ModelAdmin
|
20
|
-
|
21
|
-
from
|
14
|
+
|
15
|
+
from django_cfg.modules.django_admin import (
|
16
|
+
OptimizedModelAdmin,
|
17
|
+
DisplayMixin,
|
18
|
+
UserDisplayConfig,
|
19
|
+
MoneyDisplayConfig,
|
20
|
+
StatusBadgeConfig,
|
21
|
+
DateTimeDisplayConfig,
|
22
|
+
Icons,
|
23
|
+
display,
|
24
|
+
action,
|
25
|
+
ActionVariant
|
26
|
+
)
|
27
|
+
from django_cfg.modules.django_logger import get_logger
|
22
28
|
|
23
29
|
from ..models import UserBalance, Transaction
|
24
30
|
from .filters import BalanceRangeFilter, RecentActivityFilter
|
25
|
-
from django_cfg.modules.django_logger import get_logger
|
26
31
|
|
27
32
|
logger = get_logger("balance_admin")
|
28
33
|
|
29
34
|
|
30
35
|
@admin.register(UserBalance)
|
31
|
-
class UserBalanceAdmin(ModelAdmin):
|
36
|
+
class UserBalanceAdmin(OptimizedModelAdmin, DisplayMixin, ModelAdmin):
|
32
37
|
"""
|
33
|
-
|
38
|
+
UserBalance admin using Django Admin Utilities.
|
34
39
|
|
35
40
|
Features:
|
36
|
-
-
|
37
|
-
-
|
38
|
-
-
|
39
|
-
-
|
40
|
-
- Security features for balance modifications
|
41
|
+
- Clean display utilities with no HTML duplication
|
42
|
+
- Automatic query optimization
|
43
|
+
- Type-safe configuration
|
44
|
+
- Unfold integration
|
41
45
|
"""
|
42
46
|
|
43
|
-
#
|
44
|
-
|
47
|
+
# Performance optimization
|
48
|
+
select_related_fields = ['user']
|
49
|
+
annotations = {
|
50
|
+
'transaction_count': Count('user__payment_transactions')
|
51
|
+
}
|
45
52
|
|
53
|
+
# List configuration
|
46
54
|
list_display = [
|
47
55
|
'user_display',
|
48
56
|
'balance_display',
|
49
|
-
'
|
57
|
+
'status_display',
|
50
58
|
'transaction_count_display',
|
51
|
-
'
|
52
|
-
'created_at_display'
|
53
|
-
]
|
54
|
-
|
55
|
-
list_display_links = ['user_display']
|
56
|
-
|
57
|
-
search_fields = [
|
58
|
-
'user__email',
|
59
|
-
'user__first_name',
|
60
|
-
'user__last_name',
|
61
|
-
'user__username'
|
59
|
+
'updated_display'
|
62
60
|
]
|
63
61
|
|
64
62
|
list_filter = [
|
65
63
|
BalanceRangeFilter,
|
66
64
|
RecentActivityFilter,
|
67
|
-
'created_at'
|
68
|
-
]
|
69
|
-
|
70
|
-
readonly_fields = [
|
71
65
|
'created_at',
|
72
66
|
'updated_at',
|
73
|
-
'last_transaction_at'
|
74
67
|
]
|
75
68
|
|
76
|
-
|
77
|
-
|
78
|
-
'
|
79
|
-
'
|
80
|
-
'
|
81
|
-
'export_balance_report',
|
82
|
-
'send_low_balance_alerts'
|
69
|
+
search_fields = [
|
70
|
+
'user__username',
|
71
|
+
'user__email',
|
72
|
+
'user__first_name',
|
73
|
+
'user__last_name'
|
83
74
|
]
|
84
75
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
('Balance Details', {
|
90
|
-
'fields': [
|
91
|
-
'balance_usd',
|
92
|
-
'reserved_usd'
|
93
|
-
]
|
94
|
-
}),
|
95
|
-
('Activity Tracking', {
|
96
|
-
'fields': [
|
97
|
-
'last_transaction_at'
|
98
|
-
]
|
99
|
-
}),
|
100
|
-
('Timestamps', {
|
101
|
-
'fields': ['created_at', 'updated_at'],
|
102
|
-
'classes': ['collapse']
|
103
|
-
})
|
76
|
+
readonly_fields = [
|
77
|
+
'created_at',
|
78
|
+
'updated_at',
|
79
|
+
'balance_breakdown_display'
|
104
80
|
]
|
105
81
|
|
106
|
-
|
107
|
-
|
108
|
-
return super().get_queryset(request).select_related('user').annotate(
|
109
|
-
transaction_count=Count('user__transaction_set')
|
110
|
-
)
|
82
|
+
# Register actions
|
83
|
+
actions = ['reset_zero_balances']
|
111
84
|
|
112
|
-
|
85
|
+
# Display methods using Unfold features
|
86
|
+
@display(description="User", header=True)
|
113
87
|
def user_display(self, obj):
|
114
|
-
"""
|
115
|
-
if obj.user:
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
elif obj.balance_usd >= 100:
|
124
|
-
tier_icon = "đ"
|
125
|
-
tier_color = "text-blue-600"
|
126
|
-
tier_name = "Premium"
|
127
|
-
elif obj.balance_usd >= 10:
|
128
|
-
tier_icon = "đ°"
|
129
|
-
tier_color = "text-green-600"
|
130
|
-
tier_name = "Active"
|
131
|
-
elif obj.balance_usd > 0:
|
132
|
-
tier_icon = "đĒ"
|
133
|
-
tier_color = "text-yellow-600"
|
134
|
-
tier_name = "Basic"
|
135
|
-
else:
|
136
|
-
tier_icon = "đ¸"
|
137
|
-
tier_color = "text-gray-600"
|
138
|
-
tier_name = "Empty"
|
139
|
-
|
140
|
-
return format_html(
|
141
|
-
'<div class="flex items-center space-x-3">'
|
142
|
-
'<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white text-sm font-bold">'
|
143
|
-
'{}'
|
144
|
-
'</div>'
|
145
|
-
'<div>'
|
146
|
-
'<div class="font-medium text-gray-900 dark:text-gray-100">{}</div>'
|
147
|
-
'<div class="text-xs text-gray-500">{}</div>'
|
148
|
-
'<div class="text-xs {}"><span class="mr-1">{}</span>{}</div>'
|
149
|
-
'</div>'
|
150
|
-
'</div>',
|
151
|
-
display_name[0].upper() if display_name else 'U',
|
152
|
-
display_name,
|
153
|
-
obj.user.email,
|
154
|
-
tier_color,
|
155
|
-
tier_icon,
|
156
|
-
tier_name
|
157
|
-
)
|
158
|
-
return format_html('<span class="text-gray-500">No user</span>')
|
88
|
+
"""User display with avatar using Unfold header feature."""
|
89
|
+
if not obj.user:
|
90
|
+
return ["No user", "", ""]
|
91
|
+
|
92
|
+
return [
|
93
|
+
obj.user.get_full_name() or obj.user.username,
|
94
|
+
obj.user.email,
|
95
|
+
obj.user.get_full_name()[:2].upper() if obj.user.get_full_name() else obj.user.username[:2].upper()
|
96
|
+
]
|
159
97
|
|
160
|
-
@display(description="Balance", ordering=
|
98
|
+
@display(description="Balance", ordering="balance_usd")
|
161
99
|
def balance_display(self, obj):
|
162
|
-
"""
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
if balance < 0:
|
169
|
-
balance_color = "text-red-600 dark:text-red-400"
|
170
|
-
balance_icon = "â ī¸"
|
171
|
-
elif balance == 0:
|
172
|
-
balance_color = "text-gray-600 dark:text-gray-400"
|
173
|
-
balance_icon = "đ¸"
|
174
|
-
elif balance < 10:
|
175
|
-
balance_color = "text-yellow-600 dark:text-yellow-400"
|
176
|
-
balance_icon = "đĒ"
|
177
|
-
elif balance < 100:
|
178
|
-
balance_color = "text-green-600 dark:text-green-400"
|
179
|
-
balance_icon = "đ°"
|
180
|
-
else:
|
181
|
-
balance_color = "text-blue-600 dark:text-blue-400"
|
182
|
-
balance_icon = "đ"
|
183
|
-
|
184
|
-
html = f'''
|
185
|
-
<div class="text-right">
|
186
|
-
<div class="font-bold text-lg {balance_color}">
|
187
|
-
<span class="mr-1">{balance_icon}</span>${balance:,.2f}
|
188
|
-
</div>
|
189
|
-
'''
|
190
|
-
|
191
|
-
if reserved > 0:
|
192
|
-
html += f'''
|
193
|
-
<div class="text-xs text-orange-600 dark:text-orange-400">
|
194
|
-
Reserved: ${reserved:,.2f}
|
195
|
-
</div>
|
196
|
-
<div class="text-xs text-gray-500">
|
197
|
-
Available: ${available:,.2f}
|
198
|
-
</div>
|
199
|
-
'''
|
200
|
-
|
201
|
-
html += '</div>'
|
202
|
-
|
203
|
-
return format_html(html)
|
100
|
+
"""Balance display using utilities."""
|
101
|
+
return self.display_money_amount(
|
102
|
+
obj,
|
103
|
+
'balance_usd',
|
104
|
+
MoneyDisplayConfig(currency="USD", show_sign=False)
|
105
|
+
)
|
204
106
|
|
205
|
-
@display(description="Status"
|
206
|
-
|
207
|
-
""
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
if
|
214
|
-
|
215
|
-
elif
|
216
|
-
|
217
|
-
elif
|
218
|
-
|
107
|
+
@display(description="Status", label={
|
108
|
+
"Empty": "danger",
|
109
|
+
"Low Balance": "warning",
|
110
|
+
"Active": "success",
|
111
|
+
"High Balance": "info"
|
112
|
+
})
|
113
|
+
def status_display(self, obj):
|
114
|
+
"""Status display using Unfold label feature."""
|
115
|
+
if obj.balance_usd <= 0:
|
116
|
+
return "Empty"
|
117
|
+
elif obj.balance_usd < 10:
|
118
|
+
return "Low Balance"
|
119
|
+
elif obj.balance_usd < 100:
|
120
|
+
return "Active"
|
219
121
|
else:
|
220
|
-
|
221
|
-
|
222
|
-
if reserved > 0:
|
223
|
-
badges.append('<span class="inline-flex items-center rounded-full bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-800 dark:bg-orange-900 dark:text-orange-200">đ Reserved</span>')
|
224
|
-
|
225
|
-
return format_html('<div class="space-y-1">{}</div>', ''.join(badges))
|
122
|
+
return "High Balance"
|
226
123
|
|
227
124
|
@display(description="Transactions")
|
228
125
|
def transaction_count_display(self, obj):
|
229
|
-
"""
|
126
|
+
"""Transaction count using utilities."""
|
230
127
|
count = getattr(obj, 'transaction_count', 0)
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
recent_count = Transaction.objects.filter(
|
236
|
-
user=obj.user,
|
237
|
-
created_at__gte=recent_threshold
|
238
|
-
).count()
|
239
|
-
|
240
|
-
return format_html(
|
241
|
-
'<div class="text-center">'
|
242
|
-
'<div class="font-bold text-blue-600 dark:text-blue-400">{}</div>'
|
243
|
-
'<div class="text-xs text-gray-500">total</div>'
|
244
|
-
'{}'
|
245
|
-
'</div>',
|
246
|
-
count,
|
247
|
-
f'<div class="text-xs text-green-600 dark:text-green-400">{recent_count} recent</div>' if recent_count > 0 else ''
|
248
|
-
)
|
249
|
-
|
250
|
-
return format_html(
|
251
|
-
'<div class="text-center text-gray-500">'
|
252
|
-
'<div>0</div>'
|
253
|
-
'<div class="text-xs">No transactions</div>'
|
254
|
-
'</div>'
|
128
|
+
return self.display_count_simple(
|
129
|
+
obj,
|
130
|
+
'transaction_count',
|
131
|
+
'transactions'
|
255
132
|
)
|
256
133
|
|
257
|
-
@display(description="
|
258
|
-
def
|
259
|
-
"""
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
color = "text-green-600 dark:text-green-400"
|
265
|
-
icon = "đĸ"
|
266
|
-
elif time_ago < timedelta(days=1):
|
267
|
-
color = "text-yellow-600 dark:text-yellow-400"
|
268
|
-
icon = "đĄ"
|
269
|
-
elif time_ago < timedelta(days=7):
|
270
|
-
color = "text-orange-600 dark:text-orange-400"
|
271
|
-
icon = "đ "
|
272
|
-
else:
|
273
|
-
color = "text-red-600 dark:text-red-400"
|
274
|
-
icon = "đ´"
|
275
|
-
|
276
|
-
return format_html(
|
277
|
-
'<div class="text-xs {}">'
|
278
|
-
'<span class="mr-1">{}</span>{}'
|
279
|
-
'</div>',
|
280
|
-
color,
|
281
|
-
icon,
|
282
|
-
naturaltime(obj.last_transaction_at)
|
283
|
-
)
|
284
|
-
|
285
|
-
return format_html(
|
286
|
-
'<div class="text-xs text-gray-500">Never</div>'
|
134
|
+
@display(description="Updated")
|
135
|
+
def updated_display(self, obj):
|
136
|
+
"""Updated time using utilities."""
|
137
|
+
return self.display_datetime_relative(
|
138
|
+
obj,
|
139
|
+
'updated_at',
|
140
|
+
DateTimeDisplayConfig(show_relative=True, show_seconds=False)
|
287
141
|
)
|
288
142
|
|
289
|
-
|
290
|
-
def
|
291
|
-
"""
|
292
|
-
|
293
|
-
|
294
|
-
'<div>{}</div>'
|
295
|
-
'<div class="text-gray-500">{}</div>'
|
296
|
-
'</div>',
|
297
|
-
obj.created_at.strftime('%Y-%m-%d'),
|
298
|
-
naturaltime(obj.created_at)
|
299
|
-
)
|
300
|
-
|
301
|
-
def changelist_view(self, request, extra_context=None):
|
302
|
-
"""Add balance statistics to changelist context."""
|
303
|
-
extra_context = extra_context or {}
|
304
|
-
|
305
|
-
try:
|
306
|
-
# Basic statistics
|
307
|
-
total_balances = UserBalance.objects.count()
|
308
|
-
|
309
|
-
# Balance statistics
|
310
|
-
balance_stats = UserBalance.objects.aggregate(
|
311
|
-
total_balance=Sum('balance_usd'),
|
312
|
-
avg_balance=Avg('balance_usd'),
|
313
|
-
total_reserved=Sum('reserved_usd')
|
314
|
-
)
|
315
|
-
|
316
|
-
# Balance distribution
|
317
|
-
zero_balances = UserBalance.objects.filter(balance_usd=0).count()
|
318
|
-
negative_balances = UserBalance.objects.filter(balance_usd__lt=0).count()
|
319
|
-
low_balances = UserBalance.objects.filter(balance_usd__gt=0, balance_usd__lt=10).count()
|
320
|
-
medium_balances = UserBalance.objects.filter(balance_usd__gte=10, balance_usd__lt=100).count()
|
321
|
-
high_balances = UserBalance.objects.filter(balance_usd__gte=100, balance_usd__lt=1000).count()
|
322
|
-
whale_balances = UserBalance.objects.filter(balance_usd__gte=1000).count()
|
323
|
-
|
324
|
-
# Recent activity
|
325
|
-
recent_threshold = timezone.now() - timedelta(days=7)
|
326
|
-
active_balances = UserBalance.objects.filter(
|
327
|
-
last_transaction_at__gte=recent_threshold
|
328
|
-
).count()
|
329
|
-
|
330
|
-
# Top balances
|
331
|
-
top_balances = UserBalance.objects.filter(
|
332
|
-
balance_usd__gt=0
|
333
|
-
).order_by('-balance_usd')[:5]
|
334
|
-
|
335
|
-
extra_context.update({
|
336
|
-
'balance_stats': {
|
337
|
-
'total_balances': total_balances,
|
338
|
-
'total_balance': balance_stats['total_balance'] or 0,
|
339
|
-
'avg_balance': balance_stats['avg_balance'] or 0,
|
340
|
-
'total_reserved': balance_stats['total_reserved'] or 0,
|
341
|
-
'zero_balances': zero_balances,
|
342
|
-
'negative_balances': negative_balances,
|
343
|
-
'low_balances': low_balances,
|
344
|
-
'medium_balances': medium_balances,
|
345
|
-
'high_balances': high_balances,
|
346
|
-
'whale_balances': whale_balances,
|
347
|
-
'active_balances': active_balances,
|
348
|
-
'top_balances': top_balances,
|
349
|
-
}
|
350
|
-
})
|
351
|
-
|
352
|
-
except Exception as e:
|
353
|
-
logger.warning(f"Failed to generate balance statistics: {e}")
|
354
|
-
extra_context['balance_stats'] = None
|
355
|
-
|
356
|
-
return super().changelist_view(request, extra_context)
|
357
|
-
|
358
|
-
# ===== ADMIN ACTIONS =====
|
359
|
-
|
360
|
-
@action(
|
361
|
-
description="đ° Add Funds (Bulk)",
|
362
|
-
icon="add_circle",
|
363
|
-
variant=ActionVariant.SUCCESS
|
364
|
-
)
|
365
|
-
def add_funds_bulk(self, request, queryset):
|
366
|
-
"""Add funds to selected user balances."""
|
367
|
-
|
368
|
-
# This would typically show a form for amount input
|
369
|
-
# For now, we'll add a fixed amount as an example
|
370
|
-
amount = Decimal('10.00') # In production, this should come from a form
|
143
|
+
# Readonly field displays
|
144
|
+
def balance_breakdown_display(self, obj):
|
145
|
+
"""Detailed balance breakdown for detail view."""
|
146
|
+
if not obj.pk:
|
147
|
+
return "Save to see breakdown"
|
371
148
|
|
372
|
-
|
149
|
+
breakdown_items = []
|
373
150
|
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
description=f'Bulk funds addition by admin {request.user.username}'
|
382
|
-
)
|
383
|
-
updated_count += 1
|
384
|
-
|
385
|
-
except Exception as e:
|
386
|
-
logger.error(f"Failed to add funds to user {balance.user.id}: {e}")
|
151
|
+
# Main balance
|
152
|
+
if hasattr(obj, 'reserved_usd') and obj.reserved_usd:
|
153
|
+
available = obj.balance_usd - obj.reserved_usd
|
154
|
+
breakdown_items = [
|
155
|
+
{'label': 'Reserved', 'amount': obj.reserved_usd, 'color': 'warning'},
|
156
|
+
{'label': 'Available', 'amount': available, 'color': 'success'}
|
157
|
+
]
|
387
158
|
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
)
|
393
|
-
|
394
|
-
request,
|
395
|
-
"âšī¸ All transactions have been logged for audit purposes"
|
396
|
-
)
|
159
|
+
from django_cfg.modules.django_admin.utils.displays import MoneyDisplay
|
160
|
+
return MoneyDisplay.with_breakdown(
|
161
|
+
obj.balance_usd,
|
162
|
+
breakdown_items,
|
163
|
+
MoneyDisplayConfig(currency="USD")
|
164
|
+
)
|
397
165
|
|
398
|
-
|
399
|
-
description="đ¸ Subtract Funds (Bulk)",
|
400
|
-
icon="remove_circle",
|
401
|
-
variant=ActionVariant.WARNING
|
402
|
-
)
|
403
|
-
def subtract_funds_bulk(self, request, queryset):
|
404
|
-
"""Subtract funds from selected user balances."""
|
405
|
-
|
406
|
-
amount = Decimal('5.00') # In production, this should come from a form
|
407
|
-
|
408
|
-
updated_count = 0
|
409
|
-
insufficient_funds = 0
|
410
|
-
|
411
|
-
for balance in queryset:
|
412
|
-
try:
|
413
|
-
if balance.balance_usd >= amount:
|
414
|
-
UserBalance.objects.subtract_funds_from_user(
|
415
|
-
user=balance.user,
|
416
|
-
amount=amount,
|
417
|
-
transaction_type='admin_adjustment',
|
418
|
-
description=f'Bulk funds subtraction by admin {request.user.username}'
|
419
|
-
)
|
420
|
-
updated_count += 1
|
421
|
-
else:
|
422
|
-
insufficient_funds += 1
|
423
|
-
|
424
|
-
except Exception as e:
|
425
|
-
logger.error(f"Failed to subtract funds from user {balance.user.id}: {e}")
|
426
|
-
|
427
|
-
if updated_count > 0:
|
428
|
-
messages.success(
|
429
|
-
request,
|
430
|
-
f"đ¸ Subtracted ${amount} from {updated_count} user balances"
|
431
|
-
)
|
432
|
-
|
433
|
-
if insufficient_funds > 0:
|
434
|
-
messages.warning(
|
435
|
-
request,
|
436
|
-
f"â ī¸ Skipped {insufficient_funds} users with insufficient funds"
|
437
|
-
)
|
166
|
+
balance_breakdown_display.short_description = "Balance Breakdown"
|
438
167
|
|
439
|
-
|
440
|
-
|
441
|
-
icon="refresh",
|
442
|
-
variant=ActionVariant.INFO
|
443
|
-
)
|
168
|
+
# Actions using utilities
|
169
|
+
@action(description="Reset zero balances", variant=ActionVariant.WARNING)
|
444
170
|
def reset_zero_balances(self, request, queryset):
|
445
|
-
"""Reset
|
446
|
-
|
447
|
-
|
448
|
-
reset_count = 0
|
449
|
-
|
450
|
-
for balance in zero_balances:
|
451
|
-
if balance.reserved_usd and balance.reserved_usd > 0:
|
452
|
-
balance.reserved_usd = 0
|
453
|
-
balance.save(update_fields=['reserved_usd'])
|
454
|
-
reset_count += 1
|
455
|
-
|
456
|
-
if reset_count > 0:
|
457
|
-
messages.success(
|
458
|
-
request,
|
459
|
-
f"đ Reset reserved amounts for {reset_count} zero balances"
|
460
|
-
)
|
461
|
-
else:
|
462
|
-
messages.info(
|
463
|
-
request,
|
464
|
-
"âšī¸ No zero balances with reserved amounts found"
|
465
|
-
)
|
466
|
-
|
467
|
-
@action(
|
468
|
-
description="đ Export Balance Report",
|
469
|
-
icon="download",
|
470
|
-
variant=ActionVariant.INFO
|
471
|
-
)
|
472
|
-
def export_balance_report(self, request, queryset):
|
473
|
-
"""Export balance report to CSV."""
|
474
|
-
|
475
|
-
import csv
|
476
|
-
from django.http import HttpResponse
|
477
|
-
|
478
|
-
response = HttpResponse(content_type='text/csv')
|
479
|
-
response['Content-Disposition'] = f'attachment; filename="balance_report_{timezone.now().strftime("%Y%m%d_%H%M%S")}.csv"'
|
480
|
-
|
481
|
-
writer = csv.writer(response)
|
482
|
-
writer.writerow([
|
483
|
-
'User Email', 'User Name', 'Balance USD', 'Reserved USD', 'Available USD',
|
484
|
-
'Last Transaction', 'Created', 'Status'
|
485
|
-
])
|
486
|
-
|
487
|
-
for balance in queryset:
|
488
|
-
available = balance.balance_usd - (balance.reserved_usd or 0)
|
489
|
-
|
490
|
-
if balance.balance_usd < 0:
|
491
|
-
status = 'Negative'
|
492
|
-
elif balance.balance_usd == 0:
|
493
|
-
status = 'Empty'
|
494
|
-
elif balance.balance_usd < 10:
|
495
|
-
status = 'Low'
|
496
|
-
elif balance.balance_usd < 100:
|
497
|
-
status = 'Medium'
|
498
|
-
else:
|
499
|
-
status = 'High'
|
500
|
-
|
501
|
-
writer.writerow([
|
502
|
-
balance.user.email if balance.user else '',
|
503
|
-
balance.user.get_full_name() if balance.user else '',
|
504
|
-
balance.balance_usd,
|
505
|
-
balance.reserved_usd or 0,
|
506
|
-
available,
|
507
|
-
balance.last_transaction_at.isoformat() if balance.last_transaction_at else '',
|
508
|
-
balance.created_at.isoformat(),
|
509
|
-
status
|
510
|
-
])
|
511
|
-
|
512
|
-
messages.success(
|
171
|
+
"""Reset balances that are zero."""
|
172
|
+
updated = queryset.filter(balance_usd=0).update(reserved_usd=0)
|
173
|
+
self.message_user(
|
513
174
|
request,
|
514
|
-
f"
|
515
|
-
|
516
|
-
|
517
|
-
return response
|
518
|
-
|
519
|
-
@action(
|
520
|
-
description="đ Send Low Balance Alerts",
|
521
|
-
icon="notifications",
|
522
|
-
variant=ActionVariant.WARNING
|
523
|
-
)
|
524
|
-
def send_low_balance_alerts(self, request, queryset):
|
525
|
-
"""Send alerts for low balance users."""
|
526
|
-
|
527
|
-
low_balance_users = queryset.filter(
|
528
|
-
balance_usd__gt=0,
|
529
|
-
balance_usd__lt=10
|
175
|
+
f"Successfully reset {updated} zero balance(s).",
|
176
|
+
level='WARNING'
|
530
177
|
)
|
531
|
-
|
532
|
-
alert_count = 0
|
533
|
-
|
534
|
-
for balance in low_balance_users:
|
535
|
-
try:
|
536
|
-
# In production, this would send an actual notification
|
537
|
-
# For now, we'll just log it
|
538
|
-
logger.info(
|
539
|
-
f"Low balance alert for user {balance.user.email}: ${balance.balance_usd}"
|
540
|
-
)
|
541
|
-
alert_count += 1
|
542
|
-
|
543
|
-
except Exception as e:
|
544
|
-
logger.error(f"Failed to send alert to user {balance.user.id}: {e}")
|
545
|
-
|
546
|
-
if alert_count > 0:
|
547
|
-
messages.success(
|
548
|
-
request,
|
549
|
-
f"đ Sent low balance alerts to {alert_count} users"
|
550
|
-
)
|
551
|
-
else:
|
552
|
-
messages.info(
|
553
|
-
request,
|
554
|
-
"âšī¸ No users with low balances found in selection"
|
555
|
-
)
|
556
178
|
|
557
179
|
|
558
180
|
@admin.register(Transaction)
|
559
|
-
class TransactionAdmin(ModelAdmin):
|
181
|
+
class TransactionAdmin(OptimizedModelAdmin, DisplayMixin, ModelAdmin):
|
560
182
|
"""
|
561
|
-
Transaction admin
|
183
|
+
Transaction admin using Django Admin Utilities.
|
562
184
|
|
563
|
-
|
564
|
-
- Comprehensive transaction history
|
565
|
-
- Financial audit trail
|
566
|
-
- Transaction type filtering
|
567
|
-
- Balance impact visualization
|
185
|
+
Clean interface for transaction management.
|
568
186
|
"""
|
569
187
|
|
188
|
+
# Performance optimization
|
189
|
+
select_related_fields = ['user']
|
190
|
+
|
191
|
+
# List configuration
|
570
192
|
list_display = [
|
571
193
|
'transaction_id_display',
|
572
194
|
'user_display',
|
573
|
-
'transaction_type_badge',
|
574
195
|
'amount_display',
|
575
|
-
'
|
576
|
-
'
|
577
|
-
'
|
578
|
-
]
|
579
|
-
|
580
|
-
list_display_links = ['transaction_id_display']
|
581
|
-
|
582
|
-
search_fields = [
|
583
|
-
'id',
|
584
|
-
'user__email',
|
585
|
-
'user__username',
|
586
|
-
'description',
|
587
|
-
'payment_id'
|
196
|
+
'type_display',
|
197
|
+
'status_display',
|
198
|
+
'created_display'
|
588
199
|
]
|
589
200
|
|
590
201
|
list_filter = [
|
@@ -593,137 +204,60 @@ class TransactionAdmin(ModelAdmin):
|
|
593
204
|
'created_at'
|
594
205
|
]
|
595
206
|
|
596
|
-
|
207
|
+
search_fields = [
|
597
208
|
'id',
|
598
|
-
'
|
599
|
-
'
|
600
|
-
'
|
601
|
-
'balance_after',
|
602
|
-
'payment_id',
|
603
|
-
'description',
|
604
|
-
'created_at'
|
209
|
+
'user__username',
|
210
|
+
'user__email',
|
211
|
+
'description'
|
605
212
|
]
|
606
213
|
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
"""Disable changing transactions (audit trail integrity)."""
|
613
|
-
return False
|
614
|
-
|
615
|
-
def has_delete_permission(self, request, obj=None):
|
616
|
-
"""Disable deleting transactions (audit trail integrity)."""
|
617
|
-
return False
|
214
|
+
readonly_fields = [
|
215
|
+
'id',
|
216
|
+
'created_at',
|
217
|
+
'updated_at'
|
218
|
+
]
|
618
219
|
|
619
|
-
|
220
|
+
# Display methods
|
221
|
+
@display(description="ID")
|
620
222
|
def transaction_id_display(self, obj):
|
621
|
-
"""
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
'title="Full ID: {}">{}</span>',
|
626
|
-
obj.id,
|
627
|
-
short_id
|
223
|
+
"""Transaction ID display."""
|
224
|
+
return StatusBadge.create(
|
225
|
+
text=str(obj.id)[:8] + "...",
|
226
|
+
variant="info"
|
628
227
|
)
|
629
228
|
|
630
|
-
@display(description="User"
|
229
|
+
@display(description="User")
|
631
230
|
def user_display(self, obj):
|
632
|
-
"""
|
633
|
-
|
634
|
-
return format_html(
|
635
|
-
'<div>'
|
636
|
-
'<div class="font-medium">{}</div>'
|
637
|
-
'<div class="text-xs text-gray-500">{}</div>'
|
638
|
-
'</div>',
|
639
|
-
obj.user.get_full_name() or obj.user.username,
|
640
|
-
obj.user.email
|
641
|
-
)
|
642
|
-
return format_html('<span class="text-gray-500">No user</span>')
|
643
|
-
|
644
|
-
@display(description="Type", ordering='transaction_type')
|
645
|
-
def transaction_type_badge(self, obj):
|
646
|
-
"""Display transaction type with colored badge."""
|
647
|
-
type_config = {
|
648
|
-
'payment': ('đŗ', 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200', 'Payment'),
|
649
|
-
'deposit': ('đ°', 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200', 'Deposit'),
|
650
|
-
'withdrawal': ('đ¸', 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200', 'Withdrawal'),
|
651
|
-
'refund': ('âŠī¸', 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200', 'Refund'),
|
652
|
-
'admin_adjustment': ('âī¸', 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200', 'Admin'),
|
653
|
-
'fee': ('đ', 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200', 'Fee'),
|
654
|
-
}
|
655
|
-
|
656
|
-
icon, color_class, label = type_config.get(
|
657
|
-
obj.transaction_type,
|
658
|
-
('â', 'bg-gray-100 text-gray-800', obj.transaction_type.title())
|
659
|
-
)
|
660
|
-
|
661
|
-
return format_html(
|
662
|
-
'<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {}">'
|
663
|
-
'{} {}'
|
664
|
-
'</span>',
|
665
|
-
color_class,
|
666
|
-
icon,
|
667
|
-
label
|
668
|
-
)
|
231
|
+
"""User display."""
|
232
|
+
return self.display_user_simple(obj, 'user')
|
669
233
|
|
670
|
-
@display(description="Amount"
|
234
|
+
@display(description="Amount")
|
671
235
|
def amount_display(self, obj):
|
672
|
-
"""
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
'<span class="font-bold text-green-600 dark:text-green-400">+${:,.2f}</span>',
|
678
|
-
amount
|
679
|
-
)
|
680
|
-
elif amount < 0:
|
681
|
-
return format_html(
|
682
|
-
'<span class="font-bold text-red-600 dark:text-red-400">-${:,.2f}</span>',
|
683
|
-
abs(amount)
|
684
|
-
)
|
685
|
-
else:
|
686
|
-
return format_html(
|
687
|
-
'<span class="font-bold text-gray-600 dark:text-gray-400">${:,.2f}</span>',
|
688
|
-
amount
|
689
|
-
)
|
690
|
-
|
691
|
-
@display(description="Balance Impact")
|
692
|
-
def balance_impact_display(self, obj):
|
693
|
-
"""Display balance before/after transaction."""
|
694
|
-
# Calculate balance_before from balance_after and amount_usd
|
695
|
-
balance_before = obj.balance_after - obj.amount_usd
|
696
|
-
|
697
|
-
return format_html(
|
698
|
-
'<div class="text-xs">'
|
699
|
-
'<div>Before: <span class="font-mono">${:,.2f}</span></div>'
|
700
|
-
'<div>After: <span class="font-mono">${:,.2f}</span></div>'
|
701
|
-
'</div>',
|
702
|
-
balance_before,
|
703
|
-
obj.balance_after
|
236
|
+
"""Amount display with sign."""
|
237
|
+
return self.display_money_amount(
|
238
|
+
obj,
|
239
|
+
'amount_usd',
|
240
|
+
MoneyDisplayConfig(currency="USD", show_sign=True)
|
704
241
|
)
|
705
242
|
|
706
|
-
@display(description="
|
707
|
-
def
|
708
|
-
"""
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
'đ Payment'
|
714
|
-
'</a>',
|
715
|
-
obj.payment_id
|
716
|
-
)
|
717
|
-
return format_html('<span class="text-gray-500">â</span>')
|
243
|
+
@display(description="Type", label=True)
|
244
|
+
def type_display(self, obj):
|
245
|
+
"""Transaction type display."""
|
246
|
+
return self.display_status_auto(
|
247
|
+
type('obj', (), {'status': obj.transaction_type})(),
|
248
|
+
'status'
|
249
|
+
)
|
718
250
|
|
719
|
-
@display(description="
|
720
|
-
def
|
721
|
-
"""
|
722
|
-
|
723
|
-
|
724
|
-
'
|
725
|
-
'
|
726
|
-
'</div>',
|
727
|
-
obj.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
728
|
-
naturaltime(obj.created_at)
|
251
|
+
@display(description="Status", label=True)
|
252
|
+
def status_display(self, obj):
|
253
|
+
"""Status display."""
|
254
|
+
# Transaction model doesn't have status field, show type instead
|
255
|
+
return self.display_status_auto(
|
256
|
+
type('obj', (), {'status': obj.transaction_type})(),
|
257
|
+
'status'
|
729
258
|
)
|
259
|
+
|
260
|
+
@display(description="Created")
|
261
|
+
def created_display(self, obj):
|
262
|
+
"""Created time display."""
|
263
|
+
return self.display_datetime_compact(obj, 'created_at')
|