ipg/apps/pg/admin.py
Ali Asadi fbe43a2cf4 FEAT(pg): sortable columns + date-range filters on the IPG+ admin panel
Make every PaymentRequest changelist column sortable ascending/descending
(computed amount/user/jalali columns get an `ordering` so they sort by the
underlying DB field; only the "check success again" action button stays
unsortable). Add a default `-created_at` ordering and surface `updated_at`
as its own sortable column. The created_at/updated_at DateTimeRangeFilters
are retained and compose with sorting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 13:24:50 +03:30

156 lines
6.2 KiB
Python

import logging
import io
import jdatetime
import xlsxwriter
from django.contrib import admin, messages
from django.db.models import Sum
from django.http import HttpResponse
from django.utils import timezone
from django.utils.html import format_html
from django.shortcuts import redirect, get_object_or_404
from django.urls import path
from .constans import StateChoices
from .forms import ApplicationGatewayPolicyForm
from .models import PaymentRequest, PNA, Fake, ApplicationGatewayPolicy, SEP
from rangefilter.filters import DateTimeRangeFilter
logger = logging.getLogger(__name__)
class PNAAdmin(admin.ModelAdmin):
list_display = ['pna_status_code', 'pna_status_message', 'jajali_datetime', 'payment_request']
list_filter = ['pna_status']
search_fields = ['pna_status_code', 'pna_status_message']
class SEPAdmin(admin.ModelAdmin):
list_display = ['sep_status_code', 'sep_status_message', 'jajali_datetime', 'payment_request']
list_filter = ['sep_status']
search_fields = ['sep_status_code', 'sep_status_message']
@admin.action(description="export exel")
def export_exel(modeladmin, request, queryset):
output = io.BytesIO()
workbook = xlsxwriter.Workbook(output, options={"remove_timezone": True})
worksheet = workbook.add_worksheet()
for col, header in enumerate([]):
worksheet.write(0, col, header)
for row, payment_request in enumerate(queryset, 1):
worksheet.write(row, 0, payment_request.amount)
worksheet.write(row, 1, str(payment_request.pk))
worksheet.write(row, 2, str(payment_request.user.pk))
worksheet.write(row, 3, payment_request.jajali_datetime)
worksheet.write(row, 4, payment_request.gateway)
worksheet.write(row, 5, payment_request.get_state_display())
workbook.close()
# create a response
response = HttpResponse(content_type='application/vnd.ms-excel')
# tell the browser what the file is named
file_name = jdatetime.date.fromgregorian(date=timezone.now(), locale='fa_IR').strftime("%Y/%m/%d")
response['Content-Disposition'] = f'attachment;filename="{file_name}.xlsx"'
# put the spreadsheet data into the response
response.write(output.getvalue())
# return the response
return response
@admin.action(description="mark as registered")
def mark_as_registered(model_admin, request, queryset):
for obj in queryset:
obj.is_registered = True
obj.save(update_fields=['is_registered'])
class PaymentRequestAdmin(admin.ModelAdmin):
list_display = ['state', 'check_success_again', 'uuid', 'amount_display', 'user_display', 'jalali_datetime_display',
'is_registered', 'created_at', 'updated_at', 'application', 'gateway']
search_fields = ['application__uuid', 'application__client_id', 'amount', 'user__uuid']
list_filter = ['application', 'state', 'gateway', ('created_at', DateTimeRangeFilter), ('updated_at', DateTimeRangeFilter)]
# Every real column in list_display can be sorted ascending/descending by
# clicking its header; the computed columns declare an `ordering` so they
# sort by the underlying DB field. Only the "check success again" action
# button stays unsortable.
sortable_by = ['state', 'uuid', 'amount_display', 'user_display', 'jalali_datetime_display',
'is_registered', 'created_at', 'updated_at', 'application', 'gateway']
ordering = ['-created_at']
actions = [export_exel, mark_as_registered]
@admin.display(description='Splited amount', ordering='amount')
def amount_display(self, obj):
return obj.splited_amount
@admin.display(description='User', ordering='user')
def user_display(self, obj):
return obj.user_id
@admin.display(description='Jajali datetime', ordering='created_at')
def jalali_datetime_display(self, obj):
return obj.jajali_datetime
def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {}
cl = self.get_changelist_instance(request)
queryset = cl.result_list
totals = queryset.aggregate(
total_amount=Sum('amount'),
)
extra_context['totals'] = totals
return super().changelist_view(request, extra_context=extra_context)
def check_success_again(self, obj):
if obj.state in [StateChoices.IPG_SUCCESS, StateChoices.WALLET_SUBMIT_PENDING,
StateChoices.WALLET_SUBMIT_SUCCESS]:
return format_html(
'<a class="button" href="{}">بررسی دوباره موفقیت</a>',
f'{obj.uuid}/check_success_again/'
)
return ''
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('<uuid:payment_request_uuid>/check_success_again/',
self.admin_site.admin_view(self.check_success_again_view))
]
return custom_urls + urls
def check_success_again_view(self, request, payment_request_uuid):
logger.info(f"[Admin panel] Check success again for payment_request_uuid={payment_request_uuid}")
payment_request: PaymentRequest = get_object_or_404(PaymentRequest, uuid=payment_request_uuid)
logger.info(f"[Admin panel] initial payment_request state={payment_request.state}")
if payment_request.state == StateChoices.IPG_SUCCESS:
payment_request.charge_wallet_balance()
elif payment_request.state == StateChoices.WALLET_SUBMIT_PENDING:
payment_request.deposit_submit()
elif payment_request.state == StateChoices.WALLET_SUBMIT_SUCCESS:
payment_request.deposit_verify()
payment_request.refresh_from_db()
logger.info(f"[Admin panel] final payment_request state={payment_request.state}")
self.message_user(request, f"درخواست {payment_request_uuid} بررسی شد ✅")
return redirect(request.META.get("HTTP_REFERER", ".."))
class FakeAdmin(admin.ModelAdmin):
list_display = ['created_at']
class ApplicationGatewayPolicyAdmin(admin.ModelAdmin):
form = ApplicationGatewayPolicyForm
admin.site.register(PaymentRequest, PaymentRequestAdmin)
admin.site.register(PNA, PNAAdmin)
admin.site.register(SEP, SEPAdmin)
admin.site.register(Fake, FakeAdmin)
admin.site.register(ApplicationGatewayPolicy, ApplicationGatewayPolicyAdmin)