invoice.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import mimetypes
  2. from typing import List, Optional, Set
  3. from fastapi import APIRouter, Depends, HTTPException, Request, status, Query
  4. from sqlalchemy.orm import Session
  5. from starlette.datastructures import UploadFile as StarletteUploadFile
  6. from app.api.bx.export_common import (
  7. EXPORT_MAX_ROWS,
  8. ExportColumn,
  9. ExportValidationError,
  10. BxExportBodyBase,
  11. dump_export_row,
  12. respond_xlsx,
  13. )
  14. from app.core.bx_auth import BxUserContext, get_bx_current_user
  15. from app.core.tenant_context import get_tenant_no
  16. from app.core.database import get_db
  17. from app.schemas.bx.common import ApiResponse, PagedData
  18. from app.schemas.bx.invoice import InvoiceOut, LedgerOut, WalletSummaryOut
  19. from app.services.bx.invoice import InvoiceService
  20. router = APIRouter(tags=["发票夹"])
  21. @router.get("/invoice-wallets/mine/summary", response_model=ApiResponse[WalletSummaryOut])
  22. def get_mine_wallet_summary(
  23. userId: Optional[str] = None,
  24. db: Session = Depends(get_db),
  25. user: BxUserContext = Depends(get_bx_current_user),
  26. ):
  27. data = InvoiceService.get_mine_summary(db, user=user, owner_user_id=userId)
  28. return ApiResponse(data=WalletSummaryOut.model_validate(data))
  29. @router.get("/invoice-wallets", response_model=ApiResponse[PagedData[WalletSummaryOut]])
  30. def list_invoice_wallets(
  31. page: int = 1,
  32. pageSize: int = Query(10, ge=1, le=500),
  33. keyword: str = "",
  34. db: Session = Depends(get_db),
  35. user: BxUserContext = Depends(get_bx_current_user),
  36. ):
  37. rows, total = InvoiceService.list_wallets(
  38. db, user=user, page=page, page_size=pageSize, keyword=keyword
  39. )
  40. out_rows = [WalletSummaryOut.model_validate(InvoiceService._summary_to_dict(r)) for r in rows]
  41. return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize))
  42. WALLET_EXPORT_COLUMNS = [
  43. ExportColumn(title="员工", field="ownerUserName"),
  44. ExportColumn(title="总额(元)", field="totalAmount"),
  45. ExportColumn(title="已报销占用(元)", field="usedAmount"),
  46. ExportColumn(title="余额(元)", field="balance"),
  47. ExportColumn(title="发票张数", field="invoiceCount"),
  48. ]
  49. class WalletExportBody(BxExportBodyBase):
  50. keyword: str = ""
  51. @router.post("/invoice-wallets/export")
  52. def export_invoice_wallets(
  53. body: WalletExportBody,
  54. db: Session = Depends(get_db),
  55. user: BxUserContext = Depends(get_bx_current_user),
  56. ):
  57. try:
  58. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  59. rows, _ = InvoiceService.list_wallets(
  60. db, user=user, page=1, page_size=EXPORT_MAX_ROWS, keyword=body.keyword or ""
  61. )
  62. outs = [WalletSummaryOut.model_validate(InvoiceService._summary_to_dict(r)) for r in rows]
  63. if body.ids:
  64. id_set = {str(i) for i in body.ids}
  65. # wallet may key by ownerUserId
  66. outs = [o for o in outs if str(getattr(o, "owner_user_id", None) or getattr(o, "id", "")) in id_set or str(o.model_dump(by_alias=True).get("ownerUserId") or o.model_dump(by_alias=True).get("id") or "") in id_set]
  67. export_rows = [dump_export_row(o, status_fields=()) for o in outs]
  68. return respond_xlsx(
  69. db, tenant_no=tenant_no, sheet_title="发票夹", columns=WALLET_EXPORT_COLUMNS,
  70. rows=export_rows, filename_stem="发票夹汇总", requested_columns=body.columns,
  71. )
  72. except ExportValidationError as e:
  73. raise HTTPException(status_code=400, detail=str(e)) from e
  74. @router.get("/invoices", response_model=ApiResponse[PagedData[InvoiceOut]])
  75. def list_invoices(
  76. page: int = 1,
  77. pageSize: int = Query(10, ge=1, le=500),
  78. userId: Optional[str] = None,
  79. db: Session = Depends(get_db),
  80. user: BxUserContext = Depends(get_bx_current_user),
  81. ):
  82. tenant_no = InvoiceService._tenant_no(db, user)
  83. rows, total = InvoiceService.list_invoices(
  84. db, user=user, owner_user_id=userId, page=page, page_size=pageSize
  85. )
  86. out_rows = InvoiceService.out_rows_with_attachments(db, tenant_no, list(rows))
  87. return ApiResponse(
  88. data=PagedData(
  89. rows=[InvoiceOut.model_validate(r) for r in out_rows],
  90. total=total,
  91. page=page,
  92. page_size=pageSize,
  93. )
  94. )
  95. @router.post("/invoices", response_model=ApiResponse[InvoiceOut])
  96. async def create_invoice(
  97. request: Request,
  98. db: Session = Depends(get_db),
  99. user: BxUserContext = Depends(get_bx_current_user),
  100. ):
  101. """一次上传一条发票:多文件挂同一档案,金额为总额,只写一条额度流水。
  102. 使用 request.form().getlist,兼容只传 1 个文件时 FastAPI List[UploadFile] 的 422。
  103. """
  104. form = await request.form()
  105. amount_raw = form.get("amount")
  106. if amount_raw is None or str(amount_raw).strip() == "":
  107. raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请填写金额")
  108. try:
  109. amount = float(amount_raw)
  110. except (TypeError, ValueError) as exc:
  111. raise HTTPException(
  112. status_code=status.HTTP_400_BAD_REQUEST, detail="金额格式不正确"
  113. ) from exc
  114. remark = str(form.get("remark") or "")
  115. target_raw = form.get("targetUserId")
  116. target_user_id = str(target_raw).strip() if target_raw not in (None, "") else None
  117. upload_files: List[StarletteUploadFile] = []
  118. seen_ids: Set[int] = set()
  119. for key in ("files", "file"):
  120. for item in form.getlist(key):
  121. if not isinstance(item, StarletteUploadFile):
  122. continue
  123. obj_id = id(item)
  124. if obj_id in seen_ids:
  125. continue
  126. seen_ids.add(obj_id)
  127. upload_files.append(item)
  128. if not upload_files:
  129. raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请选择文件")
  130. file_payload = []
  131. for uf in upload_files:
  132. if not uf.filename:
  133. continue
  134. data = await uf.read()
  135. content_type = (
  136. uf.content_type
  137. or mimetypes.guess_type(uf.filename or "")[0]
  138. or "application/octet-stream"
  139. )
  140. file_payload.append((data, uf.filename or "unknown", content_type))
  141. if not file_payload:
  142. raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请选择文件")
  143. try:
  144. invoice = InvoiceService.create(
  145. db,
  146. user=user,
  147. amount=amount,
  148. files=file_payload,
  149. remark=remark,
  150. target_user_id=target_user_id,
  151. )
  152. db.commit()
  153. db.refresh(invoice)
  154. except HTTPException:
  155. db.rollback()
  156. raise
  157. except Exception as exc:
  158. db.rollback()
  159. raise HTTPException(
  160. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="上传发票失败"
  161. ) from exc
  162. tenant_no = InvoiceService._tenant_no(db, user)
  163. out = InvoiceService.out_rows_with_attachments(db, tenant_no, [invoice])[0]
  164. return ApiResponse(data=InvoiceOut.model_validate(out))
  165. @router.delete("/invoices/{invoice_id}", response_model=ApiResponse[bool])
  166. def delete_invoice(
  167. invoice_id: str,
  168. db: Session = Depends(get_db),
  169. user: BxUserContext = Depends(get_bx_current_user),
  170. ):
  171. try:
  172. success = InvoiceService.delete(db, user=user, invoice_id=invoice_id)
  173. db.commit()
  174. except HTTPException:
  175. db.rollback()
  176. raise
  177. return ApiResponse(data=success)
  178. @router.get("/invoice-ledgers", response_model=ApiResponse[List[LedgerOut]])
  179. def list_invoice_ledgers(
  180. userId: Optional[str] = None,
  181. db: Session = Depends(get_db),
  182. user: BxUserContext = Depends(get_bx_current_user),
  183. ):
  184. rows = InvoiceService.list_ledgers(db, user=user, owner_user_id=userId)
  185. return ApiResponse(data=[LedgerOut.model_validate(r) for r in rows])