django-cfg 1.3.7__py3-none-any.whl → 1.3.11__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.
Files changed (246) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/admin/__init__.py +24 -8
  3. django_cfg/apps/accounts/admin/activity_admin.py +146 -0
  4. django_cfg/apps/accounts/admin/filters.py +98 -22
  5. django_cfg/apps/accounts/admin/group_admin.py +86 -0
  6. django_cfg/apps/accounts/admin/inlines.py +42 -13
  7. django_cfg/apps/accounts/admin/otp_admin.py +115 -0
  8. django_cfg/apps/accounts/admin/registration_admin.py +173 -0
  9. django_cfg/apps/accounts/admin/resources.py +123 -19
  10. django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
  11. django_cfg/apps/accounts/admin/user_admin.py +362 -0
  12. django_cfg/apps/agents/admin/__init__.py +17 -4
  13. django_cfg/apps/agents/admin/execution_admin.py +204 -183
  14. django_cfg/apps/agents/admin/registry_admin.py +230 -255
  15. django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
  16. django_cfg/apps/agents/core/__init__.py +1 -1
  17. django_cfg/apps/agents/core/django_agent.py +221 -0
  18. django_cfg/apps/agents/core/exceptions.py +14 -0
  19. django_cfg/apps/agents/core/orchestrator.py +18 -3
  20. django_cfg/apps/knowbase/admin/__init__.py +1 -1
  21. django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
  22. django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
  23. django_cfg/apps/knowbase/admin/document_admin.py +269 -262
  24. django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
  25. django_cfg/apps/knowbase/config/settings.py +21 -4
  26. django_cfg/apps/knowbase/views/chat_views.py +3 -0
  27. django_cfg/apps/leads/admin/__init__.py +3 -1
  28. django_cfg/apps/leads/admin/leads_admin.py +235 -35
  29. django_cfg/apps/maintenance/admin/__init__.py +2 -2
  30. django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
  31. django_cfg/apps/maintenance/admin/log_admin.py +143 -61
  32. django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
  33. django_cfg/apps/maintenance/admin/site_admin.py +213 -352
  34. django_cfg/apps/newsletter/admin/__init__.py +29 -2
  35. django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
  36. django_cfg/apps/payments/admin/__init__.py +18 -27
  37. django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
  38. django_cfg/apps/payments/admin/balance_admin.py +166 -632
  39. django_cfg/apps/payments/admin/currencies_admin.py +235 -607
  40. django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
  41. django_cfg/apps/payments/admin/filters.py +83 -3
  42. django_cfg/apps/payments/admin/networks_admin.py +269 -0
  43. django_cfg/apps/payments/admin/payments_admin.py +183 -460
  44. django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
  45. django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
  46. django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +153 -34
  47. django_cfg/apps/payments/admin_interface/templates/payments/components/payment_card.html +121 -0
  48. django_cfg/apps/payments/admin_interface/templates/payments/components/payment_qr_code.html +95 -0
  49. django_cfg/apps/payments/admin_interface/templates/payments/components/progress_bar.html +37 -0
  50. django_cfg/apps/payments/admin_interface/templates/payments/components/provider_stats.html +60 -0
  51. django_cfg/apps/payments/admin_interface/templates/payments/components/status_badge.html +41 -0
  52. django_cfg/apps/payments/admin_interface/templates/payments/components/status_overview.html +83 -0
  53. django_cfg/apps/payments/admin_interface/templates/payments/payment_detail.html +363 -0
  54. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +43 -17
  55. django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
  56. django_cfg/apps/payments/admin_interface/views/api/payments.py +102 -0
  57. django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +109 -63
  58. django_cfg/apps/payments/admin_interface/views/forms.py +5 -1
  59. django_cfg/apps/payments/config/__init__.py +14 -15
  60. django_cfg/apps/payments/config/django_cfg_integration.py +59 -1
  61. django_cfg/apps/payments/config/helpers.py +8 -13
  62. django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
  63. django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
  64. django_cfg/apps/payments/middleware/api_access.py +32 -6
  65. django_cfg/apps/payments/migrations/0001_initial.py +33 -46
  66. django_cfg/apps/payments/migrations/0002_rename_payments_un_user_id_7f6e79_idx_payments_un_user_id_8ce187_idx_and_more.py +46 -0
  67. django_cfg/apps/payments/migrations/0003_universalpayment_status_changed_at.py +25 -0
  68. django_cfg/apps/payments/models/balance.py +12 -0
  69. django_cfg/apps/payments/models/currencies.py +106 -32
  70. django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
  71. django_cfg/apps/payments/models/managers/payment_managers.py +142 -25
  72. django_cfg/apps/payments/models/payments.py +94 -0
  73. django_cfg/apps/payments/services/core/base.py +4 -4
  74. django_cfg/apps/payments/services/core/currency_service.py +35 -28
  75. django_cfg/apps/payments/services/core/payment_service.py +266 -39
  76. django_cfg/apps/payments/services/providers/__init__.py +3 -0
  77. django_cfg/apps/payments/services/providers/base.py +303 -41
  78. django_cfg/apps/payments/services/providers/models/__init__.py +42 -0
  79. django_cfg/apps/payments/services/providers/models/base.py +145 -0
  80. django_cfg/apps/payments/services/providers/models/providers.py +87 -0
  81. django_cfg/apps/payments/services/providers/models/universal.py +48 -0
  82. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
  83. django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
  84. django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
  85. django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
  86. django_cfg/apps/payments/services/providers/nowpayments/provider.py +557 -0
  87. django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
  88. django_cfg/apps/payments/services/providers/registry.py +9 -37
  89. django_cfg/apps/payments/services/providers/sync_service.py +277 -0
  90. django_cfg/apps/payments/services/types/requests.py +19 -7
  91. django_cfg/apps/payments/signals/payment_signals.py +31 -2
  92. django_cfg/apps/payments/static/payments/js/api-client.js +29 -6
  93. django_cfg/apps/payments/static/payments/js/payment-detail.js +167 -0
  94. django_cfg/apps/payments/static/payments/js/payment-form.js +98 -32
  95. django_cfg/apps/payments/tasks/__init__.py +39 -0
  96. django_cfg/apps/payments/tasks/types.py +73 -0
  97. django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
  98. django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
  99. django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
  100. django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
  101. django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
  102. django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
  103. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
  104. django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
  105. django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
  106. django_cfg/apps/payments/templatetags/payment_tags.py +8 -0
  107. django_cfg/apps/payments/urls.py +3 -2
  108. django_cfg/apps/payments/urls_admin.py +1 -1
  109. django_cfg/apps/payments/views/api/currencies.py +8 -5
  110. django_cfg/apps/payments/views/overview/services.py +2 -2
  111. django_cfg/apps/payments/views/serializers/currencies.py +22 -8
  112. django_cfg/apps/support/admin/__init__.py +10 -1
  113. django_cfg/apps/support/admin/support_admin.py +338 -141
  114. django_cfg/apps/tasks/admin/__init__.py +11 -0
  115. django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
  116. django_cfg/apps/tasks/static/tasks/css/dashboard.css +68 -217
  117. django_cfg/apps/tasks/static/tasks/js/api.js +40 -84
  118. django_cfg/apps/tasks/static/tasks/js/components/DataManager.js +24 -0
  119. django_cfg/apps/tasks/static/tasks/js/components/TabManager.js +85 -0
  120. django_cfg/apps/tasks/static/tasks/js/components/TaskRenderer.js +216 -0
  121. django_cfg/apps/tasks/static/tasks/js/dashboard/main.mjs +245 -0
  122. django_cfg/apps/tasks/static/tasks/js/dashboard/overview.mjs +123 -0
  123. django_cfg/apps/tasks/static/tasks/js/dashboard/queues.mjs +120 -0
  124. django_cfg/apps/tasks/static/tasks/js/dashboard/tasks.mjs +350 -0
  125. django_cfg/apps/tasks/static/tasks/js/dashboard/workers.mjs +169 -0
  126. django_cfg/apps/tasks/tasks/__init__.py +10 -0
  127. django_cfg/apps/tasks/tasks/demo_tasks.py +133 -0
  128. django_cfg/apps/tasks/templates/tasks/components/management_actions.html +42 -45
  129. django_cfg/apps/tasks/templates/tasks/components/{status_cards.html → overview_content.html} +30 -11
  130. django_cfg/apps/tasks/templates/tasks/components/queues_content.html +19 -0
  131. django_cfg/apps/tasks/templates/tasks/components/tab_navigation.html +16 -10
  132. django_cfg/apps/tasks/templates/tasks/components/tasks_content.html +51 -0
  133. django_cfg/apps/tasks/templates/tasks/components/workers_content.html +30 -0
  134. django_cfg/apps/tasks/templates/tasks/layout/base.html +117 -0
  135. django_cfg/apps/tasks/templates/tasks/pages/dashboard.html +82 -0
  136. django_cfg/apps/tasks/templates/tasks/partials/task_row_template.html +40 -0
  137. django_cfg/apps/tasks/templates/tasks/widgets/task_filters.html +37 -0
  138. django_cfg/apps/tasks/templates/tasks/widgets/task_footer.html +41 -0
  139. django_cfg/apps/tasks/templates/tasks/widgets/task_table.html +50 -0
  140. django_cfg/apps/tasks/urls.py +2 -2
  141. django_cfg/apps/tasks/urls_admin.py +2 -2
  142. django_cfg/apps/tasks/utils/__init__.py +1 -0
  143. django_cfg/apps/tasks/utils/simulator.py +356 -0
  144. django_cfg/apps/tasks/views/__init__.py +16 -0
  145. django_cfg/apps/tasks/views/api.py +569 -0
  146. django_cfg/apps/tasks/views/dashboard.py +58 -0
  147. django_cfg/config.py +1 -1
  148. django_cfg/core/config.py +10 -5
  149. django_cfg/core/generation.py +1 -1
  150. django_cfg/core/integration/__init__.py +21 -0
  151. django_cfg/management/commands/__init__.py +13 -1
  152. django_cfg/management/commands/migrate_all.py +9 -3
  153. django_cfg/management/commands/migrator.py +11 -6
  154. django_cfg/management/commands/rundramatiq.py +3 -2
  155. django_cfg/management/commands/rundramatiq_simulator.py +430 -0
  156. django_cfg/middleware/__init__.py +0 -2
  157. django_cfg/models/api_keys.py +115 -0
  158. django_cfg/models/constance.py +0 -11
  159. django_cfg/models/payments.py +137 -3
  160. django_cfg/modules/django_admin/__init__.py +64 -0
  161. django_cfg/modules/django_admin/decorators/__init__.py +13 -0
  162. django_cfg/modules/django_admin/decorators/actions.py +106 -0
  163. django_cfg/modules/django_admin/decorators/display.py +106 -0
  164. django_cfg/modules/django_admin/mixins/__init__.py +14 -0
  165. django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
  166. django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
  167. django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
  168. django_cfg/modules/django_admin/models/__init__.py +20 -0
  169. django_cfg/modules/django_admin/models/action_models.py +33 -0
  170. django_cfg/modules/django_admin/models/badge_models.py +20 -0
  171. django_cfg/modules/django_admin/models/base.py +26 -0
  172. django_cfg/modules/django_admin/models/display_models.py +31 -0
  173. django_cfg/modules/django_admin/utils/badges.py +159 -0
  174. django_cfg/modules/django_admin/utils/displays.py +247 -0
  175. django_cfg/modules/django_currency/__init__.py +2 -2
  176. django_cfg/modules/django_currency/clients/__init__.py +2 -2
  177. django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
  178. django_cfg/modules/django_currency/core/converter.py +12 -12
  179. django_cfg/modules/django_currency/database/__init__.py +2 -2
  180. django_cfg/modules/django_currency/database/database_loader.py +93 -42
  181. django_cfg/modules/django_llm/llm/client.py +10 -2
  182. django_cfg/modules/django_tasks.py +54 -21
  183. django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
  184. django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
  185. django_cfg/modules/django_unfold/dashboard.py +14 -13
  186. django_cfg/modules/django_unfold/models/config.py +1 -1
  187. django_cfg/registry/core.py +7 -9
  188. django_cfg/registry/third_party.py +2 -2
  189. django_cfg/template_archive/django_sample.zip +0 -0
  190. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/METADATA +2 -1
  191. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/RECORD +198 -160
  192. django_cfg/apps/accounts/admin/activity.py +0 -96
  193. django_cfg/apps/accounts/admin/group.py +0 -17
  194. django_cfg/apps/accounts/admin/otp.py +0 -59
  195. django_cfg/apps/accounts/admin/registration_source.py +0 -97
  196. django_cfg/apps/accounts/admin/twilio_response.py +0 -227
  197. django_cfg/apps/accounts/admin/user.py +0 -300
  198. django_cfg/apps/agents/core/agent.py +0 -281
  199. django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
  200. django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
  201. django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
  202. django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
  203. django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
  204. django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
  205. django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
  206. django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
  207. django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
  208. django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
  209. django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
  210. django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
  211. django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
  212. django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
  213. django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
  214. django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
  215. django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
  216. django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
  217. django_cfg/apps/payments/config/constance/__init__.py +0 -22
  218. django_cfg/apps/payments/config/constance/config_service.py +0 -123
  219. django_cfg/apps/payments/config/constance/fields.py +0 -69
  220. django_cfg/apps/payments/config/constance/settings.py +0 -160
  221. django_cfg/apps/payments/services/providers/nowpayments.py +0 -478
  222. django_cfg/apps/tasks/admin.py +0 -320
  223. django_cfg/apps/tasks/static/tasks/js/dashboard.js +0 -614
  224. django_cfg/apps/tasks/static/tasks/js/modals.js +0 -452
  225. django_cfg/apps/tasks/static/tasks/js/notifications.js +0 -144
  226. django_cfg/apps/tasks/static/tasks/js/task-monitor.js +0 -454
  227. django_cfg/apps/tasks/static/tasks/js/theme.js +0 -77
  228. django_cfg/apps/tasks/templates/tasks/base.html +0 -96
  229. django_cfg/apps/tasks/templates/tasks/components/info_cards.html +0 -85
  230. django_cfg/apps/tasks/templates/tasks/components/overview_tab.html +0 -22
  231. django_cfg/apps/tasks/templates/tasks/components/queues_tab.html +0 -19
  232. django_cfg/apps/tasks/templates/tasks/components/task_details_modal.html +0 -103
  233. django_cfg/apps/tasks/templates/tasks/components/tasks_tab.html +0 -32
  234. django_cfg/apps/tasks/templates/tasks/components/workers_tab.html +0 -29
  235. django_cfg/apps/tasks/templates/tasks/dashboard.html +0 -29
  236. django_cfg/apps/tasks/views.py +0 -461
  237. django_cfg/management/commands/auto_generate.py +0 -486
  238. django_cfg/middleware/static_nocache.py +0 -55
  239. django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
  240. /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
  241. /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
  242. /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
  243. /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
  244. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/WHEEL +0 -0
  245. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/entry_points.txt +0 -0
  246. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/licenses/LICENSE +0 -0
