collection_protocol.py 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220
  1. """细胞存储 — 采集协议单 / 审核 / 财务确认 / 分期预警 API"""
  2. from datetime import datetime, timedelta
  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 sqlalchemy import and_ as sa_and, func as sa_func
  8. from app.core.database import get_db
  9. from app.core.menu_guard import require_admin_or_super_admin
  10. from app.core.response import error_response, success_response
  11. from app.core.utils import parse_datetime
  12. from app.crud import collection_protocol as cp_svc
  13. from app.crud import customer as customer_crud
  14. from app.models.models import (
  15. BizCollectionProtocol,
  16. BizCustomer,
  17. BizProtocolPartyCustomer,
  18. BizProtocolSample,
  19. BizProtocolStorageCert,
  20. SysUser,
  21. )
  22. router = APIRouter(prefix="/web/admin", tags=["细胞存储"])
  23. def _get_tenant_no(db: Session, user: dict) -> str:
  24. user_id = user.get("user_id")
  25. if not user_id:
  26. raise PermissionError("用户身份信息缺失,无法确定租户")
  27. sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
  28. if not sys_user or not sys_user.tenant_no:
  29. raise PermissionError("用户租户信息缺失")
  30. return sys_user.tenant_no
  31. def _get_current_user_id(user: dict) -> str:
  32. user_id = user.get("user_id")
  33. if not user_id:
  34. raise PermissionError("用户身份信息缺失")
  35. return str(user_id)
  36. def _protocol_accessible(
  37. db: Session, protocol_id: str, tenant_no: str, user: dict
  38. ) -> bool:
  39. """与列表接口一致:按租户 + 角色数据权限判断是否可访问该协议单。"""
  40. from app.core.data_scope import apply_data_scope
  41. q = db.query(BizCollectionProtocol).filter(
  42. BizCollectionProtocol.id == protocol_id,
  43. BizCollectionProtocol.del_flag == 0,
  44. BizCollectionProtocol.tenant_no == tenant_no,
  45. )
  46. q = apply_data_scope(q, BizCollectionProtocol, user, db, tenant_no)
  47. return q.first() is not None
  48. _CAMEL_TO_SNAKE_DATETIME = {
  49. "newbornDeliveryTime": "newborn_delivery_time",
  50. "sampleCollectTime": "sample_collect_time",
  51. "maternalBloodCollectTime": "maternal_blood_collect_time",
  52. }
  53. def _parse_datetime_fields(d: dict, camel_keys: list) -> None:
  54. for camel in camel_keys:
  55. val = d.get(camel)
  56. if val is not None and isinstance(val, str):
  57. snake = _CAMEL_TO_SNAKE_DATETIME.get(camel, camel)
  58. parsed = parse_datetime(val)
  59. if parsed:
  60. d.pop(camel, None)
  61. d[snake] = parsed
  62. else:
  63. d.pop(camel, None)
  64. d[snake] = None
  65. def _ensure_customer_for_mother(db: Session, party_customer: dict, tenant_no: str, user_id: str) -> dict:
  66. """If mother info exists but no customerId, create a customer record and link it."""
  67. if not party_customer:
  68. return party_customer
  69. customer_id = party_customer.get("customer_id") or party_customer.get("customerId")
  70. mother_name = (party_customer.get("mother_name") or party_customer.get("motherName") or "").strip()
  71. if customer_id or not mother_name:
  72. return party_customer
  73. # Build customer fields from mother info
  74. mother_phone = (party_customer.get("mother_phone") or party_customer.get("motherPhone") or "").strip() or None
  75. mother_id_card = (party_customer.get("mother_id_card") or party_customer.get("motherIdCard") or "").strip() or None
  76. # Check if customer with this id_card already exists
  77. if mother_id_card and customer_crud.id_card_taken(db, mother_id_card, tenant_no):
  78. existing = db.query(BizCustomer).filter(
  79. sa_and(
  80. BizCustomer.id_card_no == mother_id_card,
  81. BizCustomer.tenant_no == tenant_no,
  82. BizCustomer.del_flag == 0,
  83. )
  84. ).first()
  85. if existing:
  86. party_customer["customer_id"] = existing.id
  87. return party_customer
  88. # Create new customer
  89. try:
  90. obj = customer_crud.create_customer(
  91. db,
  92. customer_name=mother_name,
  93. sex="1", # female (mother)
  94. id_card_no=mother_id_card,
  95. phone_no=mother_phone,
  96. tenant_no=tenant_no,
  97. user_id=user_id,
  98. )
  99. party_customer["customer_id"] = obj.id
  100. except ValueError:
  101. # If creation fails (e.g. duplicate), try to find existing
  102. pass
  103. return party_customer
  104. # ---------------------------------------------------------------------------
  105. # 采集协议单
  106. # ---------------------------------------------------------------------------
  107. @router.get("/collection-protocols")
  108. async def list_collection_protocols(
  109. page: int = Query(1, ge=1),
  110. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  111. keyword: Optional[str] = None,
  112. protocolStatus: Optional[str] = Query(None, alias="protocolStatus"),
  113. createTimeStart: Optional[str] = Query(None, alias="createTimeStart"),
  114. createTimeEnd: Optional[str] = Query(None, alias="createTimeEnd"),
  115. sampleCollectTimeStart: Optional[str] = Query(None, alias="sampleCollectTimeStart"),
  116. sampleCollectTimeEnd: Optional[str] = Query(None, alias="sampleCollectTimeEnd"),
  117. user: dict = Depends(require_admin_or_super_admin),
  118. db: Session = Depends(get_db),
  119. ):
  120. tenant_no = _get_tenant_no(db, user)
  121. items, total = cp_svc.list_collection_protocols(
  122. db,
  123. page=page,
  124. page_size=pageSize,
  125. keyword=keyword,
  126. protocol_status=protocolStatus,
  127. create_time_start=createTimeStart,
  128. create_time_end=createTimeEnd,
  129. sample_collect_time_start=sampleCollectTimeStart,
  130. sample_collect_time_end=sampleCollectTimeEnd,
  131. tenant_no=tenant_no,
  132. user_info=user,
  133. )
  134. tp = cp_svc.total_pages(total, pageSize)
  135. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  136. @router.get("/collection-protocols/{protocol_id}")
  137. async def get_collection_protocol_detail(
  138. protocol_id: str,
  139. user: dict = Depends(require_admin_or_super_admin),
  140. db: Session = Depends(get_db),
  141. ):
  142. tenant_no = _get_tenant_no(db, user)
  143. if not _protocol_accessible(db, protocol_id, tenant_no, user):
  144. return error_response(404, "协议单不存在或无权查看")
  145. bundle = cp_svc.get_protocol_detail_bundle(db, protocol_id, tenant_no)
  146. if not bundle:
  147. return error_response(404, "协议单不存在或无权查看")
  148. return success_response(bundle)
  149. class ProtocolCreateBody(BaseModel):
  150. model_config = ConfigDict(populate_by_name=True)
  151. protocol_no: Optional[str] = Field(None, alias="protocolNo")
  152. protocol_no_id: Optional[str] = Field(None, alias="protocolNoId")
  153. protocol_status: str = Field("dsx", alias="protocolStatus")
  154. protocol_attachments: Optional[str] = Field(None, alias="protocolAttachments")
  155. consent_form_urls: Optional[str] = Field(None, alias="consentFormUrls")
  156. health_survey_form_urls: Optional[str] = Field(None, alias="healthSurveyFormUrls")
  157. register_date: Optional[str] = Field(None, alias="registerDate")
  158. collector_user_id: Optional[str] = Field(None, alias="collectorUserId")
  159. remark: Optional[str] = None
  160. party_customer: Optional[dict] = Field(None, alias="partyCustomer")
  161. collection_info: Optional[dict] = Field(None, alias="collectionInfo")
  162. samples: Optional[list] = Field(None, alias="samples")
  163. payment: Optional[dict] = Field(None, alias="payment")
  164. payment_details: Optional[list] = Field(None, alias="paymentDetails")
  165. attachments: Optional[list] = Field(None, alias="attachments")
  166. consent_attachments: Optional[list] = Field(None, alias="consentAttachments")
  167. health_survey_attachments: Optional[list] = Field(None, alias="healthSurveyAttachments")
  168. storage_cert: Optional[dict] = Field(None, alias="storageCert")
  169. class ProtocolUpdateBody(BaseModel):
  170. model_config = ConfigDict(populate_by_name=True)
  171. protocol_no: Optional[str] = Field(None, alias="protocolNo")
  172. protocol_status: Optional[str] = Field(None, alias="protocolStatus")
  173. protocol_attachments: Optional[str] = Field(None, alias="protocolAttachments")
  174. consent_form_urls: Optional[str] = Field(None, alias="consentFormUrls")
  175. health_survey_form_urls: Optional[str] = Field(None, alias="healthSurveyFormUrls")
  176. register_date: Optional[str] = Field(None, alias="registerDate")
  177. effective_time: Optional[str] = Field(None, alias="effectiveTime")
  178. effective_desc: Optional[str] = Field(None, alias="effectiveDesc")
  179. expire_time: Optional[str] = Field(None, alias="expireTime")
  180. expire_desc: Optional[str] = Field(None, alias="expireDesc")
  181. void_time: Optional[str] = Field(None, alias="voidTime")
  182. void_desc: Optional[str] = Field(None, alias="voidDesc")
  183. void_type: Optional[str] = Field(None, alias="voidType")
  184. to_research_time: Optional[str] = Field(None, alias="toResearchTime")
  185. to_research_desc: Optional[str] = Field(None, alias="toResearchDesc")
  186. collector_user_id: Optional[str] = Field(None, alias="collectorUserId")
  187. remark: Optional[str] = None
  188. party_customer: Optional[dict] = Field(None, alias="partyCustomer")
  189. collection_info: Optional[dict] = Field(None, alias="collectionInfo")
  190. samples: Optional[list] = Field(None, alias="samples")
  191. payment: Optional[dict] = Field(None, alias="payment")
  192. payment_details: Optional[list] = Field(None, alias="paymentDetails")
  193. attachments: Optional[list] = Field(None, alias="attachments")
  194. consent_attachments: Optional[list] = Field(None, alias="consentAttachments")
  195. health_survey_attachments: Optional[list] = Field(None, alias="healthSurveyAttachments")
  196. storage_cert: Optional[dict] = Field(None, alias="storageCert")
  197. def _resolve_protocol_attachments(
  198. protocol_attachments: Optional[str],
  199. attachments: Optional[list],
  200. ) -> Optional[str]:
  201. from_urls = cp_svc.attachments_to_urls(attachments)
  202. if from_urls:
  203. return from_urls
  204. return protocol_attachments
  205. def _resolve_consent_form_urls(
  206. consent_form_urls: Optional[str],
  207. consent_attachments: Optional[list],
  208. ) -> Optional[str]:
  209. from_urls = cp_svc.attachments_to_urls(consent_attachments)
  210. if from_urls:
  211. return from_urls
  212. return consent_form_urls
  213. def _resolve_health_survey_form_urls(
  214. health_survey_form_urls: Optional[str],
  215. health_survey_attachments: Optional[list],
  216. ) -> Optional[str]:
  217. from_urls = cp_svc.attachments_to_urls(health_survey_attachments)
  218. if from_urls:
  219. return from_urls
  220. return health_survey_form_urls
  221. def _persist_protocol_sub_resources(
  222. db: Session,
  223. protocol_id: str,
  224. tenant_no: str,
  225. user_id: str,
  226. *,
  227. party_customer: Optional[dict],
  228. collection_info: Optional[dict],
  229. samples: Optional[list],
  230. payment: Optional[dict],
  231. payment_details: Optional[list],
  232. storage_cert: Optional[dict],
  233. ) -> None:
  234. """持久化协议子资源。
  235. 缴费(含明细校验)须在样本之前:明细校验失败时不得已占用样本编码。
  236. 各子表 CRUD 内部仍可能分段 commit,创建接口须在失败时做补偿软删。
  237. """
  238. if party_customer:
  239. _parse_datetime_fields(party_customer, ["newbornDeliveryTime"])
  240. party_customer = _ensure_customer_for_mother(db, party_customer, tenant_no, user_id)
  241. cp_svc.upsert_party_customer(db, protocol_id, tenant_no, user_id, **party_customer)
  242. if collection_info:
  243. _parse_datetime_fields(collection_info, ["sampleCollectTime", "maternalBloodCollectTime"])
  244. cp_svc.upsert_collection_info(db, protocol_id, tenant_no, user_id, **collection_info)
  245. # 缴费先于样本:避免「应缴合计不符」等校验失败后样本编码已被占用
  246. pay = None
  247. if payment:
  248. pay = cp_svc.upsert_payment(db, protocol_id, tenant_no, user_id, **payment)
  249. if payment_details is not None:
  250. if not pay and payment:
  251. pay = cp_svc.upsert_payment(db, protocol_id, tenant_no, user_id, **payment)
  252. if not pay:
  253. pay = cp_svc.get_payment_by_protocol_id(db, protocol_id, tenant_no)
  254. if pay:
  255. aligned_details = cp_svc.align_fq_payment_details_payload(pay, payment_details)
  256. cp_svc.save_payment_details(db, pay.id, tenant_no, user_id, aligned_details)
  257. if samples is not None:
  258. cp_svc.save_samples(db, protocol_id, tenant_no, user_id, samples)
  259. if storage_cert:
  260. cp_svc.upsert_storage_cert(db, protocol_id, tenant_no, user_id, **storage_cert)
  261. @router.post("/collection-protocols")
  262. async def create_collection_protocol(
  263. body: ProtocolCreateBody,
  264. user: dict = Depends(require_admin_or_super_admin),
  265. db: Session = Depends(get_db),
  266. ):
  267. tenant_no = _get_tenant_no(db, user)
  268. user_id = _get_current_user_id(user)
  269. protocol_id: Optional[str] = None
  270. try:
  271. protocol_attachments = _resolve_protocol_attachments(
  272. body.protocol_attachments,
  273. body.attachments,
  274. )
  275. consent_form_urls = _resolve_consent_form_urls(
  276. body.consent_form_urls,
  277. body.consent_attachments,
  278. )
  279. health_survey_form_urls = _resolve_health_survey_form_urls(
  280. body.health_survey_form_urls,
  281. body.health_survey_attachments,
  282. )
  283. collector = (body.collector_user_id or "").strip()
  284. if not collector:
  285. collector = user_id
  286. obj = cp_svc.create_collection_protocol(
  287. db,
  288. protocol_no=body.protocol_no or "",
  289. protocol_no_id=body.protocol_no_id,
  290. protocol_status=body.protocol_status,
  291. protocol_attachments=protocol_attachments,
  292. consent_form_urls=consent_form_urls,
  293. health_survey_form_urls=health_survey_form_urls,
  294. register_date=body.register_date,
  295. collector_user_id=collector,
  296. remark=body.remark,
  297. tenant_no=tenant_no,
  298. user_id=user_id,
  299. )
  300. protocol_id = obj.id
  301. _persist_protocol_sub_resources(
  302. db,
  303. protocol_id,
  304. tenant_no,
  305. user_id,
  306. party_customer=body.party_customer,
  307. collection_info=body.collection_info,
  308. samples=body.samples,
  309. payment=body.payment,
  310. payment_details=body.payment_details,
  311. storage_cert=body.storage_cert,
  312. )
  313. return success_response(cp_svc._protocol_to_dict(obj), message="创建成功")
  314. except ValueError as e:
  315. # 子表分段 commit 后校验失败时,补偿软删主单及样本,释放样本编码占用
  316. if protocol_id:
  317. try:
  318. cp_svc.soft_delete_collection_protocol(
  319. db,
  320. protocol_id,
  321. tenant_no=tenant_no,
  322. user_id=user_id,
  323. )
  324. except Exception:
  325. pass
  326. return error_response(400, str(e))
  327. @router.put("/collection-protocols/{protocol_id}")
  328. async def update_collection_protocol(
  329. protocol_id: str,
  330. body: ProtocolUpdateBody,
  331. user: dict = Depends(require_admin_or_super_admin),
  332. db: Session = Depends(get_db),
  333. ):
  334. tenant_no = _get_tenant_no(db, user)
  335. user_id = _get_current_user_id(user)
  336. if not _protocol_accessible(db, protocol_id, tenant_no, user):
  337. return error_response(404, "协议单不存在或无权操作")
  338. try:
  339. payload = body.model_dump(exclude_unset=True, by_alias=False)
  340. sub_keys = {
  341. "party_customer", "collection_info", "samples",
  342. "payment", "payment_details", "attachments",
  343. "consent_attachments", "health_survey_attachments", "storage_cert",
  344. }
  345. main_payload = {k: v for k, v in payload.items() if k not in sub_keys}
  346. if body.attachments is not None:
  347. resolved = _resolve_protocol_attachments(
  348. main_payload.get("protocol_attachments"),
  349. body.attachments,
  350. )
  351. if resolved is not None:
  352. main_payload["protocol_attachments"] = resolved
  353. if body.consent_attachments is not None:
  354. resolved = _resolve_consent_form_urls(
  355. main_payload.get("consent_form_urls"),
  356. body.consent_attachments,
  357. )
  358. if resolved is not None:
  359. main_payload["consent_form_urls"] = resolved
  360. if body.health_survey_attachments is not None:
  361. resolved = _resolve_health_survey_form_urls(
  362. main_payload.get("health_survey_form_urls"),
  363. body.health_survey_attachments,
  364. )
  365. if resolved is not None:
  366. main_payload["health_survey_form_urls"] = resolved
  367. obj = cp_svc.update_collection_protocol(
  368. db,
  369. protocol_id,
  370. tenant_no=tenant_no,
  371. user_id=user_id,
  372. **{k: v for k, v in main_payload.items() if v is not None},
  373. )
  374. _persist_protocol_sub_resources(
  375. db,
  376. protocol_id,
  377. tenant_no,
  378. user_id,
  379. party_customer=body.party_customer,
  380. collection_info=body.collection_info,
  381. samples=body.samples,
  382. payment=body.payment,
  383. payment_details=body.payment_details,
  384. storage_cert=body.storage_cert,
  385. )
  386. return success_response(cp_svc._protocol_to_dict(obj), message="更新成功")
  387. except ValueError as e:
  388. return error_response(400, str(e))
  389. @router.delete("/collection-protocols/{protocol_id}")
  390. async def delete_collection_protocol(
  391. protocol_id: str,
  392. user: dict = Depends(require_admin_or_super_admin),
  393. db: Session = Depends(get_db),
  394. ):
  395. tenant_no = _get_tenant_no(db, user)
  396. user_id = _get_current_user_id(user)
  397. if not _protocol_accessible(db, protocol_id, tenant_no, user):
  398. return error_response(404, "协议单不存在或无权操作")
  399. try:
  400. cp_svc.soft_delete_collection_protocol(
  401. db,
  402. protocol_id,
  403. tenant_no=tenant_no,
  404. user_id=user_id,
  405. )
  406. return success_response(None, message="删除成功")
  407. except ValueError as e:
  408. return error_response(400, str(e))
  409. # ---------------------------------------------------------------------------
  410. # 审核
  411. # ---------------------------------------------------------------------------
  412. @router.get("/collection-protocol-audits")
  413. async def list_collection_protocol_audits(
  414. page: int = Query(1, ge=1),
  415. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  416. keyword: Optional[str] = None,
  417. auditStatus: Optional[str] = Query(None, alias="auditStatus"),
  418. collectionProtocolId: Optional[str] = Query(None, alias="collectionProtocolId"),
  419. user: dict = Depends(require_admin_or_super_admin),
  420. db: Session = Depends(get_db),
  421. ):
  422. tenant_no = _get_tenant_no(db, user)
  423. items, total = cp_svc.list_audits(
  424. db,
  425. page=page,
  426. page_size=pageSize,
  427. keyword=keyword,
  428. audit_status=auditStatus,
  429. collection_protocol_id=collectionProtocolId,
  430. tenant_no=tenant_no,
  431. user_info=user,
  432. )
  433. tp = cp_svc.total_pages(total, pageSize)
  434. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  435. class AuditCreateBody(BaseModel):
  436. model_config = ConfigDict(populate_by_name=True)
  437. audit_status: str = Field(..., alias="auditStatus")
  438. audit_remark: Optional[str] = Field(None, alias="auditRemark")
  439. @router.post("/collection-protocols/{protocol_id}/audit")
  440. async def submit_collection_protocol_audit(
  441. protocol_id: str,
  442. body: AuditCreateBody,
  443. user: dict = Depends(require_admin_or_super_admin),
  444. db: Session = Depends(get_db),
  445. ):
  446. tenant_no = _get_tenant_no(db, user)
  447. try:
  448. cp_svc.create_audit(
  449. db,
  450. collection_protocol_id=protocol_id,
  451. audit_status=body.audit_status,
  452. audit_remark=body.audit_remark,
  453. tenant_no=tenant_no,
  454. user_id=user.get("user_id"),
  455. )
  456. p = db.query(BizCollectionProtocol).filter(BizCollectionProtocol.id == protocol_id).first()
  457. pn = (p.protocol_no if p else "") or ""
  458. return success_response({"protocolNo": pn}, message="审核已提交")
  459. except ValueError as e:
  460. return error_response(400, str(e))
  461. # ---------------------------------------------------------------------------
  462. # 收款确认
  463. # ---------------------------------------------------------------------------
  464. @router.get("/protocol-payment-details")
  465. async def list_payment_details(
  466. page: int = Query(1, ge=1),
  467. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  468. payStatus: str = Query("dfj", alias="payStatus"),
  469. keyword: str = Query("", alias="keyword"),
  470. transferType: str = Query("", alias="transferType"),
  471. installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
  472. installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
  473. user: dict = Depends(require_admin_or_super_admin),
  474. db: Session = Depends(get_db),
  475. ):
  476. tenant_no = _get_tenant_no(db, user)
  477. items, total = cp_svc.list_payment_details_page(
  478. db,
  479. page=page,
  480. page_size=pageSize,
  481. pay_status=payStatus,
  482. keyword=keyword or None,
  483. transfer_type=transferType or None,
  484. installment_due_date_start=installmentDueDateStart,
  485. installment_due_date_end=installmentDueDateEnd,
  486. tenant_no=tenant_no,
  487. user_info=user,
  488. )
  489. tp = cp_svc.total_pages(total, pageSize)
  490. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  491. class BatchDetailIdsBody(BaseModel):
  492. model_config = ConfigDict(populate_by_name=True)
  493. detail_ids: List[str] = Field(..., alias="detailIds", min_length=1)
  494. class PaymentDetailDraftBody(BaseModel):
  495. model_config = ConfigDict(populate_by_name=True)
  496. transfer_type: Optional[str] = Field(None, alias="transferType")
  497. transfer_amount: Optional[float] = Field(None, alias="transferAmount")
  498. transfer_time: Optional[str] = Field(None, alias="transferTime")
  499. transfer_attachments: Optional[str] = Field(None, alias="transferAttachments")
  500. remark: Optional[str] = None
  501. def _draft_fields_from_body(body: PaymentDetailDraftBody) -> dict:
  502. return body.model_dump(exclude_unset=True)
  503. @router.put("/protocol-payment-details/{detail_id}")
  504. async def put_payment_detail_draft(
  505. detail_id: str,
  506. body: PaymentDetailDraftBody,
  507. user: dict = Depends(require_admin_or_super_admin),
  508. db: Session = Depends(get_db),
  509. ):
  510. tenant_no = _get_tenant_no(db, user)
  511. fields = _draft_fields_from_body(body)
  512. if not fields:
  513. return error_response(400, "请提供需要保存的字段")
  514. try:
  515. cp_svc.update_payment_detail_draft(
  516. db,
  517. detail_id=detail_id,
  518. tenant_no=tenant_no,
  519. user_id=user.get("user_id"),
  520. draft_fields=fields,
  521. )
  522. return success_response(None, message="保存成功")
  523. except ValueError as e:
  524. return error_response(400, str(e))
  525. class RefundFinanceSubmitBody(BaseModel):
  526. model_config = ConfigDict(populate_by_name=True)
  527. refund_amount: float = Field(..., alias="refundAmount", gt=0)
  528. refund_remark: Optional[str] = Field(None, alias="refundRemark")
  529. refund_attachments: Optional[str] = Field(None, alias="refundAttachments")
  530. class BatchRefundFinanceSubmitBody(RefundFinanceSubmitBody):
  531. model_config = ConfigDict(populate_by_name=True)
  532. detail_ids: List[str] = Field(..., alias="detailIds", min_length=1)
  533. @router.post("/protocol-payment-details/{detail_id}/submit-receipt-finance")
  534. async def post_submit_receipt_finance(
  535. detail_id: str,
  536. body: PaymentDetailDraftBody = PaymentDetailDraftBody(),
  537. user: dict = Depends(require_admin_or_super_admin),
  538. db: Session = Depends(get_db),
  539. ):
  540. tenant_no = _get_tenant_no(db, user)
  541. fields = _draft_fields_from_body(body)
  542. try:
  543. cp_svc.submit_receipt_finance(
  544. db,
  545. detail_id=detail_id,
  546. tenant_no=tenant_no,
  547. user_id=user.get("user_id"),
  548. draft_fields=fields or None,
  549. )
  550. return success_response(None, message="提交成功")
  551. except ValueError as e:
  552. return error_response(400, str(e))
  553. @router.post("/protocol-payment-details/submit-receipt-finance-batch")
  554. async def post_submit_receipt_finance_batch(
  555. body: BatchDetailIdsBody,
  556. user: dict = Depends(require_admin_or_super_admin),
  557. db: Session = Depends(get_db),
  558. ):
  559. tenant_no = _get_tenant_no(db, user)
  560. try:
  561. count = cp_svc.batch_submit_receipt_finance(
  562. db,
  563. detail_ids=body.detail_ids,
  564. tenant_no=tenant_no,
  565. user_id=user.get("user_id"),
  566. )
  567. return success_response({"count": count}, message=f"已成功提交 {count} 条")
  568. except ValueError as e:
  569. return error_response(400, str(e))
  570. @router.post("/protocol-payment-details/{detail_id}/submit-refund-finance")
  571. async def post_submit_refund_finance(
  572. detail_id: str,
  573. body: RefundFinanceSubmitBody,
  574. user: dict = Depends(require_admin_or_super_admin),
  575. db: Session = Depends(get_db),
  576. ):
  577. tenant_no = _get_tenant_no(db, user)
  578. try:
  579. cp_svc.submit_refund_finance(
  580. db,
  581. detail_id=detail_id,
  582. tenant_no=tenant_no,
  583. user_id=user.get("user_id"),
  584. refund_amount=body.refund_amount,
  585. refund_remark=body.refund_remark,
  586. refund_attachments=body.refund_attachments,
  587. )
  588. return success_response(None, message="提交成功")
  589. except ValueError as e:
  590. return error_response(400, str(e))
  591. @router.post("/protocol-payment-details/submit-refund-finance-batch")
  592. async def post_submit_refund_finance_batch(
  593. body: BatchRefundFinanceSubmitBody,
  594. user: dict = Depends(require_admin_or_super_admin),
  595. db: Session = Depends(get_db),
  596. ):
  597. tenant_no = _get_tenant_no(db, user)
  598. try:
  599. count = cp_svc.batch_submit_refund_finance(
  600. db,
  601. detail_ids=body.detail_ids,
  602. tenant_no=tenant_no,
  603. user_id=user.get("user_id"),
  604. refund_amount=body.refund_amount,
  605. refund_remark=body.refund_remark,
  606. refund_attachments=body.refund_attachments,
  607. )
  608. return success_response({"count": count}, message=f"已成功提交 {count} 条")
  609. except ValueError as e:
  610. return error_response(400, str(e))
  611. @router.get("/protocol-payment-details/receipt-confirm")
  612. async def list_receipt_confirm(
  613. page: int = Query(1, ge=1),
  614. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  615. keyword: Optional[str] = None,
  616. installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
  617. installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
  618. user: dict = Depends(require_admin_or_super_admin),
  619. db: Session = Depends(get_db),
  620. ):
  621. tenant_no = _get_tenant_no(db, user)
  622. items, total = cp_svc.list_receipt_confirm_details(
  623. db,
  624. page=page,
  625. page_size=pageSize,
  626. keyword=keyword or None,
  627. installment_due_date_start=installmentDueDateStart,
  628. installment_due_date_end=installmentDueDateEnd,
  629. tenant_no=tenant_no,
  630. user_info=user,
  631. )
  632. tp = cp_svc.total_pages(total, pageSize)
  633. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  634. @router.get("/protocol-payment-details/receipt-confirmed")
  635. async def list_receipt_confirmed(
  636. page: int = Query(1, ge=1),
  637. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  638. keyword: Optional[str] = None,
  639. installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
  640. installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
  641. user: dict = Depends(require_admin_or_super_admin),
  642. db: Session = Depends(get_db),
  643. ):
  644. tenant_no = _get_tenant_no(db, user)
  645. items, total = cp_svc.list_receipt_confirmed_details(
  646. db,
  647. page=page,
  648. page_size=pageSize,
  649. keyword=keyword or None,
  650. installment_due_date_start=installmentDueDateStart,
  651. installment_due_date_end=installmentDueDateEnd,
  652. tenant_no=tenant_no,
  653. user_info=user,
  654. )
  655. tp = cp_svc.total_pages(total, pageSize)
  656. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  657. class FinanceConfirmBody(BaseModel):
  658. model_config = ConfigDict(populate_by_name=True)
  659. finance_confirm_attachments: Optional[str] = Field(None, alias="financeConfirmAttachments")
  660. finance_remark: Optional[str] = Field(None, alias="financeRemark")
  661. class RefundFinanceConfirmBody(BaseModel):
  662. model_config = ConfigDict(populate_by_name=True)
  663. refund_finance_confirm_attachments: Optional[str] = Field(
  664. None, alias="refundFinanceConfirmAttachments"
  665. )
  666. refund_finance_remark: Optional[str] = Field(None, alias="refundFinanceRemark")
  667. @router.post("/protocol-payment-details/{detail_id}/receipt-confirm")
  668. async def post_receipt_confirm(
  669. detail_id: str,
  670. body: FinanceConfirmBody,
  671. user: dict = Depends(require_admin_or_super_admin),
  672. db: Session = Depends(get_db),
  673. ):
  674. tenant_no = _get_tenant_no(db, user)
  675. try:
  676. cp_svc.confirm_receipt(
  677. db,
  678. detail_id=detail_id,
  679. tenant_no=tenant_no,
  680. user_id=user.get("user_id"),
  681. finance_confirm_attachments=body.finance_confirm_attachments,
  682. finance_remark=body.finance_remark,
  683. )
  684. return success_response(None, message="确认成功")
  685. except ValueError as e:
  686. return error_response(400, str(e))
  687. @router.post("/protocol-payment-details/{detail_id}/receipt-reject")
  688. async def post_receipt_reject(
  689. detail_id: str,
  690. user: dict = Depends(require_admin_or_super_admin),
  691. db: Session = Depends(get_db),
  692. ):
  693. tenant_no = _get_tenant_no(db, user)
  694. try:
  695. cp_svc.reject_receipt(
  696. db,
  697. detail_id=detail_id,
  698. tenant_no=tenant_no,
  699. user_id=user.get("user_id"),
  700. )
  701. return success_response(None, message="退回成功")
  702. except ValueError as e:
  703. return error_response(400, str(e))
  704. # ---------------------------------------------------------------------------
  705. # 退费确认
  706. # ---------------------------------------------------------------------------
  707. @router.get("/protocol-payment-details/refund-confirm")
  708. async def list_refund_confirm(
  709. page: int = Query(1, ge=1),
  710. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  711. keyword: Optional[str] = None,
  712. installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
  713. installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
  714. user: dict = Depends(require_admin_or_super_admin),
  715. db: Session = Depends(get_db),
  716. ):
  717. tenant_no = _get_tenant_no(db, user)
  718. items, total = cp_svc.list_refund_confirm_details(
  719. db,
  720. page=page,
  721. page_size=pageSize,
  722. keyword=keyword or None,
  723. installment_due_date_start=installmentDueDateStart,
  724. installment_due_date_end=installmentDueDateEnd,
  725. tenant_no=tenant_no,
  726. user_info=user,
  727. )
  728. tp = cp_svc.total_pages(total, pageSize)
  729. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  730. @router.get("/protocol-payment-details/refund-confirmed")
  731. async def list_refund_confirmed(
  732. page: int = Query(1, ge=1),
  733. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  734. keyword: Optional[str] = None,
  735. installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
  736. installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
  737. user: dict = Depends(require_admin_or_super_admin),
  738. db: Session = Depends(get_db),
  739. ):
  740. tenant_no = _get_tenant_no(db, user)
  741. items, total = cp_svc.list_refund_confirmed_details(
  742. db,
  743. page=page,
  744. page_size=pageSize,
  745. keyword=keyword or None,
  746. installment_due_date_start=installmentDueDateStart,
  747. installment_due_date_end=installmentDueDateEnd,
  748. tenant_no=tenant_no,
  749. user_info=user,
  750. )
  751. tp = cp_svc.total_pages(total, pageSize)
  752. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  753. @router.post("/protocol-payment-details/{detail_id}/refund-confirm")
  754. async def post_refund_confirm(
  755. detail_id: str,
  756. body: RefundFinanceConfirmBody,
  757. user: dict = Depends(require_admin_or_super_admin),
  758. db: Session = Depends(get_db),
  759. ):
  760. tenant_no = _get_tenant_no(db, user)
  761. try:
  762. cp_svc.confirm_refund(
  763. db,
  764. detail_id=detail_id,
  765. tenant_no=tenant_no,
  766. user_id=user.get("user_id"),
  767. refund_finance_confirm_attachments=body.refund_finance_confirm_attachments,
  768. refund_finance_remark=body.refund_finance_remark,
  769. )
  770. return success_response(None, message="确认成功")
  771. except ValueError as e:
  772. return error_response(400, str(e))
  773. @router.post("/protocol-payment-details/{detail_id}/refund-reject")
  774. async def post_refund_reject(
  775. detail_id: str,
  776. user: dict = Depends(require_admin_or_super_admin),
  777. db: Session = Depends(get_db),
  778. ):
  779. tenant_no = _get_tenant_no(db, user)
  780. try:
  781. cp_svc.reject_refund(
  782. db,
  783. detail_id=detail_id,
  784. tenant_no=tenant_no,
  785. user_id=user.get("user_id"),
  786. )
  787. return success_response(None, message="退回成功")
  788. except ValueError as e:
  789. return error_response(400, str(e))
  790. # ---------------------------------------------------------------------------
  791. # 储存证明管理 / 满月待办
  792. # ---------------------------------------------------------------------------
  793. @router.get("/storage-certs")
  794. async def list_storage_certs(
  795. page: int = Query(1, ge=1),
  796. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  797. keyword: Optional[str] = None,
  798. makeStatus: Optional[str] = Query(None, alias="makeStatus"),
  799. storageInDateStart: Optional[str] = Query(None, alias="storageInDateStart"),
  800. storageInDateEnd: Optional[str] = Query(None, alias="storageInDateEnd"),
  801. user: dict = Depends(require_admin_or_super_admin),
  802. db: Session = Depends(get_db),
  803. ):
  804. tenant_no = _get_tenant_no(db, user)
  805. page_items, total = cp_svc.list_storage_certs(
  806. db,
  807. tenant_no=tenant_no,
  808. keyword=keyword,
  809. make_status=makeStatus,
  810. storage_in_date_start=storageInDateStart,
  811. storage_in_date_end=storageInDateEnd,
  812. page=page,
  813. page_size=pageSize,
  814. user_info=user,
  815. )
  816. page_safe = max(1, page)
  817. page_size_safe = min(max(1, pageSize), 100)
  818. tp = cp_svc.total_pages(total, page_size_safe)
  819. return success_response(
  820. {"list": page_items, "total": total, "page": page_safe, "pageSize": page_size_safe, "totalPages": tp}
  821. )
  822. @router.get("/fullmoon-todos")
  823. async def list_fullmoon_todos(
  824. page: int = Query(1, ge=1),
  825. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  826. keyword: Optional[str] = None,
  827. todoStatus: Optional[str] = Query(None, alias="todoStatus"),
  828. fullmoonDateStart: Optional[str] = Query(None, alias="fullmoonDateStart"),
  829. fullmoonDateEnd: Optional[str] = Query(None, alias="fullmoonDateEnd"),
  830. user: dict = Depends(require_admin_or_super_admin),
  831. db: Session = Depends(get_db),
  832. ):
  833. tenant_no = _get_tenant_no(db, user)
  834. party_join = sa_and(
  835. BizProtocolPartyCustomer.collection_protocol_id == BizCollectionProtocol.id,
  836. BizProtocolPartyCustomer.del_flag == 0,
  837. BizProtocolPartyCustomer.tenant_no == tenant_no,
  838. )
  839. q = (
  840. db.query(BizCollectionProtocol, BizProtocolPartyCustomer)
  841. .outerjoin(BizProtocolPartyCustomer, party_join)
  842. .filter(
  843. BizCollectionProtocol.del_flag == 0,
  844. BizCollectionProtocol.tenant_no == tenant_no,
  845. BizCollectionProtocol.protocol_status == "sxz",
  846. )
  847. )
  848. kw = (keyword or "").strip()
  849. if kw:
  850. like = f"%{kw}%"
  851. q = q.filter(BizProtocolPartyCustomer.mother_name.like(like))
  852. from app.core.data_scope import apply_data_scope
  853. q = apply_data_scope(q, BizCollectionProtocol, user, db, tenant_no)
  854. rows = q.order_by(BizCollectionProtocol.create_time.desc()).all()
  855. protocol_ids = [r.id for r, _ in rows]
  856. ft_map: Dict[str, Any] = {}
  857. if protocol_ids:
  858. from app.models.models import BizProtocolFullmoonTodo
  859. ft_rows = (
  860. db.query(BizProtocolFullmoonTodo)
  861. .filter(
  862. BizProtocolFullmoonTodo.collection_protocol_id.in_(protocol_ids),
  863. BizProtocolFullmoonTodo.tenant_no == tenant_no,
  864. BizProtocolFullmoonTodo.del_flag == 0,
  865. )
  866. .all()
  867. )
  868. for ft in ft_rows:
  869. ft_map[ft.collection_protocol_id] = ft
  870. now = datetime.now()
  871. _empty_ft: Dict[str, Any] = {
  872. "todoStatus": "0",
  873. "handleDate": "",
  874. "handleRemark": "",
  875. "insuranceAttachments": "",
  876. "insuranceCompanyId": "",
  877. "insuranceCompanyName": "",
  878. "insuranceType": "",
  879. "insurancePolicyNo": "",
  880. "insuranceEffectiveDate": "",
  881. "insuranceExpiryDate": "",
  882. "insuranceSubscribed": "0",
  883. "insuranceFee": None,
  884. }
  885. items: List[Dict[str, Any]] = []
  886. for proto, pc in rows:
  887. dt = pc.newborn_delivery_time if pc else None
  888. fullmoon_date_str = ""
  889. is_fullmoon_passed = False
  890. if dt:
  891. fullmoon_date = dt + timedelta(days=30)
  892. fullmoon_date_str = fullmoon_date.strftime("%Y-%m-%d")
  893. is_fullmoon_passed = fullmoon_date <= now
  894. ft = ft_map.get(proto.id)
  895. ts = (ft.todo_status or "0") if ft else "0"
  896. if todoStatus and todoStatus.strip() and ts != todoStatus.strip():
  897. continue
  898. fm_start = (fullmoonDateStart or "").strip()[:10]
  899. fm_end = (fullmoonDateEnd or "").strip()[:10]
  900. if fm_start or fm_end:
  901. if not fullmoon_date_str:
  902. continue
  903. if fm_start and fullmoon_date_str < fm_start:
  904. continue
  905. if fm_end and fullmoon_date_str > fm_end:
  906. continue
  907. ft_dict = cp_svc._fullmoon_todo_to_dict(ft) if ft else dict(_empty_ft)
  908. items.append(
  909. {
  910. "protocolId": proto.id,
  911. "id": proto.id,
  912. "protocolNo": proto.protocol_no or "",
  913. "protocolStatus": proto.protocol_status or "",
  914. "motherName": (pc.mother_name if pc else "") or "",
  915. "motherPhone": (pc.mother_phone if pc else "") or "",
  916. "newbornName": (pc.newborn_name if pc else "") or "",
  917. "newbornDeliveryTime": cp_svc._fmt_dt(dt) if dt else "",
  918. "fullmoonDate": fullmoon_date_str,
  919. "isFullmoonPassed": is_fullmoon_passed,
  920. **ft_dict,
  921. }
  922. )
  923. total = len(items)
  924. page_safe = max(1, page)
  925. page_size_safe = min(max(1, pageSize), 100)
  926. start = (page_safe - 1) * page_size_safe
  927. page_items = items[start : start + page_size_safe]
  928. tp = cp_svc.total_pages(total, page_size_safe)
  929. return success_response(
  930. {"list": page_items, "total": total, "page": page_safe, "pageSize": page_size_safe, "totalPages": tp}
  931. )
  932. class FullmoonTodoInsuranceBody(BaseModel):
  933. model_config = ConfigDict(populate_by_name=True)
  934. insurance_company_id: Optional[str] = Field(None, alias="insuranceCompanyId")
  935. insurance_company_name: Optional[str] = Field(None, alias="insuranceCompanyName")
  936. insurance_type: Optional[str] = Field(None, alias="insuranceType")
  937. insurance_policy_no: Optional[str] = Field(None, alias="insurancePolicyNo")
  938. insurance_effective_date: Optional[str] = Field(None, alias="insuranceEffectiveDate")
  939. insurance_expiry_date: Optional[str] = Field(None, alias="insuranceExpiryDate")
  940. insurance_subscribed: Optional[str] = Field(None, alias="insuranceSubscribed")
  941. insurance_fee: Optional[float] = Field(None, alias="insuranceFee")
  942. insurance_attachments: Optional[str] = Field(None, alias="insuranceAttachments")
  943. todo_status: Optional[str] = Field(None, alias="todoStatus")
  944. handle_date: Optional[str] = Field(None, alias="handleDate")
  945. handle_remark: Optional[str] = Field(None, alias="handleRemark")
  946. @router.put("/fullmoon-todos/{protocol_id}/insurance")
  947. async def update_fullmoon_todo_insurance(
  948. protocol_id: str,
  949. body: FullmoonTodoInsuranceBody,
  950. user: dict = Depends(require_admin_or_super_admin),
  951. db: Session = Depends(get_db),
  952. ):
  953. tenant_no = _get_tenant_no(db, user)
  954. user_id = _get_current_user_id(user)
  955. try:
  956. payload = body.model_dump(exclude_unset=True, by_alias=False)
  957. ft = cp_svc.upsert_fullmoon_todo(
  958. db,
  959. protocol_id,
  960. tenant_no,
  961. user_id,
  962. **payload,
  963. )
  964. return success_response(cp_svc._fullmoon_todo_to_dict(ft), message="更新成功")
  965. except ValueError as e:
  966. return error_response(400, str(e))
  967. class InvoiceRequestCreateBody(BaseModel):
  968. model_config = ConfigDict(populate_by_name=True)
  969. payment_detail_ids: List[str] = Field(..., alias="paymentDetailIds", min_length=1)
  970. invoice_title: Optional[str] = Field(None, alias="invoiceTitle")
  971. tax_no: Optional[str] = Field(None, alias="taxNo")
  972. company_address: Optional[str] = Field(None, alias="companyAddress")
  973. company_phone: Optional[str] = Field(None, alias="companyPhone")
  974. bank_name: Optional[str] = Field(None, alias="bankName")
  975. bank_account: Optional[str] = Field(None, alias="bankAccount")
  976. apply_remark: Optional[str] = Field(None, alias="applyRemark")
  977. class InvoiceRequestCompleteBody(BaseModel):
  978. model_config = ConfigDict(populate_by_name=True)
  979. invoice_attachments: str = Field(..., alias="invoiceAttachments")
  980. finance_remark: Optional[str] = Field(None, alias="financeRemark")
  981. @router.get("/collection-protocols/{protocol_id}/invoice-eligible-payments")
  982. async def get_invoice_eligible_payments(
  983. protocol_id: str,
  984. user: dict = Depends(require_admin_or_super_admin),
  985. db: Session = Depends(get_db),
  986. ):
  987. tenant_no = _get_tenant_no(db, user)
  988. items = cp_svc.list_invoice_eligible_payment_details(
  989. db, collection_protocol_id=protocol_id, tenant_no=tenant_no
  990. )
  991. return success_response({"list": items, "total": len(items)})
  992. @router.get("/collection-protocols/{protocol_id}/invoice-requests")
  993. async def get_protocol_invoice_requests(
  994. protocol_id: str,
  995. user: dict = Depends(require_admin_or_super_admin),
  996. db: Session = Depends(get_db),
  997. ):
  998. tenant_no = _get_tenant_no(db, user)
  999. items = cp_svc.list_invoice_requests_by_protocol(
  1000. db, collection_protocol_id=protocol_id, tenant_no=tenant_no
  1001. )
  1002. return success_response({"list": items, "total": len(items)})
  1003. @router.post("/collection-protocols/{protocol_id}/invoice-requests")
  1004. async def post_protocol_invoice_request(
  1005. protocol_id: str,
  1006. body: InvoiceRequestCreateBody,
  1007. user: dict = Depends(require_admin_or_super_admin),
  1008. db: Session = Depends(get_db),
  1009. ):
  1010. tenant_no = _get_tenant_no(db, user)
  1011. try:
  1012. row = cp_svc.create_invoice_request(
  1013. db,
  1014. collection_protocol_id=protocol_id,
  1015. payment_detail_ids=body.payment_detail_ids,
  1016. tenant_no=tenant_no,
  1017. user_info=user,
  1018. invoice_title=body.invoice_title,
  1019. tax_no=body.tax_no,
  1020. company_address=body.company_address,
  1021. company_phone=body.company_phone,
  1022. bank_name=body.bank_name,
  1023. bank_account=body.bank_account,
  1024. apply_remark=body.apply_remark,
  1025. )
  1026. except ValueError as e:
  1027. return error_response(400, str(e))
  1028. return success_response(row, message="提交成功")
  1029. @router.get("/protocol-invoice-requests")
  1030. async def list_protocol_invoice_requests_api(
  1031. status: str = Query(..., description="dkp|ykp"),
  1032. page: int = Query(1, ge=1),
  1033. pageSize: int = Query(10, ge=1, le=100),
  1034. keyword: Optional[str] = Query(None),
  1035. user: dict = Depends(require_admin_or_super_admin),
  1036. db: Session = Depends(get_db),
  1037. ):
  1038. tenant_no = _get_tenant_no(db, user)
  1039. items, total = cp_svc.list_protocol_invoice_requests(
  1040. db,
  1041. status=status,
  1042. page=page,
  1043. page_size=pageSize,
  1044. keyword=keyword,
  1045. tenant_no=tenant_no,
  1046. user_info=user,
  1047. )
  1048. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize})
  1049. @router.get("/protocol-invoice-requests/pending-count")
  1050. async def protocol_invoice_pending_count(
  1051. user: dict = Depends(require_admin_or_super_admin),
  1052. db: Session = Depends(get_db),
  1053. ):
  1054. tenant_no = _get_tenant_no(db, user)
  1055. count = cp_svc.count_pending_protocol_invoice_requests(
  1056. db, tenant_no=tenant_no, user_info=user
  1057. )
  1058. return success_response({"count": count})
  1059. @router.put("/protocol-invoice-requests/{request_id}/complete")
  1060. async def complete_protocol_invoice_request(
  1061. request_id: str,
  1062. body: InvoiceRequestCompleteBody,
  1063. user: dict = Depends(require_admin_or_super_admin),
  1064. db: Session = Depends(get_db),
  1065. ):
  1066. tenant_no = _get_tenant_no(db, user)
  1067. try:
  1068. row = cp_svc.complete_invoice_request(
  1069. db,
  1070. request_id=request_id,
  1071. tenant_no=tenant_no,
  1072. invoice_attachments=body.invoice_attachments,
  1073. finance_remark=body.finance_remark,
  1074. user_info=user,
  1075. )
  1076. except ValueError as e:
  1077. return error_response(400, str(e))
  1078. return success_response(row, message="已完成开票")