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")
|