guzhi/app/controllers/invoice.py
邹方成 cc352d3184 feat: 重构后端服务并添加新功能
refactor: 优化API路由和响应模型
feat(admin): 添加App用户管理接口
feat(sms): 实现阿里云短信服务集成
feat(email): 添加SMTP邮件发送功能
feat(upload): 支持文件上传接口
feat(rate-limiter): 实现手机号限流器
fix: 修复计算步骤入库问题
docs: 更新API文档和测试计划
chore: 更新依赖和配置
2025-11-19 19:36:03 +08:00

286 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from typing import Optional, List
from tortoise.queryset import QuerySet
from app.core.crud import CRUDBase
from app.models.invoice import Invoice, InvoiceHeader, PaymentReceipt
from app.schemas.invoice import (
InvoiceCreate,
InvoiceUpdate,
InvoiceOut,
InvoiceList,
InvoiceHeaderCreate,
InvoiceHeaderOut,
UpdateStatus,
UpdateType,
PaymentReceiptCreate,
PaymentReceiptOut,
)
class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]):
"""发票控制器"""
def __init__(self):
super().__init__(model=Invoice)
async def create_header(self, user_id: Optional[int], data: InvoiceHeaderCreate) -> InvoiceHeaderOut:
"""
创建发票抬头
参数:
user_id: 关联的 AppUser ID可选
data: 发票抬头创建数据
返回:
InvoiceHeaderOut: 抬头输出对象
"""
header = await InvoiceHeader.create(app_user_id=user_id, **data.model_dump())
return InvoiceHeaderOut.model_validate(header)
async def get_headers(self, user_id: Optional[int] = None) -> List[InvoiceHeaderOut]:
"""
获取发票抬头列表
参数:
user_id: 可筛选 AppUser 的抬头
返回:
List[InvoiceHeaderOut]: 抬头列表
"""
qs = InvoiceHeader.all()
if user_id is not None:
qs = qs.filter(app_user_id=user_id)
headers = await qs.order_by("-created_at")
return [InvoiceHeaderOut.model_validate(h) for h in headers]
async def get_header_by_id(self, id_: int) -> Optional[InvoiceHeaderOut]:
"""
根据ID获取抬头
参数:
id_: 抬头ID
返回:
InvoiceHeaderOut 或 None
"""
header = await InvoiceHeader.filter(id=id_).first()
return InvoiceHeaderOut.model_validate(header) if header else None
async def list(self, page: int = 1, page_size: int = 10, **filters) -> InvoiceList:
"""
获取发票列表(支持筛选与分页)
参数:
page: 页码
page_size: 每页数量
**filters: phone、company_name、tax_number、status、ticket_type、invoice_type、时间范围等
返回:
InvoiceList: 分页结果
"""
qs: QuerySet = self.model.all()
if filters.get("phone"):
qs = qs.filter(phone__icontains=filters["phone"])
if filters.get("company_name"):
qs = qs.filter(company_name__icontains=filters["company_name"])
if filters.get("tax_number"):
qs = qs.filter(tax_number__icontains=filters["tax_number"])
if filters.get("status"):
qs = qs.filter(status=filters["status"])
if filters.get("ticket_type"):
qs = qs.filter(ticket_type=filters["ticket_type"])
if filters.get("invoice_type"):
qs = qs.filter(invoice_type=filters["invoice_type"])
total = await qs.count()
rows = await qs.order_by("-created_at").offset((page - 1) * page_size).limit(page_size)
items = [
InvoiceOut(
id=row.id,
created_at=row.created_at.isoformat() if row.created_at else "",
ticket_type=row.ticket_type,
invoice_type=row.invoice_type,
phone=row.phone,
email=row.email,
company_name=row.company_name,
tax_number=row.tax_number,
register_address=row.register_address,
register_phone=row.register_phone,
bank_name=row.bank_name,
bank_account=row.bank_account,
status=row.status,
app_user_id=row.app_user_id,
header_id=row.header_id,
wechat=row.wechat,
)
for row in rows
]
return InvoiceList(items=items, total=total, page=page, page_size=page_size)
async def update_status(self, data: UpdateStatus) -> Optional[InvoiceOut]:
"""
更新发票状态
参数:
data: 包含 id 与目标状态
返回:
更新后的发票输出或 None
"""
inv = await self.model.filter(id=data.id).first()
if not inv:
return None
inv.status = data.status
await inv.save()
return await self.get_out(inv.id)
async def update_type(self, id_: int, data: UpdateType) -> Optional[InvoiceOut]:
"""
更新发票类型(电子/纸质、专票/普票)
参数:
id_: 发票ID
data: 类型更新数据
返回:
更新后的发票输出或 None
"""
inv = await self.model.filter(id=id_).first()
if not inv:
return None
inv.ticket_type = data.ticket_type
inv.invoice_type = data.invoice_type
await inv.save()
return await self.get_out(inv.id)
async def create_receipt(self, invoice_id: int, data: PaymentReceiptCreate) -> PaymentReceiptOut:
"""
上传付款凭证
参数:
invoice_id: 发票ID
data: 凭证创建数据
返回:
PaymentReceiptOut
"""
receipt = await PaymentReceipt.create(invoice_id=invoice_id, **data.model_dump())
return PaymentReceiptOut(
id=receipt.id,
url=receipt.url,
note=receipt.note,
verified=receipt.verified,
created_at=receipt.created_at.isoformat() if receipt.created_at else "",
)
async def list_receipts(self, page: int = 1, page_size: int = 10, **filters) -> dict:
"""
对公转账记录列表
参数:
page: 页码
page_size: 每页数量
**filters: 提交时间范围、手机号、微信号、公司名称/税号、状态、开票类型等
返回:
dict: { items, total, page, page_size }
"""
qs = PaymentReceipt.all().prefetch_related("invoice")
# 通过关联发票进行筛选
if filters.get("phone"):
qs = qs.filter(invoice__phone__icontains=filters["phone"])
if filters.get("wechat"):
qs = qs.filter(invoice__wechat__icontains=filters["wechat"])
if filters.get("company_name"):
qs = qs.filter(invoice__company_name__icontains=filters["company_name"])
if filters.get("tax_number"):
qs = qs.filter(invoice__tax_number__icontains=filters["tax_number"])
if filters.get("status"):
qs = qs.filter(invoice__status=filters["status"])
if filters.get("ticket_type"):
qs = qs.filter(invoice__ticket_type=filters["ticket_type"])
if filters.get("invoice_type"):
qs = qs.filter(invoice__invoice_type=filters["invoice_type"])
total = await qs.count()
rows = await qs.order_by("-created_at").offset((page - 1) * page_size).limit(page_size)
items = []
for r in rows:
inv = await r.invoice
items.append({
"submitted_at": r.created_at.isoformat() if r.created_at else "",
"receipt": {
"id": r.id,
"url": r.url,
"note": r.note,
"verified": r.verified,
},
"phone": inv.phone,
"wechat": inv.wechat,
"company_name": inv.company_name,
"tax_number": inv.tax_number,
"register_address": inv.register_address,
"register_phone": inv.register_phone,
"bank_name": inv.bank_name,
"bank_account": inv.bank_account,
"email": inv.email,
"ticket_type": inv.ticket_type,
"invoice_type": inv.invoice_type,
"status": inv.status,
})
return {"items": items, "total": total, "page": page, "page_size": page_size}
async def get_receipt_by_id(self, id_: int) -> Optional[dict]:
"""
对公转账记录详情
参数:
id_: 付款凭证ID
返回:
dict 或 None
"""
r = await PaymentReceipt.filter(id=id_).first()
if not r:
return None
inv = await r.invoice
return {
"submitted_at": r.created_at.isoformat() if r.created_at else "",
"receipt": {
"id": r.id,
"url": r.url,
"note": r.note,
"verified": r.verified,
},
"phone": inv.phone,
"wechat": inv.wechat,
"company_name": inv.company_name,
"tax_number": inv.tax_number,
"register_address": inv.register_address,
"register_phone": inv.register_phone,
"bank_name": inv.bank_name,
"bank_account": inv.bank_account,
"email": inv.email,
"ticket_type": inv.ticket_type,
"invoice_type": inv.invoice_type,
"status": inv.status,
}
async def get_out(self, id_: int) -> Optional[InvoiceOut]:
"""
根据ID返回发票输出对象
参数:
id_: 发票ID
返回:
InvoiceOut 或 None
"""
inv = await self.model.filter(id=id_).first()
if not inv:
return None
return InvoiceOut(
id=inv.id,
created_at=inv.created_at.isoformat() if inv.created_at else "",
ticket_type=inv.ticket_type,
invoice_type=inv.invoice_type,
phone=inv.phone,
email=inv.email,
company_name=inv.company_name,
tax_number=inv.tax_number,
register_address=inv.register_address,
register_phone=inv.register_phone,
bank_name=inv.bank_name,
bank_account=inv.bank_account,
status=inv.status,
app_user_id=inv.app_user_id,
header_id=inv.header_id,
wechat=inv.wechat,
)
invoice_controller = InvoiceController()