Files
zkxw/backend/app/crud/pre_transit_sample.py
T
2026-07-14 13:55:11 +08:00

1233 lines
44 KiB
Python

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