presign_order.py 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180
  1. """预签约订单 / 意向金转账 / 转正式协议 CRUD"""
  2. from __future__ import annotations
  3. from datetime import datetime
  4. from decimal import Decimal
  5. from typing import Any, Dict, List, Optional, Tuple
  6. from sqlalchemy import and_, func, or_
  7. from sqlalchemy.exc import IntegrityError
  8. from sqlalchemy.orm import Session
  9. from app.core.utils import generate_uuid, parse_datetime
  10. from app.crud.biz_no_generator import allocate_next_biz_no
  11. from app.crud.collection_protocol import urls_to_attachment_list
  12. from app.models.models import (
  13. BizCellSampleType,
  14. BizCollectionProtocol,
  15. BizPresignOrder,
  16. BizPresignOrderSampleType,
  17. BizPresignTransfer,
  18. BizProtocolPartyCustomer,
  19. SysUser,
  20. )
  21. from app.services.protocol_transfer_status import (
  22. can_confirm_receipt,
  23. can_submit_receipt,
  24. can_withdraw_receipt,
  25. )
  26. def _now() -> datetime:
  27. return datetime.now()
  28. def assert_presign_data_access(
  29. db: Session, record_id: str, tenant_no: str, user_info: dict,
  30. *, transfer: bool = False,
  31. ) -> None:
  32. """Web 单条操作沿用对应订单/财务列表的租户与多角色数据范围。"""
  33. from app.core.data_scope import apply_data_scope
  34. model = BizPresignTransfer if transfer else BizPresignOrder
  35. query = db.query(model.id).filter(
  36. model.id == record_id,
  37. model.tenant_no == tenant_no,
  38. model.del_flag == 0,
  39. )
  40. if apply_data_scope(query, model, user_info, db, tenant_no).first() is None:
  41. raise PermissionError("记录不存在或无权访问")
  42. def _dec(v: Any) -> Optional[Decimal]:
  43. if v is None or v == "":
  44. return None
  45. return Decimal(str(v))
  46. def _user_nick_name_map(
  47. db: Session, user_ids: List[str], tenant_no: str
  48. ) -> Dict[str, str]:
  49. ids = [str(x).strip() for x in user_ids if str(x or "").strip()]
  50. if not ids:
  51. return {}
  52. rows = (
  53. db.query(SysUser.id, SysUser.nick_name)
  54. .filter(
  55. SysUser.id.in_(ids),
  56. SysUser.tenant_no == tenant_no,
  57. )
  58. .all()
  59. )
  60. out: Dict[str, str] = {}
  61. for u in rows:
  62. out[str(u.id)] = (u.nick_name or "").strip()
  63. return out
  64. def assert_presign_convertible(*, order_status: Optional[str]) -> None:
  65. st = (order_status or "").strip() or "draft"
  66. if st == "converted":
  67. raise ValueError("该预签约订单已转正式协议")
  68. if st == "void":
  69. raise ValueError("已作废的预签约订单不能转正式")
  70. def resolve_convert_staff_fields(
  71. *,
  72. collector_user_id: Optional[str],
  73. sales_user_id: Optional[str],
  74. region_manager_user_id: Optional[str],
  75. fallback_user_id: Optional[str] = None,
  76. ) -> Dict[str, Optional[str]]:
  77. """转正式人员映射:采集专员→采集专员+市场专员;采集经理→地区经理。"""
  78. _ = sales_user_id # 正式单市场专员以采集专员为准,不再沿用预签市场专员
  79. collector = (collector_user_id or "").strip() or (fallback_user_id or "").strip() or None
  80. region_mgr = (region_manager_user_id or "").strip() or None
  81. return {
  82. "collector_user_id": collector,
  83. "sales_user_id": collector,
  84. "region_manager_user_id": region_mgr,
  85. }
  86. def _get_order_or_raise(
  87. db: Session, *, order_id: str, tenant_no: str, for_update: bool = False
  88. ) -> BizPresignOrder:
  89. query = (
  90. db.query(BizPresignOrder)
  91. .filter(
  92. BizPresignOrder.id == order_id,
  93. BizPresignOrder.tenant_no == tenant_no,
  94. BizPresignOrder.del_flag == 0,
  95. )
  96. )
  97. if for_update:
  98. query = query.populate_existing().with_for_update()
  99. obj = query.first()
  100. if not obj:
  101. raise ValueError("预签约订单不存在")
  102. return obj
  103. def validate_presign_person_ids(
  104. db: Session,
  105. *,
  106. tenant_no: str,
  107. collector_user_id: Optional[str],
  108. sales_user_id: Optional[str],
  109. region_manager_user_id: Optional[str],
  110. ) -> None:
  111. """所有预签人员必须是当前租户有效且启用的系统用户。"""
  112. requested = {
  113. str(user_id).strip()
  114. for user_id in (
  115. collector_user_id,
  116. sales_user_id,
  117. region_manager_user_id,
  118. )
  119. if str(user_id or "").strip()
  120. }
  121. if not requested:
  122. return
  123. existing = {
  124. str(row[0])
  125. for row in db.query(SysUser.id)
  126. .filter(
  127. SysUser.id.in_(requested),
  128. SysUser.tenant_no == tenant_no,
  129. SysUser.del_flag == 0,
  130. SysUser.status == "1",
  131. )
  132. .all()
  133. }
  134. if existing != requested:
  135. raise ValueError("人员用户不存在、不属于当前租户、已删除或已停用")
  136. def _get_transfer_or_raise(
  137. db: Session, *, transfer_id: str, tenant_no: str
  138. ) -> BizPresignTransfer:
  139. obj = (
  140. db.query(BizPresignTransfer)
  141. .filter(
  142. BizPresignTransfer.id == transfer_id,
  143. BizPresignTransfer.tenant_no == tenant_no,
  144. BizPresignTransfer.del_flag == 0,
  145. )
  146. .first()
  147. )
  148. if not obj:
  149. raise ValueError("意向金转账笔不存在")
  150. return obj
  151. def _update_transfer_expected_status(
  152. db: Session,
  153. *,
  154. transfer_id: str,
  155. tenant_no: str,
  156. expected_status: str,
  157. values: Dict[Any, Any],
  158. extra_conditions: Tuple[Any, ...] = (),
  159. commit: bool = True,
  160. ) -> BizPresignTransfer:
  161. """以旧状态作为 CAS 条件更新,并返回事务内刷新后的稳定快照。"""
  162. changed = (
  163. db.query(BizPresignTransfer)
  164. .filter(
  165. BizPresignTransfer.id == transfer_id,
  166. BizPresignTransfer.tenant_no == tenant_no,
  167. BizPresignTransfer.del_flag == 0,
  168. BizPresignTransfer.pay_status == expected_status,
  169. *extra_conditions,
  170. )
  171. .update(values, synchronize_session=False)
  172. )
  173. if changed != 1:
  174. raise ValueError("转账状态已变化,请刷新后重试")
  175. obj = (
  176. db.query(BizPresignTransfer)
  177. .filter(
  178. BizPresignTransfer.id == transfer_id,
  179. BizPresignTransfer.tenant_no == tenant_no,
  180. BizPresignTransfer.del_flag == 0,
  181. )
  182. .populate_existing()
  183. .first()
  184. )
  185. if obj is None:
  186. raise ValueError("意向金转账笔不存在")
  187. db.flush()
  188. if commit:
  189. # 提交会 expire 会话内 ORM 对象;先分离,调用方序列化时只读取
  190. # 本次事务内已刷新的标量快照,不会在提交后重新读取并发新状态。
  191. db.expunge(obj)
  192. db.commit()
  193. return obj
  194. def _transfer_to_dict(
  195. db: Session, t: BizPresignTransfer, tenant_no: str
  196. ) -> Dict[str, Any]:
  197. return {
  198. "id": t.id,
  199. "presignOrderId": t.presign_order_id,
  200. "transferAmount": float(t.transfer_amount) if t.transfer_amount is not None else None,
  201. "transferTime": t.transfer_time.strftime("%Y-%m-%d %H:%M:%S") if t.transfer_time else "",
  202. "transferType": t.transfer_type or "",
  203. "transferAttachments": t.transfer_attachments or "",
  204. "attachments": urls_to_attachment_list(t.transfer_attachments, db, tenant_no),
  205. "payStatus": t.pay_status or "",
  206. "financeConfirmAttachments": t.finance_confirm_attachments or "",
  207. "financeConfirmAttachmentFiles": urls_to_attachment_list(
  208. t.finance_confirm_attachments, db, tenant_no
  209. ),
  210. "financeRemark": t.finance_remark or "",
  211. "remark": t.remark or "",
  212. "source": "presign",
  213. "createTime": t.create_time.strftime("%Y-%m-%d %H:%M:%S") if t.create_time else "",
  214. "updateTime": t.update_time.strftime("%Y-%m-%d %H:%M:%S") if t.update_time else "",
  215. }
  216. def confirmed_deposit_amount(
  217. db: Session, *, presign_order_id: str, tenant_no: str
  218. ) -> Decimal:
  219. rows = (
  220. db.query(BizPresignTransfer.transfer_amount)
  221. .filter(
  222. BizPresignTransfer.presign_order_id == presign_order_id,
  223. BizPresignTransfer.tenant_no == tenant_no,
  224. BizPresignTransfer.del_flag == 0,
  225. BizPresignTransfer.pay_status == "yjf",
  226. )
  227. .all()
  228. )
  229. total = Decimal("0")
  230. for (amt,) in rows:
  231. if amt is not None:
  232. total += Decimal(str(amt))
  233. return total
  234. def confirmed_deposit_amount_map(
  235. db: Session, *, presign_order_ids: List[str], tenant_no: str
  236. ) -> Dict[str, Decimal]:
  237. ids = [str(order_id).strip() for order_id in presign_order_ids if str(order_id or "").strip()]
  238. if not ids:
  239. return {}
  240. rows = (
  241. db.query(
  242. BizPresignTransfer.presign_order_id,
  243. func.sum(BizPresignTransfer.transfer_amount),
  244. )
  245. .filter(
  246. BizPresignTransfer.presign_order_id.in_(ids),
  247. BizPresignTransfer.tenant_no == tenant_no,
  248. BizPresignTransfer.del_flag == 0,
  249. BizPresignTransfer.pay_status == "yjf",
  250. )
  251. .group_by(BizPresignTransfer.presign_order_id)
  252. .all()
  253. )
  254. return {
  255. str(order_id): Decimal(str(amount or 0)) for order_id, amount in rows
  256. }
  257. def _sample_type_names(
  258. db: Session, *, presign_order_id: str, tenant_no: str
  259. ) -> str:
  260. ids = list_sample_type_ids(
  261. db, presign_order_id=presign_order_id, tenant_no=tenant_no
  262. )
  263. if not ids:
  264. return ""
  265. rows = (
  266. db.query(BizCellSampleType.id, BizCellSampleType.type_name)
  267. .filter(
  268. BizCellSampleType.id.in_(ids),
  269. BizCellSampleType.del_flag == 0,
  270. )
  271. .all()
  272. )
  273. name_map = {str(r.id): (r.type_name or "").strip() for r in rows}
  274. names = [name_map.get(i, "") for i in ids if name_map.get(i)]
  275. return "、".join(names)
  276. def list_sample_type_ids(
  277. db: Session, *, presign_order_id: str, tenant_no: str
  278. ) -> List[str]:
  279. rows = (
  280. db.query(BizPresignOrderSampleType)
  281. .filter(
  282. BizPresignOrderSampleType.presign_order_id == presign_order_id,
  283. BizPresignOrderSampleType.tenant_no == tenant_no,
  284. BizPresignOrderSampleType.del_flag == 0,
  285. )
  286. .order_by(
  287. BizPresignOrderSampleType.order_num.asc(),
  288. BizPresignOrderSampleType.create_time.asc(),
  289. )
  290. .all()
  291. )
  292. return [r.sample_type_id for r in rows if r.sample_type_id]
  293. def replace_sample_types(
  294. db: Session,
  295. *,
  296. presign_order_id: str,
  297. tenant_no: str,
  298. user_id: Optional[str],
  299. sample_type_ids: List[str],
  300. create_dept_id: str = "",
  301. ) -> None:
  302. now = _now()
  303. existing = (
  304. db.query(BizPresignOrderSampleType)
  305. .filter(
  306. BizPresignOrderSampleType.presign_order_id == presign_order_id,
  307. BizPresignOrderSampleType.tenant_no == tenant_no,
  308. BizPresignOrderSampleType.del_flag == 0,
  309. )
  310. .all()
  311. )
  312. for r in existing:
  313. r.del_flag = 1
  314. r.update_by = user_id
  315. r.update_time = now
  316. db.add(r)
  317. seen = set()
  318. for idx, tid in enumerate(sample_type_ids or []):
  319. sid = (tid or "").strip()
  320. if not sid or sid in seen:
  321. continue
  322. seen.add(sid)
  323. db.add(
  324. BizPresignOrderSampleType(
  325. id=generate_uuid(),
  326. presign_order_id=presign_order_id,
  327. sample_type_id=sid,
  328. tenant_no=tenant_no,
  329. order_num=idx,
  330. create_by=user_id,
  331. create_dept_id=create_dept_id,
  332. create_time=now,
  333. update_by=user_id,
  334. update_time=now,
  335. del_flag=0,
  336. )
  337. )
  338. db.flush()
  339. def list_transfers(
  340. db: Session, *, presign_order_id: str, tenant_no: str
  341. ) -> List[Dict[str, Any]]:
  342. rows = (
  343. db.query(BizPresignTransfer)
  344. .filter(
  345. BizPresignTransfer.presign_order_id == presign_order_id,
  346. BizPresignTransfer.tenant_no == tenant_no,
  347. BizPresignTransfer.del_flag == 0,
  348. )
  349. .order_by(BizPresignTransfer.create_time.asc())
  350. .all()
  351. )
  352. return [_transfer_to_dict(db, t, tenant_no) for t in rows]
  353. def order_to_dict(
  354. db: Session,
  355. o: BizPresignOrder,
  356. tenant_no: str,
  357. *,
  358. with_children: bool = True,
  359. deposit_amount: Optional[Decimal] = None,
  360. user_name_map: Optional[Dict[str, str]] = None,
  361. ) -> Dict[str, Any]:
  362. deposit = (
  363. deposit_amount
  364. if deposit_amount is not None
  365. else confirmed_deposit_amount(
  366. db, presign_order_id=o.id, tenant_no=tenant_no
  367. )
  368. )
  369. data: Dict[str, Any] = {
  370. "id": o.id,
  371. "orderDate": o.order_date or "",
  372. "protocolNo": o.protocol_no or "",
  373. "motherName": o.mother_name or "",
  374. "expectedDeliveryDate": o.expected_delivery_date or "",
  375. "regionId": o.region_id or "",
  376. "hospitalId": o.hospital_id or "",
  377. "sampleCollectTime": o.sample_collect_time.strftime("%Y-%m-%d %H:%M:%S")
  378. if o.sample_collect_time
  379. else "",
  380. "payMode": o.pay_mode or "",
  381. "contractAmount": float(o.contract_amount) if o.contract_amount is not None else None,
  382. "collectorUserId": o.collector_user_id or "",
  383. "salesUserId": o.sales_user_id or "",
  384. "regionManagerUserId": o.region_manager_user_id or "",
  385. "orderStatus": o.order_status or "",
  386. "convertedProtocolId": o.converted_protocol_id or "",
  387. "confirmedDepositAmount": float(deposit),
  388. "remark": o.remark or "",
  389. "createTime": o.create_time.strftime("%Y-%m-%d %H:%M:%S") if o.create_time else "",
  390. "updateTime": o.update_time.strftime("%Y-%m-%d %H:%M:%S") if o.update_time else "",
  391. }
  392. name_map = user_name_map
  393. if name_map is None:
  394. name_map = _user_nick_name_map(
  395. db,
  396. [
  397. data["collectorUserId"],
  398. data["salesUserId"],
  399. data["regionManagerUserId"],
  400. ],
  401. tenant_no,
  402. )
  403. data["collectorUserName"] = name_map.get(data["collectorUserId"], "")
  404. data["salesUserName"] = name_map.get(data["salesUserId"], "")
  405. data["regionManagerUserName"] = name_map.get(data["regionManagerUserId"], "")
  406. if with_children:
  407. data["sampleTypeIds"] = list_sample_type_ids(
  408. db, presign_order_id=o.id, tenant_no=tenant_no
  409. )
  410. data["transfers"] = list_transfers(
  411. db, presign_order_id=o.id, tenant_no=tenant_no
  412. )
  413. return data
  414. def list_presign_orders(
  415. db: Session,
  416. *,
  417. tenant_no: str,
  418. page: int = 1,
  419. page_size: int = 10,
  420. keyword: Optional[str] = None,
  421. order_status: Optional[str] = None,
  422. user_info=None,
  423. ) -> Tuple[List[Dict[str, Any]], int]:
  424. q = db.query(BizPresignOrder).filter(
  425. BizPresignOrder.tenant_no == tenant_no,
  426. BizPresignOrder.del_flag == 0,
  427. )
  428. st = (order_status or "").strip()
  429. if st:
  430. q = q.filter(BizPresignOrder.order_status == st)
  431. kw = (keyword or "").strip()
  432. if kw:
  433. like = f"%{kw}%"
  434. q = q.filter(
  435. or_(
  436. BizPresignOrder.protocol_no.like(like),
  437. BizPresignOrder.mother_name.like(like),
  438. )
  439. )
  440. if user_info:
  441. from app.core.data_scope import apply_data_scope
  442. q = apply_data_scope(q, BizPresignOrder, user_info, db, tenant_no)
  443. total = q.order_by(None).with_entities(func.count(BizPresignOrder.id)).scalar() or 0
  444. page = max(1, page)
  445. page_size = min(max(1, page_size), 100)
  446. rows = (
  447. q.order_by(BizPresignOrder.create_time.desc())
  448. .offset((page - 1) * page_size)
  449. .limit(page_size)
  450. .all()
  451. )
  452. return [
  453. order_to_dict(db, o, tenant_no, with_children=False) for o in rows
  454. ], int(total)
  455. def get_presign_order(
  456. db: Session, *, order_id: str, tenant_no: str
  457. ) -> Optional[Dict[str, Any]]:
  458. obj = (
  459. db.query(BizPresignOrder)
  460. .filter(
  461. BizPresignOrder.id == order_id,
  462. BizPresignOrder.tenant_no == tenant_no,
  463. BizPresignOrder.del_flag == 0,
  464. )
  465. .first()
  466. )
  467. if not obj:
  468. return None
  469. return order_to_dict(db, obj, tenant_no, with_children=True)
  470. def create_presign_order(
  471. db: Session,
  472. *,
  473. tenant_no: str,
  474. user_id: Optional[str],
  475. fields: Dict[str, Any],
  476. sample_type_ids: Optional[List[str]] = None,
  477. transfers: Optional[List[Dict[str, Any]]] = None,
  478. create_dept_id: str = "",
  479. commit: bool = True,
  480. ) -> BizPresignOrder:
  481. now = _now()
  482. sample_collect = fields.get("sample_collect_time") or fields.get("sampleCollectTime")
  483. sct = None
  484. if sample_collect not in (None, ""):
  485. sct = (
  486. sample_collect
  487. if isinstance(sample_collect, datetime)
  488. else parse_datetime(str(sample_collect))
  489. )
  490. pay_mode = (fields.get("pay_mode") or fields.get("payMode") or "").strip() or None
  491. if pay_mode and pay_mode not in ("dj", "fq"):
  492. raise ValueError("缴费方式无效")
  493. # 协议编号由服务端发号,忽略客户端传入值
  494. protocol_no = allocate_next_biz_no(
  495. db, tenant_no=tenant_no, kind="collection", user_id=user_id
  496. )
  497. obj = BizPresignOrder(
  498. id=generate_uuid(),
  499. order_date=(fields.get("order_date") or fields.get("orderDate") or "").strip()
  500. or None,
  501. protocol_no=protocol_no,
  502. mother_name=(fields.get("mother_name") or fields.get("motherName") or "").strip()
  503. or None,
  504. expected_delivery_date=(
  505. fields.get("expected_delivery_date")
  506. or fields.get("expectedDeliveryDate")
  507. or ""
  508. ).strip()
  509. or None,
  510. region_id=(fields.get("region_id") or fields.get("regionId") or "").strip() or None,
  511. hospital_id=(fields.get("hospital_id") or fields.get("hospitalId") or "").strip()
  512. or None,
  513. sample_collect_time=sct,
  514. pay_mode=pay_mode,
  515. contract_amount=_dec(fields.get("contract_amount", fields.get("contractAmount"))),
  516. collector_user_id=(
  517. fields.get("collector_user_id") or fields.get("collectorUserId") or ""
  518. ).strip()
  519. or user_id
  520. or None,
  521. sales_user_id=(fields.get("sales_user_id") or fields.get("salesUserId") or "").strip()
  522. or None,
  523. region_manager_user_id=(
  524. fields.get("region_manager_user_id")
  525. or fields.get("regionManagerUserId")
  526. or ""
  527. ).strip()
  528. or None,
  529. order_status=(fields.get("order_status") or fields.get("orderStatus") or "active")
  530. .strip()
  531. or "active",
  532. tenant_no=tenant_no,
  533. order_num=0,
  534. remark=(fields.get("remark") or "").strip() or None,
  535. create_by=user_id,
  536. create_dept_id=create_dept_id,
  537. create_time=now,
  538. update_by=user_id,
  539. update_time=now,
  540. del_flag=0,
  541. )
  542. db.add(obj)
  543. db.flush()
  544. replace_sample_types(
  545. db,
  546. presign_order_id=obj.id,
  547. tenant_no=tenant_no,
  548. user_id=user_id,
  549. sample_type_ids=sample_type_ids or [],
  550. create_dept_id=create_dept_id,
  551. )
  552. for raw in transfers or []:
  553. if isinstance(raw, dict):
  554. add_transfer(
  555. db,
  556. presign_order_id=obj.id,
  557. tenant_no=tenant_no,
  558. user_id=user_id,
  559. fields=raw,
  560. create_dept_id=create_dept_id,
  561. commit=False,
  562. )
  563. if commit:
  564. db.commit()
  565. db.refresh(obj)
  566. else:
  567. db.flush()
  568. return obj
  569. def update_presign_order(
  570. db: Session,
  571. *,
  572. order_id: str,
  573. tenant_no: str,
  574. user_id: Optional[str],
  575. fields: Dict[str, Any],
  576. sample_type_ids: Optional[List[str]] = None,
  577. create_dept_id: str = "",
  578. commit: bool = True,
  579. ) -> BizPresignOrder:
  580. obj = _get_order_or_raise(db, order_id=order_id, tenant_no=tenant_no)
  581. if (obj.order_status or "") == "converted":
  582. raise ValueError("已转正式的预签约订单不可修改")
  583. mapping = {
  584. "order_date": "order_date",
  585. "orderDate": "order_date",
  586. "mother_name": "mother_name",
  587. "motherName": "mother_name",
  588. "expected_delivery_date": "expected_delivery_date",
  589. "expectedDeliveryDate": "expected_delivery_date",
  590. "region_id": "region_id",
  591. "regionId": "region_id",
  592. "hospital_id": "hospital_id",
  593. "hospitalId": "hospital_id",
  594. "pay_mode": "pay_mode",
  595. "payMode": "pay_mode",
  596. "collector_user_id": "collector_user_id",
  597. "collectorUserId": "collector_user_id",
  598. "sales_user_id": "sales_user_id",
  599. "salesUserId": "sales_user_id",
  600. "region_manager_user_id": "region_manager_user_id",
  601. "regionManagerUserId": "region_manager_user_id",
  602. "order_status": "order_status",
  603. "orderStatus": "order_status",
  604. "remark": "remark",
  605. }
  606. for k, attr in mapping.items():
  607. if k in fields:
  608. val = fields[k]
  609. if attr in (
  610. "order_date",
  611. "mother_name",
  612. "expected_delivery_date",
  613. "region_id",
  614. "hospital_id",
  615. "pay_mode",
  616. "collector_user_id",
  617. "sales_user_id",
  618. "region_manager_user_id",
  619. "order_status",
  620. "remark",
  621. ):
  622. setattr(obj, attr, (str(val).strip() if val is not None else "") or None)
  623. if "contract_amount" in fields or "contractAmount" in fields:
  624. obj.contract_amount = _dec(
  625. fields.get("contract_amount", fields.get("contractAmount"))
  626. )
  627. if "sample_collect_time" in fields or "sampleCollectTime" in fields:
  628. v = fields.get("sample_collect_time", fields.get("sampleCollectTime"))
  629. if v in (None, ""):
  630. obj.sample_collect_time = None
  631. elif isinstance(v, datetime):
  632. obj.sample_collect_time = v
  633. else:
  634. obj.sample_collect_time = parse_datetime(str(v))
  635. if obj.pay_mode and obj.pay_mode not in ("dj", "fq"):
  636. raise ValueError("缴费方式无效")
  637. # 协议编号不可改;历史空号在编辑保存时补发
  638. if not (obj.protocol_no or "").strip():
  639. obj.protocol_no = allocate_next_biz_no(
  640. db, tenant_no=tenant_no, kind="collection", user_id=user_id
  641. )
  642. obj.update_by = user_id
  643. obj.update_time = _now()
  644. db.add(obj)
  645. if sample_type_ids is not None:
  646. replace_sample_types(
  647. db,
  648. presign_order_id=obj.id,
  649. tenant_no=tenant_no,
  650. user_id=user_id,
  651. sample_type_ids=sample_type_ids,
  652. create_dept_id=create_dept_id,
  653. )
  654. if commit:
  655. db.commit()
  656. db.refresh(obj)
  657. else:
  658. db.flush()
  659. return obj
  660. def soft_delete_presign_order(
  661. db: Session, *, order_id: str, tenant_no: str, user_id: Optional[str]
  662. ) -> None:
  663. obj = _get_order_or_raise(db, order_id=order_id, tenant_no=tenant_no)
  664. if (obj.order_status or "") == "converted":
  665. raise ValueError("已转正式的预签约订单不可删除")
  666. now = _now()
  667. obj.del_flag = 1
  668. obj.update_by = user_id
  669. obj.update_time = now
  670. db.add(obj)
  671. db.commit()
  672. def add_transfer(
  673. db: Session,
  674. *,
  675. presign_order_id: str,
  676. tenant_no: str,
  677. user_id: Optional[str],
  678. fields: Dict[str, Any],
  679. create_dept_id: str = "",
  680. commit: bool = True,
  681. ) -> BizPresignTransfer:
  682. _get_order_or_raise(db, order_id=presign_order_id, tenant_no=tenant_no)
  683. now = _now()
  684. tt = fields.get("transfer_time", fields.get("transferTime"))
  685. transfer_time = None
  686. if tt not in (None, ""):
  687. transfer_time = tt if isinstance(tt, datetime) else parse_datetime(str(tt))
  688. obj = BizPresignTransfer(
  689. id=generate_uuid(),
  690. presign_order_id=presign_order_id,
  691. transfer_amount=_dec(fields.get("transfer_amount", fields.get("transferAmount"))),
  692. transfer_time=transfer_time,
  693. transfer_type=(
  694. fields.get("transfer_type") or fields.get("transferType") or ""
  695. ).strip()
  696. or None,
  697. transfer_attachments=(
  698. fields.get("transfer_attachments")
  699. or fields.get("transferAttachments")
  700. or ""
  701. ).strip()
  702. or None,
  703. pay_status="dfj",
  704. tenant_no=tenant_no,
  705. order_num=0,
  706. remark=(fields.get("remark") or "").strip() or None,
  707. create_by=user_id,
  708. create_dept_id=create_dept_id,
  709. create_time=now,
  710. update_by=user_id,
  711. update_time=now,
  712. del_flag=0,
  713. )
  714. db.add(obj)
  715. if commit:
  716. db.commit()
  717. db.refresh(obj)
  718. else:
  719. db.flush()
  720. return obj
  721. def update_transfer_draft(
  722. db: Session,
  723. *,
  724. transfer_id: str,
  725. tenant_no: str,
  726. user_id: Optional[str],
  727. fields: Dict[str, Any],
  728. commit: bool = True,
  729. ) -> BizPresignTransfer:
  730. t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
  731. if t.pay_status != "dfj":
  732. raise ValueError("仅已付款状态可编辑意向金转账笔")
  733. values: Dict[Any, Any] = {
  734. BizPresignTransfer.update_by: user_id,
  735. BizPresignTransfer.update_time: _now(),
  736. }
  737. if "transfer_amount" in fields or "transferAmount" in fields:
  738. values[BizPresignTransfer.transfer_amount] = _dec(
  739. fields.get("transfer_amount", fields.get("transferAmount"))
  740. )
  741. if "transfer_time" in fields or "transferTime" in fields:
  742. v = fields.get("transfer_time", fields.get("transferTime"))
  743. if v in (None, ""):
  744. values[BizPresignTransfer.transfer_time] = None
  745. elif isinstance(v, datetime):
  746. values[BizPresignTransfer.transfer_time] = v
  747. else:
  748. parsed = parse_datetime(str(v))
  749. if parsed is None:
  750. raise ValueError("转账时间格式无效")
  751. values[BizPresignTransfer.transfer_time] = parsed
  752. if "transfer_type" in fields or "transferType" in fields:
  753. values[BizPresignTransfer.transfer_type] = (
  754. str(fields.get("transfer_type", fields.get("transferType")) or "")
  755. ).strip() or None
  756. if "transfer_attachments" in fields or "transferAttachments" in fields:
  757. values[BizPresignTransfer.transfer_attachments] = (
  758. str(
  759. fields.get("transfer_attachments", fields.get("transferAttachments"))
  760. or ""
  761. )
  762. ).strip() or None
  763. if "remark" in fields:
  764. values[BizPresignTransfer.remark] = (
  765. fields.get("remark") or ""
  766. ).strip() or None
  767. return _update_transfer_expected_status(
  768. db,
  769. transfer_id=transfer_id,
  770. tenant_no=tenant_no,
  771. expected_status="dfj",
  772. values=values,
  773. commit=commit,
  774. )
  775. def submit_receipt_finance(
  776. db: Session,
  777. *,
  778. transfer_id: str,
  779. tenant_no: str,
  780. user_id: Optional[str],
  781. commit: bool = True,
  782. ) -> BizPresignTransfer:
  783. t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
  784. if not can_submit_receipt(t.pay_status):
  785. raise ValueError("仅已付款状态可提交财务确认")
  786. if not (t.transfer_type or "").strip():
  787. raise ValueError("请选择转账方式")
  788. if t.transfer_amount is None or Decimal(str(t.transfer_amount)) <= 0:
  789. raise ValueError("请填写转账金额")
  790. return _update_transfer_expected_status(
  791. db,
  792. transfer_id=transfer_id,
  793. tenant_no=tenant_no,
  794. expected_status="dfj",
  795. values={
  796. BizPresignTransfer.pay_status: "jfqrz",
  797. BizPresignTransfer.update_by: user_id,
  798. BizPresignTransfer.update_time: _now(),
  799. },
  800. extra_conditions=(
  801. BizPresignTransfer.transfer_amount.isnot(None),
  802. BizPresignTransfer.transfer_amount > 0,
  803. BizPresignTransfer.transfer_type.isnot(None),
  804. func.length(func.trim(BizPresignTransfer.transfer_type)) > 0,
  805. ),
  806. commit=commit,
  807. )
  808. def withdraw_receipt(
  809. db: Session,
  810. *,
  811. transfer_id: str,
  812. tenant_no: str,
  813. user_id: Optional[str],
  814. commit: bool = True,
  815. ) -> BizPresignTransfer:
  816. t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
  817. if not can_withdraw_receipt(t.pay_status):
  818. raise ValueError("仅付款确认中可撤回")
  819. # 故意不改 finance_remark / finance_confirm_attachments
  820. return _update_transfer_expected_status(
  821. db,
  822. transfer_id=transfer_id,
  823. tenant_no=tenant_no,
  824. expected_status="jfqrz",
  825. values={
  826. BizPresignTransfer.pay_status: "dfj",
  827. BizPresignTransfer.update_by: user_id,
  828. BizPresignTransfer.update_time: _now(),
  829. },
  830. commit=commit,
  831. )
  832. def confirm_receipt(
  833. db: Session,
  834. *,
  835. transfer_id: str,
  836. tenant_no: str,
  837. user_id: Optional[str],
  838. finance_confirm_attachments: Optional[str] = None,
  839. finance_remark: Optional[str] = None,
  840. commit: bool = True,
  841. ) -> BizPresignTransfer:
  842. t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
  843. if not can_confirm_receipt(t.pay_status):
  844. raise ValueError("仅付款确认中状态可确认收款")
  845. values: Dict[Any, Any] = {
  846. BizPresignTransfer.pay_status: "yjf",
  847. BizPresignTransfer.update_by: user_id,
  848. BizPresignTransfer.update_time: _now(),
  849. }
  850. if finance_confirm_attachments is not None:
  851. values[BizPresignTransfer.finance_confirm_attachments] = (
  852. finance_confirm_attachments or ""
  853. ).strip() or None
  854. if finance_remark is not None:
  855. values[BizPresignTransfer.finance_remark] = (
  856. finance_remark or ""
  857. ).strip() or None
  858. return _update_transfer_expected_status(
  859. db,
  860. transfer_id=transfer_id,
  861. tenant_no=tenant_no,
  862. expected_status="jfqrz",
  863. values=values,
  864. commit=commit,
  865. )
  866. def reject_receipt(
  867. db: Session,
  868. *,
  869. transfer_id: str,
  870. tenant_no: str,
  871. user_id: Optional[str],
  872. commit: bool = True,
  873. ) -> BizPresignTransfer:
  874. t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
  875. if not can_confirm_receipt(t.pay_status):
  876. raise ValueError("仅付款确认中状态可退回")
  877. return _update_transfer_expected_status(
  878. db,
  879. transfer_id=transfer_id,
  880. tenant_no=tenant_no,
  881. expected_status="jfqrz",
  882. values={
  883. BizPresignTransfer.pay_status: "dfj",
  884. BizPresignTransfer.update_by: user_id,
  885. BizPresignTransfer.update_time: _now(),
  886. },
  887. commit=commit,
  888. )
  889. def list_presign_transfers_for_finance(
  890. db: Session,
  891. *,
  892. pay_status: str,
  893. tenant_no: str,
  894. keyword: Optional[str] = None,
  895. page: int = 1,
  896. page_size: int = 10,
  897. user_info=None,
  898. ) -> Tuple[List[Dict[str, Any]], int]:
  899. if not tenant_no:
  900. return [], 0
  901. st = (pay_status or "").strip()
  902. q = (
  903. db.query(BizPresignTransfer, BizPresignOrder)
  904. .join(
  905. BizPresignOrder,
  906. and_(
  907. BizPresignOrder.id == BizPresignTransfer.presign_order_id,
  908. BizPresignOrder.del_flag == 0,
  909. ),
  910. )
  911. .filter(
  912. BizPresignTransfer.tenant_no == tenant_no,
  913. BizPresignTransfer.del_flag == 0,
  914. BizPresignTransfer.pay_status == st,
  915. )
  916. )
  917. kw = (keyword or "").strip()
  918. if kw:
  919. like = f"%{kw}%"
  920. q = q.filter(
  921. or_(
  922. BizPresignOrder.protocol_no.like(like),
  923. BizPresignOrder.mother_name.like(like),
  924. )
  925. )
  926. if user_info:
  927. from app.core.data_scope import apply_data_scope
  928. q = apply_data_scope(q, BizPresignTransfer, user_info, db, tenant_no)
  929. total = q.order_by(None).with_entities(func.count(BizPresignTransfer.id)).scalar() or 0
  930. page = max(1, page)
  931. page_size = min(max(1, page_size), 100)
  932. rows = (
  933. q.order_by(BizPresignTransfer.create_time.desc())
  934. .offset((page - 1) * page_size)
  935. .limit(page_size)
  936. .all()
  937. )
  938. out: List[Dict[str, Any]] = []
  939. for t, order in rows:
  940. item = _transfer_to_dict(db, t, tenant_no)
  941. item["protocolNo"] = (order.protocol_no if order else "") or ""
  942. item["motherName"] = (order.mother_name if order else "") or ""
  943. item["sampleCode"] = ""
  944. item["presignOrderId"] = order.id if order else ""
  945. item["orderDate"] = (order.order_date if order else "") or ""
  946. item["expectedDeliveryDate"] = (
  947. (order.expected_delivery_date if order else "") or ""
  948. )
  949. item["contractAmount"] = (
  950. float(order.contract_amount)
  951. if order and order.contract_amount is not None
  952. else None
  953. )
  954. item["contractPayMode"] = (order.pay_mode if order else "") or ""
  955. item["orderStatus"] = (order.order_status if order else "") or ""
  956. item["orderRemark"] = (order.remark if order else "") or ""
  957. name_map = _user_nick_name_map(
  958. db,
  959. [
  960. (order.collector_user_id if order else "") or "",
  961. (order.sales_user_id if order else "") or "",
  962. (order.region_manager_user_id if order else "") or "",
  963. ],
  964. tenant_no,
  965. )
  966. item["collectorUserName"] = name_map.get(
  967. (order.collector_user_id if order else "") or "", ""
  968. )
  969. item["salesUserName"] = name_map.get(
  970. (order.sales_user_id if order else "") or "", ""
  971. )
  972. item["regionManagerUserName"] = name_map.get(
  973. (order.region_manager_user_id if order else "") or "", ""
  974. )
  975. item["sampleTypeNames"] = (
  976. _sample_type_names(
  977. db, presign_order_id=order.id, tenant_no=tenant_no
  978. )
  979. if order
  980. else ""
  981. )
  982. out.append(item)
  983. return out, int(total)
  984. def convert_to_protocol(
  985. db: Session,
  986. *,
  987. order_id: str,
  988. tenant_no: str,
  989. user_id: Optional[str],
  990. create_dept_id: str = "",
  991. commit: bool = True,
  992. ) -> Dict[str, Any]:
  993. """转正式协议:创建协议主单 + 当事人/采集信息 + 样本类型行;意向金仅关联预签订单 ID,不拷贝。"""
  994. from app.crud import collection_protocol as cp_svc
  995. order = _get_order_or_raise(
  996. db, order_id=order_id, tenant_no=tenant_no, for_update=True
  997. )
  998. assert_presign_convertible(order_status=order.order_status)
  999. validate_presign_person_ids(
  1000. db,
  1001. tenant_no=tenant_no,
  1002. collector_user_id=order.collector_user_id or user_id,
  1003. sales_user_id=order.sales_user_id,
  1004. region_manager_user_id=order.region_manager_user_id,
  1005. )
  1006. staff = resolve_convert_staff_fields(
  1007. collector_user_id=order.collector_user_id,
  1008. sales_user_id=order.sales_user_id,
  1009. region_manager_user_id=order.region_manager_user_id,
  1010. fallback_user_id=user_id,
  1011. )
  1012. type_ids = list_sample_type_ids(db, presign_order_id=order.id, tenant_no=tenant_no)
  1013. try:
  1014. protocol = cp_svc.create_collection_protocol(
  1015. db,
  1016. protocol_no=order.protocol_no or "",
  1017. protocol_status="sxz",
  1018. register_date=order.order_date,
  1019. collector_user_id=staff["collector_user_id"],
  1020. source_presign_order_id=order.id,
  1021. payment_model_version=2,
  1022. protocol_total_amount=None,
  1023. remark=order.remark,
  1024. tenant_no=tenant_no,
  1025. user_id=user_id,
  1026. create_dept_id=create_dept_id,
  1027. commit=False,
  1028. exclude_presign_order_id=order.id,
  1029. )
  1030. party_fields: Dict[str, Any] = {
  1031. "mother_name": order.mother_name,
  1032. "expected_delivery_date": order.expected_delivery_date,
  1033. }
  1034. cp_svc.upsert_party_customer(
  1035. db,
  1036. protocol.id,
  1037. tenant_no,
  1038. user_id,
  1039. create_dept_id=create_dept_id,
  1040. commit=False,
  1041. **party_fields,
  1042. )
  1043. collection_fields: Dict[str, Any] = {
  1044. "region_id": order.region_id,
  1045. "hospital_id": order.hospital_id,
  1046. "sales_user_id": staff["sales_user_id"],
  1047. "region_manager_user_id": staff["region_manager_user_id"],
  1048. "sample_collect_time": order.sample_collect_time,
  1049. }
  1050. cp_svc.upsert_collection_info(
  1051. db,
  1052. protocol.id,
  1053. tenant_no,
  1054. user_id,
  1055. create_dept_id=create_dept_id,
  1056. commit=False,
  1057. **collection_fields,
  1058. )
  1059. if type_ids:
  1060. samples = [
  1061. {
  1062. "sampleTypeId": tid,
  1063. "sampleStatus": None,
  1064. }
  1065. for tid in type_ids
  1066. ]
  1067. cp_svc.save_samples(
  1068. db,
  1069. protocol.id,
  1070. tenant_no,
  1071. user_id,
  1072. samples,
  1073. create_dept_id=create_dept_id,
  1074. commit=False,
  1075. )
  1076. order.order_status = "converted"
  1077. order.converted_protocol_id = protocol.id
  1078. if not (order.protocol_no or "").strip():
  1079. order.protocol_no = protocol.protocol_no
  1080. order.update_by = user_id
  1081. order.update_time = _now()
  1082. db.add(order)
  1083. if commit:
  1084. db.commit()
  1085. db.refresh(order)
  1086. else:
  1087. db.flush()
  1088. except IntegrityError as exc:
  1089. db.rollback()
  1090. message = str(exc).lower()
  1091. if (
  1092. "uk_collection_protocol_presign_source" in message
  1093. or (
  1094. "unique constraint failed" in message
  1095. and "source_presign_order_id" in message
  1096. )
  1097. ):
  1098. raise ValueError("该预签约订单已转正式协议") from exc
  1099. raise
  1100. except Exception:
  1101. db.rollback()
  1102. raise
  1103. return {
  1104. "presignOrder": order_to_dict(db, order, tenant_no, with_children=True),
  1105. "protocolId": protocol.id,
  1106. "protocolNo": protocol.protocol_no,
  1107. }