| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558 |
- """样本接收 CRUD(质量检测)"""
- from __future__ import annotations
- from datetime import datetime
- from typing import Any, Dict, List, Optional, Tuple
- from sqlalchemy import and_, func, or_
- from sqlalchemy.orm import Session
- from app.core.utils import generate_uuid
- from app.crud.collection_protocol import urls_to_attachment_list
- from app.utils.sample_special import sample_special_to_dict
- from app.crud import sample_reception_check_result as check_result_svc
- from app.crud import sample_production_reception as prod_reception_svc
- from app.models.models import (
- BizCellSampleType,
- BizCollectionProtocol,
- BizExpressCompany,
- BizProtocolCollectionInfo,
- BizProtocolPartyCustomer,
- BizProtocolSample,
- BizProtocolSampleTransport,
- BizSampleReception,
- SysUser,
- )
- from app.models.research_donation import (
- BizResearchDonation,
- BizResearchDonationCollection,
- BizResearchDonationParty,
- BizResearchDonationSample,
- BizResearchDonationSampleTransport,
- )
- from app.core.protocol_status_display import INACTIVE_PROTOCOL_STATUSES
- from app.services.web_auth_roles import get_user_role_names
- SAMPLE_RECEPTION_EDIT_ROLE = "质量主管"
- # 科研捐赠表默认 general_ci,样本接收等为 unicode_ci,跨表比较需统一
- _UNICODE_CI = "utf8mb4_unicode_ci"
- def _donation_sample_id_col(col):
- """捐赠样本 ID 与 biz_sample_reception.donation_sample_id 比较时统一排序规则。"""
- return col.collate(_UNICODE_CI)
- def _fmt_dt(t: Optional[datetime]) -> str:
- return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
- def _sort_pending_list_items(items: List[Dict[str, Any]]) -> None:
- """待接收:etaTime 升序(空值最后),同 ETA 时 sampleCode 倒序。"""
- items.sort(key=lambda x: (x.get("sampleCode") or ""), reverse=True)
- items.sort(key=lambda x: (not x.get("etaTime"), x.get("etaTime") or ""))
- def _sort_received_list_items(items: List[Dict[str, Any]]) -> None:
- """已接收:receptionTime 倒序,同接收时间时 sampleCode 倒序。"""
- items.sort(key=lambda x: (x.get("sampleCode") or ""), reverse=True)
- items.sort(key=lambda x: (x.get("receptionTime") or ""), reverse=True)
- def _normalize_date_bound(value: str, *, end_of_day: bool = False) -> str:
- val = (value or "").strip()
- if len(val) == 10:
- return f"{val} 23:59:59" if end_of_day else f"{val} 00:00:00"
- return val
- def _dec(v: Any) -> Optional[float]:
- if v is None:
- return None
- return float(v)
- def _reception_to_dict(
- r: BizSampleReception,
- *,
- check_results: Optional[List[Dict[str, Any]]] = None,
- ) -> Dict[str, Any]:
- return {
- "id": r.id,
- "protocolSampleId": r.protocol_sample_id,
- "collectionProtocolId": r.collection_protocol_id,
- "receptionTime": _fmt_dt(r.reception_time),
- "handlerUserId": r.handler_user_id or "",
- "handlerName": r.handler_name or "",
- "receiveTemperature": r.receive_temperature or "",
- "checkResults": check_results or [],
- "acceptConclusion": r.accept_conclusion or "",
- "labStorageCode": r.lab_storage_code or "",
- "receiveAttachments": r.receive_attachments or "",
- "remark": r.remark or "",
- "createTime": _fmt_dt(r.create_time),
- "updateTime": _fmt_dt(r.update_time),
- }
- def _sample_detail_rows_query(db: Session):
- """协议样本 + 协议单 + 客户 + 运输(显式 select_from 避免多表 join 歧义)。"""
- return (
- db.query(
- BizProtocolSample,
- BizCollectionProtocol,
- BizProtocolPartyCustomer,
- BizProtocolSampleTransport,
- )
- .select_from(BizProtocolSample)
- .join(
- BizCollectionProtocol,
- and_(
- BizCollectionProtocol.id == BizProtocolSample.collection_protocol_id,
- BizCollectionProtocol.del_flag == 0,
- ),
- )
- .outerjoin(
- BizProtocolPartyCustomer,
- and_(
- BizProtocolPartyCustomer.collection_protocol_id == BizCollectionProtocol.id,
- BizProtocolPartyCustomer.del_flag == 0,
- ),
- )
- .outerjoin(
- BizProtocolSampleTransport,
- and_(
- BizProtocolSampleTransport.sample_id == BizProtocolSample.id,
- BizProtocolSampleTransport.del_flag == 0,
- ),
- )
- )
- def _donation_sample_detail_rows_query(db: Session):
- """捐赠样本 + 捐赠单 + 当事人 + 运输(四表 JOIN)。"""
- return (
- db.query(
- BizResearchDonationSample,
- BizResearchDonation,
- BizResearchDonationParty,
- BizResearchDonationSampleTransport,
- )
- .select_from(BizResearchDonationSample)
- .join(
- BizResearchDonation,
- and_(
- BizResearchDonation.id == BizResearchDonationSample.research_donation_id,
- BizResearchDonation.del_flag == 0,
- ),
- )
- .outerjoin(
- BizResearchDonationParty,
- and_(
- BizResearchDonationParty.research_donation_id == BizResearchDonation.id,
- BizResearchDonationParty.del_flag == 0,
- ),
- )
- .outerjoin(
- BizResearchDonationSampleTransport,
- and_(
- BizResearchDonationSampleTransport.sample_id == BizResearchDonationSample.id,
- BizResearchDonationSampleTransport.del_flag == 0,
- ),
- )
- )
- def _donation_sample_pending_query(db: Session, tenant_no: str, keyword: Optional[str] = None):
- """待接收:捐赠样本且尚无有效接收记录(按租户 + 样本编码判定,与细胞存储一致);排除已作废捐赠单。"""
- rec = BizSampleReception
- received_exists = (
- db.query(rec.id)
- .filter(
- rec.tenant_no == tenant_no,
- rec.del_flag == 0,
- rec.reception_source == "research_donation",
- rec.sample_code.isnot(None),
- rec.sample_code != "",
- rec.sample_code == BizResearchDonationSample.sample_code,
- )
- .exists()
- )
- q = _donation_sample_detail_rows_query(db).filter(
- BizResearchDonationSample.del_flag == 0,
- BizResearchDonationSample.tenant_no == tenant_no,
- BizResearchDonation.tenant_no == tenant_no,
- BizResearchDonation.donation_status.notin_(INACTIVE_PROTOCOL_STATUSES),
- ~received_exists,
- )
- if keyword and keyword.strip():
- kw = f"%{keyword.strip()}%"
- kw_conds = [
- BizResearchDonationSample.sample_code.like(kw),
- BizResearchDonationParty.mother_name.like(kw),
- BizResearchDonationParty.newborn_name.like(kw),
- ]
- lab_codes = sample_codes_matching_lab_storage_keyword(
- db, tenant_no, keyword, reception_source="research_donation"
- )
- if lab_codes:
- kw_conds.append(BizResearchDonationSample.sample_code.in_(lab_codes))
- q = q.filter(or_(*kw_conds))
- return q
- def _donation_rows_to_sample_list_items(
- db: Session,
- rows: list,
- tenant_no: str,
- *,
- reception_map: Optional[Dict[str, BizSampleReception]] = None,
- ) -> List[Dict[str, Any]]:
- """将捐赠样本行转为统一列表项字典(字段名与细胞存储保持一致)。"""
- type_ids = list({s.sample_type_id for s, *_ in rows if s.sample_type_id})
- type_map: Dict[str, str] = {}
- if type_ids:
- for t in (
- db.query(BizCellSampleType)
- .filter(
- BizCellSampleType.id.in_(type_ids),
- BizCellSampleType.del_flag == 0,
- )
- .all()
- ):
- type_map[t.id] = t.type_name or ""
- express_ids = list(
- {tr.express_company_id for *_, tr in rows if tr and tr.express_company_id}
- )
- express_map: Dict[str, str] = {}
- if express_ids:
- for e in (
- db.query(BizExpressCompany)
- .filter(BizExpressCompany.id.in_(express_ids), BizExpressCompany.del_flag == 0)
- .all()
- ):
- express_map[e.id] = e.express_name or ""
- donation_ids = list({donation.id for _, donation, _, _ in rows if donation and donation.id})
- collect_info_map: Dict[str, Dict[str, str]] = {}
- if donation_ids:
- for coll in (
- db.query(BizResearchDonationCollection)
- .filter(
- BizResearchDonationCollection.research_donation_id.in_(donation_ids),
- BizResearchDonationCollection.del_flag == 0,
- )
- .all()
- ):
- collect_info_map[coll.research_donation_id] = {
- "sampleCollectTime": _fmt_dt(coll.sample_collect_time),
- "maternalBloodCollectTime": _fmt_dt(coll.maternal_blood_collect_time),
- }
- reception_ids = (
- [reception_map[s.id].id for s, *_ in rows if s.id in reception_map]
- if reception_map
- else []
- )
- results_map = (
- check_result_svc.list_results_by_reception_ids(db, reception_ids, tenant_no)
- if reception_ids
- else {}
- )
- items: List[Dict[str, Any]] = []
- for sample, donation, party, transport in rows:
- item: Dict[str, Any] = {
- "protocolSampleId": sample.id,
- "collectionProtocolId": donation.id,
- "protocolNo": donation.donation_no or "",
- "protocolStatus": donation.donation_status or "",
- "sampleCode": sample.sample_code or "",
- "sampleTypeId": sample.sample_type_id or "",
- "sampleTypeName": type_map.get(sample.sample_type_id or "", ""),
- "collectAmount": _dec(sample.collect_amount),
- "motherName": (party.mother_name if party else "") or "",
- "newbornName": (party.newborn_name if party else "") or "",
- "sampleCollectTime": (
- collect_info_map.get(donation.id, {}).get("sampleCollectTime", "")
- if donation
- else ""
- ),
- "maternalBloodCollectTime": (
- collect_info_map.get(donation.id, {}).get("maternalBloodCollectTime", "")
- if donation
- else ""
- ),
- "sourceType": "research_donation",
- "sourceTypeLabel": "科研捐赠",
- "infectiousDiseaseReportAttachmentFiles": urls_to_attachment_list(
- donation.infectious_disease_report_urls if donation else None, db, tenant_no
- ),
- "etaTime": _fmt_dt(transport.eta_time) if transport and transport.eta_time else "",
- "transportType": (transport.transport_type if transport else "") or "",
- "waybillNo": (transport.waybill_no if transport else "") or "",
- "expressCompanyId": (transport.express_company_id if transport else "") or "",
- "expressCompanyName": express_map.get(
- (transport.express_company_id if transport else "") or "", ""
- ),
- "carrierName": (transport.carrier_name if transport else "") or "",
- "carrierPhone": (transport.carrier_phone if transport else "") or "",
- **sample_special_to_dict(sample),
- }
- if reception_map and sample.id in reception_map:
- rec = reception_map[sample.id]
- cr = results_map.get(rec.id)
- if not cr:
- cr = check_result_svc.get_check_results_for_reception(db, rec, tenant_no)
- item.update(_reception_to_dict(rec, check_results=cr))
- item["receiveAttachmentFiles"] = urls_to_attachment_list(
- rec.receive_attachments, db, tenant_no
- )
- items.append(item)
- rec_ids = [str(item.get("id") or "") for item in items if item.get("id")]
- if rec_ids:
- production_map = prod_reception_svc.get_map_by_reception_ids(db, rec_ids, tenant_no)
- for item in items:
- rid = str(item.get("id") or "")
- item["productionReception"] = production_map.get(rid) if rid else None
- return items
- def _sample_pending_query(db: Session, tenant_no: str, keyword: Optional[str] = None):
- """待接收:协议样本且尚无有效接收记录(仅按租户 + 样本编码判定);排除已作废采集协议单。"""
- rec = BizSampleReception
- received_exists = (
- db.query(rec.id)
- .filter(
- rec.tenant_no == tenant_no,
- rec.del_flag == 0,
- rec.reception_source == "cell_storage",
- rec.sample_code.isnot(None),
- rec.sample_code != "",
- rec.sample_code == BizProtocolSample.sample_code,
- )
- .exists()
- )
- q = _sample_detail_rows_query(db).filter(
- BizProtocolSample.del_flag == 0,
- BizProtocolSample.tenant_no == tenant_no,
- BizCollectionProtocol.tenant_no == tenant_no,
- BizCollectionProtocol.protocol_status.notin_(INACTIVE_PROTOCOL_STATUSES),
- ~received_exists,
- )
- if keyword and keyword.strip():
- kw = f"%{keyword.strip()}%"
- kw_conds = [
- BizProtocolSample.sample_code.like(kw),
- BizProtocolPartyCustomer.mother_name.like(kw),
- BizProtocolPartyCustomer.newborn_name.like(kw),
- ]
- lab_codes = sample_codes_matching_lab_storage_keyword(
- db, tenant_no, keyword, reception_source="cell_storage"
- )
- if lab_codes:
- kw_conds.append(BizProtocolSample.sample_code.in_(lab_codes))
- q = q.filter(or_(*kw_conds))
- return q
- def _rows_to_sample_list_items(
- db: Session,
- rows: list,
- tenant_no: str,
- *,
- reception_map: Optional[Dict[str, BizSampleReception]] = None,
- ) -> List[Dict[str, Any]]:
- type_ids = list({s.sample_type_id for s, *_ in rows if s.sample_type_id})
- type_map: Dict[str, str] = {}
- if type_ids:
- for t in (
- db.query(BizCellSampleType)
- .filter(
- BizCellSampleType.id.in_(type_ids),
- BizCellSampleType.del_flag == 0,
- )
- .all()
- ):
- type_map[t.id] = t.type_name or ""
- express_ids = list(
- {tr.express_company_id for *_, tr in rows if tr and tr.express_company_id}
- )
- express_map: Dict[str, str] = {}
- if express_ids:
- for e in (
- db.query(BizExpressCompany)
- .filter(BizExpressCompany.id.in_(express_ids), BizExpressCompany.del_flag == 0)
- .all()
- ):
- express_map[e.id] = e.express_name or ""
- protocol_ids = list({protocol.id for _, protocol, _, _ in rows if protocol and protocol.id})
- collect_info_map: Dict[str, Dict[str, str]] = {}
- payment_summary_map: Dict[str, Dict[str, Any]] = {}
- if protocol_ids:
- from app.services.qc_work_order_fee_summary import build_protocol_payment_summaries
- payment_summary_map = build_protocol_payment_summaries(db, protocol_ids, tenant_no)
- for coll in (
- db.query(BizProtocolCollectionInfo)
- .filter(
- BizProtocolCollectionInfo.collection_protocol_id.in_(protocol_ids),
- BizProtocolCollectionInfo.del_flag == 0,
- )
- .all()
- ):
- collect_info_map[coll.collection_protocol_id] = {
- "sampleCollectTime": _fmt_dt(coll.sample_collect_time),
- "maternalBloodCollectTime": _fmt_dt(coll.maternal_blood_collect_time),
- }
- def _map_rec(sample: BizProtocolSample) -> Optional[BizSampleReception]:
- if not reception_map:
- return None
- code = (sample.sample_code or "").strip()
- if code and code in reception_map:
- return reception_map[code]
- return None
- matched_recs = [_map_rec(s) for s, *_ in rows]
- reception_ids = [r.id for r in matched_recs if r]
- results_map = (
- check_result_svc.list_results_by_reception_ids(db, reception_ids, tenant_no)
- if reception_ids
- else {}
- )
- items: List[Dict[str, Any]] = []
- for sample, protocol, party, transport in rows:
- item: Dict[str, Any] = {
- "protocolSampleId": sample.id,
- "collectionProtocolId": protocol.id,
- "protocolNo": protocol.protocol_no or "",
- "protocolStatus": protocol.protocol_status or "",
- "sampleCode": sample.sample_code or "",
- "sampleTypeId": sample.sample_type_id or "",
- "sampleTypeName": type_map.get(sample.sample_type_id or "", ""),
- "collectAmount": _dec(sample.collect_amount),
- "sampleStatus": sample.sample_status or "",
- "motherName": (party.mother_name if party else "") or "",
- "newbornName": (party.newborn_name if party else "") or "",
- "sampleCollectTime": (
- collect_info_map.get(protocol.id, {}).get("sampleCollectTime", "")
- if protocol
- else ""
- ),
- "maternalBloodCollectTime": (
- collect_info_map.get(protocol.id, {}).get("maternalBloodCollectTime", "")
- if protocol
- else ""
- ),
- "etaTime": _fmt_dt(transport.eta_time) if transport and transport.eta_time else "",
- "transportType": (transport.transport_type if transport else "") or "",
- "waybillNo": (transport.waybill_no if transport else "") or "",
- "expressCompanyId": (transport.express_company_id if transport else "") or "",
- "expressCompanyName": express_map.get(
- (transport.express_company_id if transport else "") or "", ""
- ),
- "carrierName": (transport.carrier_name if transport else "") or "",
- "carrierPhone": (transport.carrier_phone if transport else "") or "",
- **sample_special_to_dict(sample),
- "sourceType": "cell_storage",
- "sourceTypeLabel": "细胞存储",
- "healthSurveyAttachmentFiles": urls_to_attachment_list(
- protocol.health_survey_form_urls if protocol else None, db, tenant_no
- ),
- "paymentSummary": payment_summary_map.get(protocol.id) if protocol else None,
- }
- rec = _map_rec(sample)
- if rec:
- cr = results_map.get(rec.id)
- if not cr:
- cr = check_result_svc.get_check_results_for_reception(db, rec, tenant_no)
- item.update(_reception_to_dict(rec, check_results=cr))
- item["receiveAttachmentFiles"] = urls_to_attachment_list(
- rec.receive_attachments, db, tenant_no
- )
- items.append(item)
- reception_ids = [str(item.get("id") or "") for item in items if item.get("id")]
- if reception_ids:
- production_map = prod_reception_svc.get_map_by_reception_ids(db, reception_ids, tenant_no)
- for item in items:
- rid = str(item.get("id") or "")
- item["productionReception"] = production_map.get(rid) if rid else None
- return items
- def list_pending_protocol_samples(
- db: Session,
- *,
- page: int = 1,
- page_size: int = 10,
- tenant_no: Optional[str] = None,
- keyword: Optional[str] = None,
- source_type: Optional[str] = None,
- eta_time_start: Optional[str] = None,
- eta_time_end: Optional[str] = None,
- user_info=None,
- ) -> Tuple[List[Dict[str, Any]], int]:
- if not tenant_no:
- return [], 0
- all_items: List[Dict[str, Any]] = []
- if not source_type or source_type == "cell_storage":
- q_base = _sample_pending_query(db, tenant_no, keyword)
- if user_info:
- from app.core.data_scope import apply_data_scope
- q_base = apply_data_scope(q_base, BizProtocolSample, user_info, db, tenant_no)
- cell_rows = (
- q_base.order_by(
- BizProtocolSampleTransport.eta_time.is_(None),
- BizProtocolSampleTransport.eta_time.asc(),
- BizProtocolSample.create_time.asc(),
- BizProtocolSample.sample_code.desc(),
- )
- .all()
- )
- all_items.extend(_rows_to_sample_list_items(db, cell_rows, tenant_no))
- if not source_type or source_type == "research_donation":
- q_donation = _donation_sample_pending_query(db, tenant_no, keyword)
- if user_info:
- from app.core.data_scope import apply_data_scope
- q_donation = apply_data_scope(q_donation, BizResearchDonationSample, user_info, db, tenant_no)
- donation_rows = (
- q_donation.order_by(
- BizResearchDonationSampleTransport.eta_time.is_(None),
- BizResearchDonationSampleTransport.eta_time.asc(),
- BizResearchDonationSample.create_time.asc(),
- BizResearchDonationSample.sample_code.desc(),
- )
- .all()
- )
- all_items.extend(_donation_rows_to_sample_list_items(db, donation_rows, tenant_no))
- # 统一按 etaTime 升序(空值排后),同 ETA 时 sampleCode 倒序
- _sort_pending_list_items(all_items)
- start_eta = _normalize_date_bound(eta_time_start) if eta_time_start else ""
- end_eta = _normalize_date_bound(eta_time_end, end_of_day=True) if eta_time_end else ""
- if start_eta or end_eta:
- filtered: List[Dict[str, Any]] = []
- for item in all_items:
- eta = (item.get("etaTime") or "").strip()
- if not eta:
- continue
- if start_eta and eta < start_eta:
- continue
- if end_eta and eta > end_eta:
- continue
- filtered.append(item)
- all_items = filtered
- total = len(all_items)
- page = max(1, page)
- page_size = min(max(1, page_size), 100)
- start = (page - 1) * page_size
- page_items = all_items[start:start + page_size]
- return page_items, int(total)
- def list_received_protocol_samples(
- db: Session,
- *,
- page: int = 1,
- page_size: int = 10,
- tenant_no: Optional[str] = None,
- keyword: Optional[str] = None,
- source_type: Optional[str] = None,
- reception_time_start: Optional[str] = None,
- reception_time_end: Optional[str] = None,
- user_info=None,
- ) -> Tuple[List[Dict[str, Any]], int]:
- if not tenant_no:
- return [], 0
- q_rec = db.query(BizSampleReception).filter(
- BizSampleReception.tenant_no == tenant_no,
- BizSampleReception.del_flag == 0,
- )
- if source_type:
- q_rec = q_rec.filter(BizSampleReception.reception_source == source_type)
- if keyword and keyword.strip():
- kw = f"%{keyword.strip()}%"
- cell_sample_ids_kw = (
- _sample_detail_rows_query(db)
- .with_entities(BizProtocolSample.id)
- .filter(
- BizProtocolSample.del_flag == 0,
- BizProtocolSample.tenant_no == tenant_no,
- BizCollectionProtocol.tenant_no == tenant_no,
- or_(
- BizProtocolSample.sample_code.like(kw),
- BizProtocolPartyCustomer.mother_name.like(kw),
- BizProtocolPartyCustomer.newborn_name.like(kw),
- ),
- )
- .subquery()
- )
- donation_sample_ids_kw = (
- _donation_sample_detail_rows_query(db)
- .with_entities(_donation_sample_id_col(BizResearchDonationSample.id))
- .filter(
- BizResearchDonationSample.del_flag == 0,
- BizResearchDonationSample.tenant_no == tenant_no,
- BizResearchDonation.tenant_no == tenant_no,
- or_(
- BizResearchDonationSample.sample_code.like(kw),
- BizResearchDonationParty.mother_name.like(kw),
- BizResearchDonationParty.newborn_name.like(kw),
- ),
- )
- .subquery()
- )
- q_rec = q_rec.filter(
- or_(
- BizSampleReception.handler_name.like(kw),
- BizSampleReception.lab_storage_code.like(kw),
- BizSampleReception.protocol_sample_id.in_(cell_sample_ids_kw),
- BizSampleReception.donation_sample_id.in_(donation_sample_ids_kw),
- )
- )
- if user_info:
- from app.core.data_scope import apply_data_scope
- q_rec = apply_data_scope(q_rec, BizSampleReception, user_info, db, tenant_no)
- if reception_time_start:
- q_rec = q_rec.filter(
- BizSampleReception.reception_time >= _normalize_date_bound(reception_time_start)
- )
- if reception_time_end:
- q_rec = q_rec.filter(
- BizSampleReception.reception_time
- <= _normalize_date_bound(reception_time_end, end_of_day=True)
- )
- total = q_rec.count()
- page = max(1, page)
- page_size = min(max(1, page_size), 100)
- receptions = (
- q_rec.order_by(
- BizSampleReception.reception_time.desc(),
- BizSampleReception.sample_code.desc(),
- )
- .offset((page - 1) * page_size)
- .limit(page_size)
- .all()
- )
- if not receptions:
- return [], int(total)
- # 按 reception_source 分组查询详情
- cell_receptions = [r for r in receptions if r.reception_source == "cell_storage"]
- donation_receptions = [r for r in receptions if r.reception_source == "research_donation"]
- items: List[Dict[str, Any]] = []
- if cell_receptions:
- cell_reception_map: Dict[str, BizSampleReception] = {}
- seen_codes: set = set()
- cell_rows: list = []
- for rec in cell_receptions:
- code = _reception_sample_code(db, rec)
- if not code:
- continue
- cell_reception_map[code] = rec
- if code in seen_codes:
- continue
- seen_codes.add(code)
- rows = (
- _sample_detail_rows_query(db)
- .filter(
- BizProtocolSample.sample_code == code,
- BizProtocolSample.del_flag == 0,
- BizProtocolSample.tenant_no == tenant_no,
- BizCollectionProtocol.tenant_no == tenant_no,
- )
- .all()
- )
- cell_rows.extend(rows)
- items.extend(
- _rows_to_sample_list_items(db, cell_rows, tenant_no, reception_map=cell_reception_map)
- )
- if donation_receptions:
- donation_sample_ids = [r.donation_sample_id for r in donation_receptions]
- donation_reception_map = {r.donation_sample_id: r for r in donation_receptions}
- donation_rows = (
- _donation_sample_detail_rows_query(db)
- .filter(_donation_sample_id_col(BizResearchDonationSample.id).in_(donation_sample_ids))
- .all()
- )
- donation_row_by_sample = {row[0].id: row for row in donation_rows}
- donation_ordered = [donation_row_by_sample[sid] for sid in donation_sample_ids if sid in donation_row_by_sample]
- items.extend(_donation_rows_to_sample_list_items(db, donation_ordered, tenant_no, reception_map=donation_reception_map))
- # 按 receptionTime 倒序,同时间时 sampleCode 倒序
- _sort_received_list_items(items)
- return items, int(total)
- def _finalize_reception_detail(db: Session, item: Optional[Dict[str, Any]], tenant_no: str) -> Optional[Dict[str, Any]]:
- if not item:
- return item
- return _attach_production_reception(db, item, tenant_no)
- def get_reception_by_id(
- db: Session, reception_id: str, tenant_no: str
- ) -> Optional[Dict[str, Any]]:
- rec = (
- db.query(BizSampleReception)
- .filter(
- BizSampleReception.id == reception_id,
- BizSampleReception.tenant_no == tenant_no,
- BizSampleReception.del_flag == 0,
- )
- .first()
- )
- if not rec:
- return None
- if (rec.reception_source or "cell_storage") == "research_donation":
- dsid = rec.donation_sample_id or ""
- if not dsid:
- cr = check_result_svc.get_check_results_for_reception(db, rec, tenant_no)
- out = _reception_to_dict(rec, check_results=cr)
- out["sourceType"] = "research_donation"
- out["sourceTypeLabel"] = "科研捐赠"
- out["receiveAttachmentFiles"] = urls_to_attachment_list(
- rec.receive_attachments, db, tenant_no
- )
- return _finalize_reception_detail(db, out, tenant_no)
- rows = (
- _donation_sample_detail_rows_query(db)
- .filter(_donation_sample_id_col(BizResearchDonationSample.id) == dsid)
- .all()
- )
- if not rows:
- cr = check_result_svc.get_check_results_for_reception(db, rec, tenant_no)
- out = _reception_to_dict(rec, check_results=cr)
- out["sourceType"] = "research_donation"
- out["sourceTypeLabel"] = "科研捐赠"
- out["receiveAttachmentFiles"] = urls_to_attachment_list(
- rec.receive_attachments, db, tenant_no
- )
- return _finalize_reception_detail(db, out, tenant_no)
- items = _donation_rows_to_sample_list_items(
- db, rows, tenant_no, reception_map={dsid: rec}
- )
- return items[0] if items else None
- code = _reception_sample_code(db, rec)
- if code:
- rows = (
- _sample_detail_rows_query(db)
- .filter(
- BizProtocolSample.sample_code == code,
- BizProtocolSample.del_flag == 0,
- BizProtocolSample.tenant_no == tenant_no,
- )
- .all()
- )
- if rows:
- items = _rows_to_sample_list_items(
- db, rows, tenant_no, reception_map={code: rec}
- )
- return items[0] if items else None
- cr = check_result_svc.get_check_results_for_reception(db, rec, tenant_no)
- out = _reception_to_dict(rec, check_results=cr)
- out["receiveAttachmentFiles"] = urls_to_attachment_list(
- rec.receive_attachments, db, tenant_no
- )
- return _finalize_reception_detail(db, out, tenant_no)
- def create_sample_reception(
- db: Session,
- *,
- reception_source: str = "cell_storage",
- protocol_sample_id: Optional[str] = None,
- donation_sample_id: Optional[str] = None,
- research_donation_id: Optional[str] = None,
- tenant_no: str,
- user_id: Optional[str],
- create_dept_id: str = "",
- reception_time: datetime,
- handler_user_id: Optional[str],
- handler_name: Optional[str],
- receive_temperature: Optional[str],
- volume_ml: Optional[float] = None,
- net_weight_g: Optional[float] = None,
- checks: List[Dict[str, Any]],
- accept_conclusion: str,
- receive_attachments: Optional[str],
- remark: Optional[str],
- lab_storage_code: Optional[str] = None,
- ) -> BizSampleReception:
- source = (reception_source or "").strip()
- if source not in ("cell_storage", "research_donation"):
- raise ValueError("来源类型无效")
- # 按来源校验样本并获取关联 ID
- if source == "cell_storage":
- if not protocol_sample_id:
- raise ValueError("细胞存储来源必须传入 protocolSampleId")
- sample = (
- db.query(BizProtocolSample)
- .filter(
- BizProtocolSample.id == protocol_sample_id,
- BizProtocolSample.tenant_no == tenant_no,
- BizProtocolSample.del_flag == 0,
- )
- .first()
- )
- if not sample:
- raise ValueError("样本不存在")
- _sample_code = (sample.sample_code or "").strip()
- if not _sample_code:
- raise ValueError("样本编码不能为空")
- exists = (
- db.query(BizSampleReception)
- .filter(
- BizSampleReception.tenant_no == tenant_no,
- BizSampleReception.del_flag == 0,
- BizSampleReception.reception_source == "cell_storage",
- BizSampleReception.sample_code == _sample_code,
- )
- .first()
- )
- if exists:
- raise ValueError("该样本已登记接收,请勿重复操作")
- collection_protocol_id = sample.collection_protocol_id
- _donation_sample_id = None
- _research_donation_id = None
- _sample_code = sample.sample_code or ""
- else:
- # research_donation
- if not donation_sample_id:
- raise ValueError("科研捐赠来源必须传入 donationSampleId")
- dsample = (
- db.query(BizResearchDonationSample)
- .filter(
- _donation_sample_id_col(BizResearchDonationSample.id) == donation_sample_id,
- BizResearchDonationSample.tenant_no == tenant_no,
- BizResearchDonationSample.del_flag == 0,
- )
- .first()
- )
- if not dsample:
- raise ValueError("捐赠样本不存在")
- _sample_code = (dsample.sample_code or "").strip()
- if not _sample_code:
- raise ValueError("样本编码不能为空")
- exists = (
- db.query(BizSampleReception)
- .filter(
- BizSampleReception.tenant_no == tenant_no,
- BizSampleReception.del_flag == 0,
- BizSampleReception.reception_source == "research_donation",
- BizSampleReception.sample_code == _sample_code,
- )
- .first()
- )
- if exists:
- raise ValueError("该样本已登记接收,请勿重复操作")
- collection_protocol_id = ""
- protocol_sample_id = ""
- _donation_sample_id = donation_sample_id
- _research_donation_id = dsample.research_donation_id
- conclusion = (accept_conclusion or "").strip()
- if conclusion not in ("js", "bj"):
- raise ValueError("接收结论无效")
- from app.crud import sample_reception_lab_code as lab_code_svc
- resolved_lab_code: Optional[str] = None
- code_raw = (lab_storage_code or "").strip()
- if conclusion == "js":
- resolved_lab_code = lab_code_svc.validate_lab_storage_code(code_raw)
- lab_code_svc.assert_lab_storage_code_available(
- db, tenant_no=tenant_no, code=resolved_lab_code
- )
- elif code_raw:
- resolved_lab_code = lab_code_svc.validate_lab_storage_code(code_raw)
- lab_code_svc.assert_lab_storage_code_available(
- db, tenant_no=tenant_no, code=resolved_lab_code
- )
- handler_display = (handler_name or "").strip()
- if handler_user_id and not handler_display:
- u = db.query(SysUser).filter(SysUser.id == handler_user_id).first()
- if u:
- handler_display = u.nick_name or ""
- obj = BizSampleReception(
- id=generate_uuid(),
- protocol_sample_id=protocol_sample_id or "",
- collection_protocol_id=collection_protocol_id or "",
- reception_source=source,
- donation_sample_id=_donation_sample_id,
- research_donation_id=_research_donation_id,
- sample_code=_sample_code or None,
- lab_storage_code=resolved_lab_code,
- reception_time=reception_time,
- handler_user_id=handler_user_id,
- handler_name=handler_display or None,
- receive_temperature=(receive_temperature or "").strip() or None,
- check_packaging=0,
- check_sealing=0,
- check_no_damage=0,
- check_no_leakage=0,
- check_color=0,
- check_tying_rope=0,
- check_no_stratification=0,
- accept_conclusion=conclusion,
- receive_attachments=(receive_attachments or "").strip() or None,
- tenant_no=tenant_no,
- remark=(remark or "").strip() or None,
- create_by=user_id,
- create_dept_id=create_dept_id or "",
- update_by=user_id,
- )
- db.add(obj)
- db.flush()
- check_result_svc.save_reception_check_results(
- db,
- reception_id=obj.id,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=create_dept_id or "",
- checks=checks,
- )
- from app.models.models import BizSampleReceptionCheckResult
- saved_rows = (
- db.query(BizSampleReceptionCheckResult)
- .filter(
- BizSampleReceptionCheckResult.reception_id == obj.id,
- BizSampleReceptionCheckResult.del_flag == 0,
- )
- .all()
- )
- check_result_svc.sync_legacy_columns_from_results(db, obj, saved_rows)
- db.add(obj)
- if conclusion == "js":
- from app.crud import qc_work_order as qc_wo_svc
- qc_wo_svc.create_work_order_from_reception(
- db,
- reception=obj,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=create_dept_id or "",
- )
- from app.crud import production_work_order as prod_wo_svc
- prod_wo = prod_wo_svc.create_production_work_order_from_reception(
- db,
- reception=obj,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=create_dept_id or "",
- )
- if prod_wo is not None:
- from app.crud import production_task_set as production_task_set_svc
- production_task_set_svc.try_auto_dispatch_builtin_tasks_after_reception(
- db,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=create_dept_id or "",
- work_order=prod_wo,
- lab_storage_code=getattr(obj, "lab_storage_code", None) or "",
- )
- from app.crud import storage_release_review as srr_svc
- _srr_protocol_no = ""
- _srr_sample_name = ""
- _src = getattr(obj, "reception_source", "cell_storage") or "cell_storage"
- if _src == "research_donation":
- _d = (
- db.query(BizResearchDonation)
- .filter(
- BizResearchDonation.id == (obj.research_donation_id or ""),
- BizResearchDonation.del_flag == 0,
- )
- .first()
- )
- if _d:
- _srr_protocol_no = _d.donation_no or ""
- _ds = (
- db.query(BizResearchDonationSample)
- .filter(
- BizResearchDonationSample.id == (obj.donation_sample_id or ""),
- BizResearchDonationSample.del_flag == 0,
- )
- .first()
- )
- if _ds:
- _srr_sample_name = _ds.sample_code or ""
- else:
- _p = (
- db.query(BizCollectionProtocol)
- .filter(
- BizCollectionProtocol.id == obj.collection_protocol_id,
- BizCollectionProtocol.del_flag == 0,
- )
- .first()
- )
- if _p:
- _srr_protocol_no = _p.protocol_no or ""
- _s = (
- db.query(BizProtocolSample)
- .filter(
- BizProtocolSample.id == obj.protocol_sample_id,
- BizProtocolSample.del_flag == 0,
- )
- .first()
- )
- if _s:
- _srr_sample_name = _s.sample_code or ""
- srr_svc.create_review_from_reception(
- db,
- reception=obj,
- protocol_no=_srr_protocol_no,
- sample_name=_srr_sample_name,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=create_dept_id or "",
- )
- db.commit()
- db.refresh(obj)
- return obj
- def _can_edit_sample_reception(
- db: Session,
- user_id: Optional[str],
- rec: BizSampleReception,
- ) -> bool:
- """已接收记录:仅经手人或「质量主管」角色可编辑。"""
- uid = (user_id or "").strip()
- if not uid:
- return False
- handler_id = (rec.handler_user_id or "").strip()
- if handler_id and handler_id == uid:
- return True
- return SAMPLE_RECEPTION_EDIT_ROLE in get_user_role_names(db, uid)
- def update_sample_reception(
- db: Session,
- *,
- reception_id: str,
- tenant_no: str,
- user_id: Optional[str],
- reception_time: datetime,
- handler_user_id: Optional[str],
- handler_name: Optional[str],
- receive_temperature: Optional[str],
- volume_ml: Optional[float] = None,
- net_weight_g: Optional[float] = None,
- checks: List[Dict[str, Any]],
- accept_conclusion: str,
- receive_attachments: Optional[str],
- remark: Optional[str],
- lab_storage_code: Optional[str] = None,
- ) -> BizSampleReception:
- """更新已登记样本接收信息(不重复创建质检/生产工单)。"""
- rec = (
- db.query(BizSampleReception)
- .filter(
- BizSampleReception.id == reception_id,
- BizSampleReception.tenant_no == tenant_no,
- BizSampleReception.del_flag == 0,
- )
- .first()
- )
- if not rec:
- raise ValueError("接收记录不存在")
- if not _can_edit_sample_reception(db, user_id, rec):
- raise PermissionError("仅该接收记录的经手人或质量主管可编辑")
- conclusion = (accept_conclusion or "").strip()
- if conclusion not in ("js", "bj"):
- raise ValueError("接收结论无效")
- from app.crud import sample_reception_lab_code as lab_code_svc
- code_raw = (lab_storage_code or "").strip()
- if conclusion == "js":
- parsed_code = lab_code_svc.validate_lab_storage_code(code_raw)
- lab_code_svc.assert_lab_storage_code_available(
- db,
- tenant_no=tenant_no,
- code=parsed_code,
- exclude_reception_id=reception_id,
- )
- rec.lab_storage_code = parsed_code
- elif code_raw:
- parsed_code = lab_code_svc.validate_lab_storage_code(code_raw)
- lab_code_svc.assert_lab_storage_code_available(
- db,
- tenant_no=tenant_no,
- code=parsed_code,
- exclude_reception_id=reception_id,
- )
- rec.lab_storage_code = parsed_code
- else:
- rec.lab_storage_code = None
- handler_display = (handler_name or "").strip()
- if handler_user_id and not handler_display:
- u = db.query(SysUser).filter(SysUser.id == handler_user_id).first()
- if u:
- handler_display = u.nick_name or ""
- rec.reception_time = reception_time
- rec.handler_user_id = handler_user_id
- rec.handler_name = handler_display or None
- rec.receive_temperature = (receive_temperature or "").strip() or None
- rec.accept_conclusion = conclusion
- rec.receive_attachments = (receive_attachments or "").strip() or None
- rec.remark = (remark or "").strip() or None
- rec.update_by = user_id
- rec.update_time = datetime.now()
- check_result_svc.soft_delete_reception_check_results(
- db,
- reception_id=reception_id,
- tenant_no=tenant_no,
- user_id=user_id,
- )
- check_result_svc.save_reception_check_results(
- db,
- reception_id=reception_id,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=rec.create_dept_id or "",
- checks=checks,
- )
- from app.models.models import BizSampleReceptionCheckResult
- saved_rows = (
- db.query(BizSampleReceptionCheckResult)
- .filter(
- BizSampleReceptionCheckResult.reception_id == reception_id,
- BizSampleReceptionCheckResult.del_flag == 0,
- )
- .all()
- )
- check_result_svc.sync_legacy_columns_from_results(db, rec, saved_rows)
- db.add(rec)
- db.commit()
- db.refresh(rec)
- return rec
- def _attach_production_reception(
- db: Session,
- item: Dict[str, Any],
- tenant_no: str,
- *,
- production_map: Optional[Dict[str, Dict[str, Any]]] = None,
- ) -> Dict[str, Any]:
- rid = str(item.get("id") or "").strip()
- if not rid:
- item["productionReception"] = None
- return item
- if production_map is not None:
- item["productionReception"] = production_map.get(rid)
- else:
- item["productionReception"] = prod_reception_svc.get_by_reception_id(db, rid, tenant_no)
- return item
- def _reception_detail_item(
- db: Session,
- rec: BizSampleReception,
- tenant_no: str,
- *,
- results_map: Optional[Dict[str, List[Dict[str, Any]]]] = None,
- ) -> Dict[str, Any]:
- cr = (results_map or {}).get(rec.id)
- if not cr:
- cr = check_result_svc.get_check_results_for_reception(db, rec, tenant_no)
- item = _reception_to_dict(rec, check_results=cr)
- item["receiveAttachmentFiles"] = urls_to_attachment_list(
- rec.receive_attachments, db, tenant_no
- )
- return item
- def _reception_detail_item_with_production(
- db: Session,
- rec: BizSampleReception,
- tenant_no: str,
- *,
- results_map: Optional[Dict[str, List[Dict[str, Any]]]] = None,
- production_map: Optional[Dict[str, Dict[str, Any]]] = None,
- ) -> Dict[str, Any]:
- item = _reception_detail_item(db, rec, tenant_no, results_map=results_map)
- return _attach_production_reception(db, item, tenant_no, production_map=production_map)
- def _reception_sample_code(db: Session, rec: BizSampleReception) -> str:
- code = (rec.sample_code or "").strip()
- if code:
- return code
- sid = (rec.protocol_sample_id or "").strip()
- if not sid:
- return ""
- row = (
- db.query(BizProtocolSample.sample_code)
- .filter(BizProtocolSample.id == sid)
- .first()
- )
- return (row.sample_code or "").strip() if row else ""
- def get_receptions_map_for_samples(
- db: Session,
- tenant_no: str,
- *,
- sample_code_by_id: Dict[str, str],
- ) -> Dict[str, Dict[str, Any]]:
- """按租户 + 样本编码关联接收记录(key 为当前协议样本 id),供协议单查看弹窗使用。"""
- if not tenant_no or not sample_code_by_id:
- return {}
- codes = list({(c or "").strip() for c in sample_code_by_id.values() if (c or "").strip()})
- if not codes:
- return {}
- rows = (
- db.query(BizSampleReception)
- .filter(
- BizSampleReception.tenant_no == tenant_no,
- BizSampleReception.del_flag == 0,
- BizSampleReception.reception_source == "cell_storage",
- BizSampleReception.sample_code.in_(codes),
- )
- .all()
- )
- if not rows:
- return {}
- code_to_rec: Dict[str, BizSampleReception] = {}
- for rec in rows:
- code = _reception_sample_code(db, rec)
- if code and code not in code_to_rec:
- code_to_rec[code] = rec
- reception_ids = [r.id for r in code_to_rec.values()]
- results_map = check_result_svc.list_results_by_reception_ids(
- db, reception_ids, tenant_no
- )
- production_map = prod_reception_svc.get_map_by_reception_ids(db, reception_ids, tenant_no)
- out: Dict[str, Dict[str, Any]] = {}
- for sid, raw_code in sample_code_by_id.items():
- code = (raw_code or "").strip()
- if not code:
- continue
- rec = code_to_rec.get(code)
- if not rec:
- continue
- out[sid] = _reception_detail_item_with_production(
- db, rec, tenant_no, results_map=results_map, production_map=production_map
- )
- return out
- def get_receptions_map_for_donation_samples(
- db: Session,
- sample_ids: List[str],
- tenant_no: str,
- ) -> Dict[str, Dict[str, Any]]:
- """按捐赠样本 ID 批量查询接收记录,供捐赠协议单查看弹窗使用。"""
- if not sample_ids or not tenant_no:
- return {}
- rows = (
- db.query(BizSampleReception)
- .filter(
- BizSampleReception.donation_sample_id.in_(sample_ids),
- BizSampleReception.tenant_no == tenant_no,
- BizSampleReception.del_flag == 0,
- )
- .all()
- )
- if not rows:
- return {}
- reception_ids = [r.id for r in rows]
- results_map = check_result_svc.list_results_by_reception_ids(db, reception_ids, tenant_no)
- production_map = prod_reception_svc.get_map_by_reception_ids(db, reception_ids, tenant_no)
- out: Dict[str, Dict[str, Any]] = {}
- for rec in rows:
- sid = rec.donation_sample_id
- if not sid:
- continue
- cr = results_map.get(rec.id)
- if not cr:
- cr = check_result_svc.get_check_results_for_reception(db, rec, tenant_no)
- item = _reception_to_dict(rec, check_results=cr)
- item["receiveAttachmentFiles"] = urls_to_attachment_list(
- rec.receive_attachments, db, tenant_no
- )
- _attach_production_reception(db, item, tenant_no, production_map=production_map)
- out[sid] = item
- return out
- def get_lab_storage_code_map_by_sample_codes(
- db: Session,
- tenant_no: str,
- sample_codes: List[str],
- *,
- reception_source: Optional[str] = None,
- ) -> Dict[str, str]:
- """按样本编码查询已接收(js)记录的实验室入库编码;同码多条时取最新接收时间。"""
- codes = list({(c or "").strip() for c in sample_codes if (c or "").strip()})
- if not codes or not tenant_no:
- return {}
- q = db.query(BizSampleReception.sample_code, BizSampleReception.lab_storage_code).filter(
- BizSampleReception.tenant_no == tenant_no,
- BizSampleReception.del_flag == 0,
- BizSampleReception.sample_code.in_(codes),
- BizSampleReception.accept_conclusion == "js",
- BizSampleReception.lab_storage_code.isnot(None),
- BizSampleReception.lab_storage_code != "",
- )
- if reception_source:
- q = q.filter(BizSampleReception.reception_source == reception_source)
- result: Dict[str, str] = {}
- for row in q.order_by(BizSampleReception.reception_time.desc()).all():
- sc = (row.sample_code or "").strip()
- lk = (row.lab_storage_code or "").strip()
- if sc and lk and sc not in result:
- result[sc] = lk
- missing = [c for c in codes if c not in result]
- if missing:
- join_q = (
- db.query(BizProtocolSample.sample_code, BizSampleReception.lab_storage_code)
- .join(
- BizSampleReception,
- and_(
- BizSampleReception.protocol_sample_id == BizProtocolSample.id,
- BizSampleReception.del_flag == 0,
- BizSampleReception.tenant_no == tenant_no,
- BizSampleReception.accept_conclusion == "js",
- BizSampleReception.lab_storage_code.isnot(None),
- BizSampleReception.lab_storage_code != "",
- ),
- )
- .filter(
- BizProtocolSample.sample_code.in_(missing),
- BizProtocolSample.del_flag == 0,
- BizProtocolSample.tenant_no == tenant_no,
- )
- )
- if reception_source:
- join_q = join_q.filter(BizSampleReception.reception_source == reception_source)
- for row in join_q.order_by(BizSampleReception.reception_time.desc()).all():
- sc = (row.sample_code or "").strip()
- lk = (row.lab_storage_code or "").strip()
- if sc and lk and sc not in result:
- result[sc] = lk
- return result
- def format_lab_storage_codes_csv(
- sample_codes: List[str],
- code_map: Dict[str, str],
- ) -> str:
- """与 sampleCodes 顺序对齐,逗号拼接入库编码(无则空串)。"""
- parts: List[str] = []
- for raw in sample_codes:
- c = (raw or "").strip()
- if not c:
- continue
- parts.append((code_map.get(c) or "").strip())
- return ",".join(parts)
- def _lab_storage_keyword_like(keyword: str) -> Optional[str]:
- kw = (keyword or "").strip()
- if not kw:
- return None
- return f"%{kw}%"
- def sample_codes_matching_lab_storage_keyword(
- db: Session,
- tenant_no: str,
- keyword: str,
- *,
- reception_source: Optional[str] = None,
- ) -> List[str]:
- """关键词模糊匹配实验室入库编码,返回关联样本编码列表。"""
- like = _lab_storage_keyword_like(keyword)
- if not like or not tenant_no:
- return []
- q = db.query(BizSampleReception.sample_code).filter(
- BizSampleReception.tenant_no == tenant_no,
- BizSampleReception.del_flag == 0,
- BizSampleReception.lab_storage_code.isnot(None),
- BizSampleReception.lab_storage_code != "",
- BizSampleReception.lab_storage_code.like(like),
- BizSampleReception.sample_code.isnot(None),
- BizSampleReception.sample_code != "",
- )
- if reception_source:
- q = q.filter(BizSampleReception.reception_source == reception_source)
- return list({(row[0] or "").strip() for row in q.all() if (row[0] or "").strip()})
- def reception_ids_matching_lab_storage_keyword(
- db: Session,
- tenant_no: str,
- keyword: str,
- *,
- reception_source: Optional[str] = None,
- ) -> List[str]:
- """关键词模糊匹配实验室入库编码,返回接收记录 id 列表。"""
- like = _lab_storage_keyword_like(keyword)
- if not like or not tenant_no:
- return []
- q = db.query(BizSampleReception.id).filter(
- BizSampleReception.tenant_no == tenant_no,
- BizSampleReception.del_flag == 0,
- BizSampleReception.lab_storage_code.isnot(None),
- BizSampleReception.lab_storage_code != "",
- BizSampleReception.lab_storage_code.like(like),
- )
- if reception_source:
- q = q.filter(BizSampleReception.reception_source == reception_source)
- return [row[0] for row in q.all() if row[0]]
- def protocol_ids_matching_lab_storage_keyword(
- db: Session,
- tenant_no: str,
- keyword: str,
- ) -> List[str]:
- """入库编码关键词 → 含匹配样本的采集协议 id。"""
- codes = sample_codes_matching_lab_storage_keyword(
- db, tenant_no, keyword, reception_source="cell_storage"
- )
- if not codes:
- return []
- rows = (
- db.query(BizProtocolSample.collection_protocol_id)
- .filter(
- BizProtocolSample.tenant_no == tenant_no,
- BizProtocolSample.del_flag == 0,
- BizProtocolSample.sample_code.in_(codes),
- )
- .distinct()
- .all()
- )
- return [r[0] for r in rows if r[0]]
- def donation_ids_matching_lab_storage_keyword(
- db: Session,
- tenant_no: str,
- keyword: str,
- ) -> List[str]:
- """入库编码关键词 → 含匹配样本的捐赠协议 id。"""
- codes = sample_codes_matching_lab_storage_keyword(
- db, tenant_no, keyword, reception_source="research_donation"
- )
- if not codes:
- return []
- rows = (
- db.query(BizResearchDonationSample.research_donation_id)
- .filter(
- BizResearchDonationSample.tenant_no == tenant_no,
- BizResearchDonationSample.del_flag == 0,
- BizResearchDonationSample.sample_code.in_(codes),
- )
- .distinct()
- .all()
- )
- return [r[0] for r in rows if r[0]]
- def get_reception_by_sample_code(
- db: Session,
- sample_code: str,
- source_type: Optional[str],
- tenant_no: str,
- ) -> Dict[str, Any]:
- """
- 根据样本编码查询接收记录及样本信息。
- - 有记录 → 返回 {receptionId, sampleInfo}
- - 无记录 → 返回 {receptionId: null, sampleInfo}
- """
- code = (sample_code or "").strip()
- if not code:
- return {"receptionId": None, "sampleInfo": None}
- source = (source_type or "cell_storage").strip()
- # 先查询是否有已接收记录
- q_rec = db.query(BizSampleReception).filter(
- BizSampleReception.tenant_no == tenant_no,
- BizSampleReception.del_flag == 0,
- BizSampleReception.sample_code == code,
- )
- if source:
- q_rec = q_rec.filter(BizSampleReception.reception_source == source)
- reception = q_rec.order_by(BizSampleReception.reception_time.desc()).first()
- if source == "research_donation":
- # 捐赠样本查询
- rows = (
- _donation_sample_detail_rows_query(db)
- .filter(
- _donation_sample_id_col(BizResearchDonationSample.sample_code) == code,
- BizResearchDonationSample.del_flag == 0,
- BizResearchDonationSample.tenant_no == tenant_no,
- )
- .all()
- )
- sample_info_list = _donation_rows_to_sample_list_items(db, rows, tenant_no)
- sample_info = sample_info_list[0] if sample_info_list else None
- else:
- # 细胞存储样本查询
- rows = (
- _sample_detail_rows_query(db)
- .filter(
- BizProtocolSample.sample_code == code,
- BizProtocolSample.del_flag == 0,
- BizProtocolSample.tenant_no == tenant_no,
- )
- .all()
- )
- if rows:
- if reception:
- sample_info_list = _rows_to_sample_list_items(
- db, rows, tenant_no, reception_map={code: reception}
- )
- else:
- sample_info_list = _rows_to_sample_list_items(db, rows, tenant_no)
- sample_info = sample_info_list[0] if sample_info_list else None
- else:
- sample_info = None
- return {
- "receptionId": reception.id if reception else None,
- "sampleInfo": sample_info,
- "productionReception": (
- prod_reception_svc.get_by_reception_id(db, reception.id, tenant_no) if reception else None
- ),
- }
|