sample_reception.py 55 KB

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