| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242 |
- from __future__ import annotations
- import math
- from datetime import datetime
- from typing import Any, Dict, List, Optional
- from sqlalchemy import and_, exists, func, or_
- from sqlalchemy.orm import Session
- from app.core.db_collation import eq_unicode
- from app.core.protocol_status_display import INACTIVE_PROTOCOL_STATUSES
- from app.core.utils import generate_uuid
- from app.utils.sample_special import sample_special_to_dict
- from app.models.models import (
- BizCollectionProtocol,
- BizProductionWorkOrder,
- BizProtocolPartyCustomer,
- BizProtocolSample,
- BizProtocolSampleTransport,
- BizQcWorkOrder,
- BizSampleReception,
- BizStorageReleaseReview,
- SysUser,
- )
- from app.models.research_donation import (
- BizResearchDonation,
- BizResearchDonationParty,
- BizResearchDonationSample,
- BizResearchDonationSampleTransport,
- )
- _RECEPTION_SOURCE_LABELS = {
- "cell_storage": "细胞存储",
- "research_donation": "科研捐赠",
- }
- _REVIEW_CONCLUSION_LABELS = {"tg": "通过", "btg": "不通过"}
- _STORAGE_STATUS_LABELS = {"drk": "待入库", "yrk": "已入库"}
- _PROTOCOL_STATUS_LABELS = {
- "dsx": "待生效",
- "sxz": "生效中",
- "ygq": "已过期",
- "yzf": "已作废",
- "sdfq": "属地废弃",
- "zky": "转科研",
- }
- _DONATION_STATUS_LABELS = {
- "dsx": "待生效",
- "sxz": "生效中",
- "ygq": "已过期",
- "yzf": "已作废",
- "sdfq": "属地废弃",
- "zckc": "转细胞存储",
- }
- def _fmt_dt(t: Optional[datetime]) -> str:
- return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
- def total_pages(total: int, page_size: int) -> int:
- if total <= 0:
- return 0
- return int(math.ceil(total / float(page_size)))
- def _row_to_dict(r: BizStorageReleaseReview) -> Dict[str, Any]:
- return {
- "id": r.id,
- "sampleCode": r.sample_code or "",
- "receptionSource": r.reception_source or "",
- "receptionSourceLabel": _RECEPTION_SOURCE_LABELS.get(r.reception_source or "", r.reception_source or ""),
- "protocolId": r.protocol_id or "",
- "receptionId": r.reception_id or "",
- "protocolNo": r.protocol_no or "",
- "sampleName": r.sample_name or "",
- "reviewConclusion": r.review_conclusion or "",
- "reviewConclusionLabel": _REVIEW_CONCLUSION_LABELS.get(r.review_conclusion or "", r.review_conclusion or ""),
- "reviewRemark": r.review_remark or "",
- "reviewAttachments": r.review_attachments or "",
- "reviewBy": r.review_by or "",
- "reviewByName": r.review_by_name or "",
- "reviewTime": _fmt_dt(r.review_time),
- "qcReviewConclusion": r.qc_review_conclusion or "",
- "qcReviewConclusionLabel": _REVIEW_CONCLUSION_LABELS.get(r.qc_review_conclusion or "", r.qc_review_conclusion or ""),
- "qcReviewRemark": r.qc_review_remark or "",
- "qcReviewAttachments": r.qc_review_attachments or "",
- "qcReviewBy": r.qc_review_by or "",
- "qcReviewByName": r.qc_review_by_name or "",
- "qcReviewTime": _fmt_dt(r.qc_review_time),
- "prodReviewConclusion": r.prod_review_conclusion or "",
- "prodReviewConclusionLabel": _REVIEW_CONCLUSION_LABELS.get(r.prod_review_conclusion or "", r.prod_review_conclusion or ""),
- "prodReviewRemark": r.prod_review_remark or "",
- "prodReviewAttachments": r.prod_review_attachments or "",
- "prodReviewBy": r.prod_review_by or "",
- "prodReviewByName": r.prod_review_by_name or "",
- "prodReviewTime": _fmt_dt(r.prod_review_time),
- "storageStatus": r.storage_status or "drk",
- "storageStatusLabel": _STORAGE_STATUS_LABELS.get(r.storage_status or "drk", r.storage_status or "drk"),
- "storageBy": r.storage_by or "",
- "storageByName": r.storage_by_name or "",
- "storageTime": _fmt_dt(r.storage_time),
- "remark": r.remark or "",
- "createTime": _fmt_dt(r.create_time),
- "updateTime": _fmt_dt(r.update_time),
- }
- def create_review_from_reception(
- db: Session,
- *,
- reception: BizSampleReception,
- protocol_no: str,
- sample_name: str,
- tenant_no: str,
- user_id: Optional[str] = None,
- create_dept_id: str = "",
- ) -> BizStorageReleaseReview:
- source = getattr(reception, "reception_source", "cell_storage") or "cell_storage"
- if source == "research_donation":
- protocol_id = reception.research_donation_id or ""
- else:
- protocol_id = reception.collection_protocol_id or ""
- obj = BizStorageReleaseReview(
- id=generate_uuid(),
- sample_code=reception.sample_code or "",
- reception_source=source,
- protocol_id=protocol_id,
- reception_id=reception.id,
- protocol_no=protocol_no or None,
- sample_name=sample_name or None,
- storage_status="drk",
- tenant_no=tenant_no,
- create_by=user_id,
- create_dept_id=create_dept_id or "",
- update_by=user_id,
- create_time=datetime.now(),
- update_time=datetime.now(),
- del_flag=0,
- )
- db.add(obj)
- db.flush()
- return obj
- def normalize_review_phase(review_phase: Optional[str]) -> Optional[str]:
- """Return 'pending' | 'reviewed' or None when absent/invalid."""
- p = (review_phase or "").strip().lower()
- if p in ("pending", "reviewed"):
- return p
- return None
- def conclusion_blank(value: Optional[str]) -> bool:
- return not (value or "").strip()
- def aggregate_review_conclusion(
- prod: Optional[str], qc: Optional[str]
- ) -> Optional[str]:
- p = (prod or "").strip()
- q = (qc or "").strip()
- if p == "btg" or q == "btg":
- return "btg"
- if p == "tg" and q == "tg":
- return "tg"
- return None
- def storage_transition(
- prod: Optional[str], qc: Optional[str], storage_status: Optional[str]
- ) -> str:
- """Return 'store' | 'unstore' | 'noop'."""
- agg = aggregate_review_conclusion(prod, qc)
- st = (storage_status or "drk").strip() or "drk"
- if agg == "tg" and st == "drk":
- return "store"
- if agg == "btg" and st == "yrk":
- return "unstore"
- return "noop"
- def normalize_review_side(raw: Optional[str]) -> Optional[str]:
- s = (raw or "").strip().lower()
- if s in ("overview", "production", "qc"):
- return s
- return None
- def review_side_required_menu(review_side: Optional[str]) -> Optional[str]:
- if review_side == "production":
- return "production:storageReleaseReview:list"
- if review_side == "qc":
- return "quality:storageReleaseReview:list"
- return None
- def matches_dual_review_phase(
- review_side: Optional[str],
- phase: Optional[str],
- prod: Optional[str],
- qc: Optional[str],
- ) -> bool:
- """Pure predicate for Tab semantics; SQL filter must match this."""
- side = normalize_review_side(review_side) or "overview"
- ph = normalize_review_phase(phase)
- if not ph:
- return True
- prod_blank = conclusion_blank(prod)
- qc_blank = conclusion_blank(qc)
- if side == "production":
- return prod_blank if ph == "pending" else (not prod_blank)
- if side == "qc":
- return qc_blank if ph == "pending" else (not qc_blank)
- both_filled = (not prod_blank) and (not qc_blank)
- return (not both_filled) if ph == "pending" else both_filled
- def should_copy_legacy_to_qc(
- qc_review_conclusion: Optional[str],
- legacy_review_conclusion: Optional[str],
- ) -> bool:
- return conclusion_blank(qc_review_conclusion) and not conclusion_blank(
- legacy_review_conclusion
- )
- def normalize_qc_work_order_completed(raw) -> Optional[bool]:
- """Return True/False when recognizable; None means no filter."""
- if raw is None:
- return None
- if isinstance(raw, bool):
- return raw
- s = str(raw).strip().lower()
- if s in ("true", "1", "yes"):
- return True
- if s in ("false", "0", "no"):
- return False
- return None
- def normalize_ready_for_review_only(raw) -> Optional[bool]:
- """Return True when ready-only filter should apply; None means no filter."""
- if raw is None:
- return None
- if isinstance(raw, bool):
- return True if raw else None
- s = str(raw).strip().lower()
- if s in ("true", "1", "yes"):
- return True
- return None
- def is_inactive_protocol_status_for_hide(status) -> bool:
- """True when protocol/donation status is yzf or sdfq (list-hide rule)."""
- code = (status or "").strip().lower()
- return code in INACTIVE_PROTOCOL_STATUSES
- def inbound_work_orders_all_void(statuses) -> bool:
- """True when there is ≥1 non-empty status and every one equals yzf."""
- cleaned = [(s or "").strip() for s in (statuses or [])]
- if not cleaned or not any(s for s in cleaned):
- return False
- return all(s.lower() == "yzf" for s in cleaned)
- def _qc_work_order_exists_for_review_sample(
- tenant_no: str, *, unfinished_only: bool = False
- ):
- """Correlate biz_qc_work_order to BizStorageReleaseReview.sample_code."""
- conds = [
- BizQcWorkOrder.tenant_no == tenant_no,
- BizQcWorkOrder.del_flag == 0,
- BizQcWorkOrder.sample_code.isnot(None),
- BizQcWorkOrder.sample_code != "",
- # biz_qc_work_order 与 biz_storage_release_review 的 sample_code 排序规则不同
- eq_unicode(BizQcWorkOrder.sample_code, BizStorageReleaseReview.sample_code),
- BizStorageReleaseReview.sample_code.isnot(None),
- BizStorageReleaseReview.sample_code != "",
- ]
- if unfinished_only:
- conds.append(BizQcWorkOrder.work_order_status.in_(("dpz", "ypz")))
- return (
- exists()
- .where(and_(*conds))
- .correlate(BizStorageReleaseReview)
- )
- def _production_work_order_unfinished_exists_for_review_sample(tenant_no: str):
- """True if sample has any unfinished inbound-side production WO (dpz/jxz)."""
- conds = [
- BizProductionWorkOrder.tenant_no == tenant_no,
- BizProductionWorkOrder.del_flag == 0,
- BizProductionWorkOrder.sample_code.isnot(None),
- BizProductionWorkOrder.sample_code != "",
- eq_unicode(
- BizProductionWorkOrder.sample_code, BizStorageReleaseReview.sample_code
- ),
- BizStorageReleaseReview.sample_code.isnot(None),
- BizStorageReleaseReview.sample_code != "",
- BizProductionWorkOrder.work_order_status.in_(("dpz", "jxz")),
- ]
- return (
- exists()
- .where(and_(*conds))
- .correlate(BizStorageReleaseReview)
- )
- def _inactive_protocol_exists_for_review():
- """True when linked collection/donation protocol is yzf or sdfq."""
- inactive_cell = and_(
- or_(
- BizStorageReleaseReview.reception_source == "cell_storage",
- BizStorageReleaseReview.reception_source.is_(None),
- BizStorageReleaseReview.reception_source == "",
- ),
- BizStorageReleaseReview.protocol_id.isnot(None),
- BizStorageReleaseReview.protocol_id != "",
- exists()
- .where(
- and_(
- eq_unicode(
- BizCollectionProtocol.id, BizStorageReleaseReview.protocol_id
- ),
- BizCollectionProtocol.del_flag == 0,
- BizCollectionProtocol.protocol_status.in_(INACTIVE_PROTOCOL_STATUSES),
- )
- )
- .correlate(BizStorageReleaseReview),
- )
- inactive_donation = and_(
- BizStorageReleaseReview.reception_source == "research_donation",
- BizStorageReleaseReview.protocol_id.isnot(None),
- BizStorageReleaseReview.protocol_id != "",
- exists()
- .where(
- and_(
- eq_unicode(
- BizResearchDonation.id, BizStorageReleaseReview.protocol_id
- ),
- BizResearchDonation.del_flag == 0,
- BizResearchDonation.donation_status.in_(INACTIVE_PROTOCOL_STATUSES),
- )
- )
- .correlate(BizStorageReleaseReview),
- )
- return or_(inactive_cell, inactive_donation)
- def _inbound_qc_all_void_for_review_sample(tenant_no: str):
- """True when sample has ≥1 inbound QC WO and every such WO is yzf."""
- base = [
- BizQcWorkOrder.tenant_no == tenant_no,
- BizQcWorkOrder.del_flag == 0,
- BizQcWorkOrder.qc_biz_type == "inbound",
- BizQcWorkOrder.sample_code.isnot(None),
- BizQcWorkOrder.sample_code != "",
- eq_unicode(BizQcWorkOrder.sample_code, BizStorageReleaseReview.sample_code),
- BizStorageReleaseReview.sample_code.isnot(None),
- BizStorageReleaseReview.sample_code != "",
- ]
- any_inbound = exists().where(and_(*base)).correlate(BizStorageReleaseReview)
- any_non_void = (
- exists()
- .where(
- and_(
- *base,
- or_(
- BizQcWorkOrder.work_order_status.is_(None),
- BizQcWorkOrder.work_order_status != "yzf",
- ),
- )
- )
- .correlate(BizStorageReleaseReview)
- )
- return and_(any_inbound, ~any_non_void)
- def _inbound_production_all_void_for_review_sample(tenant_no: str):
- """True when sample has ≥1 inbound production WO and every such WO is yzf."""
- base = [
- BizProductionWorkOrder.tenant_no == tenant_no,
- BizProductionWorkOrder.del_flag == 0,
- BizProductionWorkOrder.work_order_type == "inbound",
- BizProductionWorkOrder.sample_code.isnot(None),
- BizProductionWorkOrder.sample_code != "",
- eq_unicode(
- BizProductionWorkOrder.sample_code, BizStorageReleaseReview.sample_code
- ),
- BizStorageReleaseReview.sample_code.isnot(None),
- BizStorageReleaseReview.sample_code != "",
- ]
- any_inbound = exists().where(and_(*base)).correlate(BizStorageReleaseReview)
- any_non_void = (
- exists()
- .where(
- and_(
- *base,
- or_(
- BizProductionWorkOrder.work_order_status.is_(None),
- BizProductionWorkOrder.work_order_status != "yzf",
- ),
- )
- )
- .correlate(BizStorageReleaseReview)
- )
- return and_(any_inbound, ~any_non_void)
- def list_reviews(
- db: Session,
- *,
- tenant_no: str,
- page: int = 1,
- page_size: int = 10,
- reception_source: Optional[str] = None,
- review_conclusion: Optional[str] = None,
- storage_status: Optional[str] = None,
- keyword: Optional[str] = None,
- review_phase: Optional[str] = None,
- review_side: Optional[str] = None,
- prod_review_conclusion: Optional[str] = None,
- qc_review_conclusion: Optional[str] = None,
- qc_work_order_completed=None,
- ready_for_review_only=None,
- user_info=None,
- ) -> Dict[str, Any]:
- q0 = db.query(BizStorageReleaseReview).filter(
- BizStorageReleaseReview.tenant_no == tenant_no,
- BizStorageReleaseReview.del_flag == 0,
- )
- rs = (reception_source or "").strip()
- if rs:
- q0 = q0.filter(BizStorageReleaseReview.reception_source == rs)
- phase = normalize_review_phase(review_phase)
- side = normalize_review_side(review_side) or "overview"
- if phase:
- prod_blank = or_(
- BizStorageReleaseReview.prod_review_conclusion.is_(None),
- BizStorageReleaseReview.prod_review_conclusion == "",
- )
- qc_blank = or_(
- BizStorageReleaseReview.qc_review_conclusion.is_(None),
- BizStorageReleaseReview.qc_review_conclusion == "",
- )
- if side == "production":
- q0 = q0.filter(prod_blank if phase == "pending" else ~prod_blank)
- elif side == "qc":
- q0 = q0.filter(qc_blank if phase == "pending" else ~qc_blank)
- else:
- both_filled = and_(~prod_blank, ~qc_blank)
- q0 = q0.filter(~both_filled if phase == "pending" else both_filled)
- prc = (prod_review_conclusion or "").strip()
- if prc:
- q0 = q0.filter(BizStorageReleaseReview.prod_review_conclusion == prc)
- qrc = (qc_review_conclusion or "").strip()
- if qrc:
- q0 = q0.filter(BizStorageReleaseReview.qc_review_conclusion == qrc)
- rc = (review_conclusion or "").strip()
- if rc:
- q0 = q0.filter(BizStorageReleaseReview.review_conclusion == rc)
- ss = (storage_status or "").strip()
- if ss:
- q0 = q0.filter(BizStorageReleaseReview.storage_status == ss)
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- kw_conds = [
- BizStorageReleaseReview.sample_code.like(like),
- BizStorageReleaseReview.sample_name.like(like),
- ]
- from app.crud import sample_reception as reception_svc
- rec_ids = reception_svc.reception_ids_matching_lab_storage_keyword(
- db, tenant_no, kw
- )
- if rec_ids:
- kw_conds.append(BizStorageReleaseReview.reception_id.in_(rec_ids))
- lab_codes = reception_svc.sample_codes_matching_lab_storage_keyword(db, tenant_no, kw)
- if lab_codes:
- kw_conds.append(BizStorageReleaseReview.sample_code.in_(lab_codes))
- q0 = q0.filter(or_(*kw_conds))
- qc_done = normalize_qc_work_order_completed(qc_work_order_completed)
- if qc_done is True:
- q0 = q0.filter(
- _qc_work_order_exists_for_review_sample(tenant_no, unfinished_only=False),
- ~_qc_work_order_exists_for_review_sample(tenant_no, unfinished_only=True),
- )
- elif qc_done is False:
- q0 = q0.filter(
- or_(
- ~_qc_work_order_exists_for_review_sample(
- tenant_no, unfinished_only=False
- ),
- _qc_work_order_exists_for_review_sample(
- tenant_no, unfinished_only=True
- ),
- )
- )
- if normalize_ready_for_review_only(ready_for_review_only) is True:
- # 无未完成质检/生产工单 ⇒ 该侧完成(含零工单)
- q0 = q0.filter(
- ~_qc_work_order_exists_for_review_sample(
- tenant_no, unfinished_only=True
- ),
- ~_production_work_order_unfinished_exists_for_review_sample(tenant_no),
- )
- # 协议无效 或 入库质检/生产「有工单且全部 yzf」→ 列表隐藏(两 Tab 常驻)
- q0 = q0.filter(
- ~or_(
- _inactive_protocol_exists_for_review(),
- _inbound_qc_all_void_for_review_sample(tenant_no),
- _inbound_production_all_void_for_review_sample(tenant_no),
- )
- )
- if user_info:
- from app.core.data_scope import apply_data_scope
- q0 = apply_data_scope(q0, BizStorageReleaseReview, user_info, db, tenant_no)
- total = q0.order_by(None).with_entities(func.count(BizStorageReleaseReview.id)).scalar() or 0
- page = max(1, page)
- page_size = min(max(1, page_size), 100)
- rows = (
- q0.order_by(BizStorageReleaseReview.create_time.desc())
- .offset((page - 1) * page_size)
- .limit(page_size)
- .all()
- )
- items = _enrich_review_list_items(db, [_row_to_dict(r) for r in rows])
- return {
- "list": items,
- "total": int(total),
- "page": page,
- "pageSize": page_size,
- "totalPages": total_pages(int(total), page_size),
- }
- def _enrich_review_list_items(db: Session, items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
- if not items:
- return items
- from app.crud.qc_work_order import _enrich_work_order_list_items
- items = _enrich_work_order_list_items(db, items)
- cell_pids = [
- x["protocolId"]
- for x in items
- if (x.get("receptionSource") or "cell_storage") != "research_donation" and x.get("protocolId")
- ]
- donation_pids = [
- x["protocolId"]
- for x in items
- if (x.get("receptionSource") or "") == "research_donation" and x.get("protocolId")
- ]
- proto_map: Dict[str, BizCollectionProtocol] = {}
- if cell_pids:
- proto_map = {
- p.id: p
- for p in db.query(BizCollectionProtocol)
- .filter(BizCollectionProtocol.id.in_(cell_pids), BizCollectionProtocol.del_flag == 0)
- .all()
- }
- party_map: Dict[str, str] = {}
- if cell_pids:
- for party in (
- db.query(BizProtocolPartyCustomer)
- .filter(
- BizProtocolPartyCustomer.collection_protocol_id.in_(cell_pids),
- BizProtocolPartyCustomer.del_flag == 0,
- )
- .all()
- ):
- party_map[party.collection_protocol_id] = party.mother_name or ""
- donation_map: Dict[str, BizResearchDonation] = {}
- if donation_pids:
- donation_map = {
- d.id: d
- for d in db.query(BizResearchDonation)
- .filter(BizResearchDonation.id.in_(donation_pids), BizResearchDonation.del_flag == 0)
- .all()
- }
- dparty_map: Dict[str, str] = {}
- if donation_pids:
- for party in (
- db.query(BizResearchDonationParty)
- .filter(
- BizResearchDonationParty.research_donation_id.in_(donation_pids),
- BizResearchDonationParty.del_flag == 0,
- )
- .all()
- ):
- dparty_map[party.research_donation_id] = party.mother_name or ""
- for item in items:
- source = item.get("receptionSource") or "cell_storage"
- pid = item.get("protocolId") or ""
- item.setdefault("motherName", "")
- item.setdefault("protocolStatus", "")
- item.setdefault("protocolStatusLabel", "")
- if source == "research_donation":
- donation = donation_map.get(pid)
- if donation:
- st = donation.donation_status or ""
- vt = donation.void_type or ""
- item["protocolStatus"] = st
- item["voidType"] = vt
- from app.core.protocol_status_display import format_protocol_status_label
- item["protocolStatusLabel"] = format_protocol_status_label(
- st, vt, kind="donation", db=db, tenant_no=donation.tenant_no
- )
- item["motherName"] = dparty_map.get(pid, "")
- else:
- proto = proto_map.get(pid)
- if proto:
- st = getattr(proto, "protocol_status", "") or ""
- vt = getattr(proto, "void_type", "") or ""
- item["protocolStatus"] = st
- item["voidType"] = vt
- from app.core.protocol_status_display import format_protocol_status_label
- item["protocolStatusLabel"] = format_protocol_status_label(
- st, vt, kind="protocol", db=db, tenant_no=proto.tenant_no
- )
- item["motherName"] = party_map.get(pid, "")
- return items
- def _get_user_name(db: Session, user_id: str) -> str:
- u = db.query(SysUser).filter(SysUser.id == user_id).first()
- return (u.nick_name or "") if u else ""
- def get_review_detail(db: Session, review_id: str, tenant_no: str) -> Optional[Dict[str, Any]]:
- rev = (
- db.query(BizStorageReleaseReview)
- .filter(
- BizStorageReleaseReview.id == review_id,
- BizStorageReleaseReview.tenant_no == tenant_no,
- BizStorageReleaseReview.del_flag == 0,
- )
- .first()
- )
- if not rev:
- return None
- result: Dict[str, Any] = {"review": _row_to_dict(rev)}
- source = rev.reception_source or "cell_storage"
- if source == "cell_storage":
- protocol_info = _build_cell_storage_protocol_info(db, rev)
- result["protocolInfo"] = protocol_info
- transport_info = _build_cell_storage_transport_info(db, rev)
- result["transportInfo"] = transport_info
- else:
- protocol_info = _build_research_donation_protocol_info(db, rev)
- result["protocolInfo"] = protocol_info
- transport_info = _build_research_donation_transport_info(db, rev)
- result["transportInfo"] = transport_info
- from app.crud import sample_reception as sample_reception_svc
- reception_detail = sample_reception_svc.get_reception_by_id(
- db, rev.reception_id or "", tenant_no
- )
- if reception_detail:
- result["reception"] = reception_detail
- result["receptionInfo"] = {
- "receptionTime": reception_detail.get("receptionTime") or "",
- "handlerName": reception_detail.get("handlerName") or "",
- "receiveTemperature": reception_detail.get("receiveTemperature") or "",
- "volumeMl": reception_detail.get("volumeMl"),
- "netWeightG": reception_detail.get("netWeightG"),
- "acceptConclusion": reception_detail.get("acceptConclusion") or "",
- "acceptConclusionLabel": reception_detail.get("acceptConclusionLabel") or "",
- "labStorageCode": reception_detail.get("labStorageCode") or "",
- "remark": reception_detail.get("remark") or "",
- "checkResults": reception_detail.get("checkResults") or [],
- "receiveAttachments": reception_detail.get("receiveAttachments") or "",
- "receiveAttachmentFiles": reception_detail.get("receiveAttachmentFiles") or [],
- "transportType": reception_detail.get("transportType") or "",
- "expressCompanyName": reception_detail.get("expressCompanyName") or "",
- "waybillNo": reception_detail.get("waybillNo") or "",
- "carrierName": reception_detail.get("carrierName") or "",
- "carrierPhone": reception_detail.get("carrierPhone") or "",
- "etaTime": reception_detail.get("etaTime") or "",
- }
- else:
- result["receptionInfo"] = _build_reception_info(db, rev)
- result["qcInfo"] = _build_gate_qc_info(db, rev, tenant_no)
- result["processQcInfoList"] = _build_process_qc_info_list(db, rev, tenant_no)
- result["productionInfo"] = _build_production_info(db, rev, tenant_no)
- return result
- def _build_cell_storage_protocol_info(db: Session, rev: BizStorageReleaseReview) -> Dict[str, Any]:
- proto = (
- db.query(BizCollectionProtocol)
- .filter(
- BizCollectionProtocol.id == (rev.protocol_id or ""),
- BizCollectionProtocol.del_flag == 0,
- )
- .first()
- )
- if not proto:
- return {}
- sample = (
- db.query(BizProtocolSample)
- .filter(
- BizProtocolSample.collection_protocol_id == proto.id,
- BizProtocolSample.sample_code == (rev.sample_code or ""),
- BizProtocolSample.tenant_no == rev.tenant_no,
- BizProtocolSample.del_flag == 0,
- )
- .first()
- )
- party = (
- db.query(BizProtocolPartyCustomer)
- .filter(
- BizProtocolPartyCustomer.collection_protocol_id == proto.id,
- BizProtocolPartyCustomer.del_flag == 0,
- )
- .first()
- )
- info: Dict[str, Any] = {
- "protocolNo": proto.protocol_no or "",
- "protocolStatus": getattr(proto, "protocol_status", "") or "",
- "motherName": "",
- "motherPhone": "",
- "fatherName": "",
- "fatherPhone": "",
- "newbornName": "",
- "sampleCode": rev.sample_code or "",
- "sampleTypeName": "",
- "collectAmount": None,
- "storageYears": None,
- }
- if sample:
- from app.crud.qc_work_order import _load_sample_type_name_map
- type_map = _load_sample_type_name_map(db, [sample.sample_type_id]) if sample.sample_type_id else {}
- info["sampleTypeName"] = type_map.get(sample.sample_type_id or "", "")
- info["collectAmount"] = float(sample.collect_amount) if sample.collect_amount else None
- info["storageYears"] = sample.storage_years
- info.update(sample_special_to_dict(sample))
- if party:
- info["motherName"] = party.mother_name or ""
- info["motherPhone"] = party.mother_phone or ""
- info["fatherName"] = party.father_name or ""
- info["fatherPhone"] = party.father_phone or ""
- info["newbornName"] = party.newborn_name or ""
- return info
- def _build_research_donation_protocol_info(db: Session, rev: BizStorageReleaseReview) -> Dict[str, Any]:
- donation = (
- db.query(BizResearchDonation)
- .filter(
- BizResearchDonation.id == (rev.protocol_id or ""),
- BizResearchDonation.del_flag == 0,
- )
- .first()
- )
- if not donation:
- return {}
- dsample = (
- db.query(BizResearchDonationSample)
- .filter(
- BizResearchDonationSample.research_donation_id == donation.id,
- BizResearchDonationSample.sample_code == (rev.sample_code or ""),
- BizResearchDonationSample.tenant_no == rev.tenant_no,
- BizResearchDonationSample.del_flag == 0,
- )
- .first()
- )
- party = (
- db.query(BizResearchDonationParty)
- .filter(
- BizResearchDonationParty.research_donation_id == donation.id,
- BizResearchDonationParty.del_flag == 0,
- )
- .first()
- )
- info: Dict[str, Any] = {
- "protocolNo": donation.donation_no or "",
- "protocolStatus": donation.donation_status or "",
- "motherName": "",
- "motherPhone": "",
- "fatherName": "",
- "fatherPhone": "",
- "newbornName": "",
- "sampleCode": rev.sample_code or "",
- "sampleTypeName": "",
- "collectAmount": None,
- "storageYears": None,
- }
- if dsample:
- from app.crud.qc_work_order import _load_sample_type_name_map
- type_map = _load_sample_type_name_map(db, [dsample.sample_type_id]) if dsample.sample_type_id else {}
- info["sampleTypeName"] = type_map.get(dsample.sample_type_id or "", "")
- info["collectAmount"] = float(dsample.collect_amount) if dsample.collect_amount else None
- info["storageYears"] = dsample.storage_years
- info.update(sample_special_to_dict(dsample))
- if party:
- info["motherName"] = party.mother_name or ""
- info["motherPhone"] = party.mother_phone or ""
- info["fatherName"] = party.father_name or ""
- info["fatherPhone"] = party.father_phone or ""
- info["newbornName"] = party.newborn_name or ""
- return info
- def _build_cell_storage_transport_info(db: Session, rev: BizStorageReleaseReview) -> Dict[str, Any]:
- sample = (
- db.query(BizProtocolSample)
- .filter(
- BizProtocolSample.sample_code == (rev.sample_code or ""),
- BizProtocolSample.collection_protocol_id == (rev.protocol_id or ""),
- BizProtocolSample.tenant_no == rev.tenant_no,
- BizProtocolSample.del_flag == 0,
- )
- .first()
- )
- if not sample:
- return {}
- transport = (
- db.query(BizProtocolSampleTransport)
- .filter(
- BizProtocolSampleTransport.sample_id == sample.id,
- BizProtocolSampleTransport.collection_protocol_id == (rev.protocol_id or ""),
- BizProtocolSampleTransport.del_flag == 0,
- )
- .first()
- )
- if not transport:
- return {}
- return {
- "transportType": transport.transport_type or "",
- "waybillNo": transport.waybill_no or "",
- "expressFee": float(transport.express_fee) if transport.express_fee else None,
- "etaTime": _fmt_dt(transport.eta_time),
- "carrierName": transport.carrier_name or "",
- "carrierPhone": transport.carrier_phone or "",
- }
- def _build_research_donation_transport_info(db: Session, rev: BizStorageReleaseReview) -> Dict[str, Any]:
- dsample = (
- db.query(BizResearchDonationSample)
- .filter(
- BizResearchDonationSample.sample_code == (rev.sample_code or ""),
- BizResearchDonationSample.research_donation_id == (rev.protocol_id or ""),
- BizResearchDonationSample.tenant_no == rev.tenant_no,
- BizResearchDonationSample.del_flag == 0,
- )
- .first()
- )
- if not dsample:
- return {}
- transport = (
- db.query(BizResearchDonationSampleTransport)
- .filter(
- BizResearchDonationSampleTransport.sample_id == dsample.id,
- BizResearchDonationSampleTransport.research_donation_id == (rev.protocol_id or ""),
- BizResearchDonationSampleTransport.del_flag == 0,
- )
- .first()
- )
- if not transport:
- return {}
- return {
- "transportType": transport.transport_type or "",
- "waybillNo": transport.waybill_no or "",
- "expressFee": float(transport.express_fee) if transport.express_fee else None,
- "etaTime": _fmt_dt(transport.eta_time),
- "carrierName": transport.carrier_name or "",
- "carrierPhone": transport.carrier_phone or "",
- }
- def _build_reception_info(db: Session, rev: BizStorageReleaseReview) -> Dict[str, Any]:
- rec = (
- db.query(BizSampleReception)
- .filter(
- BizSampleReception.id == (rev.reception_id or ""),
- BizSampleReception.tenant_no == rev.tenant_no,
- BizSampleReception.del_flag == 0,
- )
- .first()
- )
- if not rec:
- return {}
- return {
- "receptionTime": _fmt_dt(rec.reception_time),
- "handlerName": rec.handler_name or "",
- "receiveTemperature": rec.receive_temperature or "",
- "volumeMl": float(rec.volume_ml) if rec.volume_ml else None,
- "netWeightG": float(rec.net_weight_g) if rec.net_weight_g else None,
- "acceptConclusion": rec.accept_conclusion or "",
- "labStorageCode": rec.lab_storage_code or "",
- }
- def _build_gate_qc_info(
- db: Session, rev: BizStorageReleaseReview, tenant_no: str
- ) -> Optional[Dict[str, Any]]:
- """入库放行门禁质检(仅 trigger_source=reception)。"""
- from app.crud import qc_work_order as qc_work_order_svc
- code = (rev.sample_code or "").strip()
- q = db.query(BizQcWorkOrder).filter(
- BizQcWorkOrder.tenant_no == tenant_no,
- BizQcWorkOrder.del_flag == 0,
- BizQcWorkOrder.trigger_source == "reception",
- )
- wo = q.filter(BizQcWorkOrder.sample_code == code).first() if code else None
- if not wo and rev.reception_id:
- wo = q.filter(BizQcWorkOrder.reception_id == rev.reception_id).first()
- if not wo:
- return None
- return qc_work_order_svc._qc_info_item(db, wo, tenant_no)
- def _build_process_qc_info_list(
- db: Session, rev: BizStorageReleaseReview, tenant_no: str
- ) -> List[Dict[str, Any]]:
- """生产过程触发的入库质检(只读展示,不参与放行判定)。"""
- from app.crud import qc_work_order as qc_work_order_svc
- return qc_work_order_svc.list_process_qc_info_for_sample(
- db, sample_code=rev.sample_code or "", tenant_no=tenant_no
- )
- def _build_production_info(
- db: Session, rev: BizStorageReleaseReview, tenant_no: str
- ) -> Optional[Dict[str, Any]]:
- from app.crud import production_work_order as production_work_order_svc
- code = (rev.sample_code or "").strip()
- q = db.query(BizProductionWorkOrder).filter(
- BizProductionWorkOrder.tenant_no == tenant_no,
- BizProductionWorkOrder.del_flag == 0,
- )
- pwo = q.filter(BizProductionWorkOrder.sample_code == code).first() if code else None
- if not pwo and rev.reception_id:
- pwo = q.filter(BizProductionWorkOrder.reception_id == rev.reception_id).first()
- if not pwo:
- return None
- return production_work_order_svc._production_info_item(db, pwo)
- def submit_review(
- db: Session,
- review_id: str,
- *,
- review_side: str,
- review_conclusion: str,
- review_remark: Optional[str] = None,
- review_attachments: Optional[str] = None,
- tenant_no: str,
- user_id: str,
- ) -> BizStorageReleaseReview:
- rev = (
- db.query(BizStorageReleaseReview)
- .filter(
- BizStorageReleaseReview.id == review_id,
- BizStorageReleaseReview.tenant_no == tenant_no,
- BizStorageReleaseReview.del_flag == 0,
- )
- .first()
- )
- if not rev:
- raise ValueError("审核记录不存在")
- side = normalize_review_side(review_side)
- if side not in ("production", "qc"):
- raise ValueError("审核侧无效")
- conclusion = (review_conclusion or "").strip()
- if conclusion not in ("tg", "btg"):
- raise ValueError("审核结论无效")
- user_name = _get_user_name(db, user_id)
- now = datetime.now()
- remark = (review_remark or "").strip() or None
- attachments = (review_attachments or "").strip() or None
- if side == "production":
- rev.prod_review_conclusion = conclusion
- rev.prod_review_remark = remark
- rev.prod_review_attachments = attachments
- rev.prod_review_by = user_id
- rev.prod_review_by_name = user_name
- rev.prod_review_time = now
- else:
- rev.qc_review_conclusion = conclusion
- rev.qc_review_remark = remark
- rev.qc_review_attachments = attachments
- rev.qc_review_by = user_id
- rev.qc_review_by_name = user_name
- rev.qc_review_time = now
- agg = aggregate_review_conclusion(rev.prod_review_conclusion, rev.qc_review_conclusion)
- rev.review_conclusion = agg
- rev.review_remark = remark
- rev.review_attachments = attachments
- rev.review_by = user_id
- rev.review_by_name = user_name
- rev.review_time = now
- rev.update_by = user_id
- rev.update_time = now
- db.add(rev)
- db.flush()
- action = storage_transition(
- rev.prod_review_conclusion, rev.qc_review_conclusion, rev.storage_status
- )
- if action == "store":
- store_sample(db, review_id, tenant_no=tenant_no, user_id=user_id)
- db.refresh(rev)
- elif action == "unstore":
- unstore_sample(db, review_id, tenant_no=tenant_no, user_id=user_id)
- db.refresh(rev)
- return rev
- def batch_submit_review(
- db: Session,
- ids: List[str],
- *,
- review_side: str,
- review_conclusion: str,
- review_remark: Optional[str] = None,
- review_attachments: Optional[str] = None,
- tenant_no: str,
- user_id: str,
- ) -> Dict[str, Any]:
- success = 0
- fail = 0
- for rid in ids:
- try:
- submit_review(
- db,
- rid,
- review_side=review_side,
- review_conclusion=review_conclusion,
- review_remark=review_remark,
- review_attachments=review_attachments,
- tenant_no=tenant_no,
- user_id=user_id,
- )
- success += 1
- except Exception:
- fail += 1
- return {"success": success, "fail": fail}
- def store_sample(
- db: Session,
- review_id: str,
- *,
- tenant_no: str,
- user_id: str,
- ) -> BizStorageReleaseReview:
- rev = (
- db.query(BizStorageReleaseReview)
- .filter(
- BizStorageReleaseReview.id == review_id,
- BizStorageReleaseReview.tenant_no == tenant_no,
- BizStorageReleaseReview.del_flag == 0,
- )
- .first()
- )
- if not rev:
- raise ValueError("审核记录不存在")
- if aggregate_review_conclusion(rev.prod_review_conclusion, rev.qc_review_conclusion) != "tg":
- raise ValueError("生产与质检均通过后才能入库")
- if rev.storage_status != "drk":
- raise ValueError("当前状态不允许入库操作")
- user_name = _get_user_name(db, user_id)
- rev.storage_status = "yrk"
- rev.storage_by = user_id
- rev.storage_by_name = user_name
- rev.storage_time = datetime.now()
- rev.update_by = user_id
- rev.update_time = datetime.now()
- db.add(rev)
- storage_in_yyyymmdd = rev.storage_time.strftime("%Y%m%d")
- if (rev.reception_source or "cell_storage") == "cell_storage":
- sample_code = (rev.sample_code or "").strip()
- if sample_code:
- ps = (
- db.query(BizProtocolSample)
- .filter(
- BizProtocolSample.sample_code == sample_code,
- BizProtocolSample.tenant_no == tenant_no,
- BizProtocolSample.del_flag == 0,
- )
- .first()
- )
- if ps:
- ps.sample_status = "yrk"
- ps.storage_in_date = storage_in_yyyymmdd
- ps.update_time = datetime.now()
- db.add(ps)
- db.flush()
- return rev
- def unstore_sample(
- db: Session,
- review_id: str,
- *,
- tenant_no: str,
- user_id: str,
- ) -> BizStorageReleaseReview:
- rev = (
- db.query(BizStorageReleaseReview)
- .filter(
- BizStorageReleaseReview.id == review_id,
- BizStorageReleaseReview.tenant_no == tenant_no,
- BizStorageReleaseReview.del_flag == 0,
- )
- .first()
- )
- if not rev:
- raise ValueError("审核记录不存在")
- if (rev.storage_status or "") != "yrk":
- return rev
- rev.storage_status = "drk"
- rev.storage_by = None
- rev.storage_by_name = None
- rev.storage_time = None
- rev.update_by = user_id
- rev.update_time = datetime.now()
- db.add(rev)
- if (rev.reception_source or "cell_storage") == "cell_storage":
- sample_code = (rev.sample_code or "").strip()
- if sample_code:
- ps = (
- db.query(BizProtocolSample)
- .filter(
- BizProtocolSample.sample_code == sample_code,
- BizProtocolSample.tenant_no == tenant_no,
- BizProtocolSample.del_flag == 0,
- )
- .first()
- )
- if ps and (ps.sample_status or "") == "yrk":
- ps.sample_status = "jcz"
- ps.storage_in_date = None
- ps.update_time = datetime.now()
- db.add(ps)
- db.flush()
- return rev
- def batch_store_samples(
- db: Session,
- ids: List[str],
- *,
- tenant_no: str,
- user_id: str,
- ) -> Dict[str, Any]:
- success = 0
- fail = 0
- for rid in ids:
- try:
- store_sample(db, rid, tenant_no=tenant_no, user_id=user_id)
- success += 1
- except Exception:
- fail += 1
- return {"success": success, "fail": fail}
- def get_storage_release_map_for_samples(
- db: Session,
- sample_ids: List[str],
- tenant_no: str,
- *,
- sample_code_by_id: Optional[Dict[str, str]] = None,
- ) -> Dict[str, Dict[str, Any]]:
- """按样本批量返回存储放行审核摘要。
- 当传入 sample_code_by_id 时,按租户 + 样本编码关联存储放行审核记录,
- 返回以 sample_id 为 key 的审核信息映射。
- """
- 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(BizStorageReleaseReview)
- .filter(
- BizStorageReleaseReview.tenant_no == tenant_no,
- BizStorageReleaseReview.del_flag == 0,
- BizStorageReleaseReview.sample_code.in_(codes),
- )
- .all()
- )
- code_to_review: Dict[str, BizStorageReleaseReview] = {}
- for r in rows:
- code = (r.sample_code or "").strip()
- if code and code not in code_to_review:
- code_to_review[code] = r
- 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
- rev = code_to_review.get(code)
- if not rev:
- continue
- rc = rev.review_conclusion or ""
- out[sid] = {
- "reviewConclusion": rc,
- "reviewConclusionLabel": _REVIEW_CONCLUSION_LABELS.get(rc, rc),
- "reviewByName": rev.review_by_name or "",
- "reviewTime": _fmt_dt(rev.review_time),
- "reviewRemark": rev.review_remark or "",
- "reviewAttachments": rev.review_attachments or "",
- "qcReviewConclusion": rev.qc_review_conclusion or "",
- "qcReviewConclusionLabel": _REVIEW_CONCLUSION_LABELS.get(rev.qc_review_conclusion or "", rev.qc_review_conclusion or ""),
- "qcReviewByName": rev.qc_review_by_name or "",
- "qcReviewTime": _fmt_dt(rev.qc_review_time),
- "qcReviewRemark": rev.qc_review_remark or "",
- "qcReviewAttachments": rev.qc_review_attachments or "",
- "prodReviewConclusion": rev.prod_review_conclusion or "",
- "prodReviewConclusionLabel": _REVIEW_CONCLUSION_LABELS.get(rev.prod_review_conclusion or "", rev.prod_review_conclusion or ""),
- "prodReviewByName": rev.prod_review_by_name or "",
- "prodReviewTime": _fmt_dt(rev.prod_review_time),
- "prodReviewRemark": rev.prod_review_remark or "",
- "prodReviewAttachments": rev.prod_review_attachments or "",
- "storageStatus": rev.storage_status or "drk",
- "storageStatusLabel": _STORAGE_STATUS_LABELS.get(rev.storage_status or "drk", rev.storage_status or "drk"),
- }
- return out
|