from django.contrib import admin from django.core.exceptions import PermissionDenied from django.db import connection from . import models, policies, services @admin.register(models.PaymentAccessLink) class PaymentAccessLinkAdmin(admin.ModelAdmin): list_display = ('customer', 'notice', 'receipt', 'created_at', 'expires_at', 'revoked_at', 'view_count') list_filter = ('revoked_at', 'expires_at') exclude = ('token_hash',) readonly_fields = ('customer', 'notice', 'receipt', 'created_at', 'expires_at', 'revoked_at', 'view_count', 'last_used_at', 'submitted_receipts') actions = ['revoke_links'] def has_add_permission(self, request): return False def has_delete_permission(self, request, obj=None): return False @admin.action(description='ابطال لینک‌های انتخاب‌شده') def revoke_links(self, request, queryset): from django.utils import timezone for link in queryset.filter(revoked_at__isnull=True): link.revoked_at = timezone.now() link.save(update_fields=['revoked_at']) services.audit(request.user, 'PAYMENT_LINK_REVOKED', link) from .forms import BankAccountForm class AuditedConfigurationAdmin(admin.ModelAdmin): permission = 'manage_payment_approval_policy' singleton_configuration = False def has_view_permission(self, request, obj=None): return policies.has_permission(request.user,self.permission) def has_add_permission(self, request): return self.has_view_permission(request) and not (self.singleton_configuration and self.model.objects.exists()) def has_change_permission(self, request, obj=None): return self.has_view_permission(request,obj) def has_delete_permission(self, request, obj=None): return False def save_model(self, request, obj, form, change): if not self.has_change_permission(request,obj): raise PermissionDenied current = self.model.objects.select_for_update().get(pk=obj.pk) if change else None before = services.configuration_snapshot(current) if current else {} super().save_model(request,obj,form,change) services.audit(request.user,'CONFIG_SAVED',obj,{'fields': form.changed_data, 'before':before,'after':services.configuration_snapshot(obj)}) def save_related(self, request, form, formsets, change): super().save_related(request,form,formsets,change) if isinstance(form.instance,models.PaymentApprovalPolicy): services.audit(request.user,'POLICY_ASSIGNMENTS_CHANGED',form.instance,{'reviewers':list(form.instance.reviewers.values_list('pk',flat=True)), 'seniors':list(form.instance.seniors.values_list('pk',flat=True))}) @admin.register(models.PaymentApprovalPolicy) class ApprovalPolicyAdmin(AuditedConfigurationAdmin): list_display = ['name','unit','is_active'] filter_horizontal = ['reviewers','seniors'] def formfield_for_manytomany(self, db_field, request, **kwargs): from apps.accounts.models import User if db_field.name in ['reviewers','seniors']: kwargs['queryset'] = User.objects.filter(account_type='STAFF',is_active=True).exclude(staff_profile__is_active=False) return super().formfield_for_manytomany(db_field,request,**kwargs) @admin.register(models.PaymentAccessConfiguration) class AccessAdmin(AuditedConfigurationAdmin): permission = 'manage_payment_access' singleton_configuration = True @admin.register(models.CompanyBankAccount) class AccountAdmin(AuditedConfigurationAdmin): form = BankAccountForm list_display = ['name','bank','requires_deposit_id','is_active','available_to_staff','available_to_customers'] permission = 'manage_company_bankaccounts' def has_view_permission(self, request, obj=None): return policies.can_manage_banks(request.user) def get_queryset(self, request): queryset = super().get_queryset(request) # Django wraps POST change forms in atomic(): lock before token validation. if request.method == 'POST' and connection.in_atomic_block: return queryset.select_for_update(of=('self',)) return queryset @admin.register(models.BankInstitution) class BankAdmin(AccountAdmin): form = admin.ModelAdmin.form list_display = ['name','code','is_active','historical'] class EvidenceAdmin(admin.ModelAdmin): def has_view_permission(self, request, obj=None): if self.model in [models.PaymentNotice, models.PaymentReceipt]: return super().has_view_permission(request, obj) return policies.has_permission(request.user, 'view_payment_audit') def has_add_permission(self, request): return False def has_change_permission(self, request, obj=None): return False def has_delete_permission(self, request, obj=None): return False def get_queryset(self, request): queryset = super().get_queryset(request) if queryset.model in [models.PaymentNotice,models.PaymentReceipt]: return policies.visible(queryset,request.user) return queryset if policies.has_permission(request.user,'view_payment_audit') else queryset.none() for model in [models.PaymentNotice,models.PaymentReceipt,models.PaymentReceiptRevision,models.ReceiptApprovalCycle, models.ReceiptApprovalAssignment,models.ReceiptDecision,models.PaymentAuditEvent, models.PaymentNotification,models.PaymentNotificationAttempt]: admin.site.register(model,EvidenceAdmin) class ReceiptAlertRecipientInline(admin.TabularInline): model = models.PaymentReceiptAlertRecipient extra = 1 can_delete = False def has_view_permission(self, request, obj=None): return policies.has_permission(request.user, 'manage_receipt_alerts') has_add_permission = has_view_permission has_change_permission = has_view_permission @admin.register(models.PaymentReceiptAlertConfiguration) class ReceiptAlertConfigurationAdmin(AuditedConfigurationAdmin): from .forms import ReceiptAlertConfigurationForm form = ReceiptAlertConfigurationForm permission = 'manage_receipt_alerts' singleton_configuration = True list_display = ('__str__', 'bale_enabled', 'sms_enabled', 'notify_revisions', 'updated_at') readonly_fields = ('updated_at',) inlines = [ReceiptAlertRecipientInline] fieldsets = ( ('بله — تصویر فیش', {'fields': ('bale_enabled', 'bale_token', 'clear_bale_token', 'bale_chat_id', 'bale_caption')}), ('پیامک مدیران', {'fields': ('sms_enabled', 'sms_template')}), ('فیش اصلاح‌شده', {'fields': ('notify_revisions', 'updated_at')}), ) def save_related(self, request, form, formsets, change): super().save_related(request, form, formsets, change) services.audit(request.user, 'RECEIPT_ALERT_RECIPIENTS_CHANGED', form.instance, {'recipients': list(form.instance.recipients.values('id', 'name', 'mobile', 'is_active'))}) @admin.register(models.PaymentReceiptAlertRecipient) class ReceiptAlertRecipientAdmin(AuditedConfigurationAdmin): permission = 'manage_receipt_alerts' list_display = ('name', 'mobile', 'is_active') list_filter = ('is_active',) search_fields = ('name', 'mobile') @admin.register(models.PaymentReceiptAlert) class ReceiptAlertAdmin(EvidenceAdmin): list_display = ('receipt_number', 'recipient_name', 'recipient_mobile', 'channel', 'status', 'attempts', 'delivery_status') list_select_related = ('job', 'revision__receipt', 'recipient') readonly_fields = ('channel', 'status', 'attempts', 'delivery_status', 'last_error', 'sent_at', 'delivery_description') def has_view_permission(self, request, obj=None): return policies.has_permission(request.user, 'manage_receipt_alerts') or super().has_view_permission(request, obj) def get_queryset(self, request): if policies.has_permission(request.user, 'manage_receipt_alerts'): return admin.ModelAdmin.get_queryset(self, request) return super().get_queryset(request) @admin.display(description='فیش') def receipt_number(self, obj): return obj.revision.receipt.number @admin.display(description='کانال') def channel(self, obj): return obj.job.get_channel_display() @admin.display(description='وضعیت ارسال') def status(self, obj): return obj.job.get_status_display() @admin.display(description='تعداد تلاش') def attempts(self, obj): return obj.job.attempts @admin.display(description='دریافت پیامک') def delivery_status(self, obj): return obj.job.get_delivery_status_display() or 'استعلام ثبت نشده' @admin.display(description='آخرین خطا') def last_error(self, obj): return obj.job.last_error @admin.display(description='زمان ارسال') def sent_at(self, obj): return obj.job.sent_at @admin.display(description='شرح دریافت') def delivery_description(self, obj): return obj.job.delivery_description