"""预产及在途样本 — 质检看板(协议单维度列表)""" from __future__ import annotations from datetime import datetime from typing import Any, Dict, List, Optional, Tuple from sqlalchemy import and_, exists, func, not_, or_ from sqlalchemy.orm import Session from app.crud.collection_protocol import ( _collection_info_to_dict, _fmt_dt, _load_express_name_map, _load_sample_type_name_map, _load_site_name_map, _normalize_date_bound, _party_customer_to_dict, _protocol_to_dict, total_pages, urls_to_attachment_list, ) from app.utils.sample_special import sample_special_to_dict from app.crud.research_donation import ( _collection_to_dict, _donation_to_dict, _party_to_dict, ) from app.models.models import ( BizCollectionHospital, BizCollectionProtocol, BizCollectionRegion, BizProtocolCollectionInfo, BizProtocolPartyCustomer, BizProtocolSample, BizProtocolSampleTransport, BizSampleReception, SysUser, ) from app.models.research_donation import ( BizResearchDonation, BizResearchDonationCollection, BizResearchDonationParty, BizResearchDonationSample, BizResearchDonationSampleTransport, ) _CELL_IN_TRANSIT_EXCLUDED = ("yzf", "sdfq", "zky", "ygq") _DONATION_IN_TRANSIT_EXCLUDED = ("yzf", "sdfq", "zckc", "ygq") _PRE_DELIVERY_STATUSES = ("dsx", "sxz") def _is_uncollected_status(status: Optional[str]) -> bool: return not (status or "").strip() def _is_in_transit_sample(status: Optional[str]) -> bool: return (status or "").strip() == "ycj" def _cell_received_exists(tenant_no: str): return ( exists() .where( and_( BizSampleReception.tenant_no == tenant_no, BizSampleReception.del_flag == 0, BizSampleReception.reception_source == "cell_storage", BizSampleReception.sample_code.isnot(None), BizSampleReception.sample_code != "", BizSampleReception.sample_code == BizProtocolSample.sample_code, ) ) .correlate(BizProtocolSample) ) def _donation_received_exists(tenant_no: str): return ( exists() .where( and_( BizSampleReception.tenant_no == tenant_no, BizSampleReception.del_flag == 0, BizSampleReception.reception_source == "research_donation", BizSampleReception.sample_code.isnot(None), BizSampleReception.sample_code != "", BizSampleReception.sample_code == BizResearchDonationSample.sample_code, ) ) .correlate(BizResearchDonationSample) ) def _apply_collection_keyword(q, db: Session, tenant_no: str, keyword: str): kw = (keyword or "").strip() if not kw: return q like = f"%{kw}%" party_q = db.query(BizProtocolPartyCustomer.collection_protocol_id).filter( BizProtocolPartyCustomer.del_flag == 0, or_( BizProtocolPartyCustomer.mother_name.like(like), BizProtocolPartyCustomer.mother_id_card.like(like), BizProtocolPartyCustomer.mother_phone.like(like), ), ) if tenant_no: party_q = party_q.filter(BizProtocolPartyCustomer.tenant_no == tenant_no) kw_conds = [ BizCollectionProtocol.id.in_(party_q), BizCollectionProtocol.protocol_no.like(like), ] from app.crud import sample_reception as reception_svc proto_ids = reception_svc.protocol_ids_matching_lab_storage_keyword(db, tenant_no, kw) if proto_ids: kw_conds.append(BizCollectionProtocol.id.in_(proto_ids)) sample_proto_q = ( db.query(BizProtocolSample.collection_protocol_id) .filter( BizProtocolSample.del_flag == 0, BizProtocolSample.tenant_no == tenant_no, BizProtocolSample.sample_code.like(like), ) ) kw_conds.append(BizCollectionProtocol.id.in_(sample_proto_q)) return q.filter(or_(*kw_conds)) def _apply_donation_keyword(q, db: Session, tenant_no: str, keyword: str): kw = (keyword or "").strip() if not kw: return q like = f"%{kw}%" party_q = db.query(BizResearchDonationParty.research_donation_id).filter( BizResearchDonationParty.del_flag == 0, or_( BizResearchDonationParty.mother_name.like(like), BizResearchDonationParty.mother_id_card.like(like), BizResearchDonationParty.mother_phone.like(like), ), ) if tenant_no: party_q = party_q.filter(BizResearchDonationParty.tenant_no == tenant_no) kw_conds = [ BizResearchDonation.id.in_(party_q), BizResearchDonation.donation_no.like(like), ] from app.crud import sample_reception as reception_svc donation_ids = reception_svc.donation_ids_matching_lab_storage_keyword(db, tenant_no, kw) if donation_ids: kw_conds.append(BizResearchDonation.id.in_(donation_ids)) sample_donation_q = ( db.query(BizResearchDonationSample.research_donation_id) .filter( BizResearchDonationSample.del_flag == 0, BizResearchDonationSample.tenant_no == tenant_no, BizResearchDonationSample.sample_code.like(like), ) ) kw_conds.append(BizResearchDonation.id.in_(sample_donation_q)) return q.filter(or_(*kw_conds)) def _apply_create_time_filter(q, model, create_time_start: Optional[str], create_time_end: Optional[str]): if create_time_start: q = q.filter(model.create_time >= _normalize_date_bound(create_time_start)) if create_time_end: q = q.filter(model.create_time <= _normalize_date_bound(create_time_end, end_of_day=True)) return q def _apply_collection_sample_collect_time_filter( q, db: Session, tenant_no: str, sample_collect_time_start: Optional[str], sample_collect_time_end: Optional[str], ): if not sample_collect_time_start and not sample_collect_time_end: return q ci_q = db.query(BizProtocolCollectionInfo.collection_protocol_id).filter( BizProtocolCollectionInfo.del_flag == 0, ) if tenant_no: ci_q = ci_q.filter(BizProtocolCollectionInfo.tenant_no == tenant_no) if sample_collect_time_start: ci_q = ci_q.filter( BizProtocolCollectionInfo.sample_collect_time >= _normalize_date_bound(sample_collect_time_start) ) if sample_collect_time_end: ci_q = ci_q.filter( BizProtocolCollectionInfo.sample_collect_time <= _normalize_date_bound(sample_collect_time_end, end_of_day=True) ) return q.filter(BizCollectionProtocol.id.in_(ci_q)) def _apply_donation_sample_collect_time_filter( q, db: Session, tenant_no: str, sample_collect_time_start: Optional[str], sample_collect_time_end: Optional[str], ): if not sample_collect_time_start and not sample_collect_time_end: return q coll_q = db.query(BizResearchDonationCollection.research_donation_id).filter( BizResearchDonationCollection.del_flag == 0, ) if tenant_no: coll_q = coll_q.filter(BizResearchDonationCollection.tenant_no == tenant_no) if sample_collect_time_start: coll_q = coll_q.filter( BizResearchDonationCollection.sample_collect_time >= _normalize_date_bound(sample_collect_time_start) ) if sample_collect_time_end: coll_q = coll_q.filter( BizResearchDonationCollection.sample_collect_time <= _normalize_date_bound(sample_collect_time_end, end_of_day=True) ) return q.filter(BizResearchDonation.id.in_(coll_q)) def _earliest_eta(eta_values: List[str]) -> str: cleaned = [v.strip() for v in eta_values if (v or "").strip()] if not cleaned: return "" return min(cleaned) def _build_sample_items( samples: List[Any], *, sample_type_name_map: Dict[str, str], lab_code_map: Dict[str, str], transport_by_sample_id: Dict[str, Any], site_name_map: Dict[str, str], express_name_map: Dict[str, str], sample_filter, ) -> Tuple[List[Dict[str, Any]], List[str]]: items: List[Dict[str, Any]] = [] eta_values: List[str] = [] for s in samples: if not sample_filter(s): continue type_name = sample_type_name_map.get(getattr(s, "sample_type_id", None) or "", "") code = (getattr(s, "sample_code", None) or "").strip() transport_obj = transport_by_sample_id.get(s.id) transport_dict: Optional[Dict[str, Any]] = None if transport_obj and (transport_obj.transport_type or "").strip(): dest_site_id = transport_obj.dest_site_id or "" express_company_id = transport_obj.express_company_id or "" eta = _fmt_dt(transport_obj.eta_time) if transport_obj.eta_time else "" if eta: eta_values.append(eta) transport_dict = { "transportType": transport_obj.transport_type or "", "destSiteId": dest_site_id, "destSiteName": site_name_map.get(dest_site_id, ""), "expressCompanyId": express_company_id, "expressCompanyName": express_name_map.get(express_company_id, ""), "waybillNo": transport_obj.waybill_no or "", "carrierName": transport_obj.carrier_name or "", "carrierPhone": transport_obj.carrier_phone or "", "etaTime": eta, } items.append( { "sampleTypeName": type_name, "sampleCode": code, "sampleStatus": (getattr(s, "sample_status", None) or "").strip(), "labStorageCode": lab_code_map.get(code, "") if code else "", "transport": transport_dict, **sample_special_to_dict(s), } ) return items, eta_values def _summarize_in_transit_items(items: List[Dict[str, Any]]) -> str: n = len(items) if n <= 0: return "" return f"{n}个已采集(待接收)" def _summarize_pre_delivery(samples: List[Any], pre_kind: str) -> str: if pre_kind == "no_sample": return "样本未录入" n = sum(1 for s in samples if _is_uncollected_status(getattr(s, "sample_status", None))) return f"{n}个未采集" if n else "样本未采集" def _list_in_transit_collection_ids( db: Session, *, tenant_no: str, keyword: Optional[str], create_time_start: Optional[str], create_time_end: Optional[str], sample_collect_time_start: Optional[str], sample_collect_time_end: Optional[str], user_info, ) -> List[str]: q = ( db.query(BizCollectionProtocol.id) .join( BizProtocolSample, and_( BizProtocolSample.collection_protocol_id == BizCollectionProtocol.id, BizProtocolSample.del_flag == 0, BizProtocolSample.tenant_no == tenant_no, BizProtocolSample.sample_status == "ycj", not_(_cell_received_exists(tenant_no)), ), ) .filter( BizCollectionProtocol.del_flag == 0, BizCollectionProtocol.tenant_no == tenant_no, BizCollectionProtocol.protocol_status.notin_(_CELL_IN_TRANSIT_EXCLUDED), ) ) q = _apply_collection_keyword(q, db, tenant_no, keyword or "") q = _apply_create_time_filter(q, BizCollectionProtocol, create_time_start, create_time_end) q = _apply_collection_sample_collect_time_filter( q, db, tenant_no, sample_collect_time_start, sample_collect_time_end ) if user_info: from app.core.data_scope import apply_data_scope q = apply_data_scope(q, BizCollectionProtocol, user_info, db, tenant_no) rows = q.distinct().all() return [r[0] for r in rows] def _list_in_transit_donation_ids( db: Session, *, tenant_no: str, keyword: Optional[str], create_time_start: Optional[str], create_time_end: Optional[str], sample_collect_time_start: Optional[str], sample_collect_time_end: Optional[str], user_info, ) -> List[str]: q = ( db.query(BizResearchDonation.id) .join( BizResearchDonationSample, and_( BizResearchDonationSample.research_donation_id == BizResearchDonation.id, BizResearchDonationSample.del_flag == 0, BizResearchDonationSample.tenant_no == tenant_no, BizResearchDonationSample.sample_status == "ycj", not_(_donation_received_exists(tenant_no)), ), ) .filter( BizResearchDonation.del_flag == 0, BizResearchDonation.tenant_no == tenant_no, BizResearchDonation.donation_status.notin_(_DONATION_IN_TRANSIT_EXCLUDED), ) ) q = _apply_donation_keyword(q, db, tenant_no, keyword or "") q = _apply_create_time_filter(q, BizResearchDonation, create_time_start, create_time_end) q = _apply_donation_sample_collect_time_filter( q, db, tenant_no, sample_collect_time_start, sample_collect_time_end ) if user_info: from app.core.data_scope import apply_data_scope q = apply_data_scope(q, BizResearchDonation, user_info, db, tenant_no) rows = q.distinct().all() return [r[0] for r in rows] def _list_pre_delivery_collection_ids( db: Session, *, tenant_no: str, keyword: Optional[str], create_time_start: Optional[str], create_time_end: Optional[str], sample_collect_time_start: Optional[str], sample_collect_time_end: Optional[str], user_info, ) -> List[Tuple[str, str]]: has_sample = ( exists() .where( and_( BizProtocolSample.collection_protocol_id == BizCollectionProtocol.id, BizProtocolSample.del_flag == 0, BizProtocolSample.tenant_no == tenant_no, ) ) .correlate(BizCollectionProtocol) ) has_uncollected = ( exists() .where( and_( BizProtocolSample.collection_protocol_id == BizCollectionProtocol.id, BizProtocolSample.del_flag == 0, BizProtocolSample.tenant_no == tenant_no, or_( BizProtocolSample.sample_status.is_(None), BizProtocolSample.sample_status == "", ), ) ) .correlate(BizCollectionProtocol) ) q = db.query(BizCollectionProtocol.id).filter( BizCollectionProtocol.del_flag == 0, BizCollectionProtocol.tenant_no == tenant_no, BizCollectionProtocol.protocol_status.in_(_PRE_DELIVERY_STATUSES), or_(not_(has_sample), has_uncollected), ) q = _apply_collection_keyword(q, db, tenant_no, keyword or "") q = _apply_create_time_filter(q, BizCollectionProtocol, create_time_start, create_time_end) q = _apply_collection_sample_collect_time_filter( q, db, tenant_no, sample_collect_time_start, sample_collect_time_end ) if user_info: from app.core.data_scope import apply_data_scope q = apply_data_scope(q, BizCollectionProtocol, user_info, db, tenant_no) out: List[Tuple[str, str]] = [] for (pid,) in q.all(): sample_count = ( db.query(func.count(BizProtocolSample.id)) .filter( BizProtocolSample.collection_protocol_id == pid, BizProtocolSample.del_flag == 0, BizProtocolSample.tenant_no == tenant_no, ) .scalar() or 0 ) kind = "no_sample" if sample_count == 0 else "uncollected" out.append((pid, kind)) return out def _list_pre_delivery_donation_ids( db: Session, *, tenant_no: str, keyword: Optional[str], create_time_start: Optional[str], create_time_end: Optional[str], sample_collect_time_start: Optional[str], sample_collect_time_end: Optional[str], user_info, ) -> List[Tuple[str, str]]: has_sample = ( exists() .where( and_( BizResearchDonationSample.research_donation_id == BizResearchDonation.id, BizResearchDonationSample.del_flag == 0, BizResearchDonationSample.tenant_no == tenant_no, ) ) .correlate(BizResearchDonation) ) has_uncollected = ( exists() .where( and_( BizResearchDonationSample.research_donation_id == BizResearchDonation.id, BizResearchDonationSample.del_flag == 0, BizResearchDonationSample.tenant_no == tenant_no, or_( BizResearchDonationSample.sample_status.is_(None), BizResearchDonationSample.sample_status == "", ), ) ) .correlate(BizResearchDonation) ) q = db.query(BizResearchDonation.id).filter( BizResearchDonation.del_flag == 0, BizResearchDonation.tenant_no == tenant_no, BizResearchDonation.donation_status.in_(_PRE_DELIVERY_STATUSES), or_(not_(has_sample), has_uncollected), ) q = _apply_donation_keyword(q, db, tenant_no, keyword or "") q = _apply_create_time_filter(q, BizResearchDonation, create_time_start, create_time_end) q = _apply_donation_sample_collect_time_filter( q, db, tenant_no, sample_collect_time_start, sample_collect_time_end ) if user_info: from app.core.data_scope import apply_data_scope q = apply_data_scope(q, BizResearchDonation, user_info, db, tenant_no) out: List[Tuple[str, str]] = [] for (did,) in q.all(): sample_count = ( db.query(func.count(BizResearchDonationSample.id)) .filter( BizResearchDonationSample.research_donation_id == did, BizResearchDonationSample.del_flag == 0, BizResearchDonationSample.tenant_no == tenant_no, ) .scalar() or 0 ) kind = "no_sample" if sample_count == 0 else "uncollected" out.append((did, kind)) return out def _sort_key_in_transit(eta: str, create_time: Optional[datetime]) -> Tuple[int, str, datetime]: eta_clean = (eta or "").strip() return (0 if eta_clean else 1, eta_clean or "9999-99-99 99:99:99", create_time or datetime.min) def _sort_key_pre_delivery(expected: str, create_time: Optional[datetime]) -> Tuple[int, str, datetime]: exp = (expected or "").strip() return (0 if exp else 1, exp or "", create_time or datetime.min) def _hydrate_collection_rows( db: Session, *, tenant_no: str, protocol_ids: List[str], mode: str, pre_kinds: Optional[Dict[str, str]] = None, ) -> List[Dict[str, Any]]: if not protocol_ids: return [] rows = ( db.query(BizCollectionProtocol) .filter( BizCollectionProtocol.id.in_(protocol_ids), BizCollectionProtocol.del_flag == 0, BizCollectionProtocol.tenant_no == tenant_no, ) .all() ) row_map = {r.id: r for r in rows} pc_map: Dict[str, Dict[str, Any]] = {} for pc in ( db.query(BizProtocolPartyCustomer) .filter( BizProtocolPartyCustomer.collection_protocol_id.in_(protocol_ids), BizProtocolPartyCustomer.del_flag == 0, ) .all() ): pc_map[pc.collection_protocol_id] = _party_customer_to_dict(pc) ci_map: Dict[str, Dict[str, Any]] = {} for ci in ( db.query(BizProtocolCollectionInfo) .filter( BizProtocolCollectionInfo.collection_protocol_id.in_(protocol_ids), BizProtocolCollectionInfo.del_flag == 0, ) .all() ): ci_map[ci.collection_protocol_id] = _collection_info_to_dict(ci) region_ids = list({ci_map.get(pid, {}).get("regionId", "") for pid in protocol_ids} - {""}) hospital_ids = list({ci_map.get(pid, {}).get("hospitalId", "") for pid in protocol_ids} - {""}) region_name_map: Dict[str, str] = {} hospital_name_map: Dict[str, str] = {} if region_ids: for r in ( db.query(BizCollectionRegion.id, BizCollectionRegion.region_name) .filter(BizCollectionRegion.id.in_(region_ids)) .all() ): region_name_map[r.id] = r.region_name or "" if hospital_ids: for h in ( db.query(BizCollectionHospital.id, BizCollectionHospital.hospital_name) .filter(BizCollectionHospital.id.in_(hospital_ids)) .all() ): hospital_name_map[h.id] = h.hospital_name or "" sample_map: Dict[str, List[BizProtocolSample]] = {} s_rows = ( db.query(BizProtocolSample) .filter( BizProtocolSample.collection_protocol_id.in_(protocol_ids), BizProtocolSample.del_flag == 0, ) .order_by(BizProtocolSample.order_num.asc(), BizProtocolSample.create_time.asc()) .all() ) sample_type_name_map = _load_sample_type_name_map(db, s_rows) for s in s_rows: sample_map.setdefault(s.collection_protocol_id, []).append(s) all_codes: List[str] = [] for samples in sample_map.values(): for s in samples: c = (s.sample_code or "").strip() if c: all_codes.append(c) lab_code_map: Dict[str, str] = {} if all_codes: from app.crud import sample_reception as reception_svc lab_code_map = reception_svc.get_lab_storage_code_map_by_sample_codes( db, tenant_no, all_codes, reception_source="cell_storage" ) transport_by_sample_id: Dict[str, BizProtocolSampleTransport] = {} for t in ( db.query(BizProtocolSampleTransport) .filter( BizProtocolSampleTransport.collection_protocol_id.in_(protocol_ids), BizProtocolSampleTransport.tenant_no == tenant_no, BizProtocolSampleTransport.del_flag == 0, ) .all() ): transport_by_sample_id[t.sample_id] = t site_name_map = _load_site_name_map( db, [t.dest_site_id for t in transport_by_sample_id.values() if t.dest_site_id] ) express_name_map = _load_express_name_map( db, [t.express_company_id for t in transport_by_sample_id.values() if t.express_company_id], ) received_codes: set = set() if mode == "in_transit" and all_codes: for rec in ( db.query(BizSampleReception.sample_code) .filter( BizSampleReception.tenant_no == tenant_no, BizSampleReception.del_flag == 0, BizSampleReception.reception_source == "cell_storage", BizSampleReception.sample_code.in_(all_codes), ) .all() ): if rec[0]: received_codes.add(rec[0]) user_ids = list( { *(ci_map.get(pid, {}).get("salesUserId", "") for pid in protocol_ids), *(_protocol_to_dict(row_map[pid]).get("collectorUserId", "") for pid in protocol_ids if pid in row_map), } - {""} ) user_name_map: Dict[str, str] = {} if user_ids: for u in db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(user_ids)).all(): user_name_map[u.id] = u.nick_name or "" result: List[Dict[str, Any]] = [] for pid in protocol_ids: r = row_map.get(pid) if not r: continue d = _protocol_to_dict(r) d["healthSurveyAttachmentFiles"] = urls_to_attachment_list( r.health_survey_form_urls, db, tenant_no ) d["sourceType"] = "cell_storage" d["sourceTypeLabel"] = "细胞存储" d["collectionProtocolId"] = r.id d["researchDonationId"] = "" pc = pc_map.get(pid, {}) ci = ci_map.get(pid, {}) d["motherName"] = pc.get("motherName", "") d["motherPhone"] = pc.get("motherPhone", "") d["motherIdCard"] = pc.get("motherIdCard", "") d["fatherName"] = pc.get("fatherName", "") d["newbornName"] = pc.get("newbornName", "") d["expectedDeliveryDate"] = pc.get("expectedDeliveryDate", "") d["newbornDeliveryTime"] = pc.get("newbornDeliveryTime", "") d["newbornSex"] = pc.get("newbornSex", "") d["deliveryMode"] = pc.get("deliveryMode", "") d["newbornWeightGram"] = pc.get("newbornWeightGram") d["regionName"] = region_name_map.get(ci.get("regionId", ""), "") d["hospitalName"] = hospital_name_map.get(ci.get("hospitalId", ""), "") d["salesUserName"] = user_name_map.get(ci.get("salesUserId", ""), "") d["sampleCollectTime"] = ci.get("sampleCollectTime", "") d["maternalBloodCollectTime"] = ci.get("maternalBloodCollectTime", "") d["collectorUserName"] = user_name_map.get(d.get("collectorUserId", ""), "") samples = sample_map.get(pid, []) if mode == "in_transit": def _flt(s): code = (s.sample_code or "").strip() return _is_in_transit_sample(s.sample_status) and code not in received_codes else: pre_kind = (pre_kinds or {}).get(pid, "no_sample") def _flt(s): if pre_kind == "no_sample": return False return _is_uncollected_status(s.sample_status) items, eta_values = _build_sample_items( samples, sample_type_name_map=sample_type_name_map, lab_code_map=lab_code_map, transport_by_sample_id=transport_by_sample_id, site_name_map=site_name_map, express_name_map=express_name_map, sample_filter=_flt, ) type_names = [it["sampleTypeName"] for it in items if it.get("sampleTypeName")] codes = [it["sampleCode"] for it in items if it.get("sampleCode")] d["sampleItems"] = items d["sampleTypeNames"] = ",".join(type_names) d["sampleCodes"] = ",".join(codes) d["labStorageCodes"] = ",".join( (it.get("labStorageCode") or "") for it in items if it.get("labStorageCode") ) if mode == "in_transit": d["sampleCollectSummary"] = _summarize_in_transit_items(items) d["etaTimeSummary"] = _earliest_eta(eta_values) else: pre_kind = (pre_kinds or {}).get(pid, "no_sample") d["preDeliveryKind"] = pre_kind d["preDeliveryKindLabel"] = "样本未录入" if pre_kind == "no_sample" else "样本未采集" d["sampleCollectSummary"] = _summarize_pre_delivery(samples, pre_kind) d["etaTimeSummary"] = "" result.append(d) return result def _hydrate_donation_rows( db: Session, *, tenant_no: str, donation_ids: List[str], mode: str, pre_kinds: Optional[Dict[str, str]] = None, ) -> List[Dict[str, Any]]: if not donation_ids: return [] rows = ( db.query(BizResearchDonation) .filter( BizResearchDonation.id.in_(donation_ids), BizResearchDonation.del_flag == 0, BizResearchDonation.tenant_no == tenant_no, ) .all() ) row_map = {r.id: r for r in rows} party_map: Dict[str, Dict[str, Any]] = {} for p in ( db.query(BizResearchDonationParty) .filter( BizResearchDonationParty.research_donation_id.in_(donation_ids), BizResearchDonationParty.del_flag == 0, ) .all() ): party_map[p.research_donation_id] = _party_to_dict(p) coll_map: Dict[str, Dict[str, Any]] = {} for c in ( db.query(BizResearchDonationCollection) .filter( BizResearchDonationCollection.research_donation_id.in_(donation_ids), BizResearchDonationCollection.del_flag == 0, ) .all() ): coll_map[c.research_donation_id] = _collection_to_dict(c) region_ids = list({coll_map.get(did, {}).get("regionId", "") for did in donation_ids} - {""}) hospital_ids = list({coll_map.get(did, {}).get("hospitalId", "") for did in donation_ids} - {""}) region_name_map: Dict[str, str] = {} hospital_name_map: Dict[str, str] = {} if region_ids: for r in ( db.query(BizCollectionRegion.id, BizCollectionRegion.region_name) .filter(BizCollectionRegion.id.in_(region_ids)) .all() ): region_name_map[r.id] = r.region_name or "" if hospital_ids: for h in ( db.query(BizCollectionHospital.id, BizCollectionHospital.hospital_name) .filter(BizCollectionHospital.id.in_(hospital_ids)) .all() ): hospital_name_map[h.id] = h.hospital_name or "" sample_map: Dict[str, List[BizResearchDonationSample]] = {} s_rows = ( db.query(BizResearchDonationSample) .filter( BizResearchDonationSample.research_donation_id.in_(donation_ids), BizResearchDonationSample.del_flag == 0, ) .order_by(BizResearchDonationSample.order_num.asc(), BizResearchDonationSample.create_time.asc()) .all() ) sample_type_name_map = _load_sample_type_name_map(db, s_rows) for s in s_rows: sample_map.setdefault(s.research_donation_id, []).append(s) all_codes: List[str] = [] for samples in sample_map.values(): for s in samples: c = (s.sample_code or "").strip() if c: all_codes.append(c) lab_code_map: Dict[str, str] = {} if all_codes: from app.crud import sample_reception as reception_svc lab_code_map = reception_svc.get_lab_storage_code_map_by_sample_codes( db, tenant_no, all_codes, reception_source="research_donation" ) transport_by_sample_id: Dict[str, BizResearchDonationSampleTransport] = {} for t in ( db.query(BizResearchDonationSampleTransport) .filter( BizResearchDonationSampleTransport.research_donation_id.in_(donation_ids), BizResearchDonationSampleTransport.tenant_no == tenant_no, BizResearchDonationSampleTransport.del_flag == 0, ) .all() ): transport_by_sample_id[t.sample_id] = t site_name_map = _load_site_name_map( db, [t.dest_site_id for t in transport_by_sample_id.values() if t.dest_site_id] ) express_name_map = _load_express_name_map( db, [t.express_company_id for t in transport_by_sample_id.values() if t.express_company_id], ) received_codes: set = set() if mode == "in_transit" and all_codes: for rec in ( db.query(BizSampleReception.sample_code) .filter( BizSampleReception.tenant_no == tenant_no, BizSampleReception.del_flag == 0, BizSampleReception.reception_source == "research_donation", BizSampleReception.sample_code.in_(all_codes), ) .all() ): if rec[0]: received_codes.add(rec[0]) user_ids = list( { *(coll_map.get(did, {}).get("salesUserId", "") for did in donation_ids), *(row_map[did].collector_user_id or "" for did in donation_ids if did in row_map), } - {""} ) user_name_map: Dict[str, str] = {} if user_ids: for u in db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(user_ids)).all(): user_name_map[u.id] = u.nick_name or "" result: List[Dict[str, Any]] = [] for did in donation_ids: r = row_map.get(did) if not r: continue d = _donation_to_dict(r, db, tenant_no) d["sourceType"] = "research_donation" d["sourceTypeLabel"] = "科研捐赠" d["collectionProtocolId"] = "" d["researchDonationId"] = r.id d["protocolNo"] = d.get("donationNo", "") d["protocolStatus"] = d.get("donationStatus", "") d["registerDate"] = d.get("donationDate", "") party = party_map.get(did, {}) coll = coll_map.get(did, {}) d["motherName"] = party.get("motherName", "") d["motherPhone"] = party.get("motherPhone", "") d["motherIdCard"] = party.get("motherIdCard", "") d["fatherName"] = party.get("fatherName", "") d["newbornName"] = party.get("newbornName", "") d["expectedDeliveryDate"] = party.get("expectedDeliveryDate", "") d["newbornDeliveryTime"] = party.get("newbornDeliveryTime", "") d["newbornSex"] = party.get("newbornSex", "") d["deliveryMode"] = party.get("deliveryMode", "") d["newbornWeightGram"] = party.get("newbornWeightGram") d["regionName"] = region_name_map.get(coll.get("regionId", ""), "") d["hospitalName"] = hospital_name_map.get(coll.get("hospitalId", ""), "") d["salesUserName"] = user_name_map.get(coll.get("salesUserId", ""), "") d["sampleCollectTime"] = coll.get("sampleCollectTime", "") d["maternalBloodCollectTime"] = coll.get("maternalBloodCollectTime", "") d["collectorUserName"] = user_name_map.get(r.collector_user_id or "", "") samples = sample_map.get(did, []) if mode == "in_transit": def _flt(s): code = (s.sample_code or "").strip() return _is_in_transit_sample(s.sample_status) and code not in received_codes else: pre_kind = (pre_kinds or {}).get(did, "no_sample") def _flt(s): if pre_kind == "no_sample": return False return _is_uncollected_status(s.sample_status) items, eta_values = _build_sample_items( samples, sample_type_name_map=sample_type_name_map, lab_code_map=lab_code_map, transport_by_sample_id=transport_by_sample_id, site_name_map=site_name_map, express_name_map=express_name_map, sample_filter=_flt, ) type_names = [it["sampleTypeName"] for it in items if it.get("sampleTypeName")] codes = [it["sampleCode"] for it in items if it.get("sampleCode")] d["sampleItems"] = items d["sampleTypeNames"] = ",".join(type_names) d["sampleCodes"] = ",".join(codes) d["labStorageCodes"] = ",".join( (it.get("labStorageCode") or "") for it in items if it.get("labStorageCode") ) if mode == "in_transit": d["sampleCollectSummary"] = _summarize_in_transit_items(items) d["etaTimeSummary"] = _earliest_eta(eta_values) else: pre_kind = (pre_kinds or {}).get(did, "no_sample") d["preDeliveryKind"] = pre_kind d["preDeliveryKindLabel"] = "样本未录入" if pre_kind == "no_sample" else "样本未采集" d["sampleCollectSummary"] = _summarize_pre_delivery(samples, pre_kind) d["etaTimeSummary"] = "" result.append(d) return result def list_in_transit_protocols( db: Session, *, page: int = 1, page_size: int = 10, tenant_no: str, keyword: Optional[str] = None, source_type: Optional[str] = None, create_time_start: Optional[str] = None, create_time_end: Optional[str] = None, sample_collect_time_start: Optional[str] = None, sample_collect_time_end: Optional[str] = None, user_info=None, ) -> Tuple[List[Dict[str, Any]], int]: if not tenant_no: return [], 0 candidates: List[Tuple[str, str, str, Optional[datetime]]] = [] if not source_type or source_type == "cell_storage": for pid in _list_in_transit_collection_ids( db, tenant_no=tenant_no, keyword=keyword, create_time_start=create_time_start, create_time_end=create_time_end, sample_collect_time_start=sample_collect_time_start, sample_collect_time_end=sample_collect_time_end, user_info=user_info, ): row = ( db.query(BizCollectionProtocol.create_time) .filter(BizCollectionProtocol.id == pid) .first() ) candidates.append(("cell_storage", pid, "", row[0] if row else None)) if not source_type or source_type == "research_donation": for did in _list_in_transit_donation_ids( db, tenant_no=tenant_no, keyword=keyword, create_time_start=create_time_start, create_time_end=create_time_end, sample_collect_time_start=sample_collect_time_start, sample_collect_time_end=sample_collect_time_end, user_info=user_info, ): row = ( db.query(BizResearchDonation.create_time) .filter(BizResearchDonation.id == did) .first() ) candidates.append(("research_donation", did, "", row[0] if row else None)) if not candidates: return [], 0 # 预计算 eta 用于排序 cell_ids = [pid for st, pid, _, _ in candidates if st == "cell_storage"] donation_ids = [did for st, did, _, _ in candidates if st == "research_donation"] eta_by_protocol: Dict[str, str] = {} if cell_ids: for pid, eta in ( db.query( BizProtocolSample.collection_protocol_id, func.min(BizProtocolSampleTransport.eta_time), ) .join( BizProtocolSampleTransport, and_( BizProtocolSampleTransport.sample_id == BizProtocolSample.id, BizProtocolSampleTransport.del_flag == 0, ), ) .filter( BizProtocolSample.collection_protocol_id.in_(cell_ids), BizProtocolSample.del_flag == 0, BizProtocolSample.sample_status == "ycj", not_(_cell_received_exists(tenant_no)), ) .group_by(BizProtocolSample.collection_protocol_id) .all() ): eta_by_protocol[f"cell_storage:{pid}"] = _fmt_dt(eta) if eta else "" if donation_ids: for did, eta in ( db.query( BizResearchDonationSample.research_donation_id, func.min(BizResearchDonationSampleTransport.eta_time), ) .join( BizResearchDonationSampleTransport, and_( BizResearchDonationSampleTransport.sample_id == BizResearchDonationSample.id, BizResearchDonationSampleTransport.del_flag == 0, ), ) .filter( BizResearchDonationSample.research_donation_id.in_(donation_ids), BizResearchDonationSample.del_flag == 0, BizResearchDonationSample.sample_status == "ycj", not_(_donation_received_exists(tenant_no)), ) .group_by(BizResearchDonationSample.research_donation_id) .all() ): eta_by_protocol[f"research_donation:{did}"] = _fmt_dt(eta) if eta else "" enriched: List[Tuple[str, str, str, Optional[datetime]]] = [] for st, pid, _, ct in candidates: eta = eta_by_protocol.get(f"{st}:{pid}", "") enriched.append((st, pid, eta, ct)) enriched.sort(key=lambda x: _sort_key_in_transit(x[2], x[3])) total = len(enriched) page = max(1, page) page_size = min(max(1, page_size), 100) page_slice = enriched[(page - 1) * page_size : page * page_size] cell_page = [pid for st, pid, _, _ in page_slice if st == "cell_storage"] donation_page = [pid for st, pid, _, _ in page_slice if st == "research_donation"] items: List[Dict[str, Any]] = [] items.extend( _hydrate_collection_rows(db, tenant_no=tenant_no, protocol_ids=cell_page, mode="in_transit") ) items.extend( _hydrate_donation_rows(db, tenant_no=tenant_no, donation_ids=donation_page, mode="in_transit") ) order_keys = [f"{st}:{pid}" for st, pid, _, _ in page_slice] item_map = {f"{it['sourceType']}:{it.get('collectionProtocolId') or it.get('researchDonationId')}": it for it in items} ordered = [item_map[k] for k in order_keys if k in item_map] return ordered, total def list_pre_delivery_protocols( db: Session, *, page: int = 1, page_size: int = 10, tenant_no: str, keyword: Optional[str] = None, source_type: Optional[str] = None, create_time_start: Optional[str] = None, create_time_end: Optional[str] = None, sample_collect_time_start: Optional[str] = None, sample_collect_time_end: Optional[str] = None, user_info=None, ) -> Tuple[List[Dict[str, Any]], int]: if not tenant_no: return [], 0 candidates: List[Tuple[str, str, str, Optional[datetime], str]] = [] if not source_type or source_type == "cell_storage": for pid, kind in _list_pre_delivery_collection_ids( db, tenant_no=tenant_no, keyword=keyword, create_time_start=create_time_start, create_time_end=create_time_end, sample_collect_time_start=sample_collect_time_start, sample_collect_time_end=sample_collect_time_end, user_info=user_info, ): proto = db.query(BizCollectionProtocol).filter(BizCollectionProtocol.id == pid).first() party = ( db.query(BizProtocolPartyCustomer.expected_delivery_date) .filter( BizProtocolPartyCustomer.collection_protocol_id == pid, BizProtocolPartyCustomer.del_flag == 0, ) .first() ) exp = "" if party and party[0]: from app.core.utils import format_date exp = format_date(party[0]) candidates.append( ( "cell_storage", pid, exp, proto.create_time if proto else None, kind, ) ) if not source_type or source_type == "research_donation": for did, kind in _list_pre_delivery_donation_ids( db, tenant_no=tenant_no, keyword=keyword, create_time_start=create_time_start, create_time_end=create_time_end, sample_collect_time_start=sample_collect_time_start, sample_collect_time_end=sample_collect_time_end, user_info=user_info, ): donation = db.query(BizResearchDonation).filter(BizResearchDonation.id == did).first() party = ( db.query(BizResearchDonationParty.expected_delivery_date) .filter( BizResearchDonationParty.research_donation_id == did, BizResearchDonationParty.del_flag == 0, ) .first() ) exp = "" if party and party[0]: from app.core.utils import format_date exp = format_date(party[0]) candidates.append( ( "research_donation", did, exp, donation.create_time if donation else None, kind, ) ) if not candidates: return [], 0 candidates.sort( key=lambda x: ( (x[2] or "").strip() or "0000-00-00", x[3] or datetime.min, ), reverse=True, ) total = len(candidates) page = max(1, page) page_size = min(max(1, page_size), 100) page_slice = candidates[(page - 1) * page_size : page * page_size] cell_page = [pid for st, pid, _, _, _ in page_slice if st == "cell_storage"] donation_page = [did for st, did, _, _, _ in page_slice if st == "research_donation"] pre_kinds_cell = {pid: kind for st, pid, _, _, kind in page_slice if st == "cell_storage"} pre_kinds_donation = {did: kind for st, did, _, _, kind in page_slice if st == "research_donation"} items: List[Dict[str, Any]] = [] items.extend( _hydrate_collection_rows( db, tenant_no=tenant_no, protocol_ids=cell_page, mode="pre_delivery", pre_kinds=pre_kinds_cell, ) ) items.extend( _hydrate_donation_rows( db, tenant_no=tenant_no, donation_ids=donation_page, mode="pre_delivery", pre_kinds=pre_kinds_donation, ) ) order_keys = [f"{st}:{pid}" for st, pid, _, _, _ in page_slice] item_map = {f"{it['sourceType']}:{it.get('collectionProtocolId') or it.get('researchDonationId')}": it for it in items} ordered = [item_map[k] for k in order_keys if k in item_map] return ordered, total __all__ = ["list_in_transit_protocols", "list_pre_delivery_protocols", "total_pages"]