protocol_sample_payment.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  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. prev = psp_svc.find_active_sample_payment(
  107. db,
  108. tenant_no=tenant_no,
  109. sample_payment_id=item.id,
  110. collection_protocol_id=protocol_id,
  111. sample_code=item.sample_code,
  112. )
  113. prev_mode = (prev.pay_mode or "").strip() if prev else ""
  114. sp = psp_svc.upsert_sample_payment(
  115. db,
  116. collection_protocol_id=protocol_id,
  117. tenant_no=tenant_no,
  118. user_id=user_id,
  119. sample_code=item.sample_code,
  120. pay_mode=item.pay_mode,
  121. contract_amount=item.contract_amount,
  122. protocol_sample_id=item.protocol_sample_id,
  123. installment_count=item.installment_count,
  124. installment_interval_months=item.installment_interval_months,
  125. installment_start_date=item.installment_start_date,
  126. installment_advance_days=item.installment_advance_days,
  127. installment_ratios=_ratios_to_str(item.installment_ratios),
  128. lump_sum_deadline=item.lump_sum_deadline,
  129. remark=item.remark,
  130. sample_payment_id=item.id,
  131. )
  132. next_mode = (item.pay_mode or "").strip()
  133. if (
  134. prev_mode == "fq"
  135. and next_mode in ("fgd", "dj")
  136. and psp_svc.sample_payment_has_yjf(
  137. db, sample_payment_id=sp.id, tenant_no=tenant_no
  138. )
  139. ):
  140. psp_svc.apply_fq_to_fgd_merge(
  141. db,
  142. sample_payment_id=sp.id,
  143. tenant_no=tenant_no,
  144. user_id=user_id,
  145. target_pay_mode=next_mode,
  146. )
  147. continue
  148. if item.schedules is not None:
  149. schedule_payloads = [s.model_dump(by_alias=False) for s in item.schedules]
  150. psp_svc.sync_schedules_and_transfers(
  151. db,
  152. sample_payment_id=sp.id,
  153. tenant_no=tenant_no,
  154. user_id=user_id,
  155. schedules=schedule_payloads,
  156. )
  157. db.commit()
  158. return psp_svc.get_sample_payments_bundle(db, protocol_id, tenant_no)
  159. @router.get("/payment-chase")
  160. async def list_payment_chase(
  161. page: int = Query(1, ge=1),
  162. page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
  163. keyword: Optional[str] = None,
  164. chaseType: Optional[str] = Query(None, alias="chaseType"),
  165. payMode: Optional[str] = Query(None, alias="payMode"),
  166. salesUserId: Optional[str] = Query(None, alias="salesUserId"),
  167. user: dict = Depends(require_admin_or_super_admin),
  168. db: Session = Depends(get_db),
  169. ):
  170. """催缴信息:逾期未清 ∪ 未来 30 天内应缴未清(一行一期)。"""
  171. try:
  172. tenant_no = _get_tenant_no(db, user)
  173. items, total = psp_svc.list_payment_chase_page(
  174. db,
  175. tenant_no=tenant_no,
  176. keyword=keyword,
  177. chase_type=chaseType,
  178. pay_mode=payMode,
  179. sales_user_id=salesUserId,
  180. page=page,
  181. page_size=page_size,
  182. user_info=user,
  183. )
  184. return success_response({"list": items, "total": total, "page": page, "pageSize": page_size})
  185. except Exception as e:
  186. return error_response(500, str(e))
  187. @router.get("/sample-payments")
  188. async def list_sample_payments(
  189. page: int = Query(1, ge=1),
  190. page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
  191. keyword: Optional[str] = None,
  192. payMode: Optional[str] = Query(None, alias="payMode"),
  193. progressStatus: Optional[str] = Query(None, alias="progressStatus"),
  194. installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
  195. installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
  196. user: dict = Depends(require_admin_or_super_admin),
  197. db: Session = Depends(get_db),
  198. ):
  199. """按样本缴费方案分页列表(独立缴费信息菜单)。"""
  200. try:
  201. tenant_no = _get_tenant_no(db, user)
  202. items, total = psp_svc.list_sample_payments_page(
  203. db,
  204. tenant_no=tenant_no,
  205. keyword=keyword,
  206. pay_mode=payMode,
  207. progress_status=progressStatus,
  208. due_date_start=installmentDueDateStart,
  209. due_date_end=installmentDueDateEnd,
  210. page=page,
  211. page_size=page_size,
  212. user_info=user,
  213. )
  214. return success_response(
  215. {"list": items, "total": total, "page": page, "pageSize": page_size}
  216. )
  217. except PermissionError as e:
  218. return error_response(403, str(e))
  219. except ValueError as e:
  220. return error_response(400, str(e))
  221. @router.get("/sample-payments/{sample_payment_id}")
  222. async def get_sample_payment(
  223. sample_payment_id: str,
  224. user: dict = Depends(require_admin_or_super_admin),
  225. db: Session = Depends(get_db),
  226. ):
  227. try:
  228. tenant_no = _get_tenant_no(db, user)
  229. data = psp_svc.get_sample_payment_detail(
  230. db,
  231. sample_payment_id=sample_payment_id,
  232. tenant_no=tenant_no,
  233. user_info=user,
  234. )
  235. if not data:
  236. return error_response(403, "无权查看或记录不存在")
  237. return success_response(data)
  238. except PermissionError as e:
  239. return error_response(403, str(e))
  240. except ValueError as e:
  241. return error_response(400, str(e))
  242. @router.get("/collection-protocols/{protocol_id}/sample-payments")
  243. async def get_sample_payments(
  244. protocol_id: str,
  245. user: dict = Depends(require_admin_or_super_admin),
  246. db: Session = Depends(get_db),
  247. ):
  248. try:
  249. tenant_no = _get_tenant_no(db, user)
  250. if not _protocol_accessible(db, protocol_id, tenant_no, user):
  251. return error_response(404, "协议单不存在或无权查看")
  252. data = psp_svc.get_sample_payments_bundle(db, protocol_id, tenant_no)
  253. return success_response(data)
  254. except PermissionError as e:
  255. return error_response(403, str(e))
  256. except ValueError as e:
  257. return error_response(400, str(e))
  258. @router.put("/collection-protocols/{protocol_id}/sample-payments")
  259. async def put_sample_payments(
  260. protocol_id: str,
  261. body: SamplePaymentsPutBody,
  262. user: dict = Depends(require_admin_or_super_admin),
  263. db: Session = Depends(get_db),
  264. ):
  265. try:
  266. tenant_no = _get_tenant_no(db, user)
  267. user_id = _get_current_user_id(user)
  268. if not _protocol_accessible(db, protocol_id, tenant_no, user):
  269. return error_response(404, "协议单不存在或无权操作")
  270. # pydantic v2: may need model_validate with populate
  271. payments = body.sample_payments
  272. if hasattr(body, "model_dump"):
  273. raw = body.model_dump(by_alias=True)
  274. payments = [
  275. SamplePaymentBody.model_validate(x)
  276. for x in (raw.get("samplePayments") or raw.get("sample_payments") or [])
  277. ]
  278. data = _save_sample_payments(
  279. db,
  280. protocol_id=protocol_id,
  281. tenant_no=tenant_no,
  282. user_id=user_id,
  283. payments=payments or body.sample_payments,
  284. )
  285. return success_response(data, message="保存成功")
  286. except PermissionError as e:
  287. return error_response(403, str(e))
  288. except ValueError as e:
  289. return error_response(400, str(e))
  290. @router.post("/payment-transfers")
  291. async def create_payment_transfer(
  292. body: TransferCreateBody,
  293. user: dict = Depends(require_admin_or_super_admin),
  294. db: Session = Depends(get_db),
  295. ):
  296. try:
  297. tenant_no = _get_tenant_no(db, user)
  298. user_id = _get_current_user_id(user)
  299. sch = psp_svc._get_schedule_or_raise(
  300. db, schedule_id=body.schedule_id, tenant_no=tenant_no
  301. )
  302. if not _protocol_accessible(db, sch.collection_protocol_id, tenant_no, user):
  303. return error_response(403, "无权操作该协议")
  304. obj = psp_svc.add_transfer(
  305. db,
  306. schedule_id=body.schedule_id,
  307. tenant_no=tenant_no,
  308. user_id=user_id,
  309. transfer_amount=body.transfer_amount,
  310. transfer_time=body.transfer_time,
  311. transfer_type=body.transfer_type,
  312. transfer_attachments=body.transfer_attachments,
  313. remark=body.remark,
  314. )
  315. from app.crud.protocol_sample_payment import _transfer_to_dict
  316. return success_response(
  317. _transfer_to_dict(db, obj, tenant_no), message="创建成功"
  318. )
  319. except PermissionError as e:
  320. return error_response(403, str(e))
  321. except ValueError as e:
  322. return error_response(400, str(e))
  323. @router.put("/payment-transfers/{transfer_id}")
  324. async def update_payment_transfer(
  325. transfer_id: str,
  326. body: TransferDraftBody,
  327. user: dict = Depends(require_admin_or_super_admin),
  328. db: Session = Depends(get_db),
  329. ):
  330. try:
  331. tenant_no = _get_tenant_no(db, user)
  332. user_id = _get_current_user_id(user)
  333. fields = body.model_dump(by_alias=False, exclude_unset=True)
  334. locked_transfer = psp_svc._get_transfer_with_schedule_lock(
  335. db,
  336. transfer_id=transfer_id,
  337. tenant_no=tenant_no,
  338. user_info=user,
  339. )
  340. obj = psp_svc.update_transfer_draft(
  341. db,
  342. transfer_id=transfer_id,
  343. tenant_no=tenant_no,
  344. user_id=user_id,
  345. fields=fields,
  346. locked_transfer=locked_transfer,
  347. )
  348. from app.crud.protocol_sample_payment import _transfer_to_dict
  349. return success_response(_transfer_to_dict(db, obj, tenant_no), message="保存成功")
  350. except PermissionError as e:
  351. return error_response(403, str(e))
  352. except ValueError as e:
  353. return error_response(400, str(e))
  354. @router.post("/payment-transfers/{transfer_id}/submit-receipt-finance")
  355. async def submit_payment_transfer_finance(
  356. transfer_id: str,
  357. body: Optional[TransferDraftBody] = None,
  358. user: dict = Depends(require_admin_or_super_admin),
  359. db: Session = Depends(get_db),
  360. ):
  361. try:
  362. tenant_no = _get_tenant_no(db, user)
  363. user_id = _get_current_user_id(user)
  364. draft = body.model_dump(by_alias=True, exclude_unset=True) if body else None
  365. locked_transfer = psp_svc._get_transfer_with_schedule_lock(
  366. db,
  367. transfer_id=transfer_id,
  368. tenant_no=tenant_no,
  369. user_info=user,
  370. )
  371. obj = psp_svc.submit_receipt_finance(
  372. db,
  373. transfer_id=transfer_id,
  374. tenant_no=tenant_no,
  375. user_id=user_id,
  376. draft_fields=draft,
  377. locked_transfer=locked_transfer,
  378. )
  379. from app.crud.protocol_sample_payment import _transfer_to_dict
  380. return success_response(_transfer_to_dict(db, obj, tenant_no), message="已提交财务确认")
  381. except PermissionError as e:
  382. return error_response(403, str(e))
  383. except ValueError as e:
  384. return error_response(400, str(e))
  385. @router.post("/payment-transfers/{transfer_id}/withdraw")
  386. async def withdraw_payment_transfer(
  387. transfer_id: str,
  388. user: dict = Depends(require_admin_or_super_admin),
  389. db: Session = Depends(get_db),
  390. ):
  391. try:
  392. tenant_no = _get_tenant_no(db, user)
  393. user_id = _get_current_user_id(user)
  394. locked_transfer = psp_svc._get_transfer_with_schedule_lock(
  395. db,
  396. transfer_id=transfer_id,
  397. tenant_no=tenant_no,
  398. user_info=user,
  399. )
  400. obj = psp_svc.withdraw_receipt(
  401. db,
  402. transfer_id=transfer_id,
  403. tenant_no=tenant_no,
  404. user_id=user_id,
  405. locked_transfer=locked_transfer,
  406. )
  407. from app.crud.protocol_sample_payment import _transfer_to_dict
  408. return success_response(_transfer_to_dict(db, obj, tenant_no), message="已撤回")
  409. except PermissionError as e:
  410. return error_response(403, str(e))
  411. except ValueError as e:
  412. return error_response(400, str(e))
  413. @router.delete("/payment-transfers/{transfer_id}")
  414. async def delete_payment_transfer(
  415. transfer_id: str,
  416. user: dict = Depends(require_admin_or_super_admin),
  417. db: Session = Depends(get_db),
  418. ):
  419. try:
  420. tenant_no = _get_tenant_no(db, user)
  421. user_id = _get_current_user_id(user)
  422. obj = psp_svc._get_transfer_or_raise(
  423. db, transfer_id=transfer_id, tenant_no=tenant_no
  424. )
  425. if not _protocol_accessible(db, obj.collection_protocol_id, tenant_no, user):
  426. return error_response(403, "无权操作该协议")
  427. psp_svc.soft_delete_payment_transfer(
  428. db, transfer_id=transfer_id, tenant_no=tenant_no, user_id=user_id
  429. )
  430. return success_response(None, message="删除成功")
  431. except PermissionError as e:
  432. return error_response(403, str(e))
  433. except ValueError as e:
  434. return error_response(400, str(e))
  435. @router.get("/payment-transfers")
  436. async def list_payment_transfers(
  437. page: int = Query(1, ge=1),
  438. page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
  439. payStatus: str = Query("dfj", alias="payStatus"),
  440. keyword: Optional[str] = None,
  441. installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
  442. installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
  443. user: dict = Depends(require_admin_or_super_admin),
  444. db: Session = Depends(get_db),
  445. ):
  446. """按转账笔状态分页(缴费信息看板 / 财务共用)。"""
  447. try:
  448. tenant_no = _get_tenant_no(db, user)
  449. items, total = psp_svc.list_transfers_for_finance(
  450. db,
  451. pay_status=payStatus,
  452. tenant_no=tenant_no,
  453. keyword=keyword,
  454. page=page,
  455. page_size=page_size,
  456. user_info=user,
  457. due_date_start=installmentDueDateStart,
  458. due_date_end=installmentDueDateEnd,
  459. )
  460. return success_response(
  461. {"list": items, "total": total, "page": page, "pageSize": page_size}
  462. )
  463. except PermissionError as e:
  464. return error_response(403, str(e))
  465. @router.get("/payment-transfers/receipt-confirm")
  466. async def list_receipt_confirm(
  467. page: int = Query(1, ge=1),
  468. page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
  469. keyword: Optional[str] = None,
  470. installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
  471. installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
  472. user: dict = Depends(require_admin_or_super_admin),
  473. db: Session = Depends(get_db),
  474. ):
  475. try:
  476. tenant_no = _get_tenant_no(db, user)
  477. items, total = psp_svc.list_transfers_for_finance(
  478. db,
  479. pay_status="jfqrz",
  480. tenant_no=tenant_no,
  481. keyword=keyword,
  482. page=page,
  483. page_size=page_size,
  484. user_info=user,
  485. due_date_start=installmentDueDateStart,
  486. due_date_end=installmentDueDateEnd,
  487. )
  488. return success_response({"list": items, "total": total, "page": page, "pageSize": page_size})
  489. except PermissionError as e:
  490. return error_response(403, str(e))
  491. @router.get("/payment-transfers/receipt-confirmed")
  492. async def list_receipt_confirmed(
  493. page: int = Query(1, ge=1),
  494. page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
  495. keyword: Optional[str] = None,
  496. installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
  497. installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
  498. user: dict = Depends(require_admin_or_super_admin),
  499. db: Session = Depends(get_db),
  500. ):
  501. try:
  502. tenant_no = _get_tenant_no(db, user)
  503. items, total = psp_svc.list_transfers_for_finance(
  504. db,
  505. pay_status="yjf",
  506. tenant_no=tenant_no,
  507. keyword=keyword,
  508. page=page,
  509. page_size=page_size,
  510. user_info=user,
  511. due_date_start=installmentDueDateStart,
  512. due_date_end=installmentDueDateEnd,
  513. )
  514. return success_response({"list": items, "total": total, "page": page, "pageSize": page_size})
  515. except PermissionError as e:
  516. return error_response(403, str(e))
  517. @router.post("/payment-transfers/{transfer_id}/receipt-confirm")
  518. async def confirm_payment_transfer(
  519. transfer_id: str,
  520. body: Optional[FinanceConfirmBody] = None,
  521. user: dict = Depends(require_admin_or_super_admin),
  522. db: Session = Depends(get_db),
  523. ):
  524. try:
  525. tenant_no = _get_tenant_no(db, user)
  526. user_id = _get_current_user_id(user)
  527. locked_transfer = psp_svc._get_transfer_with_schedule_lock(
  528. db,
  529. transfer_id=transfer_id,
  530. tenant_no=tenant_no,
  531. user_info=user,
  532. )
  533. obj = psp_svc.confirm_receipt(
  534. db,
  535. transfer_id=transfer_id,
  536. tenant_no=tenant_no,
  537. user_id=user_id,
  538. finance_confirm_attachments=body.finance_confirm_attachments if body else None,
  539. finance_remark=body.finance_remark if body else None,
  540. locked_transfer=locked_transfer,
  541. )
  542. from app.crud.protocol_sample_payment import _transfer_to_dict
  543. return success_response(_transfer_to_dict(db, obj, tenant_no), message="确认收款成功")
  544. except PermissionError as e:
  545. return error_response(403, str(e))
  546. except ValueError as e:
  547. return error_response(400, str(e))
  548. @router.post("/payment-transfers/{transfer_id}/receipt-reject")
  549. async def reject_payment_transfer(
  550. transfer_id: str,
  551. user: dict = Depends(require_admin_or_super_admin),
  552. db: Session = Depends(get_db),
  553. ):
  554. try:
  555. tenant_no = _get_tenant_no(db, user)
  556. user_id = _get_current_user_id(user)
  557. locked_transfer = psp_svc._get_transfer_with_schedule_lock(
  558. db,
  559. transfer_id=transfer_id,
  560. tenant_no=tenant_no,
  561. user_info=user,
  562. )
  563. obj = psp_svc.reject_receipt(
  564. db,
  565. transfer_id=transfer_id,
  566. tenant_no=tenant_no,
  567. user_id=user_id,
  568. locked_transfer=locked_transfer,
  569. )
  570. from app.crud.protocol_sample_payment import _transfer_to_dict
  571. return success_response(_transfer_to_dict(db, obj, tenant_no), message="已退回")
  572. except PermissionError as e:
  573. return error_response(403, str(e))
  574. except ValueError as e:
  575. return error_response(400, str(e))