@@ -1,590 +1,201 @@
1
1
  """
2
- Balance Admin interfaces with Unfold integration.
2
+ Balance Admin interfaces using Django Admin Utilities.
3
3
 
4
- Advanced balance and transaction management with bulk operations.
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
- from unfold.decorators import display, action
21
- from unfold.enums import ActionVariant
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
- Advanced UserBalance admin with bulk operations and financial monitoring.
38
+ UserBalance admin using Django Admin Utilities.
34
39
 
35
40
  Features:
36
- - Balance range filtering and visualization
37
- - Bulk balance adjustments with audit trail
38
- - Financial statistics and alerts
39
- - Transaction history integration
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
- # Custom template for balance statistics
44
- change_list_template = 'admin/payments/balance/change_list.html'
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
- 'balance_status',
57
+ 'status_display',
50
58
  'transaction_count_display',
51
- 'last_activity_display',
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
- # Unfold actions
77
- actions_list = [
78
- 'add_funds_bulk',
79
- 'subtract_funds_bulk',
80
- 'reset_zero_balances',
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
- fieldsets = [
86
- ('User Information', {
87
- 'fields': ['user']
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
- def get_queryset(self, request):
107
- """Optimize queryset with user data and transaction counts."""
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
- @display(description="User", ordering='user__email')
85
+ # Display methods using Unfold features
86
+ @display(description="User", header=True)
113
87
  def user_display(self, obj):
114
- """Display user information with avatar and details."""
115
- if obj.user:
116
- display_name = obj.user.get_full_name() or obj.user.username
117
-
118
- # Determine user tier based on balance
119
- if obj.balance_usd >= 1000:
120
- tier_icon = "🐋"
121
- tier_color = "text-purple-600"
122
- tier_name = "Whale"
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='balance_usd')
98
+ @display(description="Balance", ordering="balance_usd")
161
99
  def balance_display(self, obj):
162
- """Display balance with visual indicators and reserved amounts."""
163
- balance = obj.balance_usd
164
- reserved = obj.reserved_usd or 0
165
- available = balance - reserved
166
-
167
- # Color coding based on balance
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
- def balance_status(self, obj):
207
- """Display balance status with alerts."""
208
- balance = obj.balance_usd
209
- reserved = obj.reserved_usd or 0
210
-
211
- badges = []
212
-
213
- if balance < 0:
214
- badges.append('<span class="inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900 dark:text-red-200">âš ī¸ Negative</span>')
215
- elif balance == 0:
216
- badges.append('<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-800 dark:bg-gray-900 dark:text-gray-200">💸 Empty</span>')
217
- elif balance < 1:
218
- badges.append('<span class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">âš ī¸ Low</span>')
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
- badges.append('<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900 dark:text-green-200">✅ Active</span>')
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
- """Display transaction count and recent activity."""
126
+ """Transaction count using utilities."""
230
127
  count = getattr(obj, 'transaction_count', 0)
231
-
232
- if count > 0:
233
- # Get recent transaction count (last 7 days)
234
- recent_threshold = timezone.now() - timedelta(days=7)
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="Last Activity", ordering='last_transaction_at')
258
- def last_activity_display(self, obj):
259
- """Display last transaction activity."""
260
- if obj.last_transaction_at:
261
- time_ago = timezone.now() - obj.last_transaction_at
262
-
263
- if time_ago < timedelta(hours=1):
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
- @display(description="Created", ordering='created_at')
290
- def created_at_display(self, obj):
291
- """Display creation date."""
292
- return format_html(
293
- '<div class="text-xs">'
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
- updated_count = 0
149
+ breakdown_items = []
373
150
 
374
- for balance in queryset:
375
- try:
376
- # Use manager method for proper transaction handling
377
- UserBalance.objects.add_funds_to_user(
378
- user=balance.user,
379
- amount=amount,
380
- transaction_type='admin_adjustment',
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
- if updated_count > 0:
389
- messages.success(
390
- request,
391
- f"💰 Added ${amount} to {updated_count} user balances"
392
- )
393
- messages.info(
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
- @action(
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
- @action(
440
- description="🔄 Reset Zero Balances",
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 zero balances and clear reserved amounts."""
446
-
447
- zero_balances = queryset.filter(balance_usd=0)
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"📊 Exported {queryset.count()} balance records to CSV"
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 with detailed tracking and audit capabilities.
183
+ Transaction admin using Django Admin Utilities.
562
184
 
