sample_freight.py 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288
  1. """样本运费批次 CRUD(biz_sample_freight / biz_sample_freight_item)"""
  2. from __future__ import annotations
  3. from datetime import datetime, timedelta
  4. from decimal import Decimal
  5. from typing import Any, Dict, List, Optional, Tuple
  6. from sqlalchemy import and_, exists, func, or_
  7. from sqlalchemy.orm import Session
  8. from app.core.data_scope import apply_data_scope, get_user_dept_id
  9. from app.core.utils import generate_uuid
  10. from app.models.models import (
  11. BizCellSampleType,
  12. BizCollectionProtocol,
  13. BizProtocolPartyCustomer,
  14. BizProtocolSample,
  15. BizSampleFreight,
  16. BizSampleFreightItem,
  17. SysDictData,
  18. SysUser,
  19. )
  20. from app.models.research_donation import (
  21. BizResearchDonation,
  22. BizResearchDonationParty,
  23. BizResearchDonationSample,
  24. )
  25. from app.services.sample_transport_mode import DICT_TYPE, resolve_transport_mode_value
  26. from app.services.sf_express_settlement import (
  27. assert_freight_mutable,
  28. freight_amount_label,
  29. resolve_header_for_save,
  30. )
  31. SAMPLE_SOURCE_CELL = "cell_storage"
  32. SAMPLE_SOURCE_DONATION = "research_donation"
  33. def _now() -> datetime:
  34. return datetime.now()
  35. def _fmt_dt(t: Optional[datetime]) -> str:
  36. return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
  37. def _parse_amount(amount: Any) -> Decimal:
  38. if amount is None or amount == "":
  39. raise ValueError("运费不能为空")
  40. try:
  41. val = Decimal(str(amount))
  42. except Exception as exc:
  43. raise ValueError("运费格式无效") from exc
  44. if not val.is_finite():
  45. raise ValueError("运费格式无效")
  46. if val < 0:
  47. raise ValueError("运费不能为负数")
  48. return val
  49. def _item_field(raw: Dict[str, Any], camel: str, snake: str, default: str = "") -> str:
  50. v = raw.get(camel)
  51. if v is None or v == "":
  52. v = raw.get(snake)
  53. if v is None:
  54. return default
  55. return str(v).strip()
  56. def _normalize_item(raw: Any) -> Dict[str, Any]:
  57. if not isinstance(raw, dict):
  58. raise ValueError("关联样本项无效")
  59. sample_source = _item_field(raw, "sampleSource", "sample_source")
  60. sample_id = _item_field(raw, "sampleId", "sample_id")
  61. if not sample_source or not sample_id:
  62. raise ValueError("关联样本须包含 sampleSource 与 sampleId")
  63. if sample_source not in (SAMPLE_SOURCE_CELL, SAMPLE_SOURCE_DONATION):
  64. raise ValueError("sampleSource 无效")
  65. protocol_id = _item_field(raw, "protocolId", "protocol_id")
  66. sample_code = _item_field(raw, "sampleCode", "sample_code")
  67. return {
  68. "sample_source": sample_source,
  69. "sample_id": sample_id,
  70. "protocol_id": protocol_id or None,
  71. "sample_code": sample_code or None,
  72. }
  73. def _label_map(db: Session, tenant_no: str, modes: List[str]) -> Dict[str, str]:
  74. uniq = [m for m in {str(x).strip() for x in modes if str(x or "").strip()}]
  75. if not uniq:
  76. return {}
  77. rows = (
  78. db.query(SysDictData.dict_value, SysDictData.dict_label)
  79. .filter(
  80. SysDictData.dict_type == DICT_TYPE,
  81. SysDictData.tenant_no == tenant_no,
  82. SysDictData.dict_value.in_(uniq),
  83. SysDictData.del_flag == 0,
  84. )
  85. .all()
  86. )
  87. return {str(v): (lbl or v or "") for v, lbl in rows}
  88. def _linked_count(db: Session, freight_id: str, tenant_no: str) -> int:
  89. return (
  90. db.query(func.count(BizSampleFreightItem.id))
  91. .filter(
  92. BizSampleFreightItem.freight_id == freight_id,
  93. BizSampleFreightItem.tenant_no == tenant_no,
  94. BizSampleFreightItem.del_flag == 0,
  95. )
  96. .scalar()
  97. or 0
  98. )
  99. def _user_nick_name_map(db: Session, user_ids: List[str]) -> Dict[str, str]:
  100. ids = list({x for x in user_ids if x})
  101. if not ids:
  102. return {}
  103. rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(ids)).all()
  104. return {u.id: (u.nick_name or "").strip() for u in rows}
  105. def _generate_freight_no(
  106. db: Session, tenant_no: str, when: Optional[datetime] = None
  107. ) -> str:
  108. """生成批次号:YF + YYYYMMDD + 001~999(按租户、按日)。"""
  109. day = (when or _now()).strftime("%Y%m%d")
  110. prefix = f"YF{day}"
  111. existing = (
  112. db.query(BizSampleFreight.freight_no)
  113. .filter(
  114. BizSampleFreight.tenant_no == tenant_no,
  115. BizSampleFreight.freight_no.like(f"{prefix}%"),
  116. )
  117. .order_by(BizSampleFreight.freight_no.desc())
  118. .first()
  119. )
  120. seq = 1
  121. if existing and existing[0]:
  122. try:
  123. seq = int(str(existing[0])[-3:]) + 1
  124. except ValueError:
  125. seq = 1
  126. if seq > 999:
  127. raise ValueError("当日运费批次号已达上限(999)")
  128. return f"{prefix}{seq:03d}"
  129. def _row_to_dict(
  130. obj: BizSampleFreight,
  131. *,
  132. linked_sample_count: int = 0,
  133. transport_mode_label: str = "",
  134. create_by_name: str = "",
  135. ) -> Dict[str, Any]:
  136. amount = obj.freight_amount
  137. create_by = obj.create_by or ""
  138. return {
  139. "id": obj.id,
  140. "freightNo": getattr(obj, "freight_no", None) or "",
  141. "transportMode": obj.transport_mode or "",
  142. "transportModeLabel": transport_mode_label or (obj.transport_mode or ""),
  143. "freightAmount": float(amount) if amount is not None else None,
  144. "freightAmountLabel": freight_amount_label(obj.transport_mode, amount),
  145. "waybillNo": getattr(obj, "waybill_no", None) or "",
  146. "settleStatus": getattr(obj, "settle_status", None) or "",
  147. "settleTime": _fmt_dt(getattr(obj, "settle_time", None))
  148. if getattr(obj, "settle_time", None)
  149. else "",
  150. "linkedSampleCount": int(linked_sample_count or 0),
  151. "tenantNo": obj.tenant_no,
  152. "orderNum": obj.order_num if obj.order_num is not None else 0,
  153. "remark": obj.remark or "",
  154. "createBy": create_by,
  155. "createByName": create_by_name or create_by,
  156. "createTime": _fmt_dt(obj.create_time),
  157. "updateTime": _fmt_dt(obj.update_time),
  158. }
  159. def _item_to_dict(item: BizSampleFreightItem) -> Dict[str, Any]:
  160. return {
  161. "id": item.id,
  162. "freightId": item.freight_id,
  163. "sampleSource": item.sample_source or "",
  164. "sampleId": item.sample_id or "",
  165. "sampleCode": item.sample_code or "",
  166. "protocolId": item.protocol_id or "",
  167. }
  168. def _item_to_dict_enriched(
  169. item: BizSampleFreightItem,
  170. *,
  171. protocol_no: str = "",
  172. sample_type_name: str = "",
  173. mother_name: str = "",
  174. newborn_name: str = "",
  175. ) -> Dict[str, Any]:
  176. data = _item_to_dict(item)
  177. data["protocolNo"] = protocol_no or ""
  178. data["sampleTypeName"] = sample_type_name or ""
  179. data["motherName"] = mother_name or ""
  180. data["newbornName"] = newborn_name or ""
  181. return data
  182. def _normalize_date_bound(value: str, *, end_of_day: bool = False) -> datetime:
  183. raw = (value or "").strip()
  184. if not raw:
  185. raise ValueError("时间参数无效")
  186. for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d"):
  187. try:
  188. dt = datetime.strptime(raw, fmt)
  189. if fmt == "%Y-%m-%d" and end_of_day:
  190. dt = dt + timedelta(days=1) - timedelta(seconds=1)
  191. return dt
  192. except ValueError:
  193. continue
  194. try:
  195. return datetime.fromisoformat(raw.replace("Z", "+00:00")).replace(tzinfo=None)
  196. except ValueError as exc:
  197. raise ValueError("时间参数格式无效") from exc
  198. def _enrich_items_maps(
  199. db: Session,
  200. items: List[BizSampleFreightItem],
  201. tenant_no: str,
  202. ) -> Dict[str, Dict[str, str]]:
  203. """批量解析协议号/样本类型/母亲子女姓名,key = sample_source|sample_id。"""
  204. out: Dict[str, Dict[str, str]] = {}
  205. if not items:
  206. return out
  207. cell_ids = [
  208. i.sample_id
  209. for i in items
  210. if i.sample_source == SAMPLE_SOURCE_CELL and i.sample_id
  211. ]
  212. donation_ids = [
  213. i.sample_id
  214. for i in items
  215. if i.sample_source == SAMPLE_SOURCE_DONATION and i.sample_id
  216. ]
  217. if cell_ids:
  218. rows = (
  219. db.query(
  220. BizProtocolSample.id,
  221. BizCollectionProtocol.protocol_no,
  222. BizCellSampleType.type_name,
  223. BizProtocolPartyCustomer.mother_name,
  224. BizProtocolPartyCustomer.newborn_name,
  225. )
  226. .outerjoin(
  227. BizCollectionProtocol,
  228. and_(
  229. BizCollectionProtocol.id == BizProtocolSample.collection_protocol_id,
  230. BizCollectionProtocol.del_flag == 0,
  231. ),
  232. )
  233. .outerjoin(
  234. BizCellSampleType,
  235. and_(
  236. BizCellSampleType.id == BizProtocolSample.sample_type_id,
  237. BizCellSampleType.del_flag == 0,
  238. ),
  239. )
  240. .outerjoin(
  241. BizProtocolPartyCustomer,
  242. and_(
  243. BizProtocolPartyCustomer.collection_protocol_id
  244. == BizProtocolSample.collection_protocol_id,
  245. BizProtocolPartyCustomer.del_flag == 0,
  246. ),
  247. )
  248. .filter(
  249. BizProtocolSample.id.in_(cell_ids),
  250. BizProtocolSample.tenant_no == tenant_no,
  251. BizProtocolSample.del_flag == 0,
  252. )
  253. .all()
  254. )
  255. for sid, pno, tname, mname, nname in rows:
  256. out[f"{SAMPLE_SOURCE_CELL}|{sid}"] = {
  257. "protocolNo": pno or "",
  258. "sampleTypeName": tname or "",
  259. "motherName": mname or "",
  260. "newbornName": nname or "",
  261. }
  262. if donation_ids:
  263. rows = (
  264. db.query(
  265. BizResearchDonationSample.id,
  266. BizResearchDonation.donation_no,
  267. BizCellSampleType.type_name,
  268. BizResearchDonationParty.mother_name,
  269. BizResearchDonationParty.newborn_name,
  270. )
  271. .outerjoin(
  272. BizResearchDonation,
  273. and_(
  274. BizResearchDonation.id
  275. == BizResearchDonationSample.research_donation_id,
  276. BizResearchDonation.del_flag == 0,
  277. ),
  278. )
  279. .outerjoin(
  280. BizCellSampleType,
  281. and_(
  282. BizCellSampleType.id == BizResearchDonationSample.sample_type_id,
  283. BizCellSampleType.del_flag == 0,
  284. ),
  285. )
  286. .outerjoin(
  287. BizResearchDonationParty,
  288. and_(
  289. BizResearchDonationParty.research_donation_id
  290. == BizResearchDonationSample.research_donation_id,
  291. BizResearchDonationParty.del_flag == 0,
  292. ),
  293. )
  294. .filter(
  295. BizResearchDonationSample.id.in_(donation_ids),
  296. BizResearchDonationSample.tenant_no == tenant_no,
  297. BizResearchDonationSample.del_flag == 0,
  298. )
  299. .all()
  300. )
  301. for sid, dno, tname, mname, nname in rows:
  302. out[f"{SAMPLE_SOURCE_DONATION}|{sid}"] = {
  303. "protocolNo": dno or "",
  304. "sampleTypeName": tname or "",
  305. "motherName": mname or "",
  306. "newbornName": nname or "",
  307. }
  308. return out
  309. def get_freight(
  310. db: Session, freight_id: str, tenant_no: str, user_info: Optional[dict] = None
  311. ) -> Optional[BizSampleFreight]:
  312. query = db.query(BizSampleFreight).filter(
  313. BizSampleFreight.id == freight_id,
  314. BizSampleFreight.tenant_no == tenant_no,
  315. BizSampleFreight.del_flag == 0,
  316. )
  317. return apply_data_scope(query, BizSampleFreight, user_info, db, tenant_no).first()
  318. def _validated_items(
  319. db: Session, items: List[Dict[str, Any]], tenant_no: str, user_info: dict
  320. ) -> List[Dict[str, Any]]:
  321. """写入前校验全部样本的租户和数据范围,快照只从数据库派生。"""
  322. validated = []
  323. for raw in items:
  324. item = _normalize_item(raw)
  325. if item["sample_source"] == SAMPLE_SOURCE_CELL:
  326. sample_model, parent_model = BizProtocolSample, BizCollectionProtocol
  327. parent_key = BizProtocolSample.collection_protocol_id
  328. else:
  329. sample_model, parent_model = BizResearchDonationSample, BizResearchDonation
  330. parent_key = BizResearchDonationSample.research_donation_id
  331. query = db.query(sample_model, parent_model).join(
  332. parent_model, parent_model.id == parent_key
  333. ).filter(
  334. sample_model.id == item["sample_id"],
  335. sample_model.tenant_no == tenant_no,
  336. sample_model.del_flag == 0,
  337. parent_model.tenant_no == tenant_no,
  338. parent_model.del_flag == 0,
  339. )
  340. row = apply_data_scope(query, parent_model, user_info, db, tenant_no).first()
  341. if not row:
  342. raise PermissionError("样本不存在或无权访问")
  343. existing_links = db.query(BizSampleFreightItem).filter(
  344. BizSampleFreightItem.tenant_no == tenant_no,
  345. BizSampleFreightItem.sample_source == item["sample_source"],
  346. BizSampleFreightItem.sample_id == item["sample_id"],
  347. BizSampleFreightItem.del_flag == 0,
  348. ).all()
  349. if any(
  350. not get_freight(db, link.freight_id, tenant_no, user_info)
  351. for link in existing_links
  352. ):
  353. raise PermissionError("无权修改样本当前关联的运费批次")
  354. sample, parent = row
  355. item["sample_code"] = sample.sample_code
  356. item["protocol_id"] = parent.id
  357. validated.append(item)
  358. return validated
  359. def list_freights(
  360. db: Session,
  361. *,
  362. tenant_no: str,
  363. page: int = 1,
  364. page_size: int = 10,
  365. keyword: Optional[str] = None,
  366. transport_mode: Optional[str] = None,
  367. sample_source: Optional[str] = None,
  368. create_time_from: Optional[str] = None,
  369. create_time_to: Optional[str] = None,
  370. include_items: bool = False,
  371. user_info: Optional[dict] = None,
  372. ) -> Tuple[List[Dict[str, Any]], int]:
  373. page = max(1, int(page or 1))
  374. page_size = max(1, min(100, int(page_size or 10)))
  375. q = db.query(BizSampleFreight).filter(
  376. BizSampleFreight.tenant_no == tenant_no,
  377. BizSampleFreight.del_flag == 0,
  378. )
  379. q = apply_data_scope(q, BizSampleFreight, user_info, db, tenant_no)
  380. mode = (transport_mode or "").strip()
  381. if mode:
  382. q = q.filter(BizSampleFreight.transport_mode == mode)
  383. src = (sample_source or "").strip()
  384. if src:
  385. if src not in (SAMPLE_SOURCE_CELL, SAMPLE_SOURCE_DONATION):
  386. raise ValueError("sampleSource 无效")
  387. item_exists = exists().where(
  388. and_(
  389. BizSampleFreightItem.freight_id == BizSampleFreight.id,
  390. BizSampleFreightItem.tenant_no == tenant_no,
  391. BizSampleFreightItem.del_flag == 0,
  392. BizSampleFreightItem.sample_source == src,
  393. )
  394. )
  395. q = q.filter(item_exists)
  396. if create_time_from and str(create_time_from).strip():
  397. q = q.filter(
  398. BizSampleFreight.create_time
  399. >= _normalize_date_bound(str(create_time_from).strip())
  400. )
  401. if create_time_to and str(create_time_to).strip():
  402. q = q.filter(
  403. BizSampleFreight.create_time
  404. <= _normalize_date_bound(str(create_time_to).strip(), end_of_day=True)
  405. )
  406. kw = (keyword or "").strip()
  407. if kw:
  408. like = f"%{kw}%"
  409. label_values = [
  410. r[0]
  411. for r in db.query(SysDictData.dict_value)
  412. .filter(
  413. SysDictData.dict_type == DICT_TYPE,
  414. SysDictData.tenant_no == tenant_no,
  415. SysDictData.del_flag == 0,
  416. SysDictData.dict_label.like(like),
  417. )
  418. .all()
  419. ]
  420. conds = [
  421. BizSampleFreight.transport_mode.like(like),
  422. BizSampleFreight.freight_no.like(like),
  423. ]
  424. if label_values:
  425. conds.append(BizSampleFreight.transport_mode.in_(label_values))
  426. sample_code_exists = exists().where(
  427. and_(
  428. BizSampleFreightItem.freight_id == BizSampleFreight.id,
  429. BizSampleFreightItem.tenant_no == tenant_no,
  430. BizSampleFreightItem.del_flag == 0,
  431. BizSampleFreightItem.sample_code.like(like),
  432. )
  433. )
  434. conds.append(sample_code_exists)
  435. q = q.filter(or_(*conds))
  436. total = q.count()
  437. rows = (
  438. q.order_by(BizSampleFreight.create_time.desc(), BizSampleFreight.id.desc())
  439. .offset((page - 1) * page_size)
  440. .limit(page_size)
  441. .all()
  442. )
  443. labels = _label_map(db, tenant_no, [r.transport_mode for r in rows])
  444. name_map = _user_nick_name_map(db, [r.create_by for r in rows if r.create_by])
  445. items_by_freight: Dict[str, List[BizSampleFreightItem]] = {}
  446. if include_items and rows:
  447. freight_ids = [r.id for r in rows]
  448. all_items = (
  449. db.query(BizSampleFreightItem)
  450. .filter(
  451. BizSampleFreightItem.freight_id.in_(freight_ids),
  452. BizSampleFreightItem.tenant_no == tenant_no,
  453. BizSampleFreightItem.del_flag == 0,
  454. )
  455. .order_by(
  456. BizSampleFreightItem.create_time.asc(),
  457. BizSampleFreightItem.id.asc(),
  458. )
  459. .all()
  460. )
  461. for it in all_items:
  462. items_by_freight.setdefault(it.freight_id, []).append(it)
  463. enrich_map = _enrich_items_maps(db, all_items, tenant_no)
  464. else:
  465. enrich_map = {}
  466. out: List[Dict[str, Any]] = []
  467. for r in rows:
  468. linked = (
  469. len(items_by_freight.get(r.id, []))
  470. if include_items
  471. else _linked_count(db, r.id, tenant_no)
  472. )
  473. data = _row_to_dict(
  474. r,
  475. linked_sample_count=linked,
  476. transport_mode_label=labels.get(r.transport_mode or "", r.transport_mode or ""),
  477. create_by_name=name_map.get(r.create_by or "", ""),
  478. )
  479. if include_items:
  480. enriched = []
  481. for it in items_by_freight.get(r.id, []):
  482. key = f"{it.sample_source}|{it.sample_id}"
  483. info = enrich_map.get(key, {})
  484. enriched.append(
  485. _item_to_dict_enriched(
  486. it,
  487. protocol_no=info.get("protocolNo", ""),
  488. sample_type_name=info.get("sampleTypeName", ""),
  489. mother_name=info.get("motherName", ""),
  490. newborn_name=info.get("newbornName", ""),
  491. )
  492. )
  493. data["items"] = enriched
  494. out.append(data)
  495. return out, total
  496. def get_freight_detail(
  497. db: Session, freight_id: str, tenant_no: str, user_info: Optional[dict] = None
  498. ) -> Dict[str, Any]:
  499. obj = get_freight(db, freight_id, tenant_no, user_info)
  500. if not obj:
  501. raise ValueError("运费批次不存在")
  502. items = (
  503. db.query(BizSampleFreightItem)
  504. .filter(
  505. BizSampleFreightItem.freight_id == freight_id,
  506. BizSampleFreightItem.tenant_no == tenant_no,
  507. BizSampleFreightItem.del_flag == 0,
  508. )
  509. .order_by(BizSampleFreightItem.create_time.asc())
  510. .all()
  511. )
  512. labels = _label_map(db, tenant_no, [obj.transport_mode])
  513. name_map = _user_nick_name_map(db, [obj.create_by] if obj.create_by else [])
  514. data = _row_to_dict(
  515. obj,
  516. linked_sample_count=len(items),
  517. transport_mode_label=labels.get(obj.transport_mode or "", obj.transport_mode or ""),
  518. create_by_name=name_map.get(obj.create_by or "", ""),
  519. )
  520. enrich_map = _enrich_items_maps(db, items, tenant_no)
  521. enriched = []
  522. for i in items:
  523. key = f"{i.sample_source}|{i.sample_id}"
  524. info = enrich_map.get(key, {})
  525. enriched.append(
  526. _item_to_dict_enriched(
  527. i,
  528. protocol_no=info.get("protocolNo", ""),
  529. sample_type_name=info.get("sampleTypeName", ""),
  530. mother_name=info.get("motherName", ""),
  531. newborn_name=info.get("newbornName", ""),
  532. )
  533. )
  534. data["items"] = enriched
  535. return data
  536. def list_sample_options(
  537. db: Session,
  538. *,
  539. tenant_no: str,
  540. page: int = 1,
  541. page_size: int = 20,
  542. keyword: Optional[str] = None,
  543. sample_source: Optional[str] = None,
  544. user_info: Optional[dict] = None,
  545. ) -> Tuple[List[Dict[str, Any]], int]:
  546. """可选样本列表(配置运费弹窗),支持细胞存储 / 科研捐赠。"""
  547. page = max(1, int(page or 1))
  548. page_size = max(1, min(100, int(page_size or 20)))
  549. src = (sample_source or "").strip() or None
  550. if src and src not in (SAMPLE_SOURCE_CELL, SAMPLE_SOURCE_DONATION):
  551. raise ValueError("sampleSource 无效")
  552. kw = (keyword or "").strip()
  553. like = f"%{kw}%" if kw else None
  554. rows: List[Dict[str, Any]] = []
  555. if src in (None, SAMPLE_SOURCE_CELL):
  556. cq = (
  557. db.query(
  558. BizProtocolSample.id,
  559. BizProtocolSample.sample_code,
  560. BizProtocolSample.collection_protocol_id,
  561. BizProtocolSample.create_time,
  562. BizCollectionProtocol.protocol_no,
  563. BizCellSampleType.type_name,
  564. BizProtocolPartyCustomer.mother_name,
  565. BizProtocolPartyCustomer.newborn_name,
  566. )
  567. .outerjoin(
  568. BizCollectionProtocol,
  569. and_(
  570. BizCollectionProtocol.id
  571. == BizProtocolSample.collection_protocol_id,
  572. BizCollectionProtocol.del_flag == 0,
  573. ),
  574. )
  575. .outerjoin(
  576. BizCellSampleType,
  577. and_(
  578. BizCellSampleType.id == BizProtocolSample.sample_type_id,
  579. BizCellSampleType.del_flag == 0,
  580. ),
  581. )
  582. .outerjoin(
  583. BizProtocolPartyCustomer,
  584. and_(
  585. BizProtocolPartyCustomer.collection_protocol_id
  586. == BizProtocolSample.collection_protocol_id,
  587. BizProtocolPartyCustomer.del_flag == 0,
  588. ),
  589. )
  590. .filter(
  591. BizProtocolSample.tenant_no == tenant_no,
  592. BizProtocolSample.del_flag == 0,
  593. )
  594. )
  595. cq = cq.filter(
  596. BizCollectionProtocol.tenant_no == tenant_no,
  597. BizCollectionProtocol.del_flag == 0,
  598. )
  599. cq = apply_data_scope(cq, BizCollectionProtocol, user_info, db, tenant_no)
  600. if like:
  601. cq = cq.filter(
  602. or_(
  603. BizProtocolSample.sample_code.like(like),
  604. BizProtocolPartyCustomer.mother_name.like(like),
  605. BizProtocolPartyCustomer.newborn_name.like(like),
  606. )
  607. )
  608. for sid, scode, pid, ctime, pno, tname, mname, nname in cq.all():
  609. rows.append(
  610. {
  611. "sampleSource": SAMPLE_SOURCE_CELL,
  612. "sampleId": sid,
  613. "sampleCode": scode or "",
  614. "protocolId": pid or "",
  615. "protocolNo": pno or "",
  616. "sampleTypeName": tname or "",
  617. "motherName": mname or "",
  618. "newbornName": nname or "",
  619. "createTime": ctime,
  620. "freightId": "",
  621. }
  622. )
  623. if src in (None, SAMPLE_SOURCE_DONATION):
  624. dq = (
  625. db.query(
  626. BizResearchDonationSample.id,
  627. BizResearchDonationSample.sample_code,
  628. BizResearchDonationSample.research_donation_id,
  629. BizResearchDonationSample.create_time,
  630. BizResearchDonation.donation_no,
  631. BizCellSampleType.type_name,
  632. BizResearchDonationParty.mother_name,
  633. BizResearchDonationParty.newborn_name,
  634. )
  635. .outerjoin(
  636. BizResearchDonation,
  637. and_(
  638. BizResearchDonation.id
  639. == BizResearchDonationSample.research_donation_id,
  640. BizResearchDonation.del_flag == 0,
  641. ),
  642. )
  643. .outerjoin(
  644. BizCellSampleType,
  645. and_(
  646. BizCellSampleType.id == BizResearchDonationSample.sample_type_id,
  647. BizCellSampleType.del_flag == 0,
  648. ),
  649. )
  650. .outerjoin(
  651. BizResearchDonationParty,
  652. and_(
  653. BizResearchDonationParty.research_donation_id
  654. == BizResearchDonationSample.research_donation_id,
  655. BizResearchDonationParty.del_flag == 0,
  656. ),
  657. )
  658. .filter(
  659. BizResearchDonationSample.tenant_no == tenant_no,
  660. BizResearchDonationSample.del_flag == 0,
  661. )
  662. )
  663. dq = dq.filter(
  664. BizResearchDonation.tenant_no == tenant_no,
  665. BizResearchDonation.del_flag == 0,
  666. )
  667. dq = apply_data_scope(dq, BizResearchDonation, user_info, db, tenant_no)
  668. if like:
  669. dq = dq.filter(
  670. or_(
  671. BizResearchDonationSample.sample_code.like(like),
  672. BizResearchDonationParty.mother_name.like(like),
  673. BizResearchDonationParty.newborn_name.like(like),
  674. )
  675. )
  676. for sid, scode, pid, ctime, dno, tname, mname, nname in dq.all():
  677. rows.append(
  678. {
  679. "sampleSource": SAMPLE_SOURCE_DONATION,
  680. "sampleId": sid,
  681. "sampleCode": scode or "",
  682. "protocolId": pid or "",
  683. "protocolNo": dno or "",
  684. "sampleTypeName": tname or "",
  685. "motherName": mname or "",
  686. "newbornName": nname or "",
  687. "createTime": ctime,
  688. "freightId": "",
  689. }
  690. )
  691. # 左连当前运费关联
  692. if rows:
  693. sample_ids = [r["sampleId"] for r in rows]
  694. link_rows = (
  695. db.query(
  696. BizSampleFreightItem.sample_source,
  697. BizSampleFreightItem.sample_id,
  698. BizSampleFreightItem.freight_id,
  699. )
  700. .filter(
  701. BizSampleFreightItem.tenant_no == tenant_no,
  702. BizSampleFreightItem.del_flag == 0,
  703. BizSampleFreightItem.sample_id.in_(sample_ids),
  704. )
  705. .all()
  706. )
  707. freight_map = {
  708. f"{src_}|{sid}": fid for src_, sid, fid in link_rows if src_ and sid and fid
  709. }
  710. for r in rows:
  711. r["freightId"] = freight_map.get(f"{r['sampleSource']}|{r['sampleId']}", "") or ""
  712. rows.sort(
  713. key=lambda r: (r["createTime"] or datetime.min, r["sampleId"]),
  714. reverse=True,
  715. )
  716. total = len(rows)
  717. start = (page - 1) * page_size
  718. page_rows = rows[start : start + page_size]
  719. for r in page_rows:
  720. r["createTime"] = _fmt_dt(r.get("createTime"))
  721. return page_rows, total
  722. def _upsert_item(
  723. db: Session,
  724. *,
  725. freight_id: str,
  726. tenant_no: str,
  727. user_id: Optional[str],
  728. item: Dict[str, Any],
  729. ) -> None:
  730. norm = _normalize_item(item)
  731. existing = (
  732. db.query(BizSampleFreightItem)
  733. .filter(
  734. BizSampleFreightItem.tenant_no == tenant_no,
  735. BizSampleFreightItem.sample_source == norm["sample_source"],
  736. BizSampleFreightItem.sample_id == norm["sample_id"],
  737. )
  738. .order_by(BizSampleFreightItem.del_flag.asc(), BizSampleFreightItem.create_time.desc())
  739. .first()
  740. )
  741. now = _now()
  742. if existing:
  743. existing.freight_id = freight_id
  744. existing.protocol_id = norm["protocol_id"] or existing.protocol_id
  745. existing.sample_code = norm["sample_code"] or existing.sample_code
  746. existing.del_flag = 0
  747. existing.update_by = user_id
  748. existing.update_time = now
  749. db.add(existing)
  750. return
  751. db.add(
  752. BizSampleFreightItem(
  753. id=generate_uuid(),
  754. freight_id=freight_id,
  755. sample_source=norm["sample_source"],
  756. sample_id=norm["sample_id"],
  757. sample_code=norm["sample_code"],
  758. protocol_id=norm["protocol_id"],
  759. tenant_no=tenant_no,
  760. order_num=0,
  761. remark=None,
  762. create_by=user_id,
  763. create_dept_id="",
  764. create_time=now,
  765. update_by=user_id,
  766. update_time=now,
  767. del_flag=0,
  768. )
  769. )
  770. def create_freight(
  771. db: Session,
  772. *,
  773. tenant_no: str,
  774. user_id: Optional[str],
  775. transport_mode_raw: str,
  776. freight_amount: Any,
  777. items: Optional[List[Dict[str, Any]]] = None,
  778. commit: bool = True,
  779. user_info: Optional[dict] = None,
  780. waybill_no: Any = None,
  781. waybill_supplied: bool = True,
  782. amount_supplied: bool = True,
  783. ) -> Dict[str, Any]:
  784. if user_info is not None:
  785. items = _validated_items(db, items or [], tenant_no, user_info)
  786. mode = resolve_transport_mode_value(
  787. db, tenant_no=tenant_no, raw=transport_mode_raw, user_id=user_id
  788. )
  789. plan = resolve_header_for_save(
  790. transport_mode=mode,
  791. existing_waybill=None,
  792. existing_amount=None,
  793. existing_settle_status=None,
  794. waybill_supplied=waybill_supplied,
  795. waybill_no=waybill_no,
  796. amount_supplied=amount_supplied,
  797. freight_amount=freight_amount,
  798. )
  799. now = _now()
  800. obj = BizSampleFreight(
  801. id=generate_uuid(),
  802. freight_no=_generate_freight_no(db, tenant_no, when=now),
  803. transport_mode=plan.transport_mode,
  804. freight_amount=plan.freight_amount,
  805. waybill_no=plan.waybill_no,
  806. settle_status=plan.settle_status,
  807. settle_time=None,
  808. tenant_no=tenant_no,
  809. order_num=0,
  810. remark=None,
  811. create_by=user_id,
  812. create_dept_id=(get_user_dept_id(db, user_id) or "") if user_info is not None else "",
  813. create_time=now,
  814. update_by=user_id,
  815. update_time=now,
  816. del_flag=0,
  817. )
  818. db.add(obj)
  819. db.flush()
  820. for it in items or []:
  821. _upsert_item(db, freight_id=obj.id, tenant_no=tenant_no, user_id=user_id, item=it)
  822. if commit:
  823. db.commit()
  824. else:
  825. db.flush()
  826. return get_freight_detail(db, obj.id, tenant_no)
  827. def update_freight(
  828. db: Session,
  829. freight_id: str,
  830. *,
  831. tenant_no: str,
  832. user_id: Optional[str],
  833. transport_mode_raw: Optional[str] = None,
  834. freight_amount: Any = None,
  835. commit: bool = True,
  836. user_info: Optional[dict] = None,
  837. waybill_no: Any = None,
  838. waybill_supplied: bool = False,
  839. amount_supplied: bool = False,
  840. ) -> Dict[str, Any]:
  841. obj = get_freight(db, freight_id, tenant_no, user_info)
  842. if not obj:
  843. raise ValueError("运费批次不存在")
  844. mode = obj.transport_mode or ""
  845. if transport_mode_raw is not None and str(transport_mode_raw).strip() != "":
  846. mode = resolve_transport_mode_value(
  847. db, tenant_no=tenant_no, raw=str(transport_mode_raw), user_id=user_id
  848. )
  849. plan = resolve_header_for_save(
  850. transport_mode=mode,
  851. existing_waybill=obj.waybill_no,
  852. existing_amount=obj.freight_amount,
  853. existing_settle_status=obj.settle_status,
  854. waybill_supplied=waybill_supplied,
  855. waybill_no=waybill_no,
  856. amount_supplied=amount_supplied,
  857. freight_amount=freight_amount,
  858. )
  859. obj.transport_mode = plan.transport_mode
  860. obj.waybill_no = plan.waybill_no
  861. obj.freight_amount = plan.freight_amount
  862. obj.settle_status = plan.settle_status
  863. if plan.clear_settle_time:
  864. obj.settle_time = None
  865. obj.update_by = user_id
  866. obj.update_time = _now()
  867. db.add(obj)
  868. if commit:
  869. db.commit()
  870. else:
  871. db.flush()
  872. return get_freight_detail(db, freight_id, tenant_no)
  873. def link_samples(
  874. db: Session,
  875. freight_id: str,
  876. *,
  877. tenant_no: str,
  878. user_id: Optional[str],
  879. items: List[Dict[str, Any]],
  880. commit: bool = True,
  881. user_info: Optional[dict] = None,
  882. ) -> Dict[str, Any]:
  883. header = get_freight(db, freight_id, tenant_no, user_info)
  884. if not header:
  885. raise ValueError("运费批次不存在")
  886. assert_freight_mutable(header.settle_status, "items")
  887. if not items:
  888. raise ValueError("请选择要关联的样本")
  889. if user_info is not None:
  890. items = _validated_items(db, items, tenant_no, user_info)
  891. for it in items:
  892. _upsert_item(db, freight_id=freight_id, tenant_no=tenant_no, user_id=user_id, item=it)
  893. if commit:
  894. db.commit()
  895. else:
  896. db.flush()
  897. return get_freight_detail(db, freight_id, tenant_no)
  898. def unlink_samples(
  899. db: Session,
  900. freight_id: str,
  901. *,
  902. tenant_no: str,
  903. user_id: Optional[str],
  904. items: List[Dict[str, Any]],
  905. commit: bool = True,
  906. user_info: Optional[dict] = None,
  907. ) -> Dict[str, Any]:
  908. header = get_freight(db, freight_id, tenant_no, user_info)
  909. if not header:
  910. raise ValueError("运费批次不存在")
  911. assert_freight_mutable(header.settle_status, "items")
  912. obj = header
  913. now = _now()
  914. for it in items or []:
  915. norm = _normalize_item(it)
  916. row = (
  917. db.query(BizSampleFreightItem)
  918. .filter(
  919. BizSampleFreightItem.freight_id == freight_id,
  920. BizSampleFreightItem.tenant_no == tenant_no,
  921. BizSampleFreightItem.sample_source == norm["sample_source"],
  922. BizSampleFreightItem.sample_id == norm["sample_id"],
  923. BizSampleFreightItem.del_flag == 0,
  924. )
  925. .first()
  926. )
  927. if not row:
  928. continue
  929. row.del_flag = 1
  930. row.update_by = user_id
  931. row.update_time = now
  932. db.add(row)
  933. db.flush()
  934. remaining = _linked_count(db, freight_id, tenant_no)
  935. if remaining == 0:
  936. obj.del_flag = 1
  937. obj.update_by = user_id
  938. obj.update_time = now
  939. db.add(obj)
  940. if commit:
  941. db.commit()
  942. else:
  943. db.flush()
  944. if remaining == 0:
  945. return {
  946. "id": freight_id,
  947. "deleted": True,
  948. "linkedSampleCount": 0,
  949. }
  950. return get_freight_detail(db, freight_id, tenant_no)
  951. def delete_freight(
  952. db: Session,
  953. freight_id: str,
  954. *,
  955. tenant_no: str,
  956. user_id: Optional[str],
  957. commit: bool = True,
  958. user_info: Optional[dict] = None,
  959. ) -> Dict[str, Any]:
  960. """级联软删运费批次及其全部有效关联样本。"""
  961. header = get_freight(db, freight_id, tenant_no, user_info)
  962. if not header:
  963. raise ValueError("运费批次不存在")
  964. assert_freight_mutable(header.settle_status, "delete")
  965. obj = (
  966. db.query(BizSampleFreight)
  967. .filter(
  968. BizSampleFreight.id == freight_id,
  969. BizSampleFreight.tenant_no == tenant_no,
  970. BizSampleFreight.del_flag == 0,
  971. )
  972. .with_for_update()
  973. .first()
  974. )
  975. if not obj:
  976. raise ValueError("运费批次不存在")
  977. now = _now()
  978. items = (
  979. db.query(BizSampleFreightItem)
  980. .filter(
  981. BizSampleFreightItem.freight_id == freight_id,
  982. BizSampleFreightItem.tenant_no == tenant_no,
  983. BizSampleFreightItem.del_flag == 0,
  984. )
  985. .with_for_update()
  986. .all()
  987. )
  988. for row in items:
  989. row.del_flag = 1
  990. row.update_by = user_id
  991. row.update_time = now
  992. db.add(row)
  993. obj.del_flag = 1
  994. obj.update_by = user_id
  995. obj.update_time = now
  996. db.add(obj)
  997. if commit:
  998. db.commit()
  999. else:
  1000. db.flush()
  1001. return {
  1002. "id": freight_id,
  1003. "deleted": True,
  1004. "linkedSampleCount": 0,
  1005. }
  1006. def freight_amount_payload(
  1007. amount: Any, transport_mode: Optional[str], money_as_string: bool
  1008. ) -> Dict[str, Any]:
  1009. if amount is None:
  1010. freight_amount: Any = None
  1011. elif money_as_string:
  1012. freight_amount = format(Decimal(str(amount)).quantize(Decimal("0.01")), "f")
  1013. else:
  1014. freight_amount = float(amount)
  1015. return {
  1016. "freightAmount": freight_amount,
  1017. "freightAmountLabel": freight_amount_label(transport_mode, amount),
  1018. }
  1019. def freight_map_for_samples(
  1020. db: Session,
  1021. *,
  1022. tenant_no: str,
  1023. sample_source: str,
  1024. sample_ids: List[str],
  1025. money_as_string: bool = False,
  1026. ) -> Dict[str, Dict[str, Any]]:
  1027. ids = [str(x).strip() for x in sample_ids if str(x or "").strip()]
  1028. if not ids:
  1029. return {}
  1030. rows = (
  1031. db.query(BizSampleFreightItem, BizSampleFreight)
  1032. .join(
  1033. BizSampleFreight,
  1034. and_(
  1035. BizSampleFreight.id == BizSampleFreightItem.freight_id,
  1036. BizSampleFreight.tenant_no == tenant_no,
  1037. BizSampleFreight.del_flag == 0,
  1038. ),
  1039. )
  1040. .filter(
  1041. BizSampleFreightItem.tenant_no == tenant_no,
  1042. BizSampleFreightItem.sample_source == sample_source,
  1043. BizSampleFreightItem.sample_id.in_(ids),
  1044. BizSampleFreightItem.del_flag == 0,
  1045. )
  1046. .all()
  1047. )
  1048. if not rows:
  1049. return {}
  1050. freight_ids = list({f.id for _, f in rows})
  1051. count_rows = (
  1052. db.query(BizSampleFreightItem.freight_id, func.count(BizSampleFreightItem.id))
  1053. .filter(
  1054. BizSampleFreightItem.freight_id.in_(freight_ids),
  1055. BizSampleFreightItem.tenant_no == tenant_no,
  1056. BizSampleFreightItem.del_flag == 0,
  1057. )
  1058. .group_by(BizSampleFreightItem.freight_id)
  1059. .all()
  1060. )
  1061. count_map = {fid: int(c) for fid, c in count_rows}
  1062. labels = _label_map(db, tenant_no, [f.transport_mode for _, f in rows])
  1063. out: Dict[str, Dict[str, Any]] = {}
  1064. for item, freight in rows:
  1065. out[item.sample_id] = {
  1066. "id": freight.id,
  1067. "transportMode": freight.transport_mode or "",
  1068. "transportModeLabel": labels.get(
  1069. freight.transport_mode or "", freight.transport_mode or ""
  1070. ),
  1071. **freight_amount_payload(
  1072. freight.freight_amount, freight.transport_mode, money_as_string
  1073. ),
  1074. "waybillNo": getattr(freight, "waybill_no", None) or "",
  1075. "settleStatus": getattr(freight, "settle_status", None) or "",
  1076. "linkedSampleCount": count_map.get(freight.id, 0),
  1077. }
  1078. return out
  1079. def apply_freight_on_protocol_save(
  1080. db: Session,
  1081. *,
  1082. tenant_no: str,
  1083. user_id: Optional[str],
  1084. sample_source: str,
  1085. protocol_id: str,
  1086. saved_samples: list,
  1087. input_samples: list,
  1088. replaced_sample_ids: Optional[List[str]] = None,
  1089. ) -> None:
  1090. """协议保存挂钩:按 freightId / freightDraft 关联运费(flush,不单独 commit)。"""
  1091. old_ids = [
  1092. str(sample_id).strip()
  1093. for sample_id in (replaced_sample_ids or [])
  1094. if str(sample_id or "").strip()
  1095. ]
  1096. stale_filters = [BizSampleFreightItem.protocol_id == protocol_id]
  1097. if old_ids:
  1098. stale_filters.append(BizSampleFreightItem.sample_id.in_(old_ids))
  1099. stale_items = (
  1100. db.query(BizSampleFreightItem)
  1101. .filter(
  1102. BizSampleFreightItem.tenant_no == tenant_no,
  1103. BizSampleFreightItem.sample_source == sample_source,
  1104. BizSampleFreightItem.del_flag == 0,
  1105. or_(*stale_filters),
  1106. )
  1107. .order_by(BizSampleFreightItem.id.asc())
  1108. .with_for_update()
  1109. .all()
  1110. )
  1111. now = _now()
  1112. affected_freight_ids = {item.freight_id for item in stale_items}
  1113. for stale in stale_items:
  1114. stale.del_flag = 1
  1115. stale.update_by = user_id
  1116. stale.update_time = now
  1117. db.add(stale)
  1118. if stale_items:
  1119. db.flush()
  1120. draft_groups: Dict[str, Dict[str, Any]] = {}
  1121. for idx, sample_obj in enumerate(saved_samples or []):
  1122. if idx >= len(input_samples or []):
  1123. break
  1124. raw = input_samples[idx] if isinstance(input_samples[idx], dict) else {}
  1125. sample_id = getattr(sample_obj, "id", None) or ""
  1126. sample_code = (
  1127. getattr(sample_obj, "sample_code", None)
  1128. or raw.get("sampleCode")
  1129. or raw.get("sample_code")
  1130. or ""
  1131. )
  1132. if not sample_id:
  1133. continue
  1134. item = {
  1135. "sampleSource": sample_source,
  1136. "sampleId": sample_id,
  1137. "protocolId": protocol_id,
  1138. "sampleCode": sample_code,
  1139. }
  1140. freight_id = (
  1141. str(raw.get("freightId") or raw.get("freight_id") or "").strip()
  1142. or str((raw.get("freight") or {}).get("id") or "").strip()
  1143. )
  1144. draft = raw.get("freightDraft")
  1145. if draft is None:
  1146. draft = raw.get("freight_draft")
  1147. if freight_id:
  1148. link_samples(
  1149. db,
  1150. freight_id,
  1151. tenant_no=tenant_no,
  1152. user_id=user_id,
  1153. items=[item],
  1154. commit=False,
  1155. )
  1156. continue
  1157. if isinstance(draft, dict):
  1158. client_key = str(draft.get("clientKey") or "").strip()
  1159. if not client_key:
  1160. client_key = f"__idx_{idx}"
  1161. group = draft_groups.get(client_key)
  1162. if not group:
  1163. draft_groups[client_key] = {
  1164. "transportMode": draft.get("transportMode") or draft.get("transport_mode"),
  1165. "freightAmount": draft.get("freightAmount")
  1166. if draft.get("freightAmount") is not None
  1167. else draft.get("freight_amount"),
  1168. "waybillNo": draft.get("waybillNo")
  1169. if draft.get("waybillNo") is not None
  1170. else draft.get("waybill_no"),
  1171. "items": [item],
  1172. }
  1173. else:
  1174. group["items"].append(item)
  1175. for group in draft_groups.values():
  1176. create_freight(
  1177. db,
  1178. tenant_no=tenant_no,
  1179. user_id=user_id,
  1180. transport_mode_raw=str(group.get("transportMode") or ""),
  1181. freight_amount=group.get("freightAmount"),
  1182. waybill_no=group.get("waybillNo"),
  1183. waybill_supplied=True,
  1184. amount_supplied=True,
  1185. items=group["items"],
  1186. commit=False,
  1187. )
  1188. for freight_id in sorted(affected_freight_ids):
  1189. if _linked_count(db, freight_id, tenant_no) != 0:
  1190. continue
  1191. freight = (
  1192. db.query(BizSampleFreight)
  1193. .filter(
  1194. BizSampleFreight.id == freight_id,
  1195. BizSampleFreight.tenant_no == tenant_no,
  1196. BizSampleFreight.del_flag == 0,
  1197. )
  1198. .with_for_update()
  1199. .first()
  1200. )
  1201. if freight is not None:
  1202. freight.del_flag = 1
  1203. freight.update_by = user_id
  1204. freight.update_time = now
  1205. db.add(freight)
  1206. db.flush()