File size: 5,620 Bytes
b4f2238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
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 ContractPayment, Invoice, InvoiceStatus

if TYPE_CHECKING:
    BaseModelAdmin = admin.ModelAdmin
    BaseTabularInline = admin.TabularInline
else:
    try:
        import nested_admin

        BaseModelAdmin = nested_admin.NestedModelAdmin
        BaseTabularInline = nested_admin.NestedTabularInline
    except ImportError:
        BaseModelAdmin = admin.ModelAdmin
        BaseTabularInline = admin.TabularInline


class InvoiceAdminForm(forms.ModelForm[Invoice]):
    file = forms.FileField(
        required=False,
        label=_("上传发票"),
        help_text=_("支持 PDF、JPG、JPEG、PNG,最大 20MB"),
    )

    class Meta:
        model = Invoice
        fields = (
            "file",
            "original_filename",
            "invoice_code",
            "invoice_number",
            "invoice_date",
            "amount",
            "tax_amount",
            "total_amount",
            "remark",
        )
        widgets = {
            "remark": forms.Textarea(attrs={"rows": 2, "cols": 20, "style": "width:160px;resize:vertical;"}),
        }

    def save(self, commit: bool = True) -> Invoice:
        instance = super().save(commit=False)
        uploaded_file = self.cleaned_data.get("file")
        if uploaded_file:
            from apps.contracts.admin.wiring_admin import get_invoice_upload_service

            svc = get_invoice_upload_service()
            payment_id: int = instance.payment_id or self.instance.payment_id
            saved = svc.save_invoice_file(uploaded_file, payment_id)
            instance.file_path = saved.file_path
            instance.original_filename = saved.original_filename
            # 文件已由 service 创建记录,直接返回
            return saved
        if commit:
            instance.save()
        return instance


class InvoiceInline(BaseTabularInline):  # type: ignore[type-arg]
    model = Invoice
    form = InvoiceAdminForm
    extra = 1
    fields: ClassVar = (
        "file",
        "file_link",
        "total_amount",
    )
    readonly_fields: ClassVar = ("file_link",)

    # 隐藏 verbose_name_plural,避免显示识别结果
    verbose_name = _("发票")
    verbose_name_plural = _("发票")

    @admin.display(description=_("查看文件"))
    def file_link(self, obj: Invoice) -> str:
        if not obj.pk or not obj.file_path:
            return "-"
        from django.conf import settings

        # 确保 file_path 不为空字符串
        if not obj.file_path.strip():
            return "-"

        url = f"{settings.MEDIA_URL}{obj.file_path}"
        return format_html('<a href="{}" target="_blank">{}</a>', url, obj.original_filename or _("查看"))

    def delete_model(self, request: HttpRequest, obj: Invoice) -> None:
        from apps.contracts.admin.wiring_admin import get_invoice_upload_service

        svc = get_invoice_upload_service()
        try:
            svc.delete_invoice(obj.pk)
        except Exception:
            # 即使 service 抛出,也确保 Admin 不崩溃
            obj.delete()


class ContractPaymentInline(BaseTabularInline[ContractPayment, ContractPayment]):
    model = ContractPayment
    extra = 0
    fields = ("amount", "received_at", "invoiced_amount", "invoice_status", "note")

    def get_formset(self, request: HttpRequest, obj: Any = None, **kwargs: Any) -> Any:
        FormSet = super().get_formset(request, obj, **kwargs)

        original_clean = FormSet.clean

        def clean_fs(self: Any) -> None:
            original_clean(self)
            for form in self.forms:
                if not hasattr(form, "cleaned_data") or form.cleaned_data.get("DELETE"):
                    continue
                amt = form.cleaned_data.get("amount")
                inv = form.cleaned_data.get("invoiced_amount") or 0
                if amt and inv is not None:
                    if float(inv) - float(amt) > 1e-6:
                        form.add_error("invoiced_amount", _("开票金额不能大于收款金额"))
                    else:
                        if float(inv) == 0:
                            form.cleaned_data["invoice_status"] = InvoiceStatus.UNINVOICED
                        elif 0 < float(inv) < float(amt):
                            form.cleaned_data["invoice_status"] = InvoiceStatus.INVOICED_PARTIAL
                        else:
                            form.cleaned_data["invoice_status"] = InvoiceStatus.INVOICED_FULL

        FormSet.clean = clean_fs  # type: ignore[method-assign]
        return FormSet


class ContractPaymentAdmin(BaseModelAdmin[ContractPayment]):  # type: ignore[type-arg]
    change_form_template = "admin/contracts/contractpayment/change_form.html"
    list_display = ("id", "contract", "amount", "received_at", "invoice_status", "invoiced_amount")
    list_filter = ("invoice_status", "received_at")
    search_fields = ("contract__name",)
    autocomplete_fields = ("contract",)
    inlines: ClassVar = [InvoiceInline]

    class Media:
        css = {"all": ("contracts/css/invoice_recognition.css",)}
        js = ("contracts/js/invoice_recognition.js",)

    def get_queryset(self, request: HttpRequest) -> QuerySet[ContractPayment, ContractPayment]:
        return super().get_queryset(request).select_related("contract")