sample_reception.py 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558
  1. """样本接收 CRUD(质量检测)"""
  2. from __future__ import annotations
  3. from datetime import datetime
  4. from typing import Any, Dict, List, Optional, Tuple
  5. from sqlalchemy import and_, func, or_
  6. from sqlalchemy.orm import Session
  7. from app.core.utils import generate_uuid
  8. from app.crud.collection_protocol import urls_to_attachment_list
  9. from app.utils.sample_special import sample_special_to_dict
  10. from app.crud import sample_reception_check_result as check_result_svc
  11. from app.crud import sample_production_reception as prod_reception_svc
  12. from app.models.models import (
  13. BizCellSampleType,
  14. BizCollectionProtocol,
  15. BizExpressCompany,
  16. BizProtocolCollectionInfo,
  17. BizProtocolPartyCustomer,
  18. BizProtocolSample,
  19. BizProtocolSampleTransport,
  20. BizSampleReception,
  21. SysUser,
  22. )
  23. from app.models.research_donation import (
  24. BizResearchDonation,
  25. BizResearchDonationCollection,
  26. BizResearchDonationParty,
  27. BizResearchDonationSample,
  28. BizResearchDonationSampleTransport,
  29. )
  30. from app.core.protocol_status_display import INACTIVE_PROTOCOL_STATUSES
  31. from app.services.web_auth_roles import get_user_role_names
  32. SAMPLE_RECEPTION_EDIT_ROLE = "质量主管"
  33. # 科研捐赠表默认 general_ci,样本接收等为 unicode_ci,跨表比较需统一
  34. _UNICODE_CI = "utf8mb4_unicode_ci"
  35. def _donation_sample_id_col(col):
  36. """捐赠样本 ID 与 biz_sample_reception.donation_sample_id 比较时统一排序规则。"""
  37. return col.collate(_UNICODE_CI)
  38. def _fmt_dt(t: Optional[datetime]) -> str:
  39. return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
  40. def _sort_pending_list_items(items: List[Dict[str, Any]]) -> None:
  41. """待接收:etaTime 升序(空值最后),同 ETA 时 sampleCode 倒序。"""
  42. items.sort(key=lambda x: (x.get("sampleCode") or ""), reverse=True)
  43. items.sort(key=lambda x: (not x.get("etaTime"), x.get("etaTime") or ""))
  44. def _sort_received_list_items(items: List[Dict[str, Any]]) -> None:
  45. """已接收:receptionTime 倒序,同接收时间时 sampleCode 倒序。"""
  46. items.sort(key=lambda x: (x.get("sampleCode") or ""), reverse=True)
  47. items.sort(key=lambda x: (x.get("receptionTime") or ""), reverse=True)
  48. def _normalize_date_bound(value: str, *, end_of_day: bool = False) -> str:
  49. val = (value or "").strip()
  50. if len(val) == 10:
  51. return f"{val} 23:59:59" if end_of_day else f"{val} 00:00:00"
  52. return val
  53. def _dec(v: Any) -> Optional[float]:
  54. if v is None:
  55. return None
  56. return float(v)
  57. def _reception_to_dict(
  58. r: BizSampleReception,
  59. *,
  60. check_results: Optional[List[Dict[str, Any]]] = None,
  61. ) -> Dict[str, Any]:
  62. return {
  63. "id": r.id,
  64. "protocolSampleId": r.protocol_sample_id,
  65. "collectionProtocolId": r.collection_protocol_id,
  66. "receptionTime": _fmt_dt(r.reception_time),
  67. "handlerUserId": r.handler_user_id or "",
  68. "handlerName": r.handler_name or "",
  69. "receiveTemperature": r.receive_temperature or "",
  70. "checkResults": check_results or [],
  71. "acceptConclusion": r.accept_conclusion or "",
  72. "labStorageCode": r.lab_storage_code or "",
  73. "receiveAttachments": r.receive_attachments or "",
  74. "remark": r.remark or "",
  75. "createTime": _fmt_dt(r.create_time),
  76. "updateTime": _fmt_dt(r.update_time),
  77. }
  78. def _sample_detail_rows_query(db: Session):
  79. """协议样本 + 协议单 + 客户 + 运输(显式 select_from 避免多表 join 歧义)。"""
  80. return (
  81. db.query(
  82. BizProtocolSample,
  83. BizCollectionProtocol,
  84. BizProtocolPartyCustomer,
  85. BizProtocolSampleTransport,
  86. )
  87. .select_from(BizProtocolSample)
  88. .join(
  89. BizCollectionProtocol,
  90. and_(
  91. BizCollectionProtocol.id == BizProtocolSample.collection_protocol_id,
  92. BizCollectionProtocol.del_flag == 0,
  93. ),
  94. )
  95. .outerjoin(
  96. BizProtocolPartyCustomer,
  97. and_(
  98. BizProtocolPartyCustomer.collection_protocol_id == BizCollectionProtocol.id,
  99. BizProtocolPartyCustomer.del_flag == 0,
  100. ),
  101. )
  102. .outerjoin(
  103. BizProtocolSampleTransport,
  104. and_(
  105. BizProtocolSampleTransport.sample_id == BizProtocolSample.id,
  106. BizProtocolSampleTransport.del_flag == 0,
  107. ),
  108. )
  109. )
  110. def _donation_sample_detail_rows_query(db: Session):
  111. """捐赠样本 + 捐赠单 + 当事人 + 运输(四表 JOIN)。"""
  112. return (
  113. db.query(
  114. BizResearchDonationSample,
  115. BizResearchDonation,
  116. BizResearchDonationParty,
  117. BizResearchDonationSampleTransport,
  118. )
  119. .select_from(BizResearchDonationSample)
  120. .join(
  121. BizResearchDonation,
  122. and_(
  123. BizResearchDonation.id == BizResearchDonationSample.research_donation_id,
  124. BizResearchDonation.del_flag == 0,
  125. ),
  126. )
  127. .outerjoin(
  128. BizResearchDonationParty,
  129. and_(
  130. BizResearchDonationParty.research_donation_id == BizResearchDonation.id,
  131. BizResearchDonationParty.del_flag == 0,
  132. ),
  133. )
  134. .outerjoin(
  135. BizResearchDonationSampleTransport,
  136. and_(
  137. BizResearchDonationSampleTransport.sample_id == BizResearchDonationSample.id,
  138. BizResearchDonationSampleTransport.del_flag == 0,
  139. ),
  140. )
  141. )
  142. def _donation_sample_pending_query(db: Session, tenant_no: str, keyword: Optional[str] = None):
  143. """待接收:捐赠样本且尚无有效接收记录(按租户 + 样本编码判定,与细胞存储一致);排除已作废捐赠单。"""
  144. rec = BizSampleReception
  145. received_exists = (
  146. db.query(rec.id)
  147. .filter(
  148. rec.tenant_no == tenant_no,
  149. rec.del_flag == 0,
  150. rec.reception_source == "research_donation",
  151. rec.sample_code.isnot(None),
  152. rec.sample_code != "",
  153. rec.sample_code == BizResearchDonationSample.sample_code,
  154. )
  155. .exists()
  156. )
  157. q = _donation_sample_detail_rows_query(db).filter(
  158. BizResearchDonationSample.del_flag == 0,
  159. BizResearchDonationSample.tenant_no == tenant_no,
  160. BizResearchDonation.tenant_no == tenant_no,
  161. BizResearchDonation.donation_status.notin_(INACTIVE_PROTOCOL_STATUSES),
  162. ~received_exists,
  163. )
  164. if keyword and keyword.strip():
  165. kw = f"%{keyword.strip()}%"
  166. kw_conds = [
  167. BizResearchDonationSample.sample_code.like(kw),
  168. BizResearchDonationParty.mother_name.like(kw),
  169. BizResearchDonationParty.newborn_name.like(kw),
  170. ]
  171. lab_codes = sample_codes_matching_lab_storage_keyword(
  172. db, tenant_no, keyword, reception_source="research_donation"
  173. )
  174. if lab_codes:
  175. kw_conds.append(BizResearchDonationSample.sample_code.in_(lab_codes))
  176. q = q.filter(or_(*kw_conds))
  177. return q
  178. def _donation_rows_to_sample_list_items(
  179. db: Session,
  180. rows: list,
  181. tenant_no: str,
  182. *,
  183. reception_map: Optional[Dict[str, BizSampleReception]] = None,
  184. ) -> List[Dict[str, Any]]:
  185. """将捐赠样本行转为统一列表项字典(字段名与细胞存储保持一致)。"""
  186. type_ids = list({s.sample_type_id for s, *_ in rows if s.sample_type_id})
  187. type_map: Dict[str, str] = {}
  188. if type_ids:
  189. for t in (
  190. db.query(BizCellSampleType)
  191. .filter(
  192. BizCellSampleType.id.in_(type_ids),
  193. BizCellSampleType.del_flag == 0,
  194. )
  195. .all()
  196. ):
  197. type_map[t.id] = t.type_name or ""
  198. express_ids = list(
  199. {tr.express_company_id for *_, tr in rows if tr and tr.express_company_id}
  200. )
  201. express_map: Dict[str, str] = {}
  202. if express_ids:
  203. for e in (
  204. db.query(BizExpressCompany)
  205. .filter(BizExpressCompany.id.in_(express_ids), BizExpressCompany.del_flag == 0)
  206. .all()
  207. ):
  208. express_map[e.id] = e.express_name or ""
  209. donation_ids = list({donation.id for _, donation, _, _ in rows if donation and donation.id})
  210. collect_info_map: Dict[str, Dict[str, str]] = {}
  211. if donation_ids:
  212. for coll in (
  213. db.query(BizResearchDonationCollection)
  214. .filter(
  215. BizResearchDonationCollection.research_donation_id.in_(donation_ids),
  216. BizResearchDonationCollection.del_flag == 0,
  217. )
  218. .all()
  219. ):
  220. collect_info_map[coll.research_donation_id] = {
  221. "sampleCollectTime": _fmt_dt(coll.sample_collect_time),
  222. "maternalBloodCollectTime": _fmt_dt(coll.maternal_blood_collect_time),
  223. }
  224. reception_ids = (
  225. [reception_map[s.id].id for s, *_ in rows if s.id in reception_map]
  226. if reception_map
  227. else []
  228. )
  229. results_map = (
  230. check_result_svc.list_results_by_reception_ids(db, reception_ids, tenant_no)
  231. if reception_ids
  232. else {}
  233. )
  234. items: List[Dict[str, Any]] = []
  235. for sample, donation, party, transport in rows:
  236. item: Dict[str, Any] = {
  237. "protocolSampleId": sample.id,
  238. "collectionProtocolId": donation.id,
  239. "protocolNo": donation.donation_no or "",
  240. "protocolStatus": donation.donation_status or "",
  241. "sampleCode": sample.sample_code or "",
  242. "sampleTypeId": sample.sample_type_id or "",
  243. "sampleTypeName": type_map.get(sample.sample_type_id or "", ""),
  244. "collectAmount": _dec(sample.collect_amount),
  245. "motherName": (party.mother_name if party else "") or "",
  246. "newbornName": (party.newborn_name if party else "") or "",
  247. "sampleCollectTime": (
  248. collect_info_map.get(donation.id, {}).get("sampleCollectTime", "")
  249. if donation
  250. else ""
  251. ),
  252. "maternalBloodCollectTime": (
  253. collect_info_map.get(donation.id, {}).get("maternalBloodCollectTime", "")
  254. if donation
  255. else ""
  256. ),
  257. "sourceType": "research_donation",
  258. "sourceTypeLabel": "科研捐赠",
  259. "infectiousDiseaseReportAttachmentFiles": urls_to_attachment_list(
  260. donation.infectious_disease_report_urls if donation else None, db, tenant_no
  261. ),
  262. "etaTime": _fmt_dt(transport.eta_time) if transport and transport.eta_time else "",
  263. "transportType": (transport.transport_type if transport else "") or "",
  264. "waybillNo": (transport.waybill_no if transport else "") or "",
  265. "expressCompanyId": (transport.express_company_id if transport else "") or "",
  266. "expressCompanyName": express_map.get(
  267. (transport.express_company_id if transport else "") or "", ""
  268. ),
  269. "carrierName": (transport.carrier_name if transport else "") or "",
  270. "carrierPhone": (transport.carrier_phone if transport else "") or "",
  271. **sample_special_to_dict(sample),
  272. }
  273. if reception_map and sample.id in reception_map:
  274. rec = reception_map[sample.id]
  275. cr = results_map.get(rec.id)
  276. if not cr:
  277. cr = check_result_svc.get_check_results_for_reception(db, rec, tenant_no)
  278. item.update(_reception_to_dict(rec, check_results=cr))
  279. item["receiveAttachmentFiles"] = urls_to_attachment_list(
  280. rec.receive_attachments, db, tenant_no
  281. )
  282. items.append(item)
  283. rec_ids = [str(item.get("id") or "") for item in items if item.get("id")]
  284. if rec_ids:
  285. production_map = prod_reception_svc.get_map_by_reception_ids(db, rec_ids, tenant_no)
  286. for item in items:
  287. rid = str(item.get("id") or "")
  288. item["productionReception"] = production_map.get(rid) if rid else None
  289. return items
  290. def _sample_pending_query(db: Session, tenant_no: str, keyword: Optional[str] = None):
  291. """待接收:协议样本且尚无有效接收记录(仅按租户 + 样本编码判定);排除已作废采集协议单。"""
  292. rec = BizSampleReception
  293. received_exists = (
  294. db.query(rec.id)
  295. .filter(
  296. rec.tenant_no == tenant_no,
  297. rec.del_flag == 0,
  298. rec.reception_source == "cell_storage",
  299. rec.sample_code.isnot(None),
  300. rec.sample_code != "",
  301. rec.sample_code == BizProtocolSample.sample_code,
  302. )
  303. .exists()
  304. )
  305. q = _sample_detail_rows_query(db).filter(
  306. BizProtocolSample.del_flag == 0,
  307. BizProtocolSample.tenant_no == tenant_no,
  308. BizCollectionProtocol.tenant_no == tenant_no,
  309. BizCollectionProtocol.protocol_status.notin_(INACTIVE_PROTOCOL_STATUSES),
  310. ~received_exists,
  311. )
  312. if keyword and keyword.strip():
  313. kw = f"%{keyword.strip()}%"
  314. kw_conds = [
  315. BizProtocolSample.sample_code.like(kw),
  316. BizProtocolPartyCustomer.mother_name.like(kw),
  317. BizProtocolPartyCustomer.newborn_name.like(kw),
  318. ]
  319. lab_codes = sample_codes_matching_lab_storage_keyword(
  320. db, tenant_no, keyword, reception_source="cell_storage"
  321. )
  322. if lab_codes:
  323. kw_conds.append(BizProtocolSample.sample_code.in_(lab_codes))
  324. q = q.filter(or_(*kw_conds))
  325. return q
  326. def _rows_to_sample_list_items(
  327. db: Session,
  328. rows: list,
  329. tenant_no: str,
  330. *,
  331. reception_map: Optional[Dict[str, BizSampleReception]] = None,
  332. ) -> List[Dict[str, Any]]:
  333. type_ids = list({s.sample_type_id for s, *_ in rows if s.sample_type_id})
  334. type_map: Dict[str, str] = {}
  335. if type_ids:
  336. for t in (
  337. db.query(BizCellSampleType)
  338. .filter(
  339. BizCellSampleType.id.in_(type_ids),
  340. BizCellSampleType.del_flag == 0,
  341. )
  342. .all()
  343. ):
  344. type_map[t.id] = t.type_name or ""
  345. express_ids = list(
  346. {tr.express_company_id for *_, tr in rows if tr and tr.express_company_id}
  347. )
  348. express_map: Dict[str, str] = {}
  349. if express_ids:
  350. for e in (
  351. db.query(BizExpressCompany)
  352. .filter(BizExpressCompany.id.in_(express_ids), BizExpressCompany.del_flag == 0)
  353. .all()
  354. ):
  355. express_map[e.id] = e.express_name or ""
  356. protocol_ids = list({protocol.id for _, protocol, _, _ in rows if protocol and protocol.id})
  357. collect_info_map: Dict[str, Dict[str, str]] = {}
  358. payment_summary_map: Dict[str, Dict[str, Any]] = {}
  359. if protocol_ids:
  360. from app.services.qc_work_order_fee_summary import build_protocol_payment_summaries
  361. payment_summary_map = build_protocol_payment_summaries(db, protocol_ids, tenant_no)
  362. for coll in (
  363. db.query(BizProtocolCollectionInfo)
  364. .filter(
  365. BizProtocolCollectionInfo.collection_protocol_id.in_(protocol_ids),
  366. BizProtocolCollectionInfo.del_flag == 0,
  367. )
  368. .all()
  369. ):
  370. collect_info_map[coll.collection_protocol_id] = {
  371. "sampleCollectTime": _fmt_dt(coll.sample_collect_time),
  372. "maternalBloodCollectTime": _fmt_dt(coll.maternal_blood_collect_time),
  373. }
  374. def _map_rec(sample: BizProtocolSample) -> Optional[BizSampleReception]:
  375. if not reception_map:
  376. return None
  377. code = (sample.sample_code or "").strip()
  378. if code and code in reception_map:
  379. return reception_map[code]
  380. return None
  381. matched_recs = [_map_rec(s) for s, *_ in rows]
  382. reception_ids = [r.id for r in matched_recs if r]
  383. results_map = (
  384. check_result_svc.list_results_by_reception_ids(db, reception_ids, tenant_no)
  385. if reception_ids
  386. else {}
  387. )
  388. items: List[Dict[str, Any]] = []
  389. for sample, protocol, party, transport in rows:
  390. item: Dict[str, Any] = {
  391. "protocolSampleId": sample.id,
  392. "collectionProtocolId": protocol.id,
  393. "protocolNo": protocol.protocol_no or "",
  394. "protocolStatus": protocol.protocol_status or "",
  395. "sampleCode": sample.sample_code or "",
  396. "sampleTypeId": sample.sample_type_id or "",
  397. "sampleTypeName": type_map.get(sample.sample_type_id or "", ""),
  398. "collectAmount": _dec(sample.collect_amount),
  399. "sampleStatus": sample.sample_status or "",
  400. "motherName": (party.mother_name if party else "") or "",
  401. "newbornName": (party.newborn_name if party else "") or "",
  402. "sampleCollectTime": (
  403. collect_info_map.get(protocol.id, {}).get("sampleCollectTime", "")
  404. if protocol
  405. else ""
  406. ),
  407. "maternalBloodCollectTime": (
  408. collect_info_map.get(protocol.id, {}).get("maternalBloodCollectTime", "")
  409. if protocol
  410. else ""
  411. ),
  412. "etaTime": _fmt_dt(transport.eta_time) if transport and transport.eta_time else "",
  413. "transportType": (transport.transport_type if transport else "") or "",
  414. "waybillNo": (transport.waybill_no if transport else "") or "",
  415. "expressCompanyId": (transport.express_company_id if transport else "") or "",
  416. "expressCompanyName": express_map.get(
  417. (transport.express_company_id if transport else "") or "", ""
  418. ),
  419. "carrierName": (transport.carrier_name if transport else "") or "",
  420. "carrierPhone": (transport.carrier_phone if transport else "") or "",
  421. **sample_special_to_dict(sample),
  422. "sourceType": "cell_storage",
  423. "sourceTypeLabel": "细胞存储",
  424. "healthSurveyAttachmentFiles": urls_to_attachment_list(
  425. protocol.health_survey_form_urls if protocol else None, db, tenant_no
  426. ),
  427. "paymentSummary": payment_summary_map.get(protocol.id) if protocol else None,
  428. }
  429. rec = _map_rec(sample)
  430. if rec:
  431. cr = results_map.get(rec.id)
  432. if not cr:
  433. cr = check_result_svc.get_check_results_for_reception(db, rec, tenant_no)
  434. item.update(_reception_to_dict(rec, check_results=cr))
  435. item["receiveAttachmentFiles"] = urls_to_attachment_list(
  436. rec.receive_attachments, db, tenant_no
  437. )
  438. items.append(item)
  439. reception_ids = [str(item.get("id") or "") for item in items if item.get("id")]
  440. if reception_ids:
  441. production_map = prod_reception_svc.get_map_by_reception_ids(db, reception_ids, tenant_no)
  442. for item in items:
  443. rid = str(item.get("id") or "")
  444. item["productionReception"] = production_map.get(rid) if rid else None
  445. return items
  446. def list_pending_protocol_samples(
  447. db: Session,
  448. *,
  449. page: int = 1,
  450. page_size: int = 10,
  451. tenant_no: Optional[str] = None,
  452. keyword: Optional[str] = None,
  453. source_type: Optional[str] = None,
  454. eta_time_start: Optional[str] = None,
  455. eta_time_end: Optional[str] = None,
  456. user_info=None,
  457. ) -> Tuple[List[Dict[str, Any]], int]:
  458. if not tenant_no:
  459. return [], 0
  460. all_items: List[Dict[str, Any]] = []
  461. if not source_type or source_type == "cell_storage":
  462. q_base = _sample_pending_query(db, tenant_no, keyword)
  463. if user_info:
  464. from app.core.data_scope import apply_data_scope
  465. q_base = apply_data_scope(q_base, BizProtocolSample, user_info, db, tenant_no)
  466. cell_rows = (
  467. q_base.order_by(
  468. BizProtocolSampleTransport.eta_time.is_(None),
  469. BizProtocolSampleTransport.eta_time.asc(),
  470. BizProtocolSample.create_time.asc(),
  471. BizProtocolSample.sample_code.desc(),
  472. )
  473. .all()
  474. )
  475. all_items.extend(_rows_to_sample_list_items(db, cell_rows, tenant_no))
  476. if not source_type or source_type == "research_donation":
  477. q_donation = _donation_sample_pending_query(db, tenant_no, keyword)
  478. if user_info:
  479. from app.core.data_scope import apply_data_scope
  480. q_donation = apply_data_scope(q_donation, BizResearchDonationSample, user_info, db, tenant_no)
  481. donation_rows = (
  482. q_donation.order_by(
  483. BizResearchDonationSampleTransport.eta_time.is_(None),
  484. BizResearchDonationSampleTransport.eta_time.asc(),
  485. BizResearchDonationSample.create_time.asc(),
  486. BizResearchDonationSample.sample_code.desc(),
  487. )
  488. .all()
  489. )
  490. all_items.extend(_donation_rows_to_sample_list_items(db, donation_rows, tenant_no))
  491. # 统一按 etaTime 升序(空值排后),同 ETA 时 sampleCode 倒序
  492. _sort_pending_list_items(all_items)
  493. start_eta = _normalize_date_bound(eta_time_start) if eta_time_start else ""
  494. end_eta = _normalize_date_bound(eta_time_end, end_of_day=True) if eta_time_end else ""
  495. if start_eta or end_eta:
  496. filtered: List[Dict[str, Any]] = []
  497. for item in all_items:
  498. eta = (item.get("etaTime") or "").strip()
  499. if not eta:
  500. continue
  501. if start_eta and eta < start_eta:
  502. continue
  503. if end_eta and eta > end_eta:
  504. continue
  505. filtered.append(item)
  506. all_items = filtered
  507. total = len(all_items)
  508. page = max(1, page)
  509. page_size = min(max(1, page_size), 100)
  510. start = (page - 1) * page_size
  511. page_items = all_items[start:start + page_size]
  512. return page_items, int(total)
  513. def list_received_protocol_samples(
  514. db: Session,
  515. *,
  516. page: int = 1,
  517. page_size: int = 10,
  518. tenant_no: Optional[str] = None,
  519. keyword: Optional[str] = None,
  520. source_type: Optional[str] = None,
  521. reception_time_start: Optional[str] = None,
  522. reception_time_end: Optional[str] = None,
  523. user_info=None,
  524. ) -> Tuple[List[Dict[str, Any]], int]:
  525. if not tenant_no:
  526. return [], 0
  527. q_rec = db.query(BizSampleReception).filter(
  528. BizSampleReception.tenant_no == tenant_no,
  529. BizSampleReception.del_flag == 0,
  530. )
  531. if source_type:
  532. q_rec = q_rec.filter(BizSampleReception.reception_source == source_type)
  533. if keyword and keyword.strip():
  534. kw = f"%{keyword.strip()}%"
  535. cell_sample_ids_kw = (
  536. _sample_detail_rows_query(db)
  537. .with_entities(BizProtocolSample.id)
  538. .filter(
  539. BizProtocolSample.del_flag == 0,
  540. BizProtocolSample.tenant_no == tenant_no,
  541. BizCollectionProtocol.tenant_no == tenant_no,
  542. or_(
  543. BizProtocolSample.sample_code.like(kw),
  544. BizProtocolPartyCustomer.mother_name.like(kw),
  545. BizProtocolPartyCustomer.newborn_name.like(kw),
  546. ),
  547. )
  548. .subquery()
  549. )
  550. donation_sample_ids_kw = (
  551. _donation_sample_detail_rows_query(db)
  552. .with_entities(_donation_sample_id_col(BizResearchDonationSample.id))
  553. .filter(
  554. BizResearchDonationSample.del_flag == 0,
  555. BizResearchDonationSample.tenant_no == tenant_no,
  556. BizResearchDonation.tenant_no == tenant_no,
  557. or_(
  558. BizResearchDonationSample.sample_code.like(kw),
  559. BizResearchDonationParty.mother_name.like(kw),
  560. BizResearchDonationParty.newborn_name.like(kw),
  561. ),
  562. )
  563. .subquery()
  564. )
  565. q_rec = q_rec.filter(
  566. or_(
  567. BizSampleReception.handler_name.like(kw),
  568. BizSampleReception.lab_storage_code.like(kw),
  569. BizSampleReception.protocol_sample_id.in_(cell_sample_ids_kw),
  570. BizSampleReception.donation_sample_id.in_(donation_sample_ids_kw),
  571. )
  572. )
  573. if user_info:
  574. from app.core.data_scope import apply_data_scope
  575. q_rec = apply_data_scope(q_rec, BizSampleReception, user_info, db, tenant_no)
  576. if reception_time_start:
  577. q_rec = q_rec.filter(
  578. BizSampleReception.reception_time >= _normalize_date_bound(reception_time_start)
  579. )
  580. if reception_time_end:
  581. q_rec = q_rec.filter(
  582. BizSampleReception.reception_time
  583. <= _normalize_date_bound(reception_time_end, end_of_day=True)
  584. )
  585. total = q_rec.count()
  586. page = max(1, page)
  587. page_size = min(max(1, page_size), 100)
  588. receptions = (
  589. q_rec.order_by(
  590. BizSampleReception.reception_time.desc(),
  591. BizSampleReception.sample_code.desc(),
  592. )
  593. .offset((page - 1) * page_size)
  594. .limit(page_size)
  595. .all()
  596. )
  597. if not receptions:
  598. return [], int(total)
  599. # 按 reception_source 分组查询详情
  600. cell_receptions = [r for r in receptions if r.reception_source == "cell_storage"]
  601. donation_receptions = [r for r in receptions if r.reception_source == "research_donation"]
  602. items: List[Dict[str, Any]] = []
  603. if cell_receptions:
  604. cell_reception_map: Dict[str, BizSampleReception] = {}
  605. seen_codes: set = set()
  606. cell_rows: list = []
  607. for rec in cell_receptions:
  608. code = _reception_sample_code(db, rec)
  609. if not code:
  610. continue
  611. cell_reception_map[code] = rec
  612. if code in seen_codes:
  613. continue
  614. seen_codes.add(code)
  615. rows = (
  616. _sample_detail_rows_query(db)
  617. .filter(
  618. BizProtocolSample.sample_code == code,
  619. BizProtocolSample.del_flag == 0,
  620. BizProtocolSample.tenant_no == tenant_no,
  621. BizCollectionProtocol.tenant_no == tenant_no,
  622. )
  623. .all()
  624. )
  625. cell_rows.extend(rows)
  626. items.extend(
  627. _rows_to_sample_list_items(db, cell_rows, tenant_no, reception_map=cell_reception_map)
  628. )
  629. if donation_receptions:
  630. donation_sample_ids = [r.donation_sample_id for r in donation_receptions]
  631. donation_reception_map = {r.donation_sample_id: r for r in donation_receptions}
  632. donation_rows = (
  633. _donation_sample_detail_rows_query(db)
  634. .filter(_donation_sample_id_col(BizResearchDonationSample.id).in_(donation_sample_ids))
  635. .all()
  636. )
  637. donation_row_by_sample = {row[0].id: row for row in donation_rows}
  638. donation_ordered = [donation_row_by_sample[sid] for sid in donation_sample_ids if sid in donation_row_by_sample]
  639. items.extend(_donation_rows_to_sample_list_items(db, donation_ordered, tenant_no, reception_map=donation_reception_map))
  640. # 按 receptionTime 倒序,同时间时 sampleCode 倒序
  641. _sort_received_list_items(items)
  642. return items, int(total)
  643. def _finalize_reception_detail(db: Session, item: Optional[Dict[str, Any]], tenant_no: str) -> Optional[Dict[str, Any]]:
  644. if not item:
  645. return item
  646. return _attach_production_reception(db, item, tenant_no)
  647. def get_reception_by_id(
  648. db: Session, reception_id: str, tenant_no: str
  649. ) -> Optional[Dict[str, Any]]:
  650. rec = (
  651. db.query(BizSampleReception)
  652. .filter(
  653. BizSampleReception.id == reception_id,
  654. BizSampleReception.tenant_no == tenant_no,
  655. BizSampleReception.del_flag == 0,
  656. )
  657. .first()
  658. )
  659. if not rec:
  660. return None
  661. if (rec.reception_source or "cell_storage") == "research_donation":
  662. dsid = rec.donation_sample_id or ""
  663. if not dsid:
  664. cr = check_result_svc.get_check_results_for_reception(db, rec, tenant_no)
  665. out = _reception_to_dict(rec, check_results=cr)
  666. out["sourceType"] = "research_donation"
  667. out["sourceTypeLabel"] = "科研捐赠"
  668. out["receiveAttachmentFiles"] = urls_to_attachment_list(
  669. rec.receive_attachments, db, tenant_no
  670. )
  671. return _finalize_reception_detail(db, out, tenant_no)
  672. rows = (
  673. _donation_sample_detail_rows_query(db)
  674. .filter(_donation_sample_id_col(BizResearchDonationSample.id) == dsid)
  675. .all()
  676. )
  677. if not rows:
  678. cr = check_result_svc.get_check_results_for_reception(db, rec, tenant_no)
  679. out = _reception_to_dict(rec, check_results=cr)
  680. out["sourceType"] = "research_donation"
  681. out["sourceTypeLabel"] = "科研捐赠"
  682. out["receiveAttachmentFiles"] = urls_to_attachment_list(
  683. rec.receive_attachments, db, tenant_no
  684. )
  685. return _finalize_reception_detail(db, out, tenant_no)
  686. items = _donation_rows_to_sample_list_items(
  687. db, rows, tenant_no, reception_map={dsid: rec}
  688. )
  689. return items[0] if items else None
  690. code = _reception_sample_code(db, rec)
  691. if code:
  692. rows = (
  693. _sample_detail_rows_query(db)
  694. .filter(
  695. BizProtocolSample.sample_code == code,
  696. BizProtocolSample.del_flag == 0,
  697. BizProtocolSample.tenant_no == tenant_no,
  698. )
  699. .all()
  700. )
  701. if rows:
  702. items = _rows_to_sample_list_items(
  703. db, rows, tenant_no, reception_map={code: rec}
  704. )
  705. return items[0] if items else None
  706. cr = check_result_svc.get_check_results_for_reception(db, rec, tenant_no)
  707. out = _reception_to_dict(rec, check_results=cr)
  708. out["receiveAttachmentFiles"] = urls_to_attachment_list(
  709. rec.receive_attachments, db, tenant_no
  710. )
  711. return _finalize_reception_detail(db, out, tenant_no)
  712. def create_sample_reception(
  713. db: Session,
  714. *,
  715. reception_source: str = "cell_storage",
  716. protocol_sample_id: Optional[str] = None,
  717. donation_sample_id: Optional[str] = None,
  718. research_donation_id: Optional[str] = None,
  719. tenant_no: str,
  720. user_id: Optional[str],
  721. create_dept_id: str = "",
  722. reception_time: datetime,
  723. handler_user_id: Optional[str],
  724. handler_name: Optional[str],
  725. receive_temperature: Optional[str],
  726. volume_ml: Optional[float] = None,
  727. net_weight_g: Optional[float] = None,
  728. checks: List[Dict[str, Any]],
  729. accept_conclusion: str,
  730. receive_attachments: Optional[str],
  731. remark: Optional[str],
  732. lab_storage_code: Optional[str] = None,
  733. ) -> BizSampleReception:
  734. source = (reception_source or "").strip()
  735. if source not in ("cell_storage", "research_donation"):
  736. raise ValueError("来源类型无效")
  737. # 按来源校验样本并获取关联 ID
  738. if source == "cell_storage":
  739. if not protocol_sample_id:
  740. raise ValueError("细胞存储来源必须传入 protocolSampleId")
  741. sample = (
  742. db.query(BizProtocolSample)
  743. .filter(
  744. BizProtocolSample.id == protocol_sample_id,
  745. BizProtocolSample.tenant_no == tenant_no,
  746. BizProtocolSample.del_flag == 0,
  747. )
  748. .first()
  749. )
  750. if not sample:
  751. raise ValueError("样本不存在")
  752. _sample_code = (sample.sample_code or "").strip()
  753. if not _sample_code:
  754. raise ValueError("样本编码不能为空")
  755. exists = (
  756. db.query(BizSampleReception)
  757. .filter(
  758. BizSampleReception.tenant_no == tenant_no,
  759. BizSampleReception.del_flag == 0,
  760. BizSampleReception.reception_source == "cell_storage",
  761. BizSampleReception.sample_code == _sample_code,
  762. )
  763. .first()
  764. )
  765. if exists:
  766. raise ValueError("该样本已登记接收,请勿重复操作")
  767. collection_protocol_id = sample.collection_protocol_id
  768. _donation_sample_id = None
  769. _research_donation_id = None
  770. _sample_code = sample.sample_code or ""
  771. else:
  772. # research_donation
  773. if not donation_sample_id:
  774. raise ValueError("科研捐赠来源必须传入 donationSampleId")
  775. dsample = (
  776. db.query(BizResearchDonationSample)
  777. .filter(
  778. _donation_sample_id_col(BizResearchDonationSample.id) == donation_sample_id,
  779. BizResearchDonationSample.tenant_no == tenant_no,
  780. BizResearchDonationSample.del_flag == 0,
  781. )
  782. .first()
  783. )
  784. if not dsample:
  785. raise ValueError("捐赠样本不存在")
  786. _sample_code = (dsample.sample_code or "").strip()
  787. if not _sample_code:
  788. raise ValueError("样本编码不能为空")
  789. exists = (
  790. db.query(BizSampleReception)
  791. .filter(
  792. BizSampleReception.tenant_no == tenant_no,
  793. BizSampleReception.del_flag == 0,
  794. BizSampleReception.reception_source == "research_donation",
  795. BizSampleReception.sample_code == _sample_code,
  796. )
  797. .first()
  798. )
  799. if exists:
  800. raise ValueError("该样本已登记接收,请勿重复操作")
  801. collection_protocol_id = ""
  802. protocol_sample_id = ""
  803. _donation_sample_id = donation_sample_id
  804. _research_donation_id = dsample.research_donation_id
  805. conclusion = (accept_conclusion or "").strip()
  806. if conclusion not in ("js", "bj"):
  807. raise ValueError("接收结论无效")
  808. from app.crud import sample_reception_lab_code as lab_code_svc
  809. resolved_lab_code: Optional[str] = None
  810. code_raw = (lab_storage_code or "").strip()
  811. if conclusion == "js":
  812. resolved_lab_code = lab_code_svc.validate_lab_storage_code(code_raw)
  813. lab_code_svc.assert_lab_storage_code_available(
  814. db, tenant_no=tenant_no, code=resolved_lab_code
  815. )
  816. elif code_raw:
  817. resolved_lab_code = lab_code_svc.validate_lab_storage_code(code_raw)
  818. lab_code_svc.assert_lab_storage_code_available(
  819. db, tenant_no=tenant_no, code=resolved_lab_code
  820. )
  821. handler_display = (handler_name or "").strip()
  822. if handler_user_id and not handler_display:
  823. u = db.query(SysUser).filter(SysUser.id == handler_user_id).first()
  824. if u:
  825. handler_display = u.nick_name or ""
  826. obj = BizSampleReception(
  827. id=generate_uuid(),
  828. protocol_sample_id=protocol_sample_id or "",
  829. collection_protocol_id=collection_protocol_id or "",
  830. reception_source=source,
  831. donation_sample_id=_donation_sample_id,
  832. research_donation_id=_research_donation_id,
  833. sample_code=_sample_code or None,
  834. lab_storage_code=resolved_lab_code,
  835. reception_time=reception_time,
  836. handler_user_id=handler_user_id,
  837. handler_name=handler_display or None,
  838. receive_temperature=(receive_temperature or "").strip() or None,
  839. check_packaging=0,
  840. check_sealing=0,
  841. check_no_damage=0,
  842. check_no_leakage=0,
  843. check_color=0,
  844. check_tying_rope=0,
  845. check_no_stratification=0,
  846. accept_conclusion=conclusion,
  847. receive_attachments=(receive_attachments or "").strip() or None,
  848. tenant_no=tenant_no,
  849. remark=(remark or "").strip() or None,
  850. create_by=user_id,
  851. create_dept_id=create_dept_id or "",
  852. update_by=user_id,
  853. )
  854. db.add(obj)
  855. db.flush()
  856. check_result_svc.save_reception_check_results(
  857. db,
  858. reception_id=obj.id,
  859. tenant_no=tenant_no,
  860. user_id=user_id,
  861. create_dept_id=create_dept_id or "",
  862. checks=checks,
  863. )
  864. from app.models.models import BizSampleReceptionCheckResult
  865. saved_rows = (
  866. db.query(BizSampleReceptionCheckResult)
  867. .filter(
  868. BizSampleReceptionCheckResult.reception_id == obj.id,
  869. BizSampleReceptionCheckResult.del_flag == 0,
  870. )
  871. .all()
  872. )
  873. check_result_svc.sync_legacy_columns_from_results(db, obj, saved_rows)
  874. db.add(obj)
  875. if conclusion == "js":
  876. from app.crud import qc_work_order as qc_wo_svc
  877. qc_wo_svc.create_work_order_from_reception(
  878. db,
  879. reception=obj,
  880. tenant_no=tenant_no,
  881. user_id=user_id,
  882. create_dept_id=create_dept_id or "",
  883. )
  884. from app.crud import production_work_order as prod_wo_svc
  885. prod_wo = prod_wo_svc.create_production_work_order_from_reception(
  886. db,
  887. reception=obj,
  888. tenant_no=tenant_no,
  889. user_id=user_id,
  890. create_dept_id=create_dept_id or "",
  891. )
  892. if prod_wo is not None:
  893. from app.crud import production_task_set as production_task_set_svc
  894. production_task_set_svc.try_auto_dispatch_builtin_tasks_after_reception(
  895. db,
  896. tenant_no=tenant_no,
  897. user_id=user_id,
  898. create_dept_id=create_dept_id or "",
  899. work_order=prod_wo,
  900. lab_storage_code=getattr(obj, "lab_storage_code", None) or "",
  901. )
  902. from app.crud import storage_release_review as srr_svc
  903. _srr_protocol_no = ""
  904. _srr_sample_name = ""
  905. _src = getattr(obj, "reception_source", "cell_storage") or "cell_storage"
  906. if _src == "research_donation":
  907. _d = (
  908. db.query(BizResearchDonation)
  909. .filter(
  910. BizResearchDonation.id == (obj.research_donation_id or ""),
  911. BizResearchDonation.del_flag == 0,
  912. )
  913. .first()
  914. )
  915. if _d:
  916. _srr_protocol_no = _d.donation_no or ""
  917. _ds = (
  918. db.query(BizResearchDonationSample)
  919. .filter(
  920. BizResearchDonationSample.id == (obj.donation_sample_id or ""),
  921. BizResearchDonationSample.del_flag == 0,
  922. )
  923. .first()
  924. )
  925. if _ds:
  926. _srr_sample_name = _ds.sample_code or ""
  927. else:
  928. _p = (
  929. db.query(BizCollectionProtocol)
  930. .filter(
  931. BizCollectionProtocol.id == obj.collection_protocol_id,
  932. BizCollectionProtocol.del_flag == 0,
  933. )
  934. .first()
  935. )
  936. if _p:
  937. _srr_protocol_no = _p.protocol_no or ""
  938. _s = (
  939. db.query(BizProtocolSample)
  940. .filter(
  941. BizProtocolSample.id == obj.protocol_sample_id,
  942. BizProtocolSample.del_flag == 0,
  943. )
  944. .first()
  945. )
  946. if _s:
  947. _srr_sample_name = _s.sample_code or ""
  948. srr_svc.create_review_from_reception(
  949. db,
  950. reception=obj,
  951. protocol_no=_srr_protocol_no,
  952. sample_name=_srr_sample_name,
  953. tenant_no=tenant_no,
  954. user_id=user_id,
  955. create_dept_id=create_dept_id or "",
  956. )
  957. db.commit()
  958. db.refresh(obj)
  959. return obj
  960. def _can_edit_sample_reception(
  961. db: Session,
  962. user_id: Optional[str],
  963. rec: BizSampleReception,
  964. ) -> bool:
  965. """已接收记录:仅经手人或「质量主管」角色可编辑。"""
  966. uid = (user_id or "").strip()
  967. if not uid:
  968. return False
  969. handler_id = (rec.handler_user_id or "").strip()
  970. if handler_id and handler_id == uid:
  971. return True
  972. return SAMPLE_RECEPTION_EDIT_ROLE in get_user_role_names(db, uid)
  973. def update_sample_reception(
  974. db: Session,
  975. *,
  976. reception_id: str,
  977. tenant_no: str,
  978. user_id: Optional[str],
  979. reception_time: datetime,
  980. handler_user_id: Optional[str],
  981. handler_name: Optional[str],
  982. receive_temperature: Optional[str],
  983. volume_ml: Optional[float] = None,
  984. net_weight_g: Optional[float] = None,
  985. checks: List[Dict[str, Any]],
  986. accept_conclusion: str,
  987. receive_attachments: Optional[str],
  988. remark: Optional[str],
  989. lab_storage_code: Optional[str] = None,
  990. ) -> BizSampleReception:
  991. """更新已登记样本接收信息(不重复创建质检/生产工单)。"""
  992. rec = (
  993. db.query(BizSampleReception)
  994. .filter(
  995. BizSampleReception.id == reception_id,
  996. BizSampleReception.tenant_no == tenant_no,
  997. BizSampleReception.del_flag == 0,
  998. )
  999. .first()
  1000. )
  1001. if not rec:
  1002. raise ValueError("接收记录不存在")
  1003. if not _can_edit_sample_reception(db, user_id, rec):
  1004. raise PermissionError("仅该接收记录的经手人或质量主管可编辑")
  1005. conclusion = (accept_conclusion or "").strip()
  1006. if conclusion not in ("js", "bj"):
  1007. raise ValueError("接收结论无效")
  1008. from app.crud import sample_reception_lab_code as lab_code_svc
  1009. code_raw = (lab_storage_code or "").strip()
  1010. if conclusion == "js":
  1011. parsed_code = lab_code_svc.validate_lab_storage_code(code_raw)
  1012. lab_code_svc.assert_lab_storage_code_available(
  1013. db,
  1014. tenant_no=tenant_no,
  1015. code=parsed_code,
  1016. exclude_reception_id=reception_id,
  1017. )
  1018. rec.lab_storage_code = parsed_code
  1019. elif code_raw:
  1020. parsed_code = lab_code_svc.validate_lab_storage_code(code_raw)
  1021. lab_code_svc.assert_lab_storage_code_available(
  1022. db,
  1023. tenant_no=tenant_no,
  1024. code=parsed_code,
  1025. exclude_reception_id=reception_id,
  1026. )
  1027. rec.lab_storage_code = parsed_code
  1028. else:
  1029. rec.lab_storage_code = None
  1030. handler_display = (handler_name or "").strip()
  1031. if handler_user_id and not handler_display:
  1032. u = db.query(SysUser).filter(SysUser.id == handler_user_id).first()
  1033. if u:
  1034. handler_display = u.nick_name or ""
  1035. rec.reception_time = reception_time
  1036. rec.handler_user_id = handler_user_id
  1037. rec.handler_name = handler_display or None
  1038. rec.receive_temperature = (receive_temperature or "").strip() or None
  1039. rec.accept_conclusion = conclusion
  1040. rec.receive_attachments = (receive_attachments or "").strip() or None
  1041. rec.remark = (remark or "").strip() or None
  1042. rec.update_by = user_id
  1043. rec.update_time = datetime.now()
  1044. check_result_svc.soft_delete_reception_check_results(
  1045. db,
  1046. reception_id=reception_id,
  1047. tenant_no=tenant_no,
  1048. user_id=user_id,
  1049. )
  1050. check_result_svc.save_reception_check_results(
  1051. db,
  1052. reception_id=reception_id,
  1053. tenant_no=tenant_no,
  1054. user_id=user_id,
  1055. create_dept_id=rec.create_dept_id or "",
  1056. checks=checks,
  1057. )
  1058. from app.models.models import BizSampleReceptionCheckResult
  1059. saved_rows = (
  1060. db.query(BizSampleReceptionCheckResult)
  1061. .filter(
  1062. BizSampleReceptionCheckResult.reception_id == reception_id,
  1063. BizSampleReceptionCheckResult.del_flag == 0,
  1064. )
  1065. .all()
  1066. )
  1067. check_result_svc.sync_legacy_columns_from_results(db, rec, saved_rows)
  1068. db.add(rec)
  1069. db.commit()
  1070. db.refresh(rec)
  1071. return rec
  1072. def _attach_production_reception(
  1073. db: Session,
  1074. item: Dict[str, Any],
  1075. tenant_no: str,
  1076. *,
  1077. production_map: Optional[Dict[str, Dict[str, Any]]] = None,
  1078. ) -> Dict[str, Any]:
  1079. rid = str(item.get("id") or "").strip()
  1080. if not rid:
  1081. item["productionReception"] = None
  1082. return item
  1083. if production_map is not None:
  1084. item["productionReception"] = production_map.get(rid)
  1085. else:
  1086. item["productionReception"] = prod_reception_svc.get_by_reception_id(db, rid, tenant_no)
  1087. return item
  1088. def _reception_detail_item(
  1089. db: Session,
  1090. rec: BizSampleReception,
  1091. tenant_no: str,
  1092. *,
  1093. results_map: Optional[Dict[str, List[Dict[str, Any]]]] = None,
  1094. ) -> Dict[str, Any]:
  1095. cr = (results_map or {}).get(rec.id)
  1096. if not cr:
  1097. cr = check_result_svc.get_check_results_for_reception(db, rec, tenant_no)
  1098. item = _reception_to_dict(rec, check_results=cr)
  1099. item["receiveAttachmentFiles"] = urls_to_attachment_list(
  1100. rec.receive_attachments, db, tenant_no
  1101. )
  1102. return item
  1103. def _reception_detail_item_with_production(
  1104. db: Session,
  1105. rec: BizSampleReception,
  1106. tenant_no: str,
  1107. *,
  1108. results_map: Optional[Dict[str, List[Dict[str, Any]]]] = None,
  1109. production_map: Optional[Dict[str, Dict[str, Any]]] = None,
  1110. ) -> Dict[str, Any]:
  1111. item = _reception_detail_item(db, rec, tenant_no, results_map=results_map)
  1112. return _attach_production_reception(db, item, tenant_no, production_map=production_map)
  1113. def _reception_sample_code(db: Session, rec: BizSampleReception) -> str:
  1114. code = (rec.sample_code or "").strip()
  1115. if code:
  1116. return code
  1117. sid = (rec.protocol_sample_id or "").strip()
  1118. if not sid:
  1119. return ""
  1120. row = (
  1121. db.query(BizProtocolSample.sample_code)
  1122. .filter(BizProtocolSample.id == sid)
  1123. .first()
  1124. )
  1125. return (row.sample_code or "").strip() if row else ""
  1126. def get_receptions_map_for_samples(
  1127. db: Session,
  1128. tenant_no: str,
  1129. *,
  1130. sample_code_by_id: Dict[str, str],
  1131. ) -> Dict[str, Dict[str, Any]]:
  1132. """按租户 + 样本编码关联接收记录(key 为当前协议样本 id),供协议单查看弹窗使用。"""
  1133. if not tenant_no or not sample_code_by_id:
  1134. return {}
  1135. codes = list({(c or "").strip() for c in sample_code_by_id.values() if (c or "").strip()})
  1136. if not codes:
  1137. return {}
  1138. rows = (
  1139. db.query(BizSampleReception)
  1140. .filter(
  1141. BizSampleReception.tenant_no == tenant_no,
  1142. BizSampleReception.del_flag == 0,
  1143. BizSampleReception.reception_source == "cell_storage",
  1144. BizSampleReception.sample_code.in_(codes),
  1145. )
  1146. .all()
  1147. )
  1148. if not rows:
  1149. return {}
  1150. code_to_rec: Dict[str, BizSampleReception] = {}
  1151. for rec in rows:
  1152. code = _reception_sample_code(db, rec)
  1153. if code and code not in code_to_rec:
  1154. code_to_rec[code] = rec
  1155. reception_ids = [r.id for r in code_to_rec.values()]
  1156. results_map = check_result_svc.list_results_by_reception_ids(
  1157. db, reception_ids, tenant_no
  1158. )
  1159. production_map = prod_reception_svc.get_map_by_reception_ids(db, reception_ids, tenant_no)
  1160. out: Dict[str, Dict[str, Any]] = {}
  1161. for sid, raw_code in sample_code_by_id.items():
  1162. code = (raw_code or "").strip()
  1163. if not code:
  1164. continue
  1165. rec = code_to_rec.get(code)
  1166. if not rec:
  1167. continue
  1168. out[sid] = _reception_detail_item_with_production(
  1169. db, rec, tenant_no, results_map=results_map, production_map=production_map
  1170. )
  1171. return out
  1172. def get_receptions_map_for_donation_samples(
  1173. db: Session,
  1174. sample_ids: List[str],
  1175. tenant_no: str,
  1176. ) -> Dict[str, Dict[str, Any]]:
  1177. """按捐赠样本 ID 批量查询接收记录,供捐赠协议单查看弹窗使用。"""
  1178. if not sample_ids or not tenant_no:
  1179. return {}
  1180. rows = (
  1181. db.query(BizSampleReception)
  1182. .filter(
  1183. BizSampleReception.donation_sample_id.in_(sample_ids),
  1184. BizSampleReception.tenant_no == tenant_no,
  1185. BizSampleReception.del_flag == 0,
  1186. )
  1187. .all()
  1188. )
  1189. if not rows:
  1190. return {}
  1191. reception_ids = [r.id for r in rows]
  1192. results_map = check_result_svc.list_results_by_reception_ids(db, reception_ids, tenant_no)
  1193. production_map = prod_reception_svc.get_map_by_reception_ids(db, reception_ids, tenant_no)
  1194. out: Dict[str, Dict[str, Any]] = {}
  1195. for rec in rows:
  1196. sid = rec.donation_sample_id
  1197. if not sid:
  1198. continue
  1199. cr = results_map.get(rec.id)
  1200. if not cr:
  1201. cr = check_result_svc.get_check_results_for_reception(db, rec, tenant_no)
  1202. item = _reception_to_dict(rec, check_results=cr)
  1203. item["receiveAttachmentFiles"] = urls_to_attachment_list(
  1204. rec.receive_attachments, db, tenant_no
  1205. )
  1206. _attach_production_reception(db, item, tenant_no, production_map=production_map)
  1207. out[sid] = item
  1208. return out
  1209. def get_lab_storage_code_map_by_sample_codes(
  1210. db: Session,
  1211. tenant_no: str,
  1212. sample_codes: List[str],
  1213. *,
  1214. reception_source: Optional[str] = None,
  1215. ) -> Dict[str, str]:
  1216. """按样本编码查询已接收(js)记录的实验室入库编码;同码多条时取最新接收时间。"""
  1217. codes = list({(c or "").strip() for c in sample_codes if (c or "").strip()})
  1218. if not codes or not tenant_no:
  1219. return {}
  1220. q = db.query(BizSampleReception.sample_code, BizSampleReception.lab_storage_code).filter(
  1221. BizSampleReception.tenant_no == tenant_no,
  1222. BizSampleReception.del_flag == 0,
  1223. BizSampleReception.sample_code.in_(codes),
  1224. BizSampleReception.accept_conclusion == "js",
  1225. BizSampleReception.lab_storage_code.isnot(None),
  1226. BizSampleReception.lab_storage_code != "",
  1227. )
  1228. if reception_source:
  1229. q = q.filter(BizSampleReception.reception_source == reception_source)
  1230. result: Dict[str, str] = {}
  1231. for row in q.order_by(BizSampleReception.reception_time.desc()).all():
  1232. sc = (row.sample_code or "").strip()
  1233. lk = (row.lab_storage_code or "").strip()
  1234. if sc and lk and sc not in result:
  1235. result[sc] = lk
  1236. missing = [c for c in codes if c not in result]
  1237. if missing:
  1238. join_q = (
  1239. db.query(BizProtocolSample.sample_code, BizSampleReception.lab_storage_code)
  1240. .join(
  1241. BizSampleReception,
  1242. and_(
  1243. BizSampleReception.protocol_sample_id == BizProtocolSample.id,
  1244. BizSampleReception.del_flag == 0,
  1245. BizSampleReception.tenant_no == tenant_no,
  1246. BizSampleReception.accept_conclusion == "js",
  1247. BizSampleReception.lab_storage_code.isnot(None),
  1248. BizSampleReception.lab_storage_code != "",
  1249. ),
  1250. )
  1251. .filter(
  1252. BizProtocolSample.sample_code.in_(missing),
  1253. BizProtocolSample.del_flag == 0,
  1254. BizProtocolSample.tenant_no == tenant_no,
  1255. )
  1256. )
  1257. if reception_source:
  1258. join_q = join_q.filter(BizSampleReception.reception_source == reception_source)
  1259. for row in join_q.order_by(BizSampleReception.reception_time.desc()).all():
  1260. sc = (row.sample_code or "").strip()
  1261. lk = (row.lab_storage_code or "").strip()
  1262. if sc and lk and sc not in result:
  1263. result[sc] = lk
  1264. return result
  1265. def format_lab_storage_codes_csv(
  1266. sample_codes: List[str],
  1267. code_map: Dict[str, str],
  1268. ) -> str:
  1269. """与 sampleCodes 顺序对齐,逗号拼接入库编码(无则空串)。"""
  1270. parts: List[str] = []
  1271. for raw in sample_codes:
  1272. c = (raw or "").strip()
  1273. if not c:
  1274. continue
  1275. parts.append((code_map.get(c) or "").strip())
  1276. return ",".join(parts)
  1277. def _lab_storage_keyword_like(keyword: str) -> Optional[str]:
  1278. kw = (keyword or "").strip()
  1279. if not kw:
  1280. return None
  1281. return f"%{kw}%"
  1282. def sample_codes_matching_lab_storage_keyword(
  1283. db: Session,
  1284. tenant_no: str,
  1285. keyword: str,
  1286. *,
  1287. reception_source: Optional[str] = None,
  1288. ) -> List[str]:
  1289. """关键词模糊匹配实验室入库编码,返回关联样本编码列表。"""
  1290. like = _lab_storage_keyword_like(keyword)
  1291. if not like or not tenant_no:
  1292. return []
  1293. q = db.query(BizSampleReception.sample_code).filter(
  1294. BizSampleReception.tenant_no == tenant_no,
  1295. BizSampleReception.del_flag == 0,
  1296. BizSampleReception.lab_storage_code.isnot(None),
  1297. BizSampleReception.lab_storage_code != "",
  1298. BizSampleReception.lab_storage_code.like(like),
  1299. BizSampleReception.sample_code.isnot(None),
  1300. BizSampleReception.sample_code != "",
  1301. )
  1302. if reception_source:
  1303. q = q.filter(BizSampleReception.reception_source == reception_source)
  1304. return list({(row[0] or "").strip() for row in q.all() if (row[0] or "").strip()})
  1305. def reception_ids_matching_lab_storage_keyword(
  1306. db: Session,
  1307. tenant_no: str,
  1308. keyword: str,
  1309. *,
  1310. reception_source: Optional[str] = None,
  1311. ) -> List[str]:
  1312. """关键词模糊匹配实验室入库编码,返回接收记录 id 列表。"""
  1313. like = _lab_storage_keyword_like(keyword)
  1314. if not like or not tenant_no:
  1315. return []
  1316. q = db.query(BizSampleReception.id).filter(
  1317. BizSampleReception.tenant_no == tenant_no,
  1318. BizSampleReception.del_flag == 0,
  1319. BizSampleReception.lab_storage_code.isnot(None),
  1320. BizSampleReception.lab_storage_code != "",
  1321. BizSampleReception.lab_storage_code.like(like),
  1322. )
  1323. if reception_source:
  1324. q = q.filter(BizSampleReception.reception_source == reception_source)
  1325. return [row[0] for row in q.all() if row[0]]
  1326. def protocol_ids_matching_lab_storage_keyword(
  1327. db: Session,
  1328. tenant_no: str,
  1329. keyword: str,
  1330. ) -> List[str]:
  1331. """入库编码关键词 → 含匹配样本的采集协议 id。"""
  1332. codes = sample_codes_matching_lab_storage_keyword(
  1333. db, tenant_no, keyword, reception_source="cell_storage"
  1334. )
  1335. if not codes:
  1336. return []
  1337. rows = (
  1338. db.query(BizProtocolSample.collection_protocol_id)
  1339. .filter(
  1340. BizProtocolSample.tenant_no == tenant_no,
  1341. BizProtocolSample.del_flag == 0,
  1342. BizProtocolSample.sample_code.in_(codes),
  1343. )
  1344. .distinct()
  1345. .all()
  1346. )
  1347. return [r[0] for r in rows if r[0]]
  1348. def donation_ids_matching_lab_storage_keyword(
  1349. db: Session,
  1350. tenant_no: str,
  1351. keyword: str,
  1352. ) -> List[str]:
  1353. """入库编码关键词 → 含匹配样本的捐赠协议 id。"""
  1354. codes = sample_codes_matching_lab_storage_keyword(
  1355. db, tenant_no, keyword, reception_source="research_donation"
  1356. )
  1357. if not codes:
  1358. return []
  1359. rows = (
  1360. db.query(BizResearchDonationSample.research_donation_id)
  1361. .filter(
  1362. BizResearchDonationSample.tenant_no == tenant_no,
  1363. BizResearchDonationSample.del_flag == 0,
  1364. BizResearchDonationSample.sample_code.in_(codes),
  1365. )
  1366. .distinct()
  1367. .all()
  1368. )
  1369. return [r[0] for r in rows if r[0]]
  1370. def get_reception_by_sample_code(
  1371. db: Session,
  1372. sample_code: str,
  1373. source_type: Optional[str],
  1374. tenant_no: str,
  1375. ) -> Dict[str, Any]:
  1376. """
  1377. 根据样本编码查询接收记录及样本信息。
  1378. - 有记录 → 返回 {receptionId, sampleInfo}
  1379. - 无记录 → 返回 {receptionId: null, sampleInfo}
  1380. """
  1381. code = (sample_code or "").strip()
  1382. if not code:
  1383. return {"receptionId": None, "sampleInfo": None}
  1384. source = (source_type or "cell_storage").strip()
  1385. # 先查询是否有已接收记录
  1386. q_rec = db.query(BizSampleReception).filter(
  1387. BizSampleReception.tenant_no == tenant_no,
  1388. BizSampleReception.del_flag == 0,
  1389. BizSampleReception.sample_code == code,
  1390. )
  1391. if source:
  1392. q_rec = q_rec.filter(BizSampleReception.reception_source == source)
  1393. reception = q_rec.order_by(BizSampleReception.reception_time.desc()).first()
  1394. if source == "research_donation":
  1395. # 捐赠样本查询
  1396. rows = (
  1397. _donation_sample_detail_rows_query(db)
  1398. .filter(
  1399. _donation_sample_id_col(BizResearchDonationSample.sample_code) == code,
  1400. BizResearchDonationSample.del_flag == 0,
  1401. BizResearchDonationSample.tenant_no == tenant_no,
  1402. )
  1403. .all()
  1404. )
  1405. sample_info_list = _donation_rows_to_sample_list_items(db, rows, tenant_no)
  1406. sample_info = sample_info_list[0] if sample_info_list else None
  1407. else:
  1408. # 细胞存储样本查询
  1409. rows = (
  1410. _sample_detail_rows_query(db)
  1411. .filter(
  1412. BizProtocolSample.sample_code == code,
  1413. BizProtocolSample.del_flag == 0,
  1414. BizProtocolSample.tenant_no == tenant_no,
  1415. )
  1416. .all()
  1417. )
  1418. if rows:
  1419. if reception:
  1420. sample_info_list = _rows_to_sample_list_items(
  1421. db, rows, tenant_no, reception_map={code: reception}
  1422. )
  1423. else:
  1424. sample_info_list = _rows_to_sample_list_items(db, rows, tenant_no)
  1425. sample_info = sample_info_list[0] if sample_info_list else None
  1426. else:
  1427. sample_info = None
  1428. return {
  1429. "receptionId": reception.id if reception else None,
  1430. "sampleInfo": sample_info,
  1431. "productionReception": (
  1432. prod_reception_svc.get_by_reception_id(db, reception.id, tenant_no) if reception else None
  1433. ),
  1434. }