|
|
"""
|
|
|
invoice/pdf_helper.py — 发票 PDF 渲染辅助
|
|
|
|
|
|
使用 WeasyPrint + Jinja2 渲染符合中国电子发票样式的 PDF。
|
|
|
对接百望云/票通等第三方平台后,可替换此模块的内部实现。
|
|
|
"""
|
|
|
|
|
|
import hashlib
|
|
|
from datetime import datetime
|
|
|
|
|
|
from app.config.path_conf import INVOICE_DIR, TEMPLATE_DIR
|
|
|
from app.utils.pdf_generator import amount_to_cn_uppercase, amount_to_yuan, generate_pdf_from_template
|
|
|
|
|
|
from .oss_licenses_helper import load_oss_licenses
|
|
|
from .schema import InvoiceOutSchema
|
|
|
|
|
|
_INVOICE_TYPE_LABEL = {
|
|
|
"vat_normal": "电子普通发票",
|
|
|
"vat_special": "增值税专用发票",
|
|
|
}
|
|
|
|
|
|
|
|
|
def _render_invoice_pdf(invoice: InvoiceOutSchema) -> str:
|
|
|
"""
|
|
|
渲染并保存电子发票 PDF
|
|
|
|
|
|
参数:
|
|
|
- invoice (InvoiceOutSchema): 发票对象
|
|
|
|
|
|
返回:
|
|
|
- str: PDF 的相对 URL 路径(形如 /static/invoice/{tenant_id}/{invoice_no}.pdf)
|
|
|
"""
|
|
|
amount_yuan = float(amount_to_yuan(invoice.amount))
|
|
|
tax_yuan = float(amount_to_yuan(invoice.tax_amount))
|
|
|
total_yuan = amount_yuan + tax_yuan
|
|
|
|
|
|
check_code_seed = f"{invoice.invoice_no}|{invoice.amount}|{invoice.invoice_type}"
|
|
|
check_code = hashlib.md5(check_code_seed.encode()).hexdigest()[:20].upper()
|
|
|
|
|
|
variables = {
|
|
|
"invoice_no": invoice.invoice_no,
|
|
|
"invoice_date": datetime.now().strftime("%Y-%m-%d"),
|
|
|
"invoice_type": invoice.invoice_type,
|
|
|
"invoice_type_label": _INVOICE_TYPE_LABEL.get(invoice.invoice_type, "电子发票"),
|
|
|
"buyer_name": invoice.title,
|
|
|
"buyer_tax_no": invoice.tax_no or "-",
|
|
|
"buyer_address_info": invoice.address_info or "-",
|
|
|
"buyer_bank_info": invoice.bank_info or "-",
|
|
|
"seller_name": "FastapiAdmin 平台",
|
|
|
"seller_tax_no": "91310115MA1K00000X",
|
|
|
"seller_address_info": "上海市浦东新区张江高科技园区",
|
|
|
"seller_bank_info": "招商银行上海分行 1234 5678 9012 3456",
|
|
|
"items": [
|
|
|
{
|
|
|
"name": "FastapiAdmin 企业服务",
|
|
|
"spec": "SaaS 订阅",
|
|
|
"unit": "套",
|
|
|
"quantity": "1",
|
|
|
"unit_price": amount_to_yuan(invoice.amount),
|
|
|
"amount": amount_to_yuan(invoice.amount),
|
|
|
"tax_rate": 0,
|
|
|
"tax_amount": amount_to_yuan(invoice.tax_amount),
|
|
|
}
|
|
|
],
|
|
|
"amount_total_yuan": f"{total_yuan:.2f}",
|
|
|
"amount_cn_uppercase": amount_to_cn_uppercase(total_yuan),
|
|
|
"remarks": invoice.description or f"订单号: {invoice.order_id}",
|
|
|
"check_code": f"{check_code[:4]} {check_code[4:8]} {check_code[8:12]} {check_code[12:16]} {check_code[16:20]}",
|
|
|
"issuer_name": "系统开票",
|
|
|
"receiver_name": "-",
|
|
|
"reviewer_name": "-",
|
|
|
}
|
|
|
|
|
|
output_dir = INVOICE_DIR / str(invoice.tenant_id)
|
|
|
output_path = output_dir / f"{invoice.invoice_no}.pdf"
|
|
|
generate_pdf_from_template(
|
|
|
template_name="invoice/invoice.jinja2",
|
|
|
template_dir=TEMPLATE_DIR,
|
|
|
variables=variables,
|
|
|
output_path=output_path,
|
|
|
)
|
|
|
return f"/static/invoice/{invoice.tenant_id}/{invoice.invoice_no}.pdf"
|
|
|
|
|
|
|
|
|
def _render_oss_license_pdf(invoice: InvoiceOutSchema) -> str:
|
|
|
"""
|
|
|
渲染并保存开源项目授权声明函 PDF(与发票 PDF 独立存储)
|
|
|
|
|
|
参数:
|
|
|
- invoice (InvoiceOutSchema): 发票对象(用于在授权函中展示关联发票号)
|
|
|
|
|
|
返回:
|
|
|
- str: PDF 的相对 URL 路径(形如 /static/invoice/{tenant_id}/{invoice_no}_license.pdf)
|
|
|
"""
|
|
|
groups = load_oss_licenses()
|
|
|
total_packages = sum(len(g["packages"]) for g in groups)
|
|
|
|
|
|
variables = {
|
|
|
"invoice_no": invoice.invoice_no,
|
|
|
"invoice_date": datetime.now().strftime("%Y-%m-%d"),
|
|
|
"product_version": "v1.0.0",
|
|
|
"groups": groups,
|
|
|
"total_packages": total_packages,
|
|
|
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
|
}
|
|
|
|
|
|
output_dir = INVOICE_DIR / str(invoice.tenant_id)
|
|
|
output_path = output_dir / f"{invoice.invoice_no}_license.pdf"
|
|
|
generate_pdf_from_template(
|
|
|
template_name="invoice/oss_license.jinja2",
|
|
|
template_dir=TEMPLATE_DIR,
|
|
|
variables=variables,
|
|
|
output_path=output_path,
|
|
|
)
|
|
|
return f"/static/invoice/{invoice.tenant_id}/{invoice.invoice_no}_license.pdf"
|