| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232 |
- """预产及在途样本 — 质检看板(协议单维度列表)"""
- 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"]
|