invoice.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. """发票管理 API"""
  2. from typing import Optional, List
  3. from fastapi import APIRouter, Depends, HTTPException
  4. from fastapi.responses import FileResponse
  5. from sqlalchemy.orm import Session
  6. from pydantic import BaseModel, Field
  7. import os
  8. import tempfile
  9. from app.core.database import get_db
  10. from app.core.auth import get_current_user
  11. from app.core.response import success_response, error_response
  12. from app.core.utils import generate_id
  13. from app.models.models import BizInvoice
  14. from app.crud.base import CRUDBase
  15. router = APIRouter(prefix="/api/web/admin/invoice", tags=["发票管理"])
  16. class InvoiceCRUD(CRUDBase[BizInvoice]):
  17. """发票CRUD"""
  18. def __init__(self):
  19. super().__init__(BizInvoice)
  20. def create_invoice(
  21. self,
  22. db: Session,
  23. order_id: str,
  24. order_no: str,
  25. client_id: str,
  26. client_name: str,
  27. invoice_type: str,
  28. title: str,
  29. invoice_amount: float,
  30. tax_rate: float = 0,
  31. tax_amount: float = 0,
  32. tax_no: str = None,
  33. bank: str = None,
  34. bank_account: str = None,
  35. address: str = None,
  36. phone: str = None,
  37. remark: str = None,
  38. create_by: str = None,
  39. create_name: str = None
  40. ) -> BizInvoice:
  41. """创建发票"""
  42. # 生成发票号
  43. invoice_no = f"INV{datetime.now().strftime('%Y%m%d%H%M%S')}{random.randint(1000, 9999)}"
  44. data = {
  45. "id": generate_id(),
  46. "invoice_no": invoice_no,
  47. "order_id": order_id,
  48. "order_no": order_no,
  49. "client_id": client_id,
  50. "client_name": client_name,
  51. "invoice_type": invoice_type,
  52. "title": title,
  53. "invoice_amount": invoice_amount,
  54. "tax_rate": tax_rate,
  55. "tax_amount": tax_amount,
  56. "tax_no": tax_no,
  57. "bank": bank,
  58. "bank_account": bank_account,
  59. "address": address,
  60. "phone": phone,
  61. "remark": remark,
  62. "status": "pending",
  63. "create_by": create_by,
  64. "create_name": create_name
  65. }
  66. return self.create(db, data)
  67. def issue_invoice(
  68. self,
  69. db: Session,
  70. invoice_id: str,
  71. issued_by: str = None,
  72. issued_by_name: str = None,
  73. invoice_url: str = None
  74. ) -> BizInvoice:
  75. """开具发票"""
  76. invoice = self.get(db, invoice_id)
  77. if not invoice:
  78. return None
  79. invoice.status = "issued"
  80. invoice.issued_at = datetime.now()
  81. if issued_by:
  82. invoice.issued_by = issued_by
  83. if issued_by_name:
  84. invoice.issued_by_name = issued_by_name
  85. if invoice_url:
  86. invoice.invoice_url = invoice_url
  87. db.commit()
  88. db.refresh(invoice)
  89. return invoice
  90. def void_invoice(self, db: Session, invoice_id: str) -> BizInvoice:
  91. """作废发票"""
  92. invoice = self.get(db, invoice_id)
  93. if not invoice:
  94. return None
  95. invoice.status = "void"
  96. db.commit()
  97. db.refresh(invoice)
  98. return invoice
  99. def get_by_order(self, db: Session, order_id: str) -> List[BizInvoice]:
  100. """获取订单的所有发票"""
  101. return db.query(self.model).filter(
  102. self.model.order_id == order_id,
  103. self.model.is_del == 0
  104. ).all()
  105. from datetime import datetime
  106. import random
  107. invoice_crud = InvoiceCRUD()
  108. class InvoiceCreateRequest(BaseModel):
  109. """创建发票请求"""
  110. order_id: str = Field(..., description="订单ID")
  111. invoice_type: str = Field(..., description="发票类型: normal-普通发票, vat-增值税发票")
  112. title: str = Field(..., description="发票抬头")
  113. tax_rate: float = Field(0, description="税率")
  114. tax_no: Optional[str] = Field(None, description="税号")
  115. bank: Optional[str] = Field(None, description="开户银行")
  116. bank_account: Optional[str] = Field(None, description="银行账号")
  117. address: Optional[str] = Field(None, description="开票地址")
  118. phone: Optional[str] = Field(None, description="电话")
  119. remark: Optional[str] = Field(None, description="备注")
  120. @router.post("/create", response_model=dict)
  121. def create_invoice(
  122. request: InvoiceCreateRequest,
  123. db: Session = Depends(get_db),
  124. current_user: dict = Depends(get_current_user)
  125. ):
  126. """创建发票"""
  127. try:
  128. # 获取订单信息
  129. from app.crud.order import order_crud
  130. order = order_crud.get(db, request.order_id)
  131. if not order:
  132. return error_response(404, "订单不存在")
  133. # 计算税额
  134. tax_amount = request.invoice_amount * request.tax_rate / 100 if request.tax_rate else 0
  135. invoice = invoice_crud.create_invoice(
  136. db=db,
  137. order_id=request.order_id,
  138. order_no=order.order_no,
  139. client_id=order.client_id,
  140. client_name=order.client_name,
  141. invoice_type=request.invoice_type,
  142. title=request.title,
  143. invoice_amount=request.invoice_amount,
  144. tax_rate=request.tax_rate,
  145. tax_amount=tax_amount,
  146. tax_no=request.tax_no,
  147. bank=request.bank,
  148. bank_account=request.bank_account,
  149. address=request.address,
  150. phone=request.phone,
  151. remark=request.remark,
  152. create_by=current_user.get("user_id"),
  153. create_name=current_user.get("real_name")
  154. )
  155. return success_response({
  156. "id": invoice.id,
  157. "invoice_no": invoice.invoice_no,
  158. "message": "发票创建成功"
  159. })
  160. except Exception as e:
  161. return error_response(500, f"创建发票失败: {str(e)}")
  162. @router.get("/list", response_model=dict)
  163. def get_invoice_list(
  164. page: int = 1,
  165. page_size: int = 20,
  166. invoice_no: str = None,
  167. client_name: str = None,
  168. invoice_type: str = None,
  169. status: str = None,
  170. start_date: str = None,
  171. end_date: str = None,
  172. db: Session = Depends(get_db),
  173. current_user: dict = Depends(get_current_user)
  174. ):
  175. """获取发票列表"""
  176. try:
  177. query = db.query(invoice_crud.model).filter(invoice_crud.model.is_del == 0)
  178. if invoice_no:
  179. query = query.filter(invoice_crud.model.invoice_no.like(f"%{invoice_no}%"))
  180. if client_name:
  181. query = query.filter(invoice_crud.model.client_name.like(f"%{client_name}%"))
  182. if invoice_type:
  183. query = query.filter(invoice_crud.model.invoice_type == invoice_type)
  184. if status:
  185. query = query.filter(invoice_crud.model.status == status)
  186. if start_date:
  187. query = query.filter(invoice_crud.model.create_at >= start_date)
  188. if end_date:
  189. query = query.filter(invoice_crud.model.create_at <= end_date)
  190. total = query.count()
  191. items = query.order_by(invoice_crud.model.create_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
  192. return success_response({
  193. "list": [_to_response(item) for item in items],
  194. "total": total,
  195. "page": page,
  196. "page_size": page_size
  197. })
  198. except Exception as e:
  199. return error_response(500, f"获取发票列表失败: {str(e)}")
  200. @router.get("/detail/{invoice_id}", response_model=dict)
  201. def get_invoice_detail(
  202. invoice_id: str,
  203. db: Session = Depends(get_db),
  204. current_user: dict = Depends(get_current_user)
  205. ):
  206. """获取发票详情"""
  207. try:
  208. invoice = invoice_crud.get(db, invoice_id)
  209. if not invoice:
  210. return error_response(404, "发票不存在")
  211. if invoice.is_del == 1:
  212. return error_response(404, "发票已删除")
  213. return success_response(_to_response(invoice))
  214. except Exception as e:
  215. return error_response(500, f"获取发票详情失败: {str(e)}")
  216. @router.post("/issue/{invoice_id}", response_model=dict)
  217. def issue_invoice(
  218. invoice_id: str,
  219. invoice_url: str = None,
  220. db: Session = Depends(get_db),
  221. current_user: dict = Depends(get_current_user)
  222. ):
  223. """开具发票"""
  224. try:
  225. invoice = invoice_crud.issue_invoice(
  226. db=db,
  227. invoice_id=invoice_id,
  228. issued_by=current_user.get("user_id"),
  229. issued_by_name=current_user.get("real_name"),
  230. invoice_url=invoice_url
  231. )
  232. if not invoice:
  233. return error_response(404, "发票不存在")
  234. return success_response({"message": "发票开具成功"})
  235. except Exception as e:
  236. return error_response(500, f"开具发票失败: {str(e)}")
  237. @router.post("/void/{invoice_id}", response_model=dict)
  238. def void_invoice(
  239. invoice_id: str,
  240. db: Session = Depends(get_db),
  241. current_user: dict = Depends(get_current_user)
  242. ):
  243. """作废发票"""
  244. try:
  245. invoice = invoice_crud.void_invoice(db, invoice_id)
  246. if not invoice:
  247. return error_response(404, "发票不存在")
  248. return success_response({"message": "发票作废成功"})
  249. except Exception as e:
  250. return error_response(500, f"作废发票失败: {str(e)}")
  251. @router.get("/download/{invoice_id}", response_model=dict)
  252. def download_invoice_pdf(
  253. invoice_id: str,
  254. db: Session = Depends(get_db),
  255. current_user: dict = Depends(get_current_user)
  256. ):
  257. """下载发票PDF"""
  258. try:
  259. invoice = invoice_crud.get(db, invoice_id)
  260. if not invoice:
  261. return error_response(404, "发票不存在")
  262. if invoice.status != "issued":
  263. return error_response(400, "只有已开具的发票可以下载")
  264. # 如果有发票URL,直接返回
  265. if invoice.invoice_url:
  266. return success_response({
  267. "url": invoice.invoice_url,
  268. "filename": f"{invoice.invoice_no}.pdf"
  269. })
  270. # 生成PDF(模拟生成,实际需要调用PDF生成服务)
  271. pdf_path = _generate_invoice_pdf(invoice)
  272. if pdf_path:
  273. return success_response({
  274. "url": f"/files/{os.path.basename(pdf_path)}",
  275. "filename": f"{invoice.invoice_no}.pdf"
  276. })
  277. return error_response(500, "生成发票PDF失败")
  278. except Exception as e:
  279. return error_response(500, f"下载发票失败: {str(e)}")
  280. def _generate_invoice_pdf(invoice: BizInvoice) -> Optional[str]:
  281. """生成发票PDF(简化实现)"""
  282. try:
  283. from app.core.config import settings
  284. temp_dir = getattr(settings, 'TEMP_FILE_DIR', tempfile.gettempdir())
  285. os.makedirs(temp_dir, exist_ok=True)
  286. filename = f"{invoice.invoice_no}.pdf"
  287. filepath = os.path.join(temp_dir, filename)
  288. # 创建空文件作为占位符,实际应使用PDF库生成
  289. with open(filepath, 'w') as f:
  290. f.write(f"Invoice: {invoice.invoice_no}\n")
  291. f.write(f"Title: {invoice.title}\n")
  292. f.write(f"Amount: {invoice.invoice_amount}\n")
  293. return filepath
  294. except Exception:
  295. return None
  296. # =========================================
  297. # 小程序端发票接口
  298. # =========================================
  299. @router.get("/mini/client/invoices", response_model=dict)
  300. def get_client_invoices(
  301. page: int = 1,
  302. page_size: int = 20,
  303. status: str = None,
  304. db: Session = Depends(get_db),
  305. current_user: dict = Depends(get_current_user)
  306. ):
  307. """获取客户发票列表"""
  308. try:
  309. if current_user.get("role") != "client":
  310. return error_response(403, "需要客户权限")
  311. client_id = current_user.get("user_id")
  312. query = db.query(invoice_crud.model).filter(
  313. invoice_crud.model.client_id == client_id,
  314. invoice_crud.model.is_del == 0
  315. )
  316. if status:
  317. query = query.filter(invoice_crud.model.status == status)
  318. total = query.count()
  319. items = query.order_by(invoice_crud.model.create_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
  320. return success_response({
  321. "list": [_to_response(item) for item in items],
  322. "total": total,
  323. "page": page,
  324. "pageSize": page_size
  325. })
  326. except Exception as e:
  327. return error_response(500, f"获取发票列表失败: {str(e)}")
  328. @router.get("/mini/client/invoices/{invoice_id}", response_model=dict)
  329. def get_client_invoice_detail(
  330. invoice_id: str,
  331. db: Session = Depends(get_db),
  332. current_user: dict = Depends(get_current_user)
  333. ):
  334. """获取客户发票详情"""
  335. try:
  336. if current_user.get("role") != "client":
  337. return error_response(403, "需要客户权限")
  338. invoice = invoice_crud.get(db, invoice_id)
  339. if not invoice:
  340. return error_response(404, "发票不存在")
  341. if invoice.client_id != current_user.get("user_id"):
  342. return error_response(403, "无权查看此发票")
  343. return success_response(_to_response(invoice))
  344. except Exception as e:
  345. return error_response(500, f"获取发票详情失败: {str(e)}")
  346. @router.get("/mini/client/invoices/{invoice_id}/download", response_model=dict)
  347. def download_client_invoice(
  348. invoice_id: str,
  349. db: Session = Depends(get_db),
  350. current_user: dict = Depends(get_current_user)
  351. ):
  352. """小程序端下载发票"""
  353. try:
  354. if current_user.get("role") != "client":
  355. return error_response(403, "需要客户权限")
  356. invoice = invoice_crud.get(db, invoice_id)
  357. if not invoice:
  358. return error_response(404, "发票不存在")
  359. if invoice.client_id != current_user.get("user_id"):
  360. return error_response(403, "无权下载此发票")
  361. if invoice.status != "issued":
  362. return error_response(400, "只有已开具的发票可以下载")
  363. if invoice.invoice_url:
  364. return success_response({
  365. "url": invoice.invoice_url,
  366. "filename": f"{invoice.invoice_no}.pdf"
  367. })
  368. return error_response(400, "发票文件不存在")
  369. except Exception as e:
  370. return error_response(500, f"下载发票失败: {str(e)}")
  371. def _to_response(invoice: BizInvoice) -> dict:
  372. """转换为响应格式"""
  373. return {
  374. "id": invoice.id,
  375. "invoice_no": invoice.invoice_no,
  376. "order_id": invoice.order_id,
  377. "order_no": invoice.order_no,
  378. "client_id": invoice.client_id,
  379. "client_name": invoice.client_name,
  380. "invoice_type": invoice.invoice_type,
  381. "title": invoice.title,
  382. "invoice_amount": float(invoice.invoice_amount) if invoice.invoice_amount else 0,
  383. "tax_rate": float(invoice.tax_rate) if invoice.tax_rate else 0,
  384. "tax_amount": float(invoice.tax_amount) if invoice.tax_amount else 0,
  385. "tax_no": invoice.tax_no,
  386. "bank": invoice.bank,
  387. "bank_account": invoice.bank_account,
  388. "address": invoice.address,
  389. "phone": invoice.phone,
  390. "status": invoice.status,
  391. "invoice_url": invoice.invoice_url,
  392. "issued_by": invoice.issued_by,
  393. "issued_by_name": invoice.issued_by_name,
  394. "issued_at": str(invoice.issued_at) if invoice.issued_at else None,
  395. "remark": invoice.remark,
  396. "create_at": str(invoice.create_at) if invoice.create_at else None
  397. }