563
- Features:
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
- 'balance_impact_display',
576
- 'payment_link_display',
577
- 'created_at_display'
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
- readonly_fields = [
207
+ search_fields = [
597
208
  'id',
598
- 'user',
599
- 'transaction_type',
600
- 'amount_usd',
601
- 'balance_after',
602
- 'payment_id',
603
- 'description',
604
- 'created_at'
209
+ 'user__username',
210
+ 'user__email',
211
+ 'description'
605
212
  ]
606
213
 
607
- def has_add_permission(self, request):
608
- """Disable adding transactions through admin (should be created by system)."""
609
- return False
610
-
611
- def has_change_permission(self, request, obj=None):
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
- @display(description="Transaction ID", ordering='id')
220
+ # Display methods
221
+ @display(description="ID")
620
222
  def transaction_id_display(self, obj):
621
- """Display transaction ID."""
622
- short_id = str(obj.id)[:8]
623
- return format_html(
624
- '<span class="font-mono text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded" '
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", ordering='user__email')
229
+ @display(description="User")
631
230
  def user_display(self, obj):
632
- """Display user information."""
633
- if obj.user:
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", ordering='amount_usd')
234
+ @display(description="Amount")
671
235
  def amount_display(self, obj):
672
- """Display transaction amount with sign."""
673
- amount = obj.amount_usd
674
-
675
- if amount > 0:
676
- return format_html(
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="Payment")
707
- def payment_link_display(self, obj):
708
- """Display payment link if available."""
709
- if obj.payment_id:
710
- return format_html(
711
- '<a href="/admin/payments/universalpayment/{}/change/" '
712
- 'class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200">'
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="Created", ordering='created_at')
720
- def created_at_display(self, obj):
721
- """Display creation timestamp."""
722
- return format_html(
723
- '<div class="text-xs">'
724
- '<div>{}</div>'
725
- '<div class="text-gray-500">{}</div>'
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')