protocol_sample_payment.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. """按样本缴费方案 / 转账笔财务确认 API(payment_model_version=2)"""
  2. from __future__ import annotations
  3. from typing import Any, Dict, List, Optional
  4. from fastapi import APIRouter, Depends, Query
  5. from pydantic import BaseModel, ConfigDict, Field
  6. from sqlalchemy.orm import Session
  7. from app.core.database import get_db
  8. from app.core.menu_guard import require_admin_or_super_admin
  9. from app.core.response import error_response, success_response
  10. from app.crud import protocol_sample_payment as psp_svc
  11. from app.models.models import BizCollectionProtocol, SysUser
  12. router = APIRouter(prefix="/web/admin", tags=["细胞存储-按样本缴费"])
  13. def _get_tenant_no(db: Session, user: dict) -> str:
  14. user_id = user.get("user_id")
  15. if not user_id:
  16. raise PermissionError("用户身份信息缺失,无法确定租户")
  17. sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
  18. if not sys_user or not sys_user.tenant_no:
  19. raise PermissionError("用户租户信息缺失")
  20. return sys_user.tenant_no
  21. def _get_current_user_id(user: dict) -> str:
  22. user_id = user.get("user_id")
  23. if not user_id:
  24. raise PermissionError("用户身份信息缺失")
  25. return str(user_id)
  26. def _protocol_accessible(db: Session, protocol_id: str, tenant_no: str, user: dict) -> bool:
  27. from app.core.data_scope import apply_data_scope
  28. q = db.query(BizCollectionProtocol).filter(
  29. BizCollectionProtocol.id == protocol_id,
  30. BizCollectionProtocol.del_flag == 0,
  31. BizCollectionProtocol.tenant_no == tenant_no,
  32. )
  33. q = apply_data_scope(q, BizCollectionProtocol, user, db, tenant_no)
  34. return q.first() is not None
  35. class ScheduleBody(BaseModel):
  36. model_config = ConfigDict(extra="ignore", populate_by_name=True)
  37. id: Optional[str] = None
  38. schedule_no: Optional[int] = Field(None, alias="scheduleNo")
  39. due_amount: Optional[float] = Field(None, alias="dueAmount")
  40. due_date: Optional[str] = Field(None, alias="dueDate")
  41. schedule_status: Optional[str] = Field(None, alias="scheduleStatus")
  42. remark: Optional[str] = None
  43. transfers: Optional[List[Dict[str, Any]]] = None
  44. class SamplePaymentBody(BaseModel):
  45. model_config = ConfigDict(extra="ignore", populate_by_name=True)
  46. id: Optional[str] = None
  47. sample_code: str = Field(..., alias="sampleCode")
  48. protocol_sample_id: Optional[str] = Field(None, alias="protocolSampleId")
  49. pay_mode: str = Field(..., alias="payMode")
  50. contract_amount: Optional[float] = Field(None, alias="contractAmount")
  51. installment_count: Optional[int] = Field(None, alias="installmentCount")
  52. installment_interval_months: Optional[float] = Field(
  53. None, alias="installmentIntervalMonths"
  54. )
  55. installment_start_date: Optional[str] = Field(None, alias="installmentStartDate")
  56. installment_advance_days: Optional[int] = Field(None, alias="installmentAdvanceDays")
  57. installment_ratios: Optional[Any] = Field(None, alias="installmentRatios")
  58. lump_sum_deadline: Optional[str] = Field(None, alias="lumpSumDeadline")
  59. remark: Optional[str] = None
  60. schedules: Optional[List[ScheduleBody]] = None
  61. class SamplePaymentsPutBody(BaseModel):
  62. model_config = ConfigDict(extra="ignore", populate_by_name=True)
  63. sample_payments: List[SamplePaymentBody] = Field(
  64. default_factory=list, alias="samplePayments"
  65. )
  66. class TransferCreateBody(BaseModel):
  67. model_config = ConfigDict(extra="ignore", populate_by_name=True)
  68. schedule_id: str = Field(..., alias="scheduleId")
  69. transfer_amount: Optional[float] = Field(None, alias="transferAmount")
  70. transfer_time: Optional[str] = Field(None, alias="transferTime")
  71. transfer_type: Optional[str] = Field(None, alias="transferType")
  72. transfer_attachments: Optional[str] = Field(None, alias="transferAttachments")
  73. remark: Optional[str] = None
  74. class TransferDraftBody(BaseModel):
  75. model_config = ConfigDict(extra="ignore", populate_by_name=True)
  76. transfer_amount: Optional[float] = Field(None, alias="transferAmount")
  77. transfer_time: Optional[str] = Field(None, alias="transferTime")
  78. transfer_type: Optional[str] = Field(None, alias="transferType")
  79. transfer_attachments: Optional[str] = Field(None, alias="transferAttachments")
  80. remark: Optional[str] = None
  81. class FinanceConfirmBody(BaseModel):
  82. model_config = ConfigDict(extra="ignore", populate_by_name=True)
  83. finance_confirm_attachments: Optional[str] = Field(
  84. None, alias="financeConfirmAttachments"
  85. )
  86. finance_remark: Optional[str] = Field(None, alias="financeRemark")
  87. def _ratios_to_str(ratios: Any) -> Optional[str]:
  88. if ratios is None:
  89. return None
  90. if isinstance(ratios, str):
  91. return ratios.strip() or None
  92. import json
  93. try:
  94. return json.dumps(ratios, ensure_ascii=False)
  95. except Exception:
  96. return str(ratios)
  97. def _save_sample_payments(
  98. db: Session,
  99. *,
  100. protocol_id: str,
  101. tenant_no: str,
  102. user_id: str,
  103. payments: List[SamplePaymentBody],
  104. ) -> List[Dict[str, Any]]:
  105. for item in payments:
  106. sp = psp_svc.upsert_sample_payment(
  107. db,
  108. collection_protocol_id=protocol_id,
  109. tenant_no=tenant_no,
  110. user_id=user_id,
  111. sample_code=item.sample_code,
  112. pay_mode=item.pay_mode,
  113. contract_amount=item.contract_amount,
  114. protocol_sample_id=item.protocol_sample_id,
  115. installment_count=item.installment_count,
  116. installment_interval_months=item.installment_interval_months,
  117. installment_start_date=item.installment_start_date,
  118. installment_advance_days=item.installment_advance_days,
  119. installment_ratios=_ratios_to_str(item.installment_ratios),
  120. lump_sum_deadline=item.lump_sum_deadline,
  121. remark=item.remark,
  122. sample_payment_id=item.id,
  123. )
  124. if item.schedules is not None:
  125. schedule_payloads = [s.model_dump(by_alias=False) for s in item.schedules]
  126. created = psp_svc.replace_schedules(
  127. db,
  128. sample_payment_id=sp.id,
  129. tenant_no=tenant_no,
  130. user_id=user_id,
  131. schedules=schedule_payloads,
  132. )
  133. # 可选:随 PUT 带入的草稿转账(仅新建 dfj)
  134. for sch_body, sch_row in zip(item.schedules, created):
  135. for tr in sch_body.transfers or []:
  136. psp_svc.add_transfer(
  137. db,
  138. schedule_id=sch_row.id,
  139. tenant_no=tenant_no,
  140. user_id=user_id,
  141. transfer_amount=tr.get("transferAmount") or tr.get("transfer_amount"),
  142. transfer_time=tr.get("transferTime") or tr.get("transfer_time"),
  143. transfer_type=tr.get("transferType") or tr.get("transfer_type"),
  144. transfer_attachments=tr.get("transferAttachments")
  145. or tr.get("transfer_attachments"),
  146. remark=tr.get("remark"),
  147. commit=False,
  148. )
  149. db.commit()
  150. return psp_svc.get_sample_payments_bundle(db, protocol_id, tenant_no)
  151. @router.get("/sample-payments")
  152. async def list_sample_payments(
  153. page: int = Query(1, ge=1),
  154. page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
  155. keyword: Optional[str] = None,
  156. payMode: Optional[str] = Query(None, alias="payMode"),
  157. progressStatus: Optional[str] = Query(None, alias="progressStatus"),
  158. installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
  159. installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
  160. user: dict = Depends(require_admin_or_super_admin),
  161. db: Session = Depends(get_db),
  162. ):
  163. """按样本缴费方案分页列表(独立缴费信息菜单)。"""
  164. try:
  165. tenant_no = _get_tenant_no(db, user)
  166. items, total = psp_svc.list_sample_payments_page(
  167. db,
  168. tenant_no=tenant_no,
  169. keyword=keyword,
  170. pay_mode=payMode,
  171. progress_status=progressStatus,
  172. due_date_start=installmentDueDateStart,
  173. due_date_end=installmentDueDateEnd,
  174. page=page,
  175. page_size=page_size,
  176. user_info=user,
  177. )
  178. return success_response(
  179. {"list": items, "total": total, "page": page, "pageSize": page_size}
  180. )
  181. except PermissionError as e:
  182. return error_response(403, str(e))
  183. except ValueError as e:
  184. return error_response(400, str(e))
  185. @router.get("/sample-payments/{sample_payment_id}")
  186. async def get_sample_payment(
  187. sample_payment_id: str,
  188. user: dict = Depends(require_admin_or_super_admin),
  189. db: Session = Depends(get_db),
  190. ):
  191. try:
  192. tenant_no = _get_tenant_no(db, user)
  193. data = psp_svc.get_sample_payment_detail(
  194. db,
  195. sample_payment_id=sample_payment_id,
  196. tenant_no=tenant_no,
  197. user_info=user,
  198. )
  199. if not data:
  200. return error_response(403, "无权查看或记录不存在")
  201. return success_response(data)
  202. except PermissionError as e:
  203. return error_response(403, str(e))
  204. except ValueError as e:
  205. return error_response(400, str(e))
  206. @router.get("/collection-protocols/{protocol_id}/sample-payments")
  207. async def get_sample_payments(
  208. protocol_id: str,
  209. user: dict = Depends(require_admin_or_super_admin),
  210. db: Session = Depends(get_db),
  211. ):
  212. try:
  213. tenant_no = _get_tenant_no(db, user)
  214. if not _protocol_accessible(db, protocol_id, tenant_no, user):
  215. return error_response(404, "协议单不存在或无权查看")
  216. data = psp_svc.get_sample_payments_bundle(db, protocol_id, tenant_no)
  217. return success_response(data)
  218. except PermissionError as e:
  219. return error_response(403, str(e))
  220. except ValueError as e:
  221. return error_response(400, str(e))
  222. @router.put("/collection-protocols/{protocol_id}/sample-payments")
  223. async def put_sample_payments(
  224. protocol_id: str,
  225. body: SamplePaymentsPutBody,
  226. user: dict = Depends(require_admin_or_super_admin),
  227. db: Session = Depends(get_db),
  228. ):
  229. try:
  230. tenant_no = _get_tenant_no(db, user)
  231. user_id = _get_current_user_id(user)
  232. if not _protocol_accessible(db, protocol_id, tenant_no, user):
  233. return error_response(404, "协议单不存在或无权操作")
  234. # pydantic v2: may need model_validate with populate
  235. payments = body.sample_payments
  236. if hasattr(body, "model_dump"):
  237. raw = body.model_dump(by_alias=True)
  238. payments = [
  239. SamplePaymentBody.model_validate(x)
  240. for x in (raw.get("samplePayments") or raw.get("sample_payments") or [])
  241. ]
  242. data = _save_sample_payments(
  243. db,
  244. protocol_id=protocol_id,
  245. tenant_no=tenant_no,
  246. user_id=user_id,
  247. payments=payments or body.sample_payments,
  248. )
  249. return success_response(data, message="保存成功")
  250. except PermissionError as e:
  251. return error_response(403, str(e))
  252. except ValueError as e:
  253. return error_response(400, str(e))
  254. @router.post("/payment-transfers")
  255. async def create_payment_transfer(
  256. body: TransferCreateBody,
  257. user: dict = Depends(require_admin_or_super_admin),
  258. db: Session = Depends(get_db),
  259. ):
  260. try:
  261. tenant_no = _get_tenant_no(db, user)
  262. user_id = _get_current_user_id(user)
  263. sch = psp_svc._get_schedule_or_raise(
  264. db, schedule_id=body.schedule_id, tenant_no=tenant_no
  265. )
  266. if not _protocol_accessible(db, sch.collection_protocol_id, tenant_no, user):
  267. return error_response(403, "无权操作该协议")
  268. obj = psp_svc.add_transfer(
  269. db,
  270. schedule_id=body.schedule_id,
  271. tenant_no=tenant_no,
  272. user_id=user_id,
  273. transfer_amount=body.transfer_amount,
  274. transfer_time=body.transfer_time,
  275. transfer_type=body.transfer_type,
  276. transfer_attachments=body.transfer_attachments,
  277. remark=body.remark,
  278. )
  279. from app.crud.protocol_sample_payment import _transfer_to_dict
  280. return success_response(
  281. _transfer_to_dict(db, obj, tenant_no), message="创建成功"
  282. )
  283. except PermissionError as e:
  284. return error_response(403, str(e))
  285. except ValueError as e:
  286. return error_response(400, str(e))
  287. @router.put("/payment-transfers/{transfer_id}")
  288. async def update_payment_transfer(
  289. transfer_id: str,
  290. body: TransferDraftBody,
  291. user: dict = Depends(require_admin_or_super_admin),
  292. db: Session = Depends(get_db),
  293. ):
  294. try:
  295. tenant_no = _get_tenant_no(db, user)
  296. user_id = _get_current_user_id(user)
  297. fields = body.model_dump(by_alias=False, exclude_unset=True)
  298. obj = psp_svc.update_transfer_draft(
  299. db,
  300. transfer_id=transfer_id,
  301. tenant_no=tenant_no,
  302. user_id=user_id,
  303. fields=fields,
  304. )
  305. if not _protocol_accessible(db, obj.collection_protocol_id, tenant_no, user):
  306. return error_response(403, "无权操作该协议")
  307. from app.crud.protocol_sample_payment import _transfer_to_dict
  308. return success_response(_transfer_to_dict(db, obj, tenant_no), message="保存成功")
  309. except PermissionError as e:
  310. return error_response(403, str(e))
  311. except ValueError as e:
  312. return error_response(400, str(e))
  313. @router.post("/payment-transfers/{transfer_id}/submit-receipt-finance")
  314. async def submit_payment_transfer_finance(
  315. transfer_id: str,
  316. body: Optional[TransferDraftBody] = None,
  317. user: dict = Depends(require_admin_or_super_admin),
  318. db: Session = Depends(get_db),
  319. ):
  320. try:
  321. tenant_no = _get_tenant_no(db, user)
  322. user_id = _get_current_user_id(user)
  323. draft = body.model_dump(by_alias=True, exclude_unset=True) if body else None
  324. obj = psp_svc.submit_receipt_finance(
  325. db,
  326. transfer_id=transfer_id,
  327. tenant_no=tenant_no,
  328. user_id=user_id,
  329. draft_fields=draft,
  330. )
  331. if not _protocol_accessible(db, obj.collection_protocol_id, tenant_no, user):
  332. return error_response(403, "无权操作该协议")
  333. from app.crud.protocol_sample_payment import _transfer_to_dict
  334. return success_response(_transfer_to_dict(db, obj, tenant_no), message="已提交财务确认")
  335. except PermissionError as e:
  336. return error_response(403, str(e))
  337. except ValueError as e:
  338. return error_response(400, str(e))
  339. @router.get("/payment-transfers")
  340. async def list_payment_transfers(
  341. page: int = Query(1, ge=1),
  342. page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
  343. payStatus: str = Query("dfj", alias="payStatus"),
  344. keyword: Optional[str] = None,
  345. installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
  346. installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
  347. user: dict = Depends(require_admin_or_super_admin),
  348. db: Session = Depends(get_db),
  349. ):
  350. """按转账笔状态分页(缴费信息看板 / 财务共用)。"""
  351. try:
  352. tenant_no = _get_tenant_no(db, user)
  353. items, total = psp_svc.list_transfers_for_finance(
  354. db,
  355. pay_status=payStatus,
  356. tenant_no=tenant_no,
  357. keyword=keyword,
  358. page=page,
  359. page_size=page_size,
  360. user_info=user,
  361. due_date_start=installmentDueDateStart,
  362. due_date_end=installmentDueDateEnd,
  363. )
  364. return success_response(
  365. {"list": items, "total": total, "page": page, "pageSize": page_size}
  366. )
  367. except PermissionError as e:
  368. return error_response(403, str(e))
  369. @router.get("/payment-transfers/receipt-confirm")
  370. async def list_receipt_confirm(
  371. page: int = Query(1, ge=1),
  372. page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
  373. keyword: Optional[str] = None,
  374. installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
  375. installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
  376. user: dict = Depends(require_admin_or_super_admin),
  377. db: Session = Depends(get_db),
  378. ):
  379. try:
  380. tenant_no = _get_tenant_no(db, user)
  381. items, total = psp_svc.list_transfers_for_finance(
  382. db,
  383. pay_status="jfqrz",
  384. tenant_no=tenant_no,
  385. keyword=keyword,
  386. page=page,
  387. page_size=page_size,
  388. user_info=user,
  389. due_date_start=installmentDueDateStart,
  390. due_date_end=installmentDueDateEnd,
  391. )
  392. return success_response({"list": items, "total": total, "page": page, "pageSize": page_size})
  393. except PermissionError as e:
  394. return error_response(403, str(e))
  395. @router.get("/payment-transfers/receipt-confirmed")
  396. async def list_receipt_confirmed(
  397. page: int = Query(1, ge=1),
  398. page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
  399. keyword: Optional[str] = None,
  400. installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
  401. installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
  402. user: dict = Depends(require_admin_or_super_admin),
  403. db: Session = Depends(get_db),
  404. ):
  405. try:
  406. tenant_no = _get_tenant_no(db, user)
  407. items, total = psp_svc.list_transfers_for_finance(
  408. db,
  409. pay_status="yjf",
  410. tenant_no=tenant_no,
  411. keyword=keyword,
  412. page=page,
  413. page_size=page_size,
  414. user_info=user,
  415. due_date_start=installmentDueDateStart,
  416. due_date_end=installmentDueDateEnd,
  417. )
  418. return success_response({"list": items, "total": total, "page": page, "pageSize": page_size})
  419. except PermissionError as e:
  420. return error_response(403, str(e))
  421. @router.post("/payment-transfers/{transfer_id}/receipt-confirm")
  422. async def confirm_payment_transfer(
  423. transfer_id: str,
  424. body: Optional[FinanceConfirmBody] = None,
  425. user: dict = Depends(require_admin_or_super_admin),
  426. db: Session = Depends(get_db),
  427. ):
  428. try:
  429. tenant_no = _get_tenant_no(db, user)
  430. user_id = _get_current_user_id(user)
  431. obj = psp_svc.confirm_receipt(
  432. db,
  433. transfer_id=transfer_id,
  434. tenant_no=tenant_no,
  435. user_id=user_id,
  436. finance_confirm_attachments=body.finance_confirm_attachments if body else None,
  437. finance_remark=body.finance_remark if body else None,
  438. )
  439. if not _protocol_accessible(db, obj.collection_protocol_id, tenant_no, user):
  440. return error_response(403, "无权操作该协议")
  441. from app.crud.protocol_sample_payment import _transfer_to_dict
  442. return success_response(_transfer_to_dict(db, obj, tenant_no), message="确认收款成功")
  443. except PermissionError as e:
  444. return error_response(403, str(e))
  445. except ValueError as e:
  446. return error_response(400, str(e))
  447. @router.post("/payment-transfers/{transfer_id}/receipt-reject")
  448. async def reject_payment_transfer(
  449. transfer_id: str,
  450. user: dict = Depends(require_admin_or_super_admin),
  451. db: Session = Depends(get_db),
  452. ):
  453. try:
  454. tenant_no = _get_tenant_no(db, user)
  455. user_id = _get_current_user_id(user)
  456. obj = psp_svc.reject_receipt(
  457. db, transfer_id=transfer_id, tenant_no=tenant_no, user_id=user_id
  458. )
  459. if not _protocol_accessible(db, obj.collection_protocol_id, tenant_no, user):
  460. return error_response(403, "无权操作该协议")
  461. from app.crud.protocol_sample_payment import _transfer_to_dict
  462. return success_response(_transfer_to_dict(db, obj, tenant_no), message="已退回")
  463. except PermissionError as e:
  464. return error_response(403, str(e))
  465. except ValueError as e:
  466. return error_response(400, str(e))