sample_reception.py 55 KB

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