877 lines
30 KiB
Python
877 lines
30 KiB
Python
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.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),
|
|
"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 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 _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 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,
|
|
qc_work_order_completed=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)
|
|
if phase == "pending":
|
|
q0 = q0.filter(
|
|
or_(
|
|
BizStorageReleaseReview.review_conclusion.is_(None),
|
|
BizStorageReleaseReview.review_conclusion == "",
|
|
)
|
|
)
|
|
elif phase == "reviewed":
|
|
q0 = q0.filter(
|
|
BizStorageReleaseReview.review_conclusion.isnot(None),
|
|
BizStorageReleaseReview.review_conclusion != "",
|
|
)
|
|
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 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_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("审核记录不存在")
|
|
user_name = _get_user_name(db, user_id)
|
|
rev.review_conclusion = review_conclusion
|
|
rev.review_remark = (review_remark or "").strip() or None
|
|
rev.review_attachments = (review_attachments or "").strip() or None
|
|
rev.review_by = user_id
|
|
rev.review_by_name = user_name
|
|
rev.review_time = datetime.now()
|
|
rev.update_by = user_id
|
|
rev.update_time = datetime.now()
|
|
db.add(rev)
|
|
db.flush()
|
|
return rev
|
|
|
|
|
|
def batch_submit_review(
|
|
db: Session,
|
|
ids: List[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_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 rev.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 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 or not (rev.review_conclusion or "").strip():
|
|
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 "",
|
|
}
|
|
return out
|