"""客户回款记录 Admin 配置""" from __future__ import annotations from typing import TYPE_CHECKING, Any, ClassVar from django import forms from django.contrib import admin from django.db.models import QuerySet from django.http import HttpRequest from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from apps.contracts.models import ClientPaymentRecord if TYPE_CHECKING: pass class ClientPaymentRecordAdminForm(forms.ModelForm[ClientPaymentRecord]): """客户回款记录表单""" image = forms.ImageField( required=False, label=_("上传凭证图片"), help_text=_("支持 JPG、PNG、JPEG,最大 10MB"), ) class Meta: model = ClientPaymentRecord fields = ("contract", "case", "amount", "image", "note") widgets = { "note": forms.Textarea(attrs={"rows": 3, "cols": 40}), } def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) # 如果是编辑模式,根据已有的 contract_id 过滤案件 if self.instance and self.instance.pk and self.instance.contract_id: from apps.cases.models import Case self.fields["case"].queryset = Case.objects.filter(contract_id=self.instance.contract_id) # 如果是新建模式,但通过 URL 参数传入了 contract_id elif "initial" in kwargs and "contract" in kwargs["initial"]: from apps.cases.models import Case contract_id = kwargs["initial"]["contract"] self.fields["case"].queryset = Case.objects.filter(contract_id=contract_id) # 如果是新建模式且有 GET 参数 contract elif hasattr(self, "data") and self.data and "contract" in self.data: from apps.cases.models import Case contract_id = self.data.get("contract") if contract_id: self.fields["case"].queryset = Case.objects.filter(contract_id=contract_id) else: # 默认为空(用户选择合同后会通过 JS 动态加载) from apps.cases.models import Case self.fields["case"].queryset = Case.objects.none() def clean(self) -> dict[str, Any]: cleaned_data = super().clean() contract = cleaned_data.get("contract") case = cleaned_data.get("case") # 验证案件归属 if contract and case: if case.contract_id != contract.id: raise forms.ValidationError(_("所选案件不属于该合同")) return cleaned_data def save(self, commit: bool = True) -> ClientPaymentRecord: instance = super().save(commit=False) uploaded_file = self.cleaned_data.get("image") if uploaded_file: from apps.contracts.services.client_payment import ClientPaymentImageService image_service = ClientPaymentImageService() # 删除旧图片 if instance.pk and instance.image_path: image_service.delete_image(instance.image_path) # 保存新图片(需要先有 ID) if instance.pk: image_path = image_service.save_image(uploaded_file, instance.id) instance.image_path = image_path if commit: instance.save() return instance class ClientPaymentRecordAdmin(admin.ModelAdmin[ClientPaymentRecord]): """客户回款记录 Admin""" form = ClientPaymentRecordAdminForm list_display = ("id", "contract", "case", "amount", "created_at") list_filter = ("contract", "created_at") search_fields = ("contract__name", "note") autocomplete_fields = ("contract",) readonly_fields = ("created_at", "image_preview") fieldsets: ClassVar = ( ( None, { "fields": ("contract", "case", "amount", "note"), }, ), ( _("凭证图片"), { "fields": ("image", "image_preview"), }, ), ( _("系统信息"), { "fields": ("created_at",), }, ), ) def get_queryset(self, request: HttpRequest) -> QuerySet[ClientPaymentRecord, ClientPaymentRecord]: """优化查询""" return super().get_queryset(request).select_related("contract", "case") @admin.display(description=_("图片预览")) def image_preview(self, obj: ClientPaymentRecord) -> str: """展示图片预览""" if not obj.pk or not obj.image_path: return "-" from apps.contracts.services.client_payment import ClientPaymentImageService image_service = ClientPaymentImageService() url = image_service.get_image_url(obj.image_path) return format_html( '', url, url, ) def get_urls(self) -> list[Any]: """添加自定义 URL""" from django.urls import path urls = super().get_urls() custom_urls = [ path( "get-cases-by-contract/", self.admin_site.admin_view(self.get_cases_by_contract_view), name="contracts_clientpaymentrecord_get_cases", ), ] return custom_urls + urls def get_cases_by_contract_view(self, request: HttpRequest) -> Any: """AJAX 端点:根据合同 ID 获取案件列表""" from django.http import JsonResponse from apps.cases.models import Case contract_id = request.GET.get("contract_id") if not contract_id: return JsonResponse({"cases": []}) cases = Case.objects.filter(contract_id=contract_id).values("id", "name") return JsonResponse({"cases": list(cases)}) def save_model(self, request: HttpRequest, obj: ClientPaymentRecord, form: Any, change: bool) -> None: """保存模型时调用 Service 层验证""" from apps.contracts.services.client_payment import ClientPaymentImageService, ClientPaymentRecordService service = ClientPaymentRecordService() uploaded_file = form.cleaned_data.get("image") if not change: # 新建:通过 Service 创建 case_id = obj.case_id if obj.case_id else None created = service.create_payment_record( contract_id=obj.contract_id, amount=obj.amount, case_id=case_id, note=obj.note or "", ) # 更新实例 ID obj.pk = created.pk obj.id = created.id # 处理图片上传 if uploaded_file: image_service = ClientPaymentImageService() image_path = image_service.save_image(uploaded_file, created.id) created.image_path = image_path created.save(update_fields=["image_path"]) else: # 更新:通过 Service 更新 case_id = obj.case_id if obj.case_id else None service.update_payment_record( record_id=obj.id, amount=obj.amount, case_id=case_id, note=obj.note, ) # 处理图片上传 if uploaded_file: image_service = ClientPaymentImageService() # 删除旧图片 if obj.image_path: image_service.delete_image(obj.image_path) # 保存新图片 image_path = image_service.save_image(uploaded_file, obj.id) obj.image_path = image_path obj.save(update_fields=["image_path"]) def delete_model(self, request: HttpRequest, obj: ClientPaymentRecord) -> None: """删除模型时调用 Service 层""" from apps.contracts.services.client_payment import ClientPaymentRecordService service = ClientPaymentRecordService() service.delete_payment_record(obj.id) def delete_queryset( self, request: HttpRequest, queryset: QuerySet[ClientPaymentRecord, ClientPaymentRecord] ) -> None: """批量删除时调用 Service 层""" from apps.contracts.services.client_payment import ClientPaymentRecordService service = ClientPaymentRecordService() for obj in queryset: service.delete_payment_record(obj.id) class Media: js = ("admin/js/jquery.init.js", "contracts/js/client_payment_admin.js")