invoice.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import mimetypes
  2. from typing import List, Optional, Set
  3. from fastapi import APIRouter, Depends, HTTPException, Request, status
  4. from sqlalchemy.orm import Session
  5. from starlette.datastructures import UploadFile as StarletteUploadFile
  6. from app.core.bx_auth import BxUserContext, get_bx_current_user
  7. from app.core.database import get_db
  8. from app.schemas.bx.common import ApiResponse, PagedData
  9. from app.schemas.bx.invoice import InvoiceOut, LedgerOut, WalletSummaryOut
  10. from app.services.bx.invoice import InvoiceService
  11. router = APIRouter(tags=["发票夹"])
  12. @router.get("/invoice-wallets/mine/summary", response_model=ApiResponse[WalletSummaryOut])
  13. def get_mine_wallet_summary(
  14. userId: Optional[str] = None,
  15. db: Session = Depends(get_db),
  16. user: BxUserContext = Depends(get_bx_current_user),
  17. ):
  18. data = InvoiceService.get_mine_summary(db, user=user, owner_user_id=userId)
  19. return ApiResponse(data=WalletSummaryOut.model_validate(data))
  20. @router.get("/invoice-wallets", response_model=ApiResponse[PagedData[WalletSummaryOut]])
  21. def list_invoice_wallets(
  22. page: int = 1,
  23. pageSize: int = 10,
  24. keyword: str = "",
  25. db: Session = Depends(get_db),
  26. user: BxUserContext = Depends(get_bx_current_user),
  27. ):
  28. rows, total = InvoiceService.list_wallets(
  29. db, user=user, page=page, page_size=pageSize, keyword=keyword
  30. )
  31. out_rows = [WalletSummaryOut.model_validate(InvoiceService._summary_to_dict(r)) for r in rows]
  32. return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize))
  33. @router.get("/invoices", response_model=ApiResponse[PagedData[InvoiceOut]])
  34. def list_invoices(
  35. page: int = 1,
  36. pageSize: int = 10,
  37. userId: Optional[str] = None,
  38. db: Session = Depends(get_db),
  39. user: BxUserContext = Depends(get_bx_current_user),
  40. ):
  41. tenant_no = InvoiceService._tenant_no(db, user)
  42. rows, total = InvoiceService.list_invoices(
  43. db, user=user, owner_user_id=userId, page=page, page_size=pageSize
  44. )
  45. out_rows = InvoiceService.out_rows_with_attachments(db, tenant_no, list(rows))
  46. return ApiResponse(
  47. data=PagedData(
  48. rows=[InvoiceOut.model_validate(r) for r in out_rows],
  49. total=total,
  50. page=page,
  51. page_size=pageSize,
  52. )
  53. )
  54. @router.post("/invoices", response_model=ApiResponse[InvoiceOut])
  55. async def create_invoice(
  56. request: Request,
  57. db: Session = Depends(get_db),
  58. user: BxUserContext = Depends(get_bx_current_user),
  59. ):
  60. """一次上传一条发票:多文件挂同一档案,金额为总额,只写一条额度流水。
  61. 使用 request.form().getlist,兼容只传 1 个文件时 FastAPI List[UploadFile] 的 422。
  62. """
  63. form = await request.form()
  64. amount_raw = form.get("amount")
  65. if amount_raw is None or str(amount_raw).strip() == "":
  66. raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请填写金额")
  67. try:
  68. amount = float(amount_raw)
  69. except (TypeError, ValueError) as exc:
  70. raise HTTPException(
  71. status_code=status.HTTP_400_BAD_REQUEST, detail="金额格式不正确"
  72. ) from exc
  73. remark = str(form.get("remark") or "")
  74. target_raw = form.get("targetUserId")
  75. target_user_id = str(target_raw).strip() if target_raw not in (None, "") else None
  76. upload_files: List[StarletteUploadFile] = []
  77. seen_ids: Set[int] = set()
  78. for key in ("files", "file"):
  79. for item in form.getlist(key):
  80. if not isinstance(item, StarletteUploadFile):
  81. continue
  82. obj_id = id(item)
  83. if obj_id in seen_ids:
  84. continue
  85. seen_ids.add(obj_id)
  86. upload_files.append(item)
  87. if not upload_files:
  88. raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请选择文件")
  89. file_payload = []
  90. for uf in upload_files:
  91. if not uf.filename:
  92. continue
  93. data = await uf.read()
  94. content_type = (
  95. uf.content_type
  96. or mimetypes.guess_type(uf.filename or "")[0]
  97. or "application/octet-stream"
  98. )
  99. file_payload.append((data, uf.filename or "unknown", content_type))
  100. if not file_payload:
  101. raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请选择文件")
  102. try:
  103. invoice = InvoiceService.create(
  104. db,
  105. user=user,
  106. amount=amount,
  107. files=file_payload,
  108. remark=remark,
  109. target_user_id=target_user_id,
  110. )
  111. db.commit()
  112. db.refresh(invoice)
  113. except HTTPException:
  114. db.rollback()
  115. raise
  116. except Exception as exc:
  117. db.rollback()
  118. raise HTTPException(
  119. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="上传发票失败"
  120. ) from exc
  121. tenant_no = InvoiceService._tenant_no(db, user)
  122. out = InvoiceService.out_rows_with_attachments(db, tenant_no, [invoice])[0]
  123. return ApiResponse(data=InvoiceOut.model_validate(out))
  124. @router.delete("/invoices/{invoice_id}", response_model=ApiResponse[bool])
  125. def delete_invoice(
  126. invoice_id: str,
  127. db: Session = Depends(get_db),
  128. user: BxUserContext = Depends(get_bx_current_user),
  129. ):
  130. try:
  131. success = InvoiceService.delete(db, user=user, invoice_id=invoice_id)
  132. db.commit()
  133. except HTTPException:
  134. db.rollback()
  135. raise
  136. return ApiResponse(data=success)
  137. @router.get("/invoice-ledgers", response_model=ApiResponse[List[LedgerOut]])
  138. def list_invoice_ledgers(
  139. userId: Optional[str] = None,
  140. db: Session = Depends(get_db),
  141. user: BxUserContext = Depends(get_bx_current_user),
  142. ):
  143. rows = InvoiceService.list_ledgers(db, user=user, owner_user_id=userId)
  144. return ApiResponse(data=[LedgerOut.model_validate(r) for r in rows])