pre_transit_sample.py 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232
  1. """预产及在途样本 — 质检看板(协议单维度列表)"""
  2. from __future__ import annotations
  3. from datetime import datetime
  4. from typing import Any, Dict, List, Optional, Tuple
  5. from sqlalchemy import and_, exists, func, not_, or_
  6. from sqlalchemy.orm import Session
  7. from app.crud.collection_protocol import (
  8. _collection_info_to_dict,
  9. _fmt_dt,
  10. _load_express_name_map,
  11. _load_sample_type_name_map,
  12. _load_site_name_map,
  13. _normalize_date_bound,
  14. _party_customer_to_dict,
  15. _protocol_to_dict,
  16. total_pages,
  17. urls_to_attachment_list,
  18. )
  19. from app.utils.sample_special import sample_special_to_dict
  20. from app.crud.research_donation import (
  21. _collection_to_dict,
  22. _donation_to_dict,
  23. _party_to_dict,
  24. )
  25. from app.models.models import (
  26. BizCollectionHospital,
  27. BizCollectionProtocol,
  28. BizCollectionRegion,
  29. BizProtocolCollectionInfo,
  30. BizProtocolPartyCustomer,
  31. BizProtocolSample,
  32. BizProtocolSampleTransport,
  33. BizSampleReception,
  34. SysUser,
  35. )
  36. from app.models.research_donation import (
  37. BizResearchDonation,
  38. BizResearchDonationCollection,
  39. BizResearchDonationParty,
  40. BizResearchDonationSample,
  41. BizResearchDonationSampleTransport,
  42. )
  43. _CELL_IN_TRANSIT_EXCLUDED = ("yzf", "sdfq", "zky", "ygq")
  44. _DONATION_IN_TRANSIT_EXCLUDED = ("yzf", "sdfq", "zckc", "ygq")
  45. _PRE_DELIVERY_STATUSES = ("dsx", "sxz")
  46. def _is_uncollected_status(status: Optional[str]) -> bool:
  47. return not (status or "").strip()
  48. def _is_in_transit_sample(status: Optional[str]) -> bool:
  49. return (status or "").strip() == "ycj"
  50. def _cell_received_exists(tenant_no: str):
  51. return (
  52. exists()
  53. .where(
  54. and_(
  55. BizSampleReception.tenant_no == tenant_no,
  56. BizSampleReception.del_flag == 0,
  57. BizSampleReception.reception_source == "cell_storage",
  58. BizSampleReception.sample_code.isnot(None),
  59. BizSampleReception.sample_code != "",
  60. BizSampleReception.sample_code == BizProtocolSample.sample_code,
  61. )
  62. )
  63. .correlate(BizProtocolSample)
  64. )
  65. def _donation_received_exists(tenant_no: str):
  66. return (
  67. exists()
  68. .where(
  69. and_(
  70. BizSampleReception.tenant_no == tenant_no,
  71. BizSampleReception.del_flag == 0,
  72. BizSampleReception.reception_source == "research_donation",
  73. BizSampleReception.sample_code.isnot(None),
  74. BizSampleReception.sample_code != "",
  75. BizSampleReception.sample_code == BizResearchDonationSample.sample_code,
  76. )
  77. )
  78. .correlate(BizResearchDonationSample)
  79. )
  80. def _apply_collection_keyword(q, db: Session, tenant_no: str, keyword: str):
  81. kw = (keyword or "").strip()
  82. if not kw:
  83. return q
  84. like = f"%{kw}%"
  85. party_q = db.query(BizProtocolPartyCustomer.collection_protocol_id).filter(
  86. BizProtocolPartyCustomer.del_flag == 0,
  87. or_(
  88. BizProtocolPartyCustomer.mother_name.like(like),
  89. BizProtocolPartyCustomer.mother_id_card.like(like),
  90. BizProtocolPartyCustomer.mother_phone.like(like),
  91. ),
  92. )
  93. if tenant_no:
  94. party_q = party_q.filter(BizProtocolPartyCustomer.tenant_no == tenant_no)
  95. kw_conds = [
  96. BizCollectionProtocol.id.in_(party_q),
  97. BizCollectionProtocol.protocol_no.like(like),
  98. ]
  99. from app.crud import sample_reception as reception_svc
  100. proto_ids = reception_svc.protocol_ids_matching_lab_storage_keyword(db, tenant_no, kw)
  101. if proto_ids:
  102. kw_conds.append(BizCollectionProtocol.id.in_(proto_ids))
  103. sample_proto_q = (
  104. db.query(BizProtocolSample.collection_protocol_id)
  105. .filter(
  106. BizProtocolSample.del_flag == 0,
  107. BizProtocolSample.tenant_no == tenant_no,
  108. BizProtocolSample.sample_code.like(like),
  109. )
  110. )
  111. kw_conds.append(BizCollectionProtocol.id.in_(sample_proto_q))
  112. return q.filter(or_(*kw_conds))
  113. def _apply_donation_keyword(q, db: Session, tenant_no: str, keyword: str):
  114. kw = (keyword or "").strip()
  115. if not kw:
  116. return q
  117. like = f"%{kw}%"
  118. party_q = db.query(BizResearchDonationParty.research_donation_id).filter(
  119. BizResearchDonationParty.del_flag == 0,
  120. or_(
  121. BizResearchDonationParty.mother_name.like(like),
  122. BizResearchDonationParty.mother_id_card.like(like),
  123. BizResearchDonationParty.mother_phone.like(like),
  124. ),
  125. )
  126. if tenant_no:
  127. party_q = party_q.filter(BizResearchDonationParty.tenant_no == tenant_no)
  128. kw_conds = [
  129. BizResearchDonation.id.in_(party_q),
  130. BizResearchDonation.donation_no.like(like),
  131. ]
  132. from app.crud import sample_reception as reception_svc
  133. donation_ids = reception_svc.donation_ids_matching_lab_storage_keyword(db, tenant_no, kw)
  134. if donation_ids:
  135. kw_conds.append(BizResearchDonation.id.in_(donation_ids))
  136. sample_donation_q = (
  137. db.query(BizResearchDonationSample.research_donation_id)
  138. .filter(
  139. BizResearchDonationSample.del_flag == 0,
  140. BizResearchDonationSample.tenant_no == tenant_no,
  141. BizResearchDonationSample.sample_code.like(like),
  142. )
  143. )
  144. kw_conds.append(BizResearchDonation.id.in_(sample_donation_q))
  145. return q.filter(or_(*kw_conds))
  146. def _apply_create_time_filter(q, model, create_time_start: Optional[str], create_time_end: Optional[str]):
  147. if create_time_start:
  148. q = q.filter(model.create_time >= _normalize_date_bound(create_time_start))
  149. if create_time_end:
  150. q = q.filter(model.create_time <= _normalize_date_bound(create_time_end, end_of_day=True))
  151. return q
  152. def _apply_collection_sample_collect_time_filter(
  153. q,
  154. db: Session,
  155. tenant_no: str,
  156. sample_collect_time_start: Optional[str],
  157. sample_collect_time_end: Optional[str],
  158. ):
  159. if not sample_collect_time_start and not sample_collect_time_end:
  160. return q
  161. ci_q = db.query(BizProtocolCollectionInfo.collection_protocol_id).filter(
  162. BizProtocolCollectionInfo.del_flag == 0,
  163. )
  164. if tenant_no:
  165. ci_q = ci_q.filter(BizProtocolCollectionInfo.tenant_no == tenant_no)
  166. if sample_collect_time_start:
  167. ci_q = ci_q.filter(
  168. BizProtocolCollectionInfo.sample_collect_time
  169. >= _normalize_date_bound(sample_collect_time_start)
  170. )
  171. if sample_collect_time_end:
  172. ci_q = ci_q.filter(
  173. BizProtocolCollectionInfo.sample_collect_time
  174. <= _normalize_date_bound(sample_collect_time_end, end_of_day=True)
  175. )
  176. return q.filter(BizCollectionProtocol.id.in_(ci_q))
  177. def _apply_donation_sample_collect_time_filter(
  178. q,
  179. db: Session,
  180. tenant_no: str,
  181. sample_collect_time_start: Optional[str],
  182. sample_collect_time_end: Optional[str],
  183. ):
  184. if not sample_collect_time_start and not sample_collect_time_end:
  185. return q
  186. coll_q = db.query(BizResearchDonationCollection.research_donation_id).filter(
  187. BizResearchDonationCollection.del_flag == 0,
  188. )
  189. if tenant_no:
  190. coll_q = coll_q.filter(BizResearchDonationCollection.tenant_no == tenant_no)
  191. if sample_collect_time_start:
  192. coll_q = coll_q.filter(
  193. BizResearchDonationCollection.sample_collect_time
  194. >= _normalize_date_bound(sample_collect_time_start)
  195. )
  196. if sample_collect_time_end:
  197. coll_q = coll_q.filter(
  198. BizResearchDonationCollection.sample_collect_time
  199. <= _normalize_date_bound(sample_collect_time_end, end_of_day=True)
  200. )
  201. return q.filter(BizResearchDonation.id.in_(coll_q))
  202. def _earliest_eta(eta_values: List[str]) -> str:
  203. cleaned = [v.strip() for v in eta_values if (v or "").strip()]
  204. if not cleaned:
  205. return ""
  206. return min(cleaned)
  207. def _build_sample_items(
  208. samples: List[Any],
  209. *,
  210. sample_type_name_map: Dict[str, str],
  211. lab_code_map: Dict[str, str],
  212. transport_by_sample_id: Dict[str, Any],
  213. site_name_map: Dict[str, str],
  214. express_name_map: Dict[str, str],
  215. sample_filter,
  216. ) -> Tuple[List[Dict[str, Any]], List[str]]:
  217. items: List[Dict[str, Any]] = []
  218. eta_values: List[str] = []
  219. for s in samples:
  220. if not sample_filter(s):
  221. continue
  222. type_name = sample_type_name_map.get(getattr(s, "sample_type_id", None) or "", "")
  223. code = (getattr(s, "sample_code", None) or "").strip()
  224. transport_obj = transport_by_sample_id.get(s.id)
  225. transport_dict: Optional[Dict[str, Any]] = None
  226. if transport_obj and (transport_obj.transport_type or "").strip():
  227. dest_site_id = transport_obj.dest_site_id or ""
  228. express_company_id = transport_obj.express_company_id or ""
  229. eta = _fmt_dt(transport_obj.eta_time) if transport_obj.eta_time else ""
  230. if eta:
  231. eta_values.append(eta)
  232. transport_dict = {
  233. "transportType": transport_obj.transport_type or "",
  234. "destSiteId": dest_site_id,
  235. "destSiteName": site_name_map.get(dest_site_id, ""),
  236. "expressCompanyId": express_company_id,
  237. "expressCompanyName": express_name_map.get(express_company_id, ""),
  238. "waybillNo": transport_obj.waybill_no or "",
  239. "carrierName": transport_obj.carrier_name or "",
  240. "carrierPhone": transport_obj.carrier_phone or "",
  241. "etaTime": eta,
  242. }
  243. items.append(
  244. {
  245. "sampleTypeName": type_name,
  246. "sampleCode": code,
  247. "sampleStatus": (getattr(s, "sample_status", None) or "").strip(),
  248. "labStorageCode": lab_code_map.get(code, "") if code else "",
  249. "transport": transport_dict,
  250. **sample_special_to_dict(s),
  251. }
  252. )
  253. return items, eta_values
  254. def _summarize_in_transit_items(items: List[Dict[str, Any]]) -> str:
  255. n = len(items)
  256. if n <= 0:
  257. return ""
  258. return f"{n}个已采集(待接收)"
  259. def _summarize_pre_delivery(samples: List[Any], pre_kind: str) -> str:
  260. if pre_kind == "no_sample":
  261. return "样本未录入"
  262. n = sum(1 for s in samples if _is_uncollected_status(getattr(s, "sample_status", None)))
  263. return f"{n}个未采集" if n else "样本未采集"
  264. def _list_in_transit_collection_ids(
  265. db: Session,
  266. *,
  267. tenant_no: str,
  268. keyword: Optional[str],
  269. create_time_start: Optional[str],
  270. create_time_end: Optional[str],
  271. sample_collect_time_start: Optional[str],
  272. sample_collect_time_end: Optional[str],
  273. user_info,
  274. ) -> List[str]:
  275. q = (
  276. db.query(BizCollectionProtocol.id)
  277. .join(
  278. BizProtocolSample,
  279. and_(
  280. BizProtocolSample.collection_protocol_id == BizCollectionProtocol.id,
  281. BizProtocolSample.del_flag == 0,
  282. BizProtocolSample.tenant_no == tenant_no,
  283. BizProtocolSample.sample_status == "ycj",
  284. not_(_cell_received_exists(tenant_no)),
  285. ),
  286. )
  287. .filter(
  288. BizCollectionProtocol.del_flag == 0,
  289. BizCollectionProtocol.tenant_no == tenant_no,
  290. BizCollectionProtocol.protocol_status.notin_(_CELL_IN_TRANSIT_EXCLUDED),
  291. )
  292. )
  293. q = _apply_collection_keyword(q, db, tenant_no, keyword or "")
  294. q = _apply_create_time_filter(q, BizCollectionProtocol, create_time_start, create_time_end)
  295. q = _apply_collection_sample_collect_time_filter(
  296. q, db, tenant_no, sample_collect_time_start, sample_collect_time_end
  297. )
  298. if user_info:
  299. from app.core.data_scope import apply_data_scope
  300. q = apply_data_scope(q, BizCollectionProtocol, user_info, db, tenant_no)
  301. rows = q.distinct().all()
  302. return [r[0] for r in rows]
  303. def _list_in_transit_donation_ids(
  304. db: Session,
  305. *,
  306. tenant_no: str,
  307. keyword: Optional[str],
  308. create_time_start: Optional[str],
  309. create_time_end: Optional[str],
  310. sample_collect_time_start: Optional[str],
  311. sample_collect_time_end: Optional[str],
  312. user_info,
  313. ) -> List[str]:
  314. q = (
  315. db.query(BizResearchDonation.id)
  316. .join(
  317. BizResearchDonationSample,
  318. and_(
  319. BizResearchDonationSample.research_donation_id == BizResearchDonation.id,
  320. BizResearchDonationSample.del_flag == 0,
  321. BizResearchDonationSample.tenant_no == tenant_no,
  322. BizResearchDonationSample.sample_status == "ycj",
  323. not_(_donation_received_exists(tenant_no)),
  324. ),
  325. )
  326. .filter(
  327. BizResearchDonation.del_flag == 0,
  328. BizResearchDonation.tenant_no == tenant_no,
  329. BizResearchDonation.donation_status.notin_(_DONATION_IN_TRANSIT_EXCLUDED),
  330. )
  331. )
  332. q = _apply_donation_keyword(q, db, tenant_no, keyword or "")
  333. q = _apply_create_time_filter(q, BizResearchDonation, create_time_start, create_time_end)
  334. q = _apply_donation_sample_collect_time_filter(
  335. q, db, tenant_no, sample_collect_time_start, sample_collect_time_end
  336. )
  337. if user_info:
  338. from app.core.data_scope import apply_data_scope
  339. q = apply_data_scope(q, BizResearchDonation, user_info, db, tenant_no)
  340. rows = q.distinct().all()
  341. return [r[0] for r in rows]
  342. def _list_pre_delivery_collection_ids(
  343. db: Session,
  344. *,
  345. tenant_no: str,
  346. keyword: Optional[str],
  347. create_time_start: Optional[str],
  348. create_time_end: Optional[str],
  349. sample_collect_time_start: Optional[str],
  350. sample_collect_time_end: Optional[str],
  351. user_info,
  352. ) -> List[Tuple[str, str]]:
  353. has_sample = (
  354. exists()
  355. .where(
  356. and_(
  357. BizProtocolSample.collection_protocol_id == BizCollectionProtocol.id,
  358. BizProtocolSample.del_flag == 0,
  359. BizProtocolSample.tenant_no == tenant_no,
  360. )
  361. )
  362. .correlate(BizCollectionProtocol)
  363. )
  364. has_uncollected = (
  365. exists()
  366. .where(
  367. and_(
  368. BizProtocolSample.collection_protocol_id == BizCollectionProtocol.id,
  369. BizProtocolSample.del_flag == 0,
  370. BizProtocolSample.tenant_no == tenant_no,
  371. or_(
  372. BizProtocolSample.sample_status.is_(None),
  373. BizProtocolSample.sample_status == "",
  374. ),
  375. )
  376. )
  377. .correlate(BizCollectionProtocol)
  378. )
  379. q = db.query(BizCollectionProtocol.id).filter(
  380. BizCollectionProtocol.del_flag == 0,
  381. BizCollectionProtocol.tenant_no == tenant_no,
  382. BizCollectionProtocol.protocol_status.in_(_PRE_DELIVERY_STATUSES),
  383. or_(not_(has_sample), has_uncollected),
  384. )
  385. q = _apply_collection_keyword(q, db, tenant_no, keyword or "")
  386. q = _apply_create_time_filter(q, BizCollectionProtocol, create_time_start, create_time_end)
  387. q = _apply_collection_sample_collect_time_filter(
  388. q, db, tenant_no, sample_collect_time_start, sample_collect_time_end
  389. )
  390. if user_info:
  391. from app.core.data_scope import apply_data_scope
  392. q = apply_data_scope(q, BizCollectionProtocol, user_info, db, tenant_no)
  393. out: List[Tuple[str, str]] = []
  394. for (pid,) in q.all():
  395. sample_count = (
  396. db.query(func.count(BizProtocolSample.id))
  397. .filter(
  398. BizProtocolSample.collection_protocol_id == pid,
  399. BizProtocolSample.del_flag == 0,
  400. BizProtocolSample.tenant_no == tenant_no,
  401. )
  402. .scalar()
  403. or 0
  404. )
  405. kind = "no_sample" if sample_count == 0 else "uncollected"
  406. out.append((pid, kind))
  407. return out
  408. def _list_pre_delivery_donation_ids(
  409. db: Session,
  410. *,
  411. tenant_no: str,
  412. keyword: Optional[str],
  413. create_time_start: Optional[str],
  414. create_time_end: Optional[str],
  415. sample_collect_time_start: Optional[str],
  416. sample_collect_time_end: Optional[str],
  417. user_info,
  418. ) -> List[Tuple[str, str]]:
  419. has_sample = (
  420. exists()
  421. .where(
  422. and_(
  423. BizResearchDonationSample.research_donation_id == BizResearchDonation.id,
  424. BizResearchDonationSample.del_flag == 0,
  425. BizResearchDonationSample.tenant_no == tenant_no,
  426. )
  427. )
  428. .correlate(BizResearchDonation)
  429. )
  430. has_uncollected = (
  431. exists()
  432. .where(
  433. and_(
  434. BizResearchDonationSample.research_donation_id == BizResearchDonation.id,
  435. BizResearchDonationSample.del_flag == 0,
  436. BizResearchDonationSample.tenant_no == tenant_no,
  437. or_(
  438. BizResearchDonationSample.sample_status.is_(None),
  439. BizResearchDonationSample.sample_status == "",
  440. ),
  441. )
  442. )
  443. .correlate(BizResearchDonation)
  444. )
  445. q = db.query(BizResearchDonation.id).filter(
  446. BizResearchDonation.del_flag == 0,
  447. BizResearchDonation.tenant_no == tenant_no,
  448. BizResearchDonation.donation_status.in_(_PRE_DELIVERY_STATUSES),
  449. or_(not_(has_sample), has_uncollected),
  450. )
  451. q = _apply_donation_keyword(q, db, tenant_no, keyword or "")
  452. q = _apply_create_time_filter(q, BizResearchDonation, create_time_start, create_time_end)
  453. q = _apply_donation_sample_collect_time_filter(
  454. q, db, tenant_no, sample_collect_time_start, sample_collect_time_end
  455. )
  456. if user_info:
  457. from app.core.data_scope import apply_data_scope
  458. q = apply_data_scope(q, BizResearchDonation, user_info, db, tenant_no)
  459. out: List[Tuple[str, str]] = []
  460. for (did,) in q.all():
  461. sample_count = (
  462. db.query(func.count(BizResearchDonationSample.id))
  463. .filter(
  464. BizResearchDonationSample.research_donation_id == did,
  465. BizResearchDonationSample.del_flag == 0,
  466. BizResearchDonationSample.tenant_no == tenant_no,
  467. )
  468. .scalar()
  469. or 0
  470. )
  471. kind = "no_sample" if sample_count == 0 else "uncollected"
  472. out.append((did, kind))
  473. return out
  474. def _sort_key_in_transit(eta: str, create_time: Optional[datetime]) -> Tuple[int, str, datetime]:
  475. eta_clean = (eta or "").strip()
  476. return (0 if eta_clean else 1, eta_clean or "9999-99-99 99:99:99", create_time or datetime.min)
  477. def _sort_key_pre_delivery(expected: str, create_time: Optional[datetime]) -> Tuple[int, str, datetime]:
  478. exp = (expected or "").strip()
  479. return (0 if exp else 1, exp or "", create_time or datetime.min)
  480. def _hydrate_collection_rows(
  481. db: Session,
  482. *,
  483. tenant_no: str,
  484. protocol_ids: List[str],
  485. mode: str,
  486. pre_kinds: Optional[Dict[str, str]] = None,
  487. ) -> List[Dict[str, Any]]:
  488. if not protocol_ids:
  489. return []
  490. rows = (
  491. db.query(BizCollectionProtocol)
  492. .filter(
  493. BizCollectionProtocol.id.in_(protocol_ids),
  494. BizCollectionProtocol.del_flag == 0,
  495. BizCollectionProtocol.tenant_no == tenant_no,
  496. )
  497. .all()
  498. )
  499. row_map = {r.id: r for r in rows}
  500. pc_map: Dict[str, Dict[str, Any]] = {}
  501. for pc in (
  502. db.query(BizProtocolPartyCustomer)
  503. .filter(
  504. BizProtocolPartyCustomer.collection_protocol_id.in_(protocol_ids),
  505. BizProtocolPartyCustomer.del_flag == 0,
  506. )
  507. .all()
  508. ):
  509. pc_map[pc.collection_protocol_id] = _party_customer_to_dict(pc)
  510. ci_map: Dict[str, Dict[str, Any]] = {}
  511. for ci in (
  512. db.query(BizProtocolCollectionInfo)
  513. .filter(
  514. BizProtocolCollectionInfo.collection_protocol_id.in_(protocol_ids),
  515. BizProtocolCollectionInfo.del_flag == 0,
  516. )
  517. .all()
  518. ):
  519. ci_map[ci.collection_protocol_id] = _collection_info_to_dict(ci)
  520. region_ids = list({ci_map.get(pid, {}).get("regionId", "") for pid in protocol_ids} - {""})
  521. hospital_ids = list({ci_map.get(pid, {}).get("hospitalId", "") for pid in protocol_ids} - {""})
  522. region_name_map: Dict[str, str] = {}
  523. hospital_name_map: Dict[str, str] = {}
  524. if region_ids:
  525. for r in (
  526. db.query(BizCollectionRegion.id, BizCollectionRegion.region_name)
  527. .filter(BizCollectionRegion.id.in_(region_ids))
  528. .all()
  529. ):
  530. region_name_map[r.id] = r.region_name or ""
  531. if hospital_ids:
  532. for h in (
  533. db.query(BizCollectionHospital.id, BizCollectionHospital.hospital_name)
  534. .filter(BizCollectionHospital.id.in_(hospital_ids))
  535. .all()
  536. ):
  537. hospital_name_map[h.id] = h.hospital_name or ""
  538. sample_map: Dict[str, List[BizProtocolSample]] = {}
  539. s_rows = (
  540. db.query(BizProtocolSample)
  541. .filter(
  542. BizProtocolSample.collection_protocol_id.in_(protocol_ids),
  543. BizProtocolSample.del_flag == 0,
  544. )
  545. .order_by(BizProtocolSample.order_num.asc(), BizProtocolSample.create_time.asc())
  546. .all()
  547. )
  548. sample_type_name_map = _load_sample_type_name_map(db, s_rows)
  549. for s in s_rows:
  550. sample_map.setdefault(s.collection_protocol_id, []).append(s)
  551. all_codes: List[str] = []
  552. for samples in sample_map.values():
  553. for s in samples:
  554. c = (s.sample_code or "").strip()
  555. if c:
  556. all_codes.append(c)
  557. lab_code_map: Dict[str, str] = {}
  558. if all_codes:
  559. from app.crud import sample_reception as reception_svc
  560. lab_code_map = reception_svc.get_lab_storage_code_map_by_sample_codes(
  561. db, tenant_no, all_codes, reception_source="cell_storage"
  562. )
  563. transport_by_sample_id: Dict[str, BizProtocolSampleTransport] = {}
  564. for t in (
  565. db.query(BizProtocolSampleTransport)
  566. .filter(
  567. BizProtocolSampleTransport.collection_protocol_id.in_(protocol_ids),
  568. BizProtocolSampleTransport.tenant_no == tenant_no,
  569. BizProtocolSampleTransport.del_flag == 0,
  570. )
  571. .all()
  572. ):
  573. transport_by_sample_id[t.sample_id] = t
  574. site_name_map = _load_site_name_map(
  575. db, [t.dest_site_id for t in transport_by_sample_id.values() if t.dest_site_id]
  576. )
  577. express_name_map = _load_express_name_map(
  578. db,
  579. [t.express_company_id for t in transport_by_sample_id.values() if t.express_company_id],
  580. )
  581. received_codes: set = set()
  582. if mode == "in_transit" and all_codes:
  583. for rec in (
  584. db.query(BizSampleReception.sample_code)
  585. .filter(
  586. BizSampleReception.tenant_no == tenant_no,
  587. BizSampleReception.del_flag == 0,
  588. BizSampleReception.reception_source == "cell_storage",
  589. BizSampleReception.sample_code.in_(all_codes),
  590. )
  591. .all()
  592. ):
  593. if rec[0]:
  594. received_codes.add(rec[0])
  595. user_ids = list(
  596. {
  597. *(ci_map.get(pid, {}).get("salesUserId", "") for pid in protocol_ids),
  598. *(_protocol_to_dict(row_map[pid]).get("collectorUserId", "") for pid in protocol_ids if pid in row_map),
  599. }
  600. - {""}
  601. )
  602. user_name_map: Dict[str, str] = {}
  603. if user_ids:
  604. for u in db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(user_ids)).all():
  605. user_name_map[u.id] = u.nick_name or ""
  606. result: List[Dict[str, Any]] = []
  607. for pid in protocol_ids:
  608. r = row_map.get(pid)
  609. if not r:
  610. continue
  611. d = _protocol_to_dict(r)
  612. d["healthSurveyAttachmentFiles"] = urls_to_attachment_list(
  613. r.health_survey_form_urls, db, tenant_no
  614. )
  615. d["sourceType"] = "cell_storage"
  616. d["sourceTypeLabel"] = "细胞存储"
  617. d["collectionProtocolId"] = r.id
  618. d["researchDonationId"] = ""
  619. pc = pc_map.get(pid, {})
  620. ci = ci_map.get(pid, {})
  621. d["motherName"] = pc.get("motherName", "")
  622. d["motherPhone"] = pc.get("motherPhone", "")
  623. d["motherIdCard"] = pc.get("motherIdCard", "")
  624. d["fatherName"] = pc.get("fatherName", "")
  625. d["newbornName"] = pc.get("newbornName", "")
  626. d["expectedDeliveryDate"] = pc.get("expectedDeliveryDate", "")
  627. d["newbornDeliveryTime"] = pc.get("newbornDeliveryTime", "")
  628. d["newbornSex"] = pc.get("newbornSex", "")
  629. d["deliveryMode"] = pc.get("deliveryMode", "")
  630. d["newbornWeightGram"] = pc.get("newbornWeightGram")
  631. d["regionName"] = region_name_map.get(ci.get("regionId", ""), "")
  632. d["hospitalName"] = hospital_name_map.get(ci.get("hospitalId", ""), "")
  633. d["salesUserName"] = user_name_map.get(ci.get("salesUserId", ""), "")
  634. d["sampleCollectTime"] = ci.get("sampleCollectTime", "")
  635. d["maternalBloodCollectTime"] = ci.get("maternalBloodCollectTime", "")
  636. d["collectorUserName"] = user_name_map.get(d.get("collectorUserId", ""), "")
  637. samples = sample_map.get(pid, [])
  638. if mode == "in_transit":
  639. def _flt(s):
  640. code = (s.sample_code or "").strip()
  641. return _is_in_transit_sample(s.sample_status) and code not in received_codes
  642. else:
  643. pre_kind = (pre_kinds or {}).get(pid, "no_sample")
  644. def _flt(s):
  645. if pre_kind == "no_sample":
  646. return False
  647. return _is_uncollected_status(s.sample_status)
  648. items, eta_values = _build_sample_items(
  649. samples,
  650. sample_type_name_map=sample_type_name_map,
  651. lab_code_map=lab_code_map,
  652. transport_by_sample_id=transport_by_sample_id,
  653. site_name_map=site_name_map,
  654. express_name_map=express_name_map,
  655. sample_filter=_flt,
  656. )
  657. type_names = [it["sampleTypeName"] for it in items if it.get("sampleTypeName")]
  658. codes = [it["sampleCode"] for it in items if it.get("sampleCode")]
  659. d["sampleItems"] = items
  660. d["sampleTypeNames"] = ",".join(type_names)
  661. d["sampleCodes"] = ",".join(codes)
  662. d["labStorageCodes"] = ",".join(
  663. (it.get("labStorageCode") or "") for it in items if it.get("labStorageCode")
  664. )
  665. if mode == "in_transit":
  666. d["sampleCollectSummary"] = _summarize_in_transit_items(items)
  667. d["etaTimeSummary"] = _earliest_eta(eta_values)
  668. else:
  669. pre_kind = (pre_kinds or {}).get(pid, "no_sample")
  670. d["preDeliveryKind"] = pre_kind
  671. d["preDeliveryKindLabel"] = "样本未录入" if pre_kind == "no_sample" else "样本未采集"
  672. d["sampleCollectSummary"] = _summarize_pre_delivery(samples, pre_kind)
  673. d["etaTimeSummary"] = ""
  674. result.append(d)
  675. return result
  676. def _hydrate_donation_rows(
  677. db: Session,
  678. *,
  679. tenant_no: str,
  680. donation_ids: List[str],
  681. mode: str,
  682. pre_kinds: Optional[Dict[str, str]] = None,
  683. ) -> List[Dict[str, Any]]:
  684. if not donation_ids:
  685. return []
  686. rows = (
  687. db.query(BizResearchDonation)
  688. .filter(
  689. BizResearchDonation.id.in_(donation_ids),
  690. BizResearchDonation.del_flag == 0,
  691. BizResearchDonation.tenant_no == tenant_no,
  692. )
  693. .all()
  694. )
  695. row_map = {r.id: r for r in rows}
  696. party_map: Dict[str, Dict[str, Any]] = {}
  697. for p in (
  698. db.query(BizResearchDonationParty)
  699. .filter(
  700. BizResearchDonationParty.research_donation_id.in_(donation_ids),
  701. BizResearchDonationParty.del_flag == 0,
  702. )
  703. .all()
  704. ):
  705. party_map[p.research_donation_id] = _party_to_dict(p)
  706. coll_map: Dict[str, Dict[str, Any]] = {}
  707. for c in (
  708. db.query(BizResearchDonationCollection)
  709. .filter(
  710. BizResearchDonationCollection.research_donation_id.in_(donation_ids),
  711. BizResearchDonationCollection.del_flag == 0,
  712. )
  713. .all()
  714. ):
  715. coll_map[c.research_donation_id] = _collection_to_dict(c)
  716. region_ids = list({coll_map.get(did, {}).get("regionId", "") for did in donation_ids} - {""})
  717. hospital_ids = list({coll_map.get(did, {}).get("hospitalId", "") for did in donation_ids} - {""})
  718. region_name_map: Dict[str, str] = {}
  719. hospital_name_map: Dict[str, str] = {}
  720. if region_ids:
  721. for r in (
  722. db.query(BizCollectionRegion.id, BizCollectionRegion.region_name)
  723. .filter(BizCollectionRegion.id.in_(region_ids))
  724. .all()
  725. ):
  726. region_name_map[r.id] = r.region_name or ""
  727. if hospital_ids:
  728. for h in (
  729. db.query(BizCollectionHospital.id, BizCollectionHospital.hospital_name)
  730. .filter(BizCollectionHospital.id.in_(hospital_ids))
  731. .all()
  732. ):
  733. hospital_name_map[h.id] = h.hospital_name or ""
  734. sample_map: Dict[str, List[BizResearchDonationSample]] = {}
  735. s_rows = (
  736. db.query(BizResearchDonationSample)
  737. .filter(
  738. BizResearchDonationSample.research_donation_id.in_(donation_ids),
  739. BizResearchDonationSample.del_flag == 0,
  740. )
  741. .order_by(BizResearchDonationSample.order_num.asc(), BizResearchDonationSample.create_time.asc())
  742. .all()
  743. )
  744. sample_type_name_map = _load_sample_type_name_map(db, s_rows)
  745. for s in s_rows:
  746. sample_map.setdefault(s.research_donation_id, []).append(s)
  747. all_codes: List[str] = []
  748. for samples in sample_map.values():
  749. for s in samples:
  750. c = (s.sample_code or "").strip()
  751. if c:
  752. all_codes.append(c)
  753. lab_code_map: Dict[str, str] = {}
  754. if all_codes:
  755. from app.crud import sample_reception as reception_svc
  756. lab_code_map = reception_svc.get_lab_storage_code_map_by_sample_codes(
  757. db, tenant_no, all_codes, reception_source="research_donation"
  758. )
  759. transport_by_sample_id: Dict[str, BizResearchDonationSampleTransport] = {}
  760. for t in (
  761. db.query(BizResearchDonationSampleTransport)
  762. .filter(
  763. BizResearchDonationSampleTransport.research_donation_id.in_(donation_ids),
  764. BizResearchDonationSampleTransport.tenant_no == tenant_no,
  765. BizResearchDonationSampleTransport.del_flag == 0,
  766. )
  767. .all()
  768. ):
  769. transport_by_sample_id[t.sample_id] = t
  770. site_name_map = _load_site_name_map(
  771. db, [t.dest_site_id for t in transport_by_sample_id.values() if t.dest_site_id]
  772. )
  773. express_name_map = _load_express_name_map(
  774. db,
  775. [t.express_company_id for t in transport_by_sample_id.values() if t.express_company_id],
  776. )
  777. received_codes: set = set()
  778. if mode == "in_transit" and all_codes:
  779. for rec in (
  780. db.query(BizSampleReception.sample_code)
  781. .filter(
  782. BizSampleReception.tenant_no == tenant_no,
  783. BizSampleReception.del_flag == 0,
  784. BizSampleReception.reception_source == "research_donation",
  785. BizSampleReception.sample_code.in_(all_codes),
  786. )
  787. .all()
  788. ):
  789. if rec[0]:
  790. received_codes.add(rec[0])
  791. user_ids = list(
  792. {
  793. *(coll_map.get(did, {}).get("salesUserId", "") for did in donation_ids),
  794. *(row_map[did].collector_user_id or "" for did in donation_ids if did in row_map),
  795. }
  796. - {""}
  797. )
  798. user_name_map: Dict[str, str] = {}
  799. if user_ids:
  800. for u in db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(user_ids)).all():
  801. user_name_map[u.id] = u.nick_name or ""
  802. result: List[Dict[str, Any]] = []
  803. for did in donation_ids:
  804. r = row_map.get(did)
  805. if not r:
  806. continue
  807. d = _donation_to_dict(r, db, tenant_no)
  808. d["sourceType"] = "research_donation"
  809. d["sourceTypeLabel"] = "科研捐赠"
  810. d["collectionProtocolId"] = ""
  811. d["researchDonationId"] = r.id
  812. d["protocolNo"] = d.get("donationNo", "")
  813. d["protocolStatus"] = d.get("donationStatus", "")
  814. d["registerDate"] = d.get("donationDate", "")
  815. party = party_map.get(did, {})
  816. coll = coll_map.get(did, {})
  817. d["motherName"] = party.get("motherName", "")
  818. d["motherPhone"] = party.get("motherPhone", "")
  819. d["motherIdCard"] = party.get("motherIdCard", "")
  820. d["fatherName"] = party.get("fatherName", "")
  821. d["newbornName"] = party.get("newbornName", "")
  822. d["expectedDeliveryDate"] = party.get("expectedDeliveryDate", "")
  823. d["newbornDeliveryTime"] = party.get("newbornDeliveryTime", "")
  824. d["newbornSex"] = party.get("newbornSex", "")
  825. d["deliveryMode"] = party.get("deliveryMode", "")
  826. d["newbornWeightGram"] = party.get("newbornWeightGram")
  827. d["regionName"] = region_name_map.get(coll.get("regionId", ""), "")
  828. d["hospitalName"] = hospital_name_map.get(coll.get("hospitalId", ""), "")
  829. d["salesUserName"] = user_name_map.get(coll.get("salesUserId", ""), "")
  830. d["sampleCollectTime"] = coll.get("sampleCollectTime", "")
  831. d["maternalBloodCollectTime"] = coll.get("maternalBloodCollectTime", "")
  832. d["collectorUserName"] = user_name_map.get(r.collector_user_id or "", "")
  833. samples = sample_map.get(did, [])
  834. if mode == "in_transit":
  835. def _flt(s):
  836. code = (s.sample_code or "").strip()
  837. return _is_in_transit_sample(s.sample_status) and code not in received_codes
  838. else:
  839. pre_kind = (pre_kinds or {}).get(did, "no_sample")
  840. def _flt(s):
  841. if pre_kind == "no_sample":
  842. return False
  843. return _is_uncollected_status(s.sample_status)
  844. items, eta_values = _build_sample_items(
  845. samples,
  846. sample_type_name_map=sample_type_name_map,
  847. lab_code_map=lab_code_map,
  848. transport_by_sample_id=transport_by_sample_id,
  849. site_name_map=site_name_map,
  850. express_name_map=express_name_map,
  851. sample_filter=_flt,
  852. )
  853. type_names = [it["sampleTypeName"] for it in items if it.get("sampleTypeName")]
  854. codes = [it["sampleCode"] for it in items if it.get("sampleCode")]
  855. d["sampleItems"] = items
  856. d["sampleTypeNames"] = ",".join(type_names)
  857. d["sampleCodes"] = ",".join(codes)
  858. d["labStorageCodes"] = ",".join(
  859. (it.get("labStorageCode") or "") for it in items if it.get("labStorageCode")
  860. )
  861. if mode == "in_transit":
  862. d["sampleCollectSummary"] = _summarize_in_transit_items(items)
  863. d["etaTimeSummary"] = _earliest_eta(eta_values)
  864. else:
  865. pre_kind = (pre_kinds or {}).get(did, "no_sample")
  866. d["preDeliveryKind"] = pre_kind
  867. d["preDeliveryKindLabel"] = "样本未录入" if pre_kind == "no_sample" else "样本未采集"
  868. d["sampleCollectSummary"] = _summarize_pre_delivery(samples, pre_kind)
  869. d["etaTimeSummary"] = ""
  870. result.append(d)
  871. return result
  872. def list_in_transit_protocols(
  873. db: Session,
  874. *,
  875. page: int = 1,
  876. page_size: int = 10,
  877. tenant_no: str,
  878. keyword: Optional[str] = None,
  879. source_type: Optional[str] = None,
  880. create_time_start: Optional[str] = None,
  881. create_time_end: Optional[str] = None,
  882. sample_collect_time_start: Optional[str] = None,
  883. sample_collect_time_end: Optional[str] = None,
  884. user_info=None,
  885. ) -> Tuple[List[Dict[str, Any]], int]:
  886. if not tenant_no:
  887. return [], 0
  888. candidates: List[Tuple[str, str, str, Optional[datetime]]] = []
  889. if not source_type or source_type == "cell_storage":
  890. for pid in _list_in_transit_collection_ids(
  891. db,
  892. tenant_no=tenant_no,
  893. keyword=keyword,
  894. create_time_start=create_time_start,
  895. create_time_end=create_time_end,
  896. sample_collect_time_start=sample_collect_time_start,
  897. sample_collect_time_end=sample_collect_time_end,
  898. user_info=user_info,
  899. ):
  900. row = (
  901. db.query(BizCollectionProtocol.create_time)
  902. .filter(BizCollectionProtocol.id == pid)
  903. .first()
  904. )
  905. candidates.append(("cell_storage", pid, "", row[0] if row else None))
  906. if not source_type or source_type == "research_donation":
  907. for did in _list_in_transit_donation_ids(
  908. db,
  909. tenant_no=tenant_no,
  910. keyword=keyword,
  911. create_time_start=create_time_start,
  912. create_time_end=create_time_end,
  913. sample_collect_time_start=sample_collect_time_start,
  914. sample_collect_time_end=sample_collect_time_end,
  915. user_info=user_info,
  916. ):
  917. row = (
  918. db.query(BizResearchDonation.create_time)
  919. .filter(BizResearchDonation.id == did)
  920. .first()
  921. )
  922. candidates.append(("research_donation", did, "", row[0] if row else None))
  923. if not candidates:
  924. return [], 0
  925. # 预计算 eta 用于排序
  926. cell_ids = [pid for st, pid, _, _ in candidates if st == "cell_storage"]
  927. donation_ids = [did for st, did, _, _ in candidates if st == "research_donation"]
  928. eta_by_protocol: Dict[str, str] = {}
  929. if cell_ids:
  930. for pid, eta in (
  931. db.query(
  932. BizProtocolSample.collection_protocol_id,
  933. func.min(BizProtocolSampleTransport.eta_time),
  934. )
  935. .join(
  936. BizProtocolSampleTransport,
  937. and_(
  938. BizProtocolSampleTransport.sample_id == BizProtocolSample.id,
  939. BizProtocolSampleTransport.del_flag == 0,
  940. ),
  941. )
  942. .filter(
  943. BizProtocolSample.collection_protocol_id.in_(cell_ids),
  944. BizProtocolSample.del_flag == 0,
  945. BizProtocolSample.sample_status == "ycj",
  946. not_(_cell_received_exists(tenant_no)),
  947. )
  948. .group_by(BizProtocolSample.collection_protocol_id)
  949. .all()
  950. ):
  951. eta_by_protocol[f"cell_storage:{pid}"] = _fmt_dt(eta) if eta else ""
  952. if donation_ids:
  953. for did, eta in (
  954. db.query(
  955. BizResearchDonationSample.research_donation_id,
  956. func.min(BizResearchDonationSampleTransport.eta_time),
  957. )
  958. .join(
  959. BizResearchDonationSampleTransport,
  960. and_(
  961. BizResearchDonationSampleTransport.sample_id == BizResearchDonationSample.id,
  962. BizResearchDonationSampleTransport.del_flag == 0,
  963. ),
  964. )
  965. .filter(
  966. BizResearchDonationSample.research_donation_id.in_(donation_ids),
  967. BizResearchDonationSample.del_flag == 0,
  968. BizResearchDonationSample.sample_status == "ycj",
  969. not_(_donation_received_exists(tenant_no)),
  970. )
  971. .group_by(BizResearchDonationSample.research_donation_id)
  972. .all()
  973. ):
  974. eta_by_protocol[f"research_donation:{did}"] = _fmt_dt(eta) if eta else ""
  975. enriched: List[Tuple[str, str, str, Optional[datetime]]] = []
  976. for st, pid, _, ct in candidates:
  977. eta = eta_by_protocol.get(f"{st}:{pid}", "")
  978. enriched.append((st, pid, eta, ct))
  979. enriched.sort(key=lambda x: _sort_key_in_transit(x[2], x[3]))
  980. total = len(enriched)
  981. page = max(1, page)
  982. page_size = min(max(1, page_size), 100)
  983. page_slice = enriched[(page - 1) * page_size : page * page_size]
  984. cell_page = [pid for st, pid, _, _ in page_slice if st == "cell_storage"]
  985. donation_page = [pid for st, pid, _, _ in page_slice if st == "research_donation"]
  986. items: List[Dict[str, Any]] = []
  987. items.extend(
  988. _hydrate_collection_rows(db, tenant_no=tenant_no, protocol_ids=cell_page, mode="in_transit")
  989. )
  990. items.extend(
  991. _hydrate_donation_rows(db, tenant_no=tenant_no, donation_ids=donation_page, mode="in_transit")
  992. )
  993. order_keys = [f"{st}:{pid}" for st, pid, _, _ in page_slice]
  994. item_map = {f"{it['sourceType']}:{it.get('collectionProtocolId') or it.get('researchDonationId')}": it for it in items}
  995. ordered = [item_map[k] for k in order_keys if k in item_map]
  996. return ordered, total
  997. def list_pre_delivery_protocols(
  998. db: Session,
  999. *,
  1000. page: int = 1,
  1001. page_size: int = 10,
  1002. tenant_no: str,
  1003. keyword: Optional[str] = None,
  1004. source_type: Optional[str] = None,
  1005. create_time_start: Optional[str] = None,
  1006. create_time_end: Optional[str] = None,
  1007. sample_collect_time_start: Optional[str] = None,
  1008. sample_collect_time_end: Optional[str] = None,
  1009. user_info=None,
  1010. ) -> Tuple[List[Dict[str, Any]], int]:
  1011. if not tenant_no:
  1012. return [], 0
  1013. candidates: List[Tuple[str, str, str, Optional[datetime], str]] = []
  1014. if not source_type or source_type == "cell_storage":
  1015. for pid, kind in _list_pre_delivery_collection_ids(
  1016. db,
  1017. tenant_no=tenant_no,
  1018. keyword=keyword,
  1019. create_time_start=create_time_start,
  1020. create_time_end=create_time_end,
  1021. sample_collect_time_start=sample_collect_time_start,
  1022. sample_collect_time_end=sample_collect_time_end,
  1023. user_info=user_info,
  1024. ):
  1025. proto = db.query(BizCollectionProtocol).filter(BizCollectionProtocol.id == pid).first()
  1026. party = (
  1027. db.query(BizProtocolPartyCustomer.expected_delivery_date)
  1028. .filter(
  1029. BizProtocolPartyCustomer.collection_protocol_id == pid,
  1030. BizProtocolPartyCustomer.del_flag == 0,
  1031. )
  1032. .first()
  1033. )
  1034. exp = ""
  1035. if party and party[0]:
  1036. from app.core.utils import format_date
  1037. exp = format_date(party[0])
  1038. candidates.append(
  1039. (
  1040. "cell_storage",
  1041. pid,
  1042. exp,
  1043. proto.create_time if proto else None,
  1044. kind,
  1045. )
  1046. )
  1047. if not source_type or source_type == "research_donation":
  1048. for did, kind in _list_pre_delivery_donation_ids(
  1049. db,
  1050. tenant_no=tenant_no,
  1051. keyword=keyword,
  1052. create_time_start=create_time_start,
  1053. create_time_end=create_time_end,
  1054. sample_collect_time_start=sample_collect_time_start,
  1055. sample_collect_time_end=sample_collect_time_end,
  1056. user_info=user_info,
  1057. ):
  1058. donation = db.query(BizResearchDonation).filter(BizResearchDonation.id == did).first()
  1059. party = (
  1060. db.query(BizResearchDonationParty.expected_delivery_date)
  1061. .filter(
  1062. BizResearchDonationParty.research_donation_id == did,
  1063. BizResearchDonationParty.del_flag == 0,
  1064. )
  1065. .first()
  1066. )
  1067. exp = ""
  1068. if party and party[0]:
  1069. from app.core.utils import format_date
  1070. exp = format_date(party[0])
  1071. candidates.append(
  1072. (
  1073. "research_donation",
  1074. did,
  1075. exp,
  1076. donation.create_time if donation else None,
  1077. kind,
  1078. )
  1079. )
  1080. if not candidates:
  1081. return [], 0
  1082. candidates.sort(
  1083. key=lambda x: (
  1084. (x[2] or "").strip() or "0000-00-00",
  1085. x[3] or datetime.min,
  1086. ),
  1087. reverse=True,
  1088. )
  1089. total = len(candidates)
  1090. page = max(1, page)
  1091. page_size = min(max(1, page_size), 100)
  1092. page_slice = candidates[(page - 1) * page_size : page * page_size]
  1093. cell_page = [pid for st, pid, _, _, _ in page_slice if st == "cell_storage"]
  1094. donation_page = [did for st, did, _, _, _ in page_slice if st == "research_donation"]
  1095. pre_kinds_cell = {pid: kind for st, pid, _, _, kind in page_slice if st == "cell_storage"}
  1096. pre_kinds_donation = {did: kind for st, did, _, _, kind in page_slice if st == "research_donation"}
  1097. items: List[Dict[str, Any]] = []
  1098. items.extend(
  1099. _hydrate_collection_rows(
  1100. db,
  1101. tenant_no=tenant_no,
  1102. protocol_ids=cell_page,
  1103. mode="pre_delivery",
  1104. pre_kinds=pre_kinds_cell,
  1105. )
  1106. )
  1107. items.extend(
  1108. _hydrate_donation_rows(
  1109. db,
  1110. tenant_no=tenant_no,
  1111. donation_ids=donation_page,
  1112. mode="pre_delivery",
  1113. pre_kinds=pre_kinds_donation,
  1114. )
  1115. )
  1116. order_keys = [f"{st}:{pid}" for st, pid, _, _, _ in page_slice]
  1117. item_map = {f"{it['sourceType']}:{it.get('collectionProtocolId') or it.get('researchDonationId')}": it for it in items}
  1118. ordered = [item_map[k] for k in order_keys if k in item_map]
  1119. return ordered, total
  1120. __all__ = ["list_in_transit_protocols", "list_pre_delivery_protocols", "total_pages"]