"""样本接收 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 _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(), ) .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(), ) .all() ) all_items.extend(_donation_rows_to_sample_list_items(db, donation_rows, tenant_no)) # 统一按 etaTime 排序(空值排后) all_items.sort(key=lambda x: (not x.get("etaTime"), x.get("etaTime") or "")) 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()) .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)) # 按 reception_time DESC 排序 items.sort(key=lambda x: x.get("receptionTime") or "", reverse=True) 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_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 "", ) 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 ), }