Files
zkxw/backend/app/crud/qc_work_order.py
T
zlwuandCursor a3af039e74 feat: 质检工单结论、库位批量/筛选与协议样本接收质检摘要
落库并展示质检工单级结论(可编辑/任务派生),库位树支持批量新增子节点与货架格子检索,协议/捐赠列表补充接收与门禁质检摘要,并同步生产物料与工单卡片展示优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 18:46:24 +08:00

3674 lines
127 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""质检工单 CRUD"""
from __future__ import annotations
from collections import defaultdict
from datetime import datetime
from decimal import Decimal
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.db_collation import eq_unicode, like_unicode, unicode_ci
from app.core.utils import generate_uuid
from app.crud import qc_lab_test_item as lab_item_svc
from app.crud import qc_task as task_svc
from app.crud import qc_task_assignee as assignee_svc
from app.crud import qc_third_party_item as third_item_svc
from app.crud.qc_third_party_company import get_company
from app.crud.collection_protocol import urls_to_attachment_list
from app.utils.sample_special import apply_special_to_item_from_samples
from app.crud import sample_reception as sample_reception_svc
from app.crud.sample_reception import _donation_sample_id_col
from app.crud import sample_reception_check_result as check_result_svc
from app.crud.cell_product import _enrich_product_dicts as _cp_enrich, _row_to_dict as _cp_row_to_dict, get_cell_product
from app.services.qc_work_order_fee_summary import enrich_work_order_fee_summaries
from app.services.qc_work_order_conclusion import (
build_qc_conclusion_fields,
work_order_qc_conclusion_label,
)
from app.models.models import (
BizCellProduct,
BizCellSampleType,
BizCollectionProtocol,
BizPilotApplication,
BizPilotInfusionPlan,
BizPilotInfusionSchedule,
BizPilotInfusionScheduleProduct,
BizPilotPatient,
BizProtocolPartyCustomer,
BizProtocolSample,
BizProductionTask,
BizProductionTaskStep,
BizProductionTaskStepOutput,
BizProductionWorkOrder,
BizOutputType,
BizQcLabTestItem,
BizQcTask,
BizQcTaskAssignee,
BizQcThirdPartyItem,
BizQcWorkOrder,
BizQcWorkOrderLabItem,
BizQcWorkOrderThirdItem,
BizSampleReception,
SysUser,
)
from app.models.qc_param import BizQcTaskResult
from app.models.research_donation import (
BizResearchDonation,
BizResearchDonationParty,
BizResearchDonationSample,
)
_RECEPTION_SOURCE_LABELS = {
"cell_storage": "细胞存储",
"research_donation": "科研捐赠",
"outbound": "出库",
}
_QC_BIZ_TYPE_LABELS = {
"inbound": "入库质检",
"outbound": "出库质检",
"temporary": "临时质检",
}
_TRIGGER_SOURCE_LABELS = {
"reception": "样本接收",
"schedule": "计划质检",
"production": "生产过程",
"manual": "手动创建",
}
_ACCEPT_CONCLUSION_LABELS = {"js": "接收", "bj": "拒收"}
_WORK_ORDER_STATUS_LABELS = {"dpz": "待配置", "ypz": "已配置", "ywc": "已完成", "yzf": "已作废"}
_VALID_QC_WORK_ORDER_STATUSES = frozenset(_WORK_ORDER_STATUS_LABELS.keys())
_VALID_QC_WORK_ORDER_CONCLUSIONS = frozenset({"tg", "btg"})
_VALID_QC_DISPATCH_OUTPUT_TYPE_NAME = "有效物"
_TASK_TYPE_LABELS = {"sysz": "实验室自检", "dsfjc": "第三方检测"}
_TASK_STATUS_LABELS = {"dzx": "待执行", "jxz": "进行中", "ywc": "已完成"}
_PROTOCOL_STATUS_LABELS = {
"dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "sdfq": "属地废弃", "zky": "转科研"
}
_DONATION_STATUS_LABELS = {
"dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "sdfq": "属地废弃", "zckc": "转细胞存储"
}
_CONCLUSION_LABELS = {"tg": "通过", "btg": "不通过"}
_OUTBOUND_BIZ_TYPE_LABELS = {
"pilot_service": "中试服务",
"clinical_application": "临床应用",
"basic_experiment": "基础实验",
}
_OUTBOUND_BIZ_TYPES = tuple(_OUTBOUND_BIZ_TYPE_LABELS.keys())
def resolve_outbound_biz_type_for_schedule(
db: Session, schedule_id: str, tenant_no: str
) -> str:
"""按输注计划追溯申请单,得到出库生产/质检工单应使用的 biz_type。"""
if not schedule_id:
return "pilot_service"
sched = (
db.query(BizPilotInfusionSchedule)
.filter(
BizPilotInfusionSchedule.id == schedule_id,
BizPilotInfusionSchedule.tenant_no == tenant_no,
BizPilotInfusionSchedule.del_flag == 0,
)
.first()
)
if not sched or not sched.infusion_plan_id:
return "pilot_service"
plan = (
db.query(BizPilotInfusionPlan)
.filter(
BizPilotInfusionPlan.id == sched.infusion_plan_id,
BizPilotInfusionPlan.tenant_no == tenant_no,
BizPilotInfusionPlan.del_flag == 0,
)
.first()
)
if not plan or not plan.pilot_application_id:
return "pilot_service"
app = (
db.query(BizPilotApplication)
.filter(
BizPilotApplication.id == plan.pilot_application_id,
BizPilotApplication.tenant_no == tenant_no,
BizPilotApplication.del_flag == 0,
)
.first()
)
bt = ((app.biz_type if app else None) or "").strip()
return bt if bt in _OUTBOUND_BIZ_TYPE_LABELS else "pilot_service"
def _dec(v: Any) -> Optional[float]:
if v is None:
return None
return float(v)
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 _reception_source_label(source: Optional[str]) -> str:
key = (source or "").strip() or "cell_storage"
return _RECEPTION_SOURCE_LABELS.get(key, key)
def _qc_biz_type_label(biz_type: Optional[str]) -> str:
key = (biz_type or "").strip() or "inbound"
return _QC_BIZ_TYPE_LABELS.get(key, key)
def _work_order_qc_biz_type(wo: BizQcWorkOrder) -> str:
wo_type = getattr(wo, "work_order_type", None) or "inbound"
if wo_type == "temporary":
return "temporary"
return getattr(wo, "qc_biz_type", None) or "inbound"
def _validate_cell_product_id(
db: Session, cell_product_id: Optional[str], tenant_no: str
) -> str:
cp_id = (cell_product_id or "").strip()
if not cp_id:
raise ValueError("请选择质检对象(细胞产品)")
cp = get_cell_product(db, cp_id, tenant_no)
if not cp:
raise ValueError("细胞产品不存在")
return cp_id
def _enrich_work_order_cell_products(
db: Session, tenant_no: str, items: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
if not items:
return items
cp_ids = list({x.get("cellProductId") for x in items if x.get("cellProductId")})
if not cp_ids:
return items
products = (
db.query(BizCellProduct)
.filter(
BizCellProduct.id.in_(cp_ids),
BizCellProduct.tenant_no == tenant_no,
BizCellProduct.del_flag == 0,
)
.all()
)
product_dicts = _cp_enrich(db, tenant_no, [_cp_row_to_dict(p) for p in products])
product_map = {d["id"]: d for d in product_dicts}
for item in items:
cp = product_map.get(item.get("cellProductId") or "")
item["cellProductCode"] = cp.get("productCode", "") if cp else ""
item["cellProductName"] = cp.get("productName", "") if cp else ""
item["cellProductTypeName"] = cp.get("productTypeName", "") if cp else ""
item["cellProductPassage"] = cp.get("passage", "") if cp else ""
item["cellProductSpecName"] = cp.get("specName", "") if cp else ""
return items
def _enrich_work_order_step_outputs(
db: Session, tenant_no: str, items: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""步骤产出物触发的质检:无细胞产品时用工单条码/名称展示质检对象。"""
if not items:
return items
step_output_ids = list(
{
x.get("productionStepOutputId")
for x in items
if (x.get("productionStepOutputId") or "").strip()
and not (x.get("cellProductId") or "").strip()
}
)
if not step_output_ids:
return items
rows = (
db.query(BizProductionTaskStepOutput)
.filter(
BizProductionTaskStepOutput.id.in_(step_output_ids),
BizProductionTaskStepOutput.tenant_no == tenant_no,
BizProductionTaskStepOutput.del_flag == 0,
)
.all()
)
row_map = {r.id: r for r in rows}
for item in items:
if (item.get("cellProductId") or "").strip():
continue
so = row_map.get(item.get("productionStepOutputId") or "")
if not so:
continue
if not (item.get("cellProductCode") or "").strip():
item["cellProductCode"] = (so.output_barcode or "").strip()
if not (item.get("cellProductName") or "").strip():
item["cellProductName"] = (so.output_name or "").strip()
return items
def _enrich_work_order_qc_targets(
db: Session, tenant_no: str, items: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
items = _enrich_work_order_cell_products(db, tenant_no, items)
return _enrich_work_order_step_outputs(db, tenant_no, items)
def _enrich_outbound_work_orders(
db: Session, tenant_no: str, items: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""为 outbound 类型工单批量补充申请单/用户姓名/疗程序号/输注计划信息。"""
from app.crud.pilot_infusion_schedule_mgmt import (
_get_plan_schedule_course_meta,
_resolve_course_display,
)
outbound = [x for x in items if x.get("workOrderType") == "outbound"]
if not outbound:
return items
schedule_ids = list({x.get("infusionScheduleId") for x in outbound if x.get("infusionScheduleId")})
if not schedule_ids:
return items
products_map: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
for product in (
db.query(BizPilotInfusionScheduleProduct)
.filter(
BizPilotInfusionScheduleProduct.infusion_schedule_id.in_(schedule_ids),
BizPilotInfusionScheduleProduct.tenant_no == tenant_no,
BizPilotInfusionScheduleProduct.del_flag == 0,
)
.order_by(BizPilotInfusionScheduleProduct.order_num.asc())
.all()
):
products_map[product.infusion_schedule_id].append(
{
"projectCode": product.project_code or "",
"projectName": product.project_name or "",
"cellTypeName": product.cell_type_name or "",
"specText": product.spec_text or "",
"agreementPrice": _dec(product.agreement_price),
"actualPrice": _dec(product.actual_price),
"quantity": product.quantity,
"productStatus": product.product_status or "",
}
)
# 查输注计划 → 输注方案 → 申请单 → 患者
schedules = {
s.id: s
for s in db.query(BizPilotInfusionSchedule)
.filter(
BizPilotInfusionSchedule.id.in_(schedule_ids),
BizPilotInfusionSchedule.tenant_no == tenant_no,
BizPilotInfusionSchedule.del_flag == 0,
)
.all()
}
plan_ids = list({s.infusion_plan_id for s in schedules.values() if s.infusion_plan_id})
plans = {
p.id: p
for p in db.query(BizPilotInfusionPlan)
.filter(
BizPilotInfusionPlan.id.in_(plan_ids),
BizPilotInfusionPlan.tenant_no == tenant_no,
BizPilotInfusionPlan.del_flag == 0,
)
.all()
} if plan_ids else {}
app_ids = list({p.pilot_application_id for p in plans.values() if p.pilot_application_id})
apps = {
a.id: a
for a in db.query(BizPilotApplication)
.filter(
BizPilotApplication.id.in_(app_ids),
BizPilotApplication.tenant_no == tenant_no,
BizPilotApplication.del_flag == 0,
)
.all()
} if app_ids else {}
patient_map: Dict[str, BizPilotPatient] = {}
if app_ids:
patients = (
db.query(BizPilotPatient)
.filter(
BizPilotPatient.pilot_application_id.in_(app_ids),
BizPilotPatient.tenant_no == tenant_no,
BizPilotPatient.del_flag == 0,
)
.all()
)
patient_map = {p.pilot_application_id: p for p in patients}
plan_course_meta = _get_plan_schedule_course_meta(db, plan_ids, tenant_no)
for item in outbound:
sid = item.get("infusionScheduleId") or ""
sched = schedules.get(sid)
if not sched:
continue
plan = plans.get(sched.infusion_plan_id or "")
app = apps.get(plan.pilot_application_id or "") if plan else None
patient = (
patient_map.get(plan.pilot_application_id)
if plan and plan.pilot_application_id
else None
)
plan_meta = (
plan_course_meta.get(sched.infusion_plan_id) if sched.infusion_plan_id else None
)
course = _resolve_course_display(sched, plan, plan_meta)
item["applicationNo"] = app.application_no or "" if app else ""
item["applicationName"] = app.application_name or "" if app else ""
item["channelName"] = app.channel_name or "" if app else ""
if app:
item["projectTitle"] = app.project_title or ""
item["customerName"] = app.customer_name or ""
item["patientName"] = patient.patient_name if patient else ""
item["sortNo"] = course.get("sortNo")
item["totalCourseCount"] = course.get("totalCourseCount")
item["cellType"] = sched.cell_type or ""
item["specText"] = sched.spec_text or ""
item["infusionTime"] = _fmt_dt(sched.infusion_time)
item["products"] = products_map.get(sid, [])
return items
def _outbound_qc_work_order_ids_by_keyword(
db: Session, tenant_no: str, keyword: str
) -> List[str]:
"""出库质检工单关键词:按关联申请单号、申请单名称匹配。"""
kw = (keyword or "").strip()
if not kw:
return []
like = f"%{kw}%"
rows = (
db.query(BizQcWorkOrder.id)
.join(
BizPilotInfusionSchedule,
eq_unicode(BizQcWorkOrder.infusion_schedule_id, BizPilotInfusionSchedule.id),
)
.join(
BizPilotInfusionPlan,
eq_unicode(
BizPilotInfusionSchedule.infusion_plan_id,
BizPilotInfusionPlan.id,
),
)
.join(
BizPilotApplication,
eq_unicode(
BizPilotInfusionPlan.pilot_application_id,
BizPilotApplication.id,
),
)
.filter(
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.work_order_type == "outbound",
BizPilotInfusionSchedule.del_flag == 0,
BizPilotInfusionPlan.del_flag == 0,
BizPilotApplication.del_flag == 0,
or_(
like_unicode(BizPilotApplication.application_no, like),
like_unicode(BizPilotApplication.application_name, like),
like_unicode(BizPilotApplication.project_title, like),
like_unicode(BizPilotApplication.customer_name, like),
),
)
.all()
)
return [r[0] for r in rows]
def _inbound_qc_work_order_ids_by_mother_name(
db: Session,
tenant_no: str,
mother_name: str,
*,
reception_source: Optional[str] = None,
) -> List[str]:
"""入库质检工单:按母亲姓名匹配(细胞存储协议 / 科研捐赠单)。"""
mn = (mother_name or "").strip()
if not mn or not tenant_no:
return []
like = f"%{mn}%"
ids: set = set()
rs = (reception_source or "").strip()
if not rs or rs == "cell_storage":
for (wid,) in (
db.query(BizQcWorkOrder.id)
.join(
BizProtocolPartyCustomer,
eq_unicode(
BizProtocolPartyCustomer.collection_protocol_id,
BizQcWorkOrder.collection_protocol_id,
),
)
.filter(
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.work_order_type == "inbound",
or_(
BizQcWorkOrder.reception_source == "cell_storage",
BizQcWorkOrder.reception_source.is_(None),
BizQcWorkOrder.reception_source == "",
),
BizProtocolPartyCustomer.del_flag == 0,
like_unicode(BizProtocolPartyCustomer.mother_name, like),
)
.all()
):
ids.add(wid)
if not rs or rs == "research_donation":
for (wid,) in (
db.query(BizQcWorkOrder.id)
.join(
BizResearchDonationParty,
eq_unicode(
BizResearchDonationParty.research_donation_id,
BizQcWorkOrder.collection_protocol_id,
),
)
.filter(
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.work_order_type == "inbound",
BizQcWorkOrder.reception_source == "research_donation",
BizResearchDonationParty.del_flag == 0,
like_unicode(BizResearchDonationParty.mother_name, like),
)
.all()
):
ids.add(wid)
return list(ids)
def _accept_conclusion_label(v: Optional[str]) -> str:
key = (v or "").strip()
return _ACCEPT_CONCLUSION_LABELS.get(key, key)
def _load_sample_type_name_map(db: Session, type_ids: List[str]) -> Dict[str, str]:
ids = [x for x in type_ids if x]
if not ids:
return {}
return {
t.id: t.type_name or ""
for t in db.query(BizCellSampleType)
.filter(BizCellSampleType.id.in_(ids), BizCellSampleType.del_flag == 0)
.all()
}
def _effective_reception_source(item: Dict[str, Any], rec: Optional[BizSampleReception]) -> str:
"""列表展示以工单 receptionSource 为准,接收记录作兜底。"""
return (
(item.get("receptionSource") or "").strip()
or (getattr(rec, "reception_source", None) if rec else None)
or "cell_storage"
)
def _effective_donation_sample_id(item: Dict[str, Any], rec: Optional[BizSampleReception]) -> str:
"""科研捐赠:接收记录 donation_sample_id;工单 protocol_sample_id 存捐赠样本 id。"""
if _effective_reception_source(item, rec) != "research_donation":
return ""
return (
(getattr(rec, "donation_sample_id", None) if rec else None) or ""
).strip() or (item.get("protocolSampleId") or "").strip()
def _effective_protocol_sample_id(item: Dict[str, Any], rec: Optional[BizSampleReception]) -> str:
if _effective_reception_source(item, rec) == "research_donation":
return ""
return (
(getattr(rec, "protocol_sample_id", None) if rec else None) or ""
).strip() or (item.get("protocolSampleId") or "").strip()
def _sample_type_name_for_item(
*,
source: str,
donation_sample_id: str,
protocol_sample_id: str,
type_map: Dict[str, str],
protocol_samples: Dict[str, BizProtocolSample],
donation_samples: Dict[str, BizResearchDonationSample],
) -> str:
if source == "research_donation":
sample = donation_samples.get(donation_sample_id or "")
tid = sample.sample_type_id if sample else ""
else:
sample = protocol_samples.get(protocol_sample_id or "")
tid = sample.sample_type_id if sample else ""
return type_map.get(tid or "", "")
def _sample_type_name_for_reception(
db: Session,
rec: BizSampleReception,
type_map: Dict[str, str],
*,
protocol_samples: Dict[str, BizProtocolSample],
donation_samples: Dict[str, BizResearchDonationSample],
item: Optional[Dict[str, Any]] = None,
) -> str:
"""按接收记录解析样本类型;传入 item 时与工单列表 enrich 规则一致。"""
_ = db
ctx = item if item is not None else {}
source = _effective_reception_source(ctx, rec)
return _sample_type_name_for_item(
source=source,
donation_sample_id=_effective_donation_sample_id(ctx, rec),
protocol_sample_id=_effective_protocol_sample_id(ctx, rec),
type_map=type_map,
protocol_samples=protocol_samples,
donation_samples=donation_samples,
)
def _enrich_work_order_list_items(
db: Session, items: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
if not items:
return items
rec_ids = [x["receptionId"] for x in items if x.get("receptionId")]
sample_codes_no_rec = list(
{
(x.get("sampleCode") or "").strip()
for x in items
if not (x.get("receptionId") or "").strip() and (x.get("sampleCode") or "").strip()
}
)
rec_map = {}
if rec_ids:
rec_map = {
r.id: r
for r in db.query(BizSampleReception)
.filter(BizSampleReception.id.in_(rec_ids), BizSampleReception.del_flag == 0)
.all()
}
sample_code_rec_map: Dict[str, BizSampleReception] = {}
if sample_codes_no_rec:
for r in (
db.query(BizSampleReception)
.filter(
BizSampleReception.sample_code.in_(sample_codes_no_rec),
BizSampleReception.del_flag == 0,
)
.order_by(BizSampleReception.create_time.desc())
.all()
):
code = (r.sample_code or "").strip()
if code and code not in sample_code_rec_map:
sample_code_rec_map[code] = r
if not rec_map and not sample_code_rec_map:
return items
def _resolve_reception(item: Dict[str, Any]) -> Optional[BizSampleReception]:
rec = rec_map.get(item.get("receptionId") or "")
if rec:
return rec
code = (item.get("sampleCode") or "").strip()
return sample_code_rec_map.get(code) if code else None
cell_sample_ids: List[str] = []
donation_sample_ids: List[str] = []
cell_proto_ids: List[str] = []
donation_ids: List[str] = []
for item in items:
rec = _resolve_reception(item)
source = _effective_reception_source(item, rec)
if source == "research_donation":
dsid = _effective_donation_sample_id(item, rec)
if dsid:
donation_sample_ids.append(dsid)
did = (
(getattr(rec, "research_donation_id", None) if rec else None) or ""
).strip() or (item.get("collectionProtocolId") or "").strip()
if did:
donation_ids.append(did)
else:
psid = _effective_protocol_sample_id(item, rec)
if psid:
cell_sample_ids.append(psid)
pid = (
(getattr(rec, "collection_protocol_id", None) if rec else None) or ""
).strip() or (item.get("collectionProtocolId") or "").strip()
if pid:
cell_proto_ids.append(pid)
donation_sample_ids = list(dict.fromkeys(donation_sample_ids))
cell_sample_ids = list(dict.fromkeys(cell_sample_ids))
donation_ids = list(dict.fromkeys(donation_ids))
cell_proto_ids = list(dict.fromkeys(cell_proto_ids))
protocol_samples: Dict[str, BizProtocolSample] = {}
if cell_sample_ids:
protocol_samples = {
s.id: s
for s in db.query(BizProtocolSample)
.filter(BizProtocolSample.id.in_(cell_sample_ids), BizProtocolSample.del_flag == 0)
.all()
}
donation_samples: Dict[str, BizResearchDonationSample] = {}
if donation_sample_ids:
donation_samples = {}
for s in (
db.query(BizResearchDonationSample)
.filter(
_donation_sample_id_col(BizResearchDonationSample.id).in_(donation_sample_ids),
BizResearchDonationSample.del_flag == 0,
)
.all()
):
donation_samples[s.id] = s
donation_samples[(s.id or "").strip()] = s
type_ids = list(
{
s.sample_type_id
for s in list(protocol_samples.values()) + list(donation_samples.values())
if s.sample_type_id
}
)
type_map = _load_sample_type_name_map(db, type_ids)
proto_status_map: Dict[str, str] = {}
proto_void_type_map: Dict[str, str] = {}
tenant_no_for_status = ""
if cell_proto_ids:
for p in (
db.query(
BizCollectionProtocol.id,
BizCollectionProtocol.protocol_status,
BizCollectionProtocol.void_type,
BizCollectionProtocol.tenant_no,
)
.filter(BizCollectionProtocol.id.in_(cell_proto_ids), BizCollectionProtocol.del_flag == 0)
.all()
):
proto_status_map[p.id] = p.protocol_status or ""
proto_void_type_map[p.id] = p.void_type or ""
if not tenant_no_for_status and (p.tenant_no or "").strip():
tenant_no_for_status = (p.tenant_no or "").strip()
donation_status_map: Dict[str, str] = {}
donation_void_type_map: Dict[str, str] = {}
if donation_ids:
for d in (
db.query(
BizResearchDonation.id,
BizResearchDonation.donation_status,
BizResearchDonation.void_type,
BizResearchDonation.tenant_no,
)
.filter(BizResearchDonation.id.in_(donation_ids), BizResearchDonation.del_flag == 0)
.all()
):
donation_status_map[d.id] = d.donation_status or ""
donation_void_type_map[d.id] = d.void_type or ""
if not tenant_no_for_status and (d.tenant_no or "").strip():
tenant_no_for_status = (d.tenant_no or "").strip()
cell_party_map: Dict[str, Tuple[str, str]] = {}
if cell_proto_ids:
for party in (
db.query(BizProtocolPartyCustomer)
.filter(
BizProtocolPartyCustomer.collection_protocol_id.in_(cell_proto_ids),
BizProtocolPartyCustomer.del_flag == 0,
)
.all()
):
cell_party_map[party.collection_protocol_id] = (
party.mother_name or "",
party.newborn_name or "",
)
donation_party_map: Dict[str, Tuple[str, str]] = {}
if donation_ids:
for party in (
db.query(BizResearchDonationParty)
.filter(
BizResearchDonationParty.research_donation_id.in_(donation_ids),
BizResearchDonationParty.del_flag == 0,
)
.all()
):
donation_party_map[party.research_donation_id] = (
party.mother_name or "",
party.newborn_name or "",
)
for item in items:
if item.get("workOrderType") == "temporary":
item["receptionSource"] = ""
item["receptionSourceLabel"] = ""
item["motherName"] = ""
item["newbornName"] = ""
continue
rec = _resolve_reception(item)
source = _effective_reception_source(item, rec)
item["receptionSource"] = source
item["receptionSourceLabel"] = _reception_source_label(source)
if source == "research_donation":
_did = (
(getattr(rec, "research_donation_id", None) if rec else None) or ""
).strip() or (item.get("collectionProtocolId") or "").strip()
_mother, _newborn = donation_party_map.get(_did or "", ("", ""))
item["motherName"] = _mother
item["newbornName"] = _newborn
else:
_pid = (
(getattr(rec, "collection_protocol_id", None) if rec else None) or ""
).strip() or (item.get("collectionProtocolId") or "").strip()
_mother, _newborn = cell_party_map.get(_pid or "", ("", ""))
item["motherName"] = _mother
item["newbornName"] = _newborn
if not rec:
item.setdefault("receptionTime", "")
item.setdefault("handlerName", "")
item.setdefault("sampleTypeName", "")
item.setdefault("labStorageCode", "")
continue
item["receptionTime"] = _fmt_dt(rec.reception_time)
item["handlerName"] = rec.handler_name or ""
item["labStorageCode"] = rec.lab_storage_code or ""
item["sampleTypeName"] = _sample_type_name_for_item(
source=source,
donation_sample_id=_effective_donation_sample_id(item, rec),
protocol_sample_id=_effective_protocol_sample_id(item, rec),
type_map=type_map,
protocol_samples=protocol_samples,
donation_samples=donation_samples,
)
apply_special_to_item_from_samples(
item,
source=source,
protocol_sample_id=_effective_protocol_sample_id(item, rec),
donation_sample_id=_effective_donation_sample_id(item, rec),
protocol_samples=protocol_samples,
donation_samples=donation_samples,
)
if source == "research_donation":
_did = (
(getattr(rec, "research_donation_id", None) or "").strip()
or (item.get("collectionProtocolId") or "").strip()
)
_st = donation_status_map.get(_did or "", "")
_vt = donation_void_type_map.get(_did 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=tenant_no_for_status or None
)
else:
_pid = (
(getattr(rec, "collection_protocol_id", None) or "").strip()
or (item.get("collectionProtocolId") or "").strip()
)
_st = proto_status_map.get(_pid or "", "")
_vt = proto_void_type_map.get(_pid 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=tenant_no_for_status or None
)
return _enrich_protocol_sibling_samples(db, items)
def _protocol_group_key_for_item(item: Dict[str, Any]) -> str:
wo_type = (item.get("workOrderType") or "inbound").strip() or "inbound"
if wo_type != "inbound":
return ""
source = (item.get("receptionSource") or "cell_storage").strip() or "cell_storage"
proto_id = (item.get("collectionProtocolId") or "").strip()
if proto_id:
if source == "research_donation":
return f"donation:{proto_id}"
return f"protocol:{proto_id}"
protocol_no = (item.get("protocolNo") or "").strip()
if protocol_no:
return f"protocol-no:{protocol_no}"
return ""
def _enrich_protocol_sibling_samples(
db: Session, items: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""入库列表:补充同协议下全部样本(供卡片标题栏「同协议样本」展示)。"""
if not items:
return items
cell_proto_ids: List[str] = []
donation_ids: List[str] = []
for item in items:
if (item.get("workOrderType") or "inbound").strip() not in ("", "inbound"):
continue
pid = (item.get("collectionProtocolId") or "").strip()
if not pid:
continue
source = (item.get("receptionSource") or "cell_storage").strip() or "cell_storage"
if source == "research_donation":
if pid not in donation_ids:
donation_ids.append(pid)
elif pid not in cell_proto_ids:
cell_proto_ids.append(pid)
siblings_by_key: Dict[str, List[Dict[str, str]]] = {}
if cell_proto_ids:
proto_samples = (
db.query(BizProtocolSample)
.filter(
BizProtocolSample.collection_protocol_id.in_(cell_proto_ids),
BizProtocolSample.del_flag == 0,
)
.all()
)
by_proto: Dict[str, List[BizProtocolSample]] = defaultdict(list)
type_ids: List[str] = []
tenant_no = ""
for sample in proto_samples:
pid = (sample.collection_protocol_id or "").strip()
if pid:
by_proto[pid].append(sample)
if sample.sample_type_id and sample.sample_type_id not in type_ids:
type_ids.append(sample.sample_type_id)
if not tenant_no and (sample.tenant_no or "").strip():
tenant_no = (sample.tenant_no or "").strip()
type_map = _load_sample_type_name_map(db, type_ids)
sample_codes = list(
{
(s.sample_code or "").strip()
for samples in by_proto.values()
for s in samples
if (s.sample_code or "").strip()
}
)
lab_map = (
sample_reception_svc.get_lab_storage_code_map_by_sample_codes(
db, tenant_no, sample_codes, reception_source="cell_storage"
)
if tenant_no and sample_codes
else {}
)
for pid, samples in by_proto.items():
entries: List[Dict[str, str]] = []
for sample in sorted(samples, key=lambda row: (row.sample_code or "")):
code = (sample.sample_code or "").strip()
if not code:
continue
entries.append(
{
"sampleCode": code,
"labStorageCode": lab_map.get(code, ""),
"sampleTypeName": type_map.get(sample.sample_type_id or "", ""),
}
)
if len(entries) > 1:
siblings_by_key[f"protocol:{pid}"] = entries
if donation_ids:
donation_samples = (
db.query(BizResearchDonationSample)
.filter(
BizResearchDonationSample.research_donation_id.in_(donation_ids),
BizResearchDonationSample.del_flag == 0,
)
.all()
)
by_donation: Dict[str, List[BizResearchDonationSample]] = defaultdict(list)
type_ids = []
tenant_no = ""
for sample in donation_samples:
did = (sample.research_donation_id or "").strip()
if did:
by_donation[did].append(sample)
if sample.sample_type_id and sample.sample_type_id not in type_ids:
type_ids.append(sample.sample_type_id)
if not tenant_no and (sample.tenant_no or "").strip():
tenant_no = (sample.tenant_no or "").strip()
type_map = _load_sample_type_name_map(db, type_ids)
sample_codes = list(
{
(s.sample_code or "").strip()
for samples in by_donation.values()
for s in samples
if (s.sample_code or "").strip()
}
)
lab_map = (
sample_reception_svc.get_lab_storage_code_map_by_sample_codes(
db, tenant_no, sample_codes, reception_source="research_donation"
)
if tenant_no and sample_codes
else {}
)
for did, samples in by_donation.items():
entries = []
for sample in sorted(samples, key=lambda row: (row.sample_code or "")):
code = (sample.sample_code or "").strip()
if not code:
continue
entries.append(
{
"sampleCode": code,
"labStorageCode": lab_map.get(code, ""),
"sampleTypeName": type_map.get(sample.sample_type_id or "", ""),
}
)
if len(entries) > 1:
siblings_by_key[f"donation:{did}"] = entries
for item in items:
key = _protocol_group_key_for_item(item)
siblings = siblings_by_key.get(key)
if siblings:
item["protocolSiblingSamples"] = siblings
return items
def _enrich_production_work_order_refs(
db: Session, items: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
if not items:
return items
pwo_ids = list(
{x.get("productionWorkOrderId") for x in items if (x.get("productionWorkOrderId") or "").strip()}
)
pwo_map: Dict[str, str] = {}
if pwo_ids:
rows = (
db.query(BizProductionWorkOrder.id, BizProductionWorkOrder.work_order_no)
.filter(
BizProductionWorkOrder.id.in_(pwo_ids),
BizProductionWorkOrder.del_flag == 0,
)
.all()
)
pwo_map = {r.id: r.work_order_no or "" for r in rows}
for item in items:
pid = (item.get("productionWorkOrderId") or "").strip()
item["productionWorkOrderNo"] = pwo_map.get(pid, "") if pid else ""
return items
def _gen_daily_no(db: Session, tenant_no: str, prefix: str) -> str:
day = datetime.now().strftime("%Y%m%d")
like = f"{prefix}{day}%"
cnt = (
db.query(BizQcWorkOrder)
.filter(
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.work_order_no.like(like),
)
.count()
)
return f"{prefix}{day}{cnt + 1:04d}"
def _user_nick_name_map(db: Session, user_ids: List[str]) -> Dict[str, str]:
ids = list({x for x in user_ids if x})
if not ids:
return {}
rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(ids)).all()
return {u.id: (u.nick_name or "").strip() for u in rows}
def _wo_to_dict(
wo: BizQcWorkOrder,
*,
outbound_info: Optional[Dict[str, Any]] = None,
configured_by_name: str = "",
) -> Dict[str, Any]:
wo_type = getattr(wo, "work_order_type", None) or "inbound"
recv_source_raw = getattr(wo, "reception_source", None)
if wo_type == "temporary":
recv_source = (recv_source_raw or "").strip()
else:
recv_source = (recv_source_raw or "cell_storage").strip() or "cell_storage"
biz_type = getattr(wo, "biz_type", None) or ""
is_outbound = wo_type == "outbound"
ob = outbound_info or {}
return {
"id": wo.id,
"workOrderNo": wo.work_order_no,
"workOrderType": wo_type,
"receptionId": wo.reception_id,
"protocolSampleId": wo.protocol_sample_id,
"collectionProtocolId": wo.collection_protocol_id,
"protocolNo": wo.protocol_no or "",
"sampleCode": wo.sample_code or "",
"receptionSource": recv_source,
"receptionSourceLabel": _reception_source_label(recv_source) if recv_source else "",
"qcBizType": _work_order_qc_biz_type(wo),
"qcBizTypeLabel": _qc_biz_type_label(_work_order_qc_biz_type(wo)),
"infusionScheduleId": getattr(wo, "infusion_schedule_id", None) or None,
"bizType": biz_type if is_outbound else "",
"bizTypeLabel": _OUTBOUND_BIZ_TYPE_LABELS.get(biz_type, "") if is_outbound else "",
"applicationNo": ob.get("applicationNo", "") if is_outbound else "",
"applicationName": ob.get("applicationName", "") if is_outbound else "",
"channelName": ob.get("channelName", "") if is_outbound else "",
"infusionTime": ob.get("infusionTime", "") if is_outbound else "",
"cellProductId": getattr(wo, "cell_product_id", None) or "",
"cellProductCode": "",
"cellProductName": "",
"cellProductTypeName": "",
"cellProductPassage": "",
"cellProductSpecName": "",
"triggerSource": getattr(wo, "trigger_source", None) or "reception",
"triggerSourceLabel": _TRIGGER_SOURCE_LABELS.get(
getattr(wo, "trigger_source", None) or "reception", ""
),
"productionWorkOrderId": getattr(wo, "production_work_order_id", None) or "",
"productionWorkOrderNo": "",
"productionTaskId": getattr(wo, "production_task_id", None) or "",
"productionStepOutputId": getattr(wo, "production_step_output_id", None) or "",
"receptionTime": "",
"handlerName": "",
"sampleTypeName": "",
"workOrderStatus": wo.work_order_status,
"workOrderStatusLabel": _WORK_ORDER_STATUS_LABELS.get(
wo.work_order_status or "", wo.work_order_status or ""
),
"qcConclusion": (
(getattr(wo, "conclusion", None) or "").strip()
if (getattr(wo, "conclusion", None) or "").strip() in _VALID_QC_WORK_ORDER_CONCLUSIONS
else ""
),
"qcConclusionLabel": work_order_qc_conclusion_label(getattr(wo, "conclusion", None) or ""),
"hasLab": wo.has_lab == "1",
"hasThirdParty": wo.has_third_party == "1",
"configuredTime": _fmt_dt(wo.configured_time),
"configuredBy": wo.configured_by or "",
"configuredByName": configured_by_name or wo.configured_by or "",
"thirdPartyFeeTotal": _dec(wo.third_party_fee_total),
"remark": wo.remark or "",
"createTime": _fmt_dt(wo.create_time),
"updateTime": _fmt_dt(wo.update_time),
}
def create_work_order_from_reception(
db: Session,
*,
reception,
tenant_no: str,
user_id: Optional[str] = None,
create_dept_id: str = "",
) -> Optional[BizQcWorkOrder]:
"""接收结论为 js 时创建待配置工单(flush,不 commit)。"""
if (reception.accept_conclusion or "").strip() != "js":
return None
exists = (
db.query(BizQcWorkOrder)
.filter(
BizQcWorkOrder.reception_id == reception.id,
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.del_flag == 0,
)
.first()
)
if exists:
return exists
source = getattr(reception, "reception_source", "cell_storage") or "cell_storage"
if source == "research_donation":
protocol_no = ""
d = (
db.query(BizResearchDonation)
.filter(
BizResearchDonation.id == (reception.research_donation_id or ""),
BizResearchDonation.del_flag == 0,
)
.first()
)
if d:
protocol_no = d.donation_no or ""
sample_code = ""
ds = (
db.query(BizResearchDonationSample)
.filter(
BizResearchDonationSample.id == (reception.donation_sample_id or ""),
BizResearchDonationSample.del_flag == 0,
)
.first()
)
if ds:
sample_code = ds.sample_code or ""
wo = BizQcWorkOrder(
id=generate_uuid(),
work_order_no=_gen_daily_no(db, tenant_no, "ZJ"),
reception_id=reception.id,
protocol_sample_id=reception.donation_sample_id or "",
collection_protocol_id=reception.research_donation_id or "",
protocol_no=protocol_no or None,
sample_code=sample_code or None,
reception_source="research_donation",
qc_biz_type="inbound",
trigger_source="reception",
work_order_status="dpz",
has_lab="0",
has_third_party="0",
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,
)
else:
protocol_no = ""
p = (
db.query(BizCollectionProtocol)
.filter(
BizCollectionProtocol.id == reception.collection_protocol_id,
BizCollectionProtocol.del_flag == 0,
)
.first()
)
if p:
protocol_no = p.protocol_no or ""
sample_code = ""
s = (
db.query(BizProtocolSample)
.filter(BizProtocolSample.id == reception.protocol_sample_id, BizProtocolSample.del_flag == 0)
.first()
)
if s:
sample_code = s.sample_code or ""
wo = BizQcWorkOrder(
id=generate_uuid(),
work_order_no=_gen_daily_no(db, tenant_no, "ZJ"),
reception_id=reception.id,
protocol_sample_id=reception.protocol_sample_id,
collection_protocol_id=reception.collection_protocol_id,
protocol_no=protocol_no or None,
sample_code=sample_code or None,
reception_source="cell_storage",
qc_biz_type="inbound",
trigger_source="reception",
work_order_status="dpz",
has_lab="0",
has_third_party="0",
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(wo)
db.flush()
return wo
def _valid_qc_dispatch_output_type_ids(db: Session, tenant_no: str) -> set:
rows = (
db.query(BizOutputType)
.filter(
BizOutputType.tenant_no == tenant_no,
BizOutputType.del_flag == 0,
BizOutputType.type_name == _VALID_QC_DISPATCH_OUTPUT_TYPE_NAME,
)
.all()
)
return {r.id for r in rows}
def _resolve_cell_product_id_by_barcode(
db: Session, barcode: Optional[str], tenant_no: str
) -> Optional[str]:
code = (barcode or "").strip()
if not code:
return None
cp = (
db.query(BizCellProduct)
.filter(
BizCellProduct.product_code == code,
BizCellProduct.tenant_no == tenant_no,
BizCellProduct.del_flag == 0,
)
.first()
)
return cp.id if cp else None
def create_qc_work_order_from_production(
db: Session,
*,
production_work_order_id: str,
production_step_output_id: str,
remark: str = "",
tenant_no: str,
user_id: Optional[str] = None,
create_dept_id: str = "",
) -> BizQcWorkOrder:
"""生产过程派发质检工单(flush,不 commit)。"""
pwo = (
db.query(BizProductionWorkOrder)
.filter(
BizProductionWorkOrder.id == production_work_order_id,
BizProductionWorkOrder.tenant_no == tenant_no,
BizProductionWorkOrder.del_flag == 0,
)
.first()
)
if not pwo:
raise ValueError("生产工单不存在")
wo_type = (getattr(pwo, "work_order_type", None) or "inbound").strip()
if wo_type not in ("inbound", "outbound", "temporary"):
raise ValueError("仅入库、出库或临时生产工单可派发质检")
status = (pwo.work_order_status or "").strip()
if status == "yzf":
raise ValueError("已作废的生产工单不可派发质检")
if status not in ("jxz", "ywc"):
raise ValueError("仅进行中或已完成的生产工单可派发质检")
step_output_id = (production_step_output_id or "").strip()
if not step_output_id:
raise ValueError("请选择步骤产出物")
options = list_step_output_dispatch_options_for_production_work_order(
db, production_work_order_id=production_work_order_id, tenant_no=tenant_no
)
option_map = {o["id"]: o for o in options}
selected = option_map.get(step_output_id)
if not selected:
raise ValueError("所选步骤产出物不在该生产工单的可派发列表中")
step_output = (
db.query(BizProductionTaskStepOutput)
.filter(
BizProductionTaskStepOutput.id == step_output_id,
BizProductionTaskStepOutput.tenant_no == tenant_no,
BizProductionTaskStepOutput.del_flag == 0,
)
.first()
)
if not step_output:
raise ValueError("步骤产出物不存在")
step = (
db.query(BizProductionTaskStep)
.filter(
BizProductionTaskStep.id == step_output.step_id,
BizProductionTaskStep.tenant_no == tenant_no,
BizProductionTaskStep.del_flag == 0,
)
.first()
)
if not step:
raise ValueError("关联工艺步骤不存在")
task = (
db.query(BizProductionTask)
.filter(
BizProductionTask.id == step.task_id,
BizProductionTask.work_order_id == pwo.id,
BizProductionTask.tenant_no == tenant_no,
BizProductionTask.del_flag == 0,
)
.first()
)
if not task:
raise ValueError("关联生产任务不存在")
cp_id = _resolve_cell_product_id_by_barcode(db, step_output.output_barcode, tenant_no)
task_id = task.id
common_fields = dict(
trigger_source="production",
cell_product_id=cp_id,
production_work_order_id=pwo.id,
production_task_id=task_id,
production_step_output_id=step_output_id,
work_order_status="dpz",
has_lab="0",
has_third_party="0",
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,
create_time=datetime.now(),
update_time=datetime.now(),
del_flag=0,
)
if wo_type == "outbound":
schedule_id = (getattr(pwo, "infusion_schedule_id", None) or "").strip()
if not schedule_id:
raise ValueError("出库生产工单缺少输注计划关联")
biz_type = (getattr(pwo, "biz_type", None) or "pilot_service").strip() or "pilot_service"
if biz_type not in _OUTBOUND_BIZ_TYPE_LABELS:
biz_type = "pilot_service"
wo = BizQcWorkOrder(
id=generate_uuid(),
work_order_no=_gen_daily_no(db, tenant_no, "CKZJ"),
work_order_type="outbound",
reception_id=None,
protocol_sample_id=None,
collection_protocol_id=None,
protocol_no=None,
sample_code=None,
reception_source="outbound",
qc_biz_type="outbound",
biz_type=biz_type,
infusion_schedule_id=schedule_id,
**common_fields,
)
elif wo_type == "temporary":
wo = BizQcWorkOrder(
id=generate_uuid(),
work_order_no=_gen_daily_no(db, tenant_no, "LSZJ"),
work_order_type="temporary",
reception_id=None,
protocol_sample_id=None,
collection_protocol_id=None,
protocol_no=None,
sample_code=None,
reception_source=None,
qc_biz_type="temporary",
**common_fields,
)
else:
wo = BizQcWorkOrder(
id=generate_uuid(),
work_order_no=_gen_daily_no(db, tenant_no, "ZJ"),
work_order_type="inbound",
reception_id=None,
protocol_sample_id=pwo.protocol_sample_id,
collection_protocol_id=pwo.collection_protocol_id,
protocol_no=pwo.protocol_no or None,
sample_code=pwo.sample_code or None,
reception_source=getattr(pwo, "reception_source", "cell_storage") or "cell_storage",
qc_biz_type="inbound",
**common_fields,
)
db.add(wo)
db.flush()
return wo
def list_step_output_dispatch_options_for_production_work_order(
db: Session,
*,
production_work_order_id: str,
tenant_no: str,
) -> List[Dict[str, Any]]:
"""汇总生产工单下可派发的步骤产出物:类型为有效物且有条码。"""
pwo = (
db.query(BizProductionWorkOrder)
.filter(
BizProductionWorkOrder.id == production_work_order_id,
BizProductionWorkOrder.tenant_no == tenant_no,
BizProductionWorkOrder.del_flag == 0,
)
.first()
)
if not pwo:
raise ValueError("生产工单不存在")
valid_type_ids = _valid_qc_dispatch_output_type_ids(db, tenant_no)
if not valid_type_ids:
return []
tasks = (
db.query(BizProductionTask)
.filter(
BizProductionTask.work_order_id == pwo.id,
BizProductionTask.tenant_no == tenant_no,
BizProductionTask.del_flag == 0,
)
.all()
)
if not tasks:
return []
task_map = {t.id: t for t in tasks}
task_ids = list(task_map.keys())
steps = (
db.query(BizProductionTaskStep)
.filter(
BizProductionTaskStep.task_id.in_(task_ids),
BizProductionTaskStep.tenant_no == tenant_no,
BizProductionTaskStep.del_flag == 0,
)
.all()
)
if not steps:
return []
step_map = {s.id: s for s in steps}
step_ids = list(step_map.keys())
type_rows = (
db.query(BizOutputType)
.filter(
BizOutputType.id.in_(list(valid_type_ids)),
BizOutputType.del_flag == 0,
)
.all()
)
type_name_map = {t.id: t.type_name or "" for t in type_rows}
outputs = (
db.query(BizProductionTaskStepOutput)
.filter(
BizProductionTaskStepOutput.step_id.in_(step_ids),
BizProductionTaskStepOutput.tenant_no == tenant_no,
BizProductionTaskStepOutput.del_flag == 0,
BizProductionTaskStepOutput.output_type_id.in_(list(valid_type_ids)),
)
.order_by(
BizProductionTaskStepOutput.order_num.asc(),
BizProductionTaskStepOutput.create_time.asc(),
)
.all()
)
items: List[Dict[str, Any]] = []
for out in outputs:
barcode = (out.output_barcode or "").strip()
if not barcode:
continue
step = step_map.get(out.step_id)
if not step:
continue
task = task_map.get(step.task_id)
if not task:
continue
type_id = (out.output_type_id or "").strip()
cell_product_id = _resolve_cell_product_id_by_barcode(db, barcode, tenant_no)
items.append(
{
"id": out.id,
"stepOutputId": out.id,
"taskId": task.id,
"taskNo": task.task_no or "",
"taskName": task.task_name or "",
"stepId": step.id,
"processName": step.process_name or "",
"outputTypeId": type_id,
"outputTypeName": type_name_map.get(type_id, _VALID_QC_DISPATCH_OUTPUT_TYPE_NAME),
"outputName": (out.output_name or "").strip(),
"outputBarcode": barcode,
"cellProductId": cell_product_id or "",
}
)
return items
def list_cell_product_options_for_production_work_order(
db: Session,
*,
production_work_order_id: str,
tenant_no: str,
) -> List[Dict[str, Any]]:
"""兼容旧接口名:返回步骤产出物派发选项。"""
return list_step_output_dispatch_options_for_production_work_order(
db,
production_work_order_id=production_work_order_id,
tenant_no=tenant_no,
)
def _protocol_sample_ids_for_sample_type(
db: Session,
tenant_no: str,
sample_type_id: str,
reception_source: Optional[str] = None,
) -> List[str]:
stid = (sample_type_id or "").strip()
if not stid or not tenant_no:
return []
ids: List[str] = []
rs = (reception_source or "").strip()
if not rs or rs == "cell_storage":
cell_rows = (
db.query(BizProtocolSample.id)
.filter(
BizProtocolSample.tenant_no == tenant_no,
BizProtocolSample.del_flag == 0,
BizProtocolSample.sample_type_id == stid,
)
.all()
)
ids.extend([r[0] for r in cell_rows if r[0]])
if not rs or rs == "research_donation":
donation_rows = (
db.query(BizResearchDonationSample.id)
.filter(
BizResearchDonationSample.tenant_no == tenant_no,
BizResearchDonationSample.del_flag == 0,
BizResearchDonationSample.sample_type_id == stid,
)
.all()
)
ids.extend([r[0] for r in donation_rows if r[0]])
return ids
def list_work_orders(
db: Session,
*,
page: int = 1,
page_size: int = 10,
keyword: Optional[str] = None,
work_order_status: Optional[str] = None,
work_order_type: Optional[str] = None,
biz_type: Optional[str] = None,
reception_source: Optional[str] = None,
trigger_source: Optional[str] = None,
sample_type_id: Optional[str] = None,
create_time_start: Optional[str] = None,
create_time_end: Optional[str] = None,
infusion_time_start: Optional[str] = None,
infusion_time_end: Optional[str] = None,
tenant_no: Optional[str] = None,
user_info=None,
) -> Tuple[List[Dict[str, Any]], int]:
def _filtered():
q0 = db.query(BizQcWorkOrder).filter(BizQcWorkOrder.del_flag == 0)
if tenant_no:
q0 = q0.filter(BizQcWorkOrder.tenant_no == tenant_no)
wt = (work_order_type or "").strip()
if wt:
q0 = q0.filter(BizQcWorkOrder.work_order_type == wt)
else:
q0 = q0.filter(
~and_(
BizQcWorkOrder.work_order_type == "outbound",
BizQcWorkOrder.biz_type == "clinical_application",
)
)
bt = (biz_type or "").strip()
if bt:
q0 = q0.filter(BizQcWorkOrder.biz_type == bt)
rs = (reception_source or "").strip()
if rs:
q0 = q0.filter(BizQcWorkOrder.reception_source == rs)
ts = (trigger_source or "").strip()
if ts:
if ts == "temp_manual":
q0 = q0.filter(
BizQcWorkOrder.work_order_type == "temporary",
BizQcWorkOrder.trigger_source != "production",
)
elif ts == "temp_production":
q0 = q0.filter(
BizQcWorkOrder.work_order_type == "temporary",
BizQcWorkOrder.trigger_source == "production",
)
elif ts == "manual" and wt == "temporary":
q0 = q0.filter(BizQcWorkOrder.trigger_source != "production")
elif ts == "production" and not wt:
q0 = q0.filter(
BizQcWorkOrder.trigger_source == "production",
BizQcWorkOrder.work_order_type != "temporary",
)
else:
q0 = q0.filter(BizQcWorkOrder.trigger_source == ts)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
keyword_conditions = [
BizQcWorkOrder.work_order_no.like(like),
BizQcWorkOrder.sample_code.like(like),
BizQcWorkOrder.protocol_no.like(like),
]
if tenant_no:
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:
keyword_conditions.append(BizQcWorkOrder.reception_id.in_(rec_ids))
lab_codes = reception_svc.sample_codes_matching_lab_storage_keyword(
db, tenant_no, kw
)
if lab_codes:
keyword_conditions.append(BizQcWorkOrder.sample_code.in_(lab_codes))
if tenant_no and (wt == "outbound" or not wt):
outbound_ids = _outbound_qc_work_order_ids_by_keyword(db, tenant_no, kw)
if outbound_ids:
keyword_conditions.append(BizQcWorkOrder.id.in_(outbound_ids))
if tenant_no and (not wt or wt == "inbound"):
inbound_mother_ids = _inbound_qc_work_order_ids_by_mother_name(
db, tenant_no, kw, reception_source=rs or None
)
if inbound_mother_ids:
keyword_conditions.append(BizQcWorkOrder.id.in_(inbound_mother_ids))
q0 = q0.filter(or_(*keyword_conditions))
st_raw = (work_order_status or "").strip()
if st_raw:
statuses = [s.strip() for s in st_raw.split(",") if s.strip()]
if statuses:
q0 = q0.filter(BizQcWorkOrder.work_order_status.in_(statuses))
use_infusion_time = wt == "outbound" and (
(infusion_time_start or "").strip() or (infusion_time_end or "").strip()
)
if use_infusion_time:
q0 = q0.join(
BizPilotInfusionSchedule,
eq_unicode(BizQcWorkOrder.infusion_schedule_id, BizPilotInfusionSchedule.id),
).filter(
BizPilotInfusionSchedule.del_flag == 0,
BizPilotInfusionSchedule.tenant_no == tenant_no,
)
if (infusion_time_start or "").strip():
q0 = q0.filter(
BizPilotInfusionSchedule.infusion_time
>= _normalize_date_bound(infusion_time_start.strip())
)
if (infusion_time_end or "").strip():
q0 = q0.filter(
BizPilotInfusionSchedule.infusion_time
<= _normalize_date_bound(infusion_time_end.strip(), end_of_day=True)
)
else:
if create_time_start:
q0 = q0.filter(
BizQcWorkOrder.create_time >= _normalize_date_bound(create_time_start)
)
if create_time_end:
q0 = q0.filter(
BizQcWorkOrder.create_time
<= _normalize_date_bound(create_time_end, end_of_day=True)
)
stid = (sample_type_id or "").strip()
if stid and tenant_no:
ps_ids = _protocol_sample_ids_for_sample_type(db, tenant_no, stid, rs or None)
if not ps_ids:
q0 = q0.filter(BizQcWorkOrder.id.in_([]))
else:
q0 = q0.filter(
BizQcWorkOrder.work_order_type == "inbound",
BizQcWorkOrder.protocol_sample_id.in_(ps_ids),
)
if user_info:
from app.core.data_scope import apply_data_scope
q0 = apply_data_scope(q0, BizQcWorkOrder, user_info, db, tenant_no)
return q0
total = _filtered().order_by(None).with_entities(func.count(BizQcWorkOrder.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
_filtered()
.order_by(BizQcWorkOrder.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
items = [_wo_to_dict(x) for x in rows]
items = _enrich_work_order_list_items(db, items)
if tenant_no:
items = _enrich_work_order_qc_targets(db, tenant_no, items)
items = _enrich_production_work_order_refs(db, items)
items = _enrich_outbound_work_orders(db, tenant_no, items)
items = enrich_work_order_fee_summaries(db, items, tenant_no)
items = _enrich_unfinished_badge_counts(db, items)
items = _enrich_work_order_qc_conclusions(db, items)
return items, int(total)
def list_work_orders_by_sample(
db: Session,
*,
sample_code: str,
reception_source: str,
tenant_no: Optional[str],
user_info: Optional[Dict[str, Any]] = None,
) -> List[Dict[str, Any]]:
"""按样本编码精确查询入库质检工单(同协议样本跳转插入卡)。"""
code = (sample_code or "").strip()
rs = (reception_source or "").strip()
if not code or not rs:
return []
q = db.query(BizQcWorkOrder).filter(
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.work_order_type == "inbound",
BizQcWorkOrder.sample_code == code,
BizQcWorkOrder.reception_source == rs,
)
if tenant_no:
q = q.filter(BizQcWorkOrder.tenant_no == tenant_no)
if user_info:
from app.core.data_scope import apply_data_scope
q = apply_data_scope(q, BizQcWorkOrder, user_info, db, tenant_no)
rows = q.order_by(BizQcWorkOrder.create_time.desc()).all()
items = [_wo_to_dict(x) for x in rows]
items = _enrich_work_order_list_items(db, items)
if tenant_no:
items = _enrich_work_order_qc_targets(db, tenant_no, items)
items = _enrich_production_work_order_refs(db, items)
items = _enrich_outbound_work_orders(db, tenant_no, items)
items = enrich_work_order_fee_summaries(db, items, tenant_no)
items = _enrich_unfinished_badge_counts(db, items)
items = _enrich_work_order_qc_conclusions(db, items)
return items
def build_work_order_list_summaries(
db: Session,
work_orders: List[BizQcWorkOrder],
tenant_no: Optional[str],
) -> Dict[str, Dict[str, Any]]:
"""批量构建列表级工单摘要(与 list_work_orders 单项字段一致,非详情接口)。"""
if not work_orders:
return {}
unique: Dict[str, BizQcWorkOrder] = {}
for wo in work_orders:
wid = (wo.id or "").strip()
if wid and wid not in unique:
unique[wid] = wo
items = [_wo_to_dict(wo) for wo in unique.values()]
items = _enrich_work_order_list_items(db, items)
if tenant_no:
items = _enrich_work_order_qc_targets(db, tenant_no, items)
items = _enrich_production_work_order_refs(db, items)
items = _enrich_outbound_work_orders(db, tenant_no, items)
items = enrich_work_order_fee_summaries(db, items, tenant_no)
items = _enrich_unfinished_badge_counts(db, items)
items = _enrich_work_order_qc_conclusions(db, items)
return {item["id"]: item for item in items if item.get("id")}
_QC_TAB_COUNT_KEYS = (
"all",
"inbound_cell_storage",
"inbound_research_donation",
"outbound_pilot_service",
"outbound_basic_experiment",
"temporary",
)
def _empty_qc_tab_counts() -> Dict[str, int]:
return {k: 0 for k in _QC_TAB_COUNT_KEYS}
def _resolve_wo_tab_key(
wo_type: Optional[str],
reception_source: Optional[str],
biz_type: Optional[str],
) -> str:
wt = (wo_type or "inbound").strip() or "inbound"
if wt == "temporary":
return "temporary"
if wt == "outbound":
bt = (biz_type or "pilot_service").strip() or "pilot_service"
if bt == "basic_experiment":
return "outbound_basic_experiment"
if bt == "clinical_application":
return "outbound_clinical_application"
return "outbound_pilot_service"
rs = (reception_source or "cell_storage").strip() or "cell_storage"
return "inbound_research_donation" if rs == "research_donation" else "inbound_cell_storage"
def _unfinished_work_order_filter(q):
return q.filter(
BizQcWorkOrder.work_order_status.in_(("dpz", "ypz")),
~and_(
BizQcWorkOrder.work_order_type == "outbound",
BizQcWorkOrder.biz_type == "clinical_application",
),
)
def _enrich_unfinished_badge_counts(
db: Session, items: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""列表项补充未完成卡片角标:待配置=1,已配置=未完成质检任务数(至少 1)。"""
if not items:
return items
unfinished_ids = [
x["id"]
for x in items
if (x.get("id") or "").strip() and x.get("workOrderStatus") in ("dpz", "ypz")
]
task_counts: Dict[str, int] = {}
if unfinished_ids:
rows = (
db.query(BizQcTask.work_order_id, func.count(BizQcTask.id))
.filter(
BizQcTask.del_flag == 0,
BizQcTask.work_order_id.in_(unfinished_ids),
BizQcTask.task_status.notin_(("ywc", "yzf")),
)
.group_by(BizQcTask.work_order_id)
.all()
)
task_counts = {wo_id: int(cnt or 0) for wo_id, cnt in rows}
for item in items:
status = item.get("workOrderStatus") or ""
if status == "dpz":
item["unfinishedBadgeCount"] = 1
elif status == "ypz":
pending = task_counts.get(item.get("id") or "", 0)
item["unfinishedBadgeCount"] = max(pending, 1)
else:
item["unfinishedBadgeCount"] = 0
return items
def _enrich_work_order_qc_conclusions(
db: Session, items: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""列表项补充工单级质检结论:优先落库 conclusion,否则由任务结论派生。"""
if not items:
return items
need_derive_ids = [
(x.get("id") or "").strip()
for x in items
if (x.get("id") or "").strip()
and (x.get("qcConclusion") or "").strip() not in _VALID_QC_WORK_ORDER_CONCLUSIONS
]
conclusions_by_wo: Dict[str, List[Optional[str]]] = defaultdict(list)
if need_derive_ids:
rows = (
db.query(BizQcTask.work_order_id, BizQcTask.conclusion)
.filter(
BizQcTask.del_flag == 0,
BizQcTask.work_order_id.in_(need_derive_ids),
)
.all()
)
for wo_id, conclusion in rows:
wid = (wo_id or "").strip()
if wid:
conclusions_by_wo[wid].append(conclusion)
for item in items:
stored = (item.get("qcConclusion") or "").strip()
if stored in _VALID_QC_WORK_ORDER_CONCLUSIONS:
item["qcConclusion"] = stored
item["qcConclusionLabel"] = work_order_qc_conclusion_label(stored)
continue
wid = (item.get("id") or "").strip()
fields = build_qc_conclusion_fields(conclusions_by_wo.get(wid, []))
item["qcConclusion"] = fields["qcConclusion"]
item["qcConclusionLabel"] = fields["qcConclusionLabel"]
return items
def _aggregate_qc_tab_counts(rows) -> Dict[str, int]:
counts = _empty_qc_tab_counts()
for wo_type, rs, bt, cnt in rows:
tab_key = _resolve_wo_tab_key(wo_type, rs, bt)
if tab_key == "outbound_clinical_application":
continue
n = int(cnt or 0)
counts[tab_key] = counts.get(tab_key, 0) + n
counts["all"] += n
return counts
def count_unfinished_work_orders_by_tab(
db: Session,
*,
tenant_no: str,
user_info=None,
) -> Dict[str, int]:
"""按 Tab 统计未完成质检工单数(待配置 + 已配置)。"""
q = db.query(BizQcWorkOrder).filter(
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.tenant_no == tenant_no,
)
q = _unfinished_work_order_filter(q)
if user_info:
from app.core.data_scope import apply_data_scope
q = apply_data_scope(q, BizQcWorkOrder, user_info, db, tenant_no)
rows = (
q.with_entities(
BizQcWorkOrder.work_order_type,
BizQcWorkOrder.reception_source,
BizQcWorkOrder.biz_type,
func.count(BizQcWorkOrder.id),
)
.group_by(
BizQcWorkOrder.work_order_type,
BizQcWorkOrder.reception_source,
BizQcWorkOrder.biz_type,
)
.all()
)
return _aggregate_qc_tab_counts(rows)
def count_mine_unfinished_work_orders_by_tab(
db: Session,
*,
tenant_no: str,
assignee_user_id: str,
) -> Dict[str, int]:
"""我的质检任务:按 Tab 统计当前用户负责且未完成的质检工单数。"""
q = (
db.query(
BizQcWorkOrder.work_order_type,
BizQcWorkOrder.reception_source,
BizQcWorkOrder.biz_type,
func.count(func.distinct(BizQcWorkOrder.id)),
)
.join(
BizQcTask,
and_(
BizQcTask.work_order_id == BizQcWorkOrder.id,
BizQcTask.del_flag == 0,
),
)
.join(
BizQcTaskAssignee,
and_(
eq_unicode(BizQcTaskAssignee.task_id, BizQcTask.id),
unicode_ci(BizQcTaskAssignee.user_id) == assignee_user_id,
BizQcTaskAssignee.del_flag == 0,
),
)
.filter(
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.tenant_no == tenant_no,
)
)
q = _unfinished_work_order_filter(q)
rows = (
q.group_by(
BizQcWorkOrder.work_order_type,
BizQcWorkOrder.reception_source,
BizQcWorkOrder.biz_type,
)
.all()
)
return _aggregate_qc_tab_counts(rows)
def get_work_order(
db: Session, row_id: str, tenant_no: Optional[str] = None
) -> Optional[BizQcWorkOrder]:
q = db.query(BizQcWorkOrder).filter(BizQcWorkOrder.id == row_id, BizQcWorkOrder.del_flag == 0)
if tenant_no:
q = q.filter(BizQcWorkOrder.tenant_no == tenant_no)
return q.first()
def _lab_lines(db: Session, work_order_id: str) -> List[Dict[str, Any]]:
rows = (
db.query(BizQcWorkOrderLabItem)
.filter(
BizQcWorkOrderLabItem.work_order_id == work_order_id,
BizQcWorkOrderLabItem.del_flag == 0,
)
.order_by(BizQcWorkOrderLabItem.order_num.asc())
.all()
)
return [
{
"id": r.id,
"labTestItemId": r.lab_test_item_id,
"itemCode": r.item_code,
"itemName": r.item_name,
}
for r in rows
]
def _third_lines(db: Session, work_order_id: str) -> List[Dict[str, Any]]:
rows = (
db.query(BizQcWorkOrderThirdItem)
.filter(
BizQcWorkOrderThirdItem.work_order_id == work_order_id,
BizQcWorkOrderThirdItem.del_flag == 0,
)
.order_by(BizQcWorkOrderThirdItem.order_num.asc())
.all()
)
return [
{
"id": r.id,
"thirdPartyItemId": r.third_party_item_id,
"companyId": r.company_id,
"companyName": r.company_name,
"itemCode": r.item_code,
"itemName": r.item_name,
"feeAmount": _dec(r.fee_amount),
}
for r in rows
]
def _qc_info_item(db: Session, wo: BizQcWorkOrder, tenant_no: str) -> Dict[str, Any]:
from app.crud import qc_inspection_batch as batch_svc
tasks = task_svc.list_tasks_by_work_order(db, wo.id, tenant_no)
for t in tasks:
tt = (t.get("taskType") or "").strip()
ts = (t.get("taskStatus") or "").strip()
tc = (t.get("conclusion") or "").strip()
t["taskTypeLabel"] = _TASK_TYPE_LABELS.get(tt, tt)
t["taskStatusLabel"] = _TASK_STATUS_LABELS.get(ts, ts)
t["conclusionLabel"] = t.get("conclusionLabel") or _CONCLUSION_LABELS.get(tc, tc or "—")
wo_status = (wo.work_order_status or "").strip()
biz = _work_order_qc_biz_type(wo)
return {
**_wo_to_dict(wo),
"workOrderStatusLabel": _WORK_ORDER_STATUS_LABELS.get(wo_status, wo_status),
"qcBizTypeLabel": _qc_biz_type_label(biz),
"labItems": _lab_lines(db, wo.id),
"thirdPartyItems": _third_lines(db, wo.id),
"inspectionBatches": batch_svc.list_inspection_batches(db, wo.id, tenant_no),
"tasks": tasks,
}
def get_qc_info_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]]:
"""按样本批量返回门禁质检工单摘要(trigger_source=reception),供协议/捐赠查看弹窗使用。
细胞存储:传入 sample_code_by_id 时,按租户 + 样本编码关联。
科研捐赠等:仅传 sample_ids 时,仍按 protocol_sample_id(存捐赠样本 id)关联。
"""
if not tenant_no:
return {}
if sample_code_by_id:
codes = list({(c or "").strip() for c in sample_code_by_id.values() if (c or "").strip()})
if not codes:
return {}
wos = (
db.query(BizQcWorkOrder)
.filter(
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.reception_source == "cell_storage",
BizQcWorkOrder.trigger_source == "reception",
BizQcWorkOrder.sample_code.in_(codes),
)
.all()
)
code_to_wo: Dict[str, BizQcWorkOrder] = {}
for wo in wos:
code = (wo.sample_code or "").strip()
if code and code not in code_to_wo:
code_to_wo[code] = wo
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
wo = code_to_wo.get(code)
if wo:
out[sid] = _qc_info_item(db, wo, tenant_no)
return out
if not sample_ids:
return {}
wos = (
db.query(BizQcWorkOrder)
.filter(
BizQcWorkOrder.protocol_sample_id.in_(sample_ids),
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.trigger_source == "reception",
)
.all()
)
out = {}
for wo in wos:
sid = (wo.protocol_sample_id or "").strip()
if sid and sid not in out:
out[sid] = _qc_info_item(db, wo, tenant_no)
return out
def _process_qc_info_items_from_wos(
db: Session, wos: List[BizQcWorkOrder], tenant_no: str
) -> List[Dict[str, Any]]:
if not wos:
return []
items = [_qc_info_item(db, wo, tenant_no) for wo in wos]
items = _enrich_work_order_list_items(db, items)
items = _enrich_work_order_qc_targets(db, tenant_no, items)
items = _enrich_production_work_order_refs(db, items)
return items
def get_process_qc_info_map_for_samples(
db: Session,
sample_ids: List[str],
tenant_no: str,
*,
sample_code_by_id: Optional[Dict[str, str]] = None,
) -> Dict[str, List[Dict[str, Any]]]:
"""按样本批量返回生产过程触发的入库质检工单摘要。"""
if not tenant_no:
return {}
if sample_code_by_id:
codes = list({(c or "").strip() for c in sample_code_by_id.values() if (c or "").strip()})
if not codes:
return {}
wos = (
db.query(BizQcWorkOrder)
.filter(
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.work_order_type == "inbound",
BizQcWorkOrder.trigger_source == "production",
BizQcWorkOrder.sample_code.in_(codes),
)
.order_by(BizQcWorkOrder.create_time.asc())
.all()
)
code_to_wos: Dict[str, List[BizQcWorkOrder]] = {}
for wo in wos:
code = (wo.sample_code or "").strip()
if code:
code_to_wos.setdefault(code, []).append(wo)
out: Dict[str, List[Dict[str, Any]]] = {}
for sid, raw_code in sample_code_by_id.items():
code = (raw_code or "").strip()
if not code:
continue
batch = code_to_wos.get(code)
if batch:
out[sid] = _process_qc_info_items_from_wos(db, batch, tenant_no)
return out
if not sample_ids:
return {}
wos = (
db.query(BizQcWorkOrder)
.filter(
BizQcWorkOrder.protocol_sample_id.in_(sample_ids),
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.work_order_type == "inbound",
BizQcWorkOrder.trigger_source == "production",
)
.order_by(BizQcWorkOrder.create_time.asc())
.all()
)
sid_to_wos: Dict[str, List[BizQcWorkOrder]] = {}
for wo in wos:
sid = (wo.protocol_sample_id or "").strip()
if sid:
sid_to_wos.setdefault(sid, []).append(wo)
out: Dict[str, List[Dict[str, Any]]] = {}
for sid, batch in sid_to_wos.items():
out[sid] = _process_qc_info_items_from_wos(db, batch, tenant_no)
return out
def list_process_qc_info_for_sample(
db: Session,
*,
sample_code: str,
tenant_no: str,
) -> List[Dict[str, Any]]:
"""按样本编码返回生产过程触发的入库质检工单摘要(供入库放行查看等)。"""
code = (sample_code or "").strip()
if not code or not tenant_no:
return []
wos = (
db.query(BizQcWorkOrder)
.filter(
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.work_order_type == "inbound",
BizQcWorkOrder.trigger_source == "production",
BizQcWorkOrder.sample_code == code,
)
.order_by(BizQcWorkOrder.create_time.asc())
.all()
)
return _process_qc_info_items_from_wos(db, wos, tenant_no)
def get_schedule_gate_qc_info(
db: Session,
*,
schedule_id: str,
tenant_no: str,
) -> Optional[Dict[str, Any]]:
"""出库计划自动质检(trigger_source=schedule),供输注计划/出库放行查看。"""
sid = (schedule_id or "").strip()
if not sid or not tenant_no:
return None
wo = (
db.query(BizQcWorkOrder)
.filter(
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.work_order_type == "outbound",
BizQcWorkOrder.infusion_schedule_id == sid,
BizQcWorkOrder.trigger_source == "schedule",
)
.order_by(BizQcWorkOrder.create_time.asc())
.first()
)
if not wo:
return None
return _qc_info_item(db, wo, tenant_no)
def list_process_qc_info_for_schedule(
db: Session,
*,
schedule_id: str,
tenant_no: str,
) -> List[Dict[str, Any]]:
"""按输注计划返回生产过程触发的出库质检工单摘要。"""
sid = (schedule_id or "").strip()
if not sid or not tenant_no:
return []
wos = (
db.query(BizQcWorkOrder)
.filter(
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.work_order_type == "outbound",
BizQcWorkOrder.infusion_schedule_id == sid,
BizQcWorkOrder.trigger_source == "production",
)
.order_by(BizQcWorkOrder.create_time.asc())
.all()
)
return _process_qc_info_items_from_wos(db, wos, tenant_no)
def list_process_qc_info_for_production_work_order(
db: Session,
*,
production_work_order_id: str,
tenant_no: str,
) -> List[Dict[str, Any]]:
"""按生产工单返回生产过程派发的质检工单摘要。"""
pid = (production_work_order_id or "").strip()
if not pid or not tenant_no:
return []
wos = (
db.query(BizQcWorkOrder)
.filter(
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.production_work_order_id == pid,
BizQcWorkOrder.trigger_source == "production",
)
.order_by(BizQcWorkOrder.create_time.asc())
.all()
)
return _process_qc_info_items_from_wos(db, wos, tenant_no)
def _gate_qc_info_for_inbound_production_work_order(
db: Session, pwo, tenant_no: str
) -> Optional[Dict[str, Any]]:
"""入库生产工单关联的样本接收质检(trigger_source=reception)。"""
from app.models.models import BizProductionWorkOrder
if not isinstance(pwo, BizProductionWorkOrder):
return None
source = (getattr(pwo, "reception_source", None) or "cell_storage").strip()
if source not in ("cell_storage", "research_donation"):
return None
q = db.query(BizQcWorkOrder).filter(
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.trigger_source == "reception",
)
wo = None
if source == "cell_storage":
code = (pwo.sample_code or "").strip()
if code:
wo = q.filter(
BizQcWorkOrder.reception_source == "cell_storage",
BizQcWorkOrder.sample_code == code,
).first()
else:
psid = (pwo.protocol_sample_id or "").strip()
if psid:
wo = q.filter(BizQcWorkOrder.protocol_sample_id == psid).first()
if not wo:
code = (pwo.sample_code or "").strip()
if code:
wo = q.filter(BizQcWorkOrder.sample_code == code).first()
if not wo and pwo.reception_id:
wo = q.filter(BizQcWorkOrder.reception_id == pwo.reception_id).first()
if not wo:
return None
return _qc_info_item(db, wo, tenant_no)
def get_production_work_order_qc_info(
db: Session,
*,
production_work_order_id: str,
tenant_no: str,
) -> Dict[str, Any]:
"""生产工单质检只读摘要:入库含样本接收质检 + 本工单派发的生产过程质检。"""
from app.models.models import BizProductionWorkOrder, BizSampleReception
pid = (production_work_order_id or "").strip()
if not pid or not tenant_no:
raise ValueError("生产工单不存在")
pwo = (
db.query(BizProductionWorkOrder)
.filter(
BizProductionWorkOrder.id == pid,
BizProductionWorkOrder.tenant_no == tenant_no,
BizProductionWorkOrder.del_flag == 0,
)
.first()
)
if not pwo:
raise ValueError("生产工单不存在")
wo_type = (getattr(pwo, "work_order_type", None) or "inbound").strip()
reception_source = (getattr(pwo, "reception_source", None) or "cell_storage").strip()
show_gate_section = (
wo_type == "inbound"
and reception_source in ("cell_storage", "research_donation")
)
gate_qc_info = None
sample_reception = None
if show_gate_section:
gate_qc_info = _gate_qc_info_for_inbound_production_work_order(db, pwo, tenant_no)
rec = None
if pwo.reception_id:
rec = (
db.query(BizSampleReception)
.filter(
BizSampleReception.id == pwo.reception_id,
BizSampleReception.del_flag == 0,
)
.first()
)
if rec:
sample_reception = {"acceptConclusion": rec.accept_conclusion or ""}
process_qc_info_list = list_process_qc_info_for_production_work_order(
db, production_work_order_id=pwo.id, tenant_no=tenant_no
)
return {
"showGateSection": show_gate_section,
"gateQcInfo": gate_qc_info,
"processQcInfoList": process_qc_info_list,
"sampleReception": sample_reception,
}
def _resolve_reception_id_for_work_order_detail(
db: Session, wo: BizQcWorkOrder, tenant_no: str
) -> Optional[str]:
"""详情接口解析接收记录 ID:优先 reception_id,生产过程工单按 sample_code 回查。"""
rid = (wo.reception_id or "").strip()
if rid:
return rid
code = (wo.sample_code or "").strip()
if not code:
return None
q = db.query(BizSampleReception).filter(
BizSampleReception.tenant_no == tenant_no,
BizSampleReception.del_flag == 0,
BizSampleReception.sample_code == code,
)
source = (getattr(wo, "reception_source", None) or "").strip()
if source:
q = q.filter(BizSampleReception.reception_source == source)
rec = q.order_by(BizSampleReception.reception_time.desc()).first()
return rec.id if rec else None
def get_work_order_detail(
db: Session, row_id: str, tenant_no: str
) -> Dict[str, Any]:
wo = get_work_order(db, row_id, tenant_no=tenant_no)
if not wo:
raise ValueError("工单不存在")
out = _wo_to_dict(wo)
enriched_list = _enrich_work_order_list_items(db, [out])
if tenant_no:
enriched_list = _enrich_work_order_qc_targets(db, tenant_no, enriched_list)
enriched_list = _enrich_work_order_qc_conclusions(db, enriched_list)
enriched = enriched_list[0]
out.update(
{
"receptionSource": enriched.get("receptionSource", out.get("receptionSource", "")),
"receptionTime": enriched.get("receptionTime", ""),
"handlerName": enriched.get("handlerName", ""),
"labStorageCode": enriched.get("labStorageCode", ""),
"sampleTypeName": enriched.get("sampleTypeName", ""),
"receptionSourceLabel": enriched.get("receptionSourceLabel", ""),
"motherName": enriched.get("motherName", ""),
"newbornName": enriched.get("newbornName", ""),
"protocolStatus": enriched.get("protocolStatus", ""),
"protocolStatusLabel": enriched.get("protocolStatusLabel", ""),
"cellProductCode": enriched.get("cellProductCode", ""),
"cellProductName": enriched.get("cellProductName", ""),
"cellProductTypeName": enriched.get("cellProductTypeName", ""),
"cellProductPassage": enriched.get("cellProductPassage", ""),
"cellProductSpecName": enriched.get("cellProductSpecName", ""),
"qcConclusion": enriched.get("qcConclusion", ""),
"qcConclusionLabel": enriched.get("qcConclusionLabel", ""),
}
)
resolved_reception_id = _resolve_reception_id_for_work_order_detail(db, wo, tenant_no)
reception_detail = (
sample_reception_svc.get_reception_by_id(db, resolved_reception_id, tenant_no)
if resolved_reception_id
else None
)
if reception_detail:
out["reception"] = reception_detail
out["productionReception"] = reception_detail.get("productionReception")
else:
rec = (
db.query(BizSampleReception)
.filter(
BizSampleReception.id == resolved_reception_id,
BizSampleReception.del_flag == 0,
)
.first()
if resolved_reception_id
else None
)
cr = (
check_result_svc.get_check_results_for_reception(db, rec, tenant_no) if rec else []
)
out["reception"] = {
"receptionTime": _fmt_dt(rec.reception_time) if rec else "",
"handlerName": (rec.handler_name if rec else "") or "",
"labStorageCode": (rec.lab_storage_code if rec else "") or "",
"acceptConclusion": (rec.accept_conclusion if rec else "") or "",
"acceptConclusionLabel": _accept_conclusion_label(
rec.accept_conclusion if rec else ""
),
"sourceType": wo.reception_source or "cell_storage",
"sourceTypeLabel": _reception_source_label(wo.reception_source),
"sampleTypeName": enriched.get("sampleTypeName", ""),
"checkResults": cr,
"receiveAttachments": (rec.receive_attachments if rec else "") or "",
"receiveAttachmentFiles": urls_to_attachment_list(
rec.receive_attachments if rec else None, db, tenant_no
),
}
out["labItems"] = _lab_lines(db, wo.id)
out["thirdPartyItems"] = _third_lines(db, wo.id)
out["tasks"] = task_svc.list_tasks_by_work_order(db, wo.id, tenant_no)
from app.crud import qc_inspection_batch as batch_svc
out["inspectionBatches"] = batch_svc.list_inspection_batches(db, wo.id, tenant_no)
enriched_fee = enrich_work_order_fee_summaries(db, [out], tenant_no)
if enriched_fee:
out["paymentSummary"] = enriched_fee[0].get("paymentSummary")
out["feeSummary"] = enriched_fee[0].get("feeSummary")
if tenant_no:
_enrich_production_work_order_refs(db, [out])
return out
def _soft_delete_tasks_by_source_line_ids(
db: Session, source_line_ids: List[str], tenant_no: str
) -> None:
"""软删指定明细行关联的质检任务及其检测结果。"""
if not source_line_ids:
return
tasks = (
db.query(BizQcTask)
.filter(
BizQcTask.source_line_id.in_(source_line_ids),
BizQcTask.tenant_no == tenant_no,
BizQcTask.del_flag == 0,
)
.all()
)
if not tasks:
return
now = datetime.now()
task_ids = [t.id for t in tasks]
assignee_svc.soft_delete_assignees_by_task_ids(db, task_ids, tenant_no)
db.query(BizQcTaskResult).filter(
BizQcTaskResult.task_id.in_(task_ids),
BizQcTaskResult.tenant_no == tenant_no,
BizQcTaskResult.del_flag == 0,
).update(
{"del_flag": 1, "update_time": now},
synchronize_session="fetch",
)
for t in tasks:
t.del_flag = 1
t.update_time = now
db.add(t)
def _get_task_by_source_line_id(
db: Session, source_line_id: str, tenant_no: str
) -> Optional[BizQcTask]:
return (
db.query(BizQcTask)
.filter(
BizQcTask.source_line_id == source_line_id,
BizQcTask.tenant_no == tenant_no,
BizQcTask.del_flag == 0,
)
.first()
)
def _apply_item_assignees(
db: Session,
task: Optional[BizQcTask],
item_master_id: str,
assignees_map: Optional[Dict[str, List[str]]],
*,
tenant_no: str,
user_id: Optional[str],
create_dept_id: str,
item_label: str,
) -> None:
if not task:
return
user_ids = (assignees_map or {}).get(item_master_id) or []
if not user_ids:
raise ValueError(f"请为{item_label}指定负责人")
assignee_svc.sync_assignees_for_task(
db,
task.id,
user_ids,
tenant_no=tenant_no,
operator_id=user_id,
create_dept_id=create_dept_id,
)
def _sync_work_order_lab_items(
db: Session,
wo: BizQcWorkOrder,
*,
order_source: str,
lab_item_ids: List[str],
lab_item_assignees: Optional[Dict[str, List[str]]] = None,
tenant_no: str,
user_id: Optional[str],
create_dept_id: str,
seq_start: int = 0,
) -> int:
"""比对同步实验室项目明细:保留已有任务数据,仅增删变更项。"""
now = datetime.now()
existing_rows = (
db.query(BizQcWorkOrderLabItem)
.filter(
BizQcWorkOrderLabItem.work_order_id == wo.id,
BizQcWorkOrderLabItem.order_source == order_source,
BizQcWorkOrderLabItem.del_flag == 0,
)
.all()
)
existing_by_item_id = {r.lab_test_item_id: r for r in existing_rows}
new_id_set = set(lab_item_ids)
old_id_set = set(existing_by_item_id.keys())
remove_line_ids = [existing_by_item_id[i].id for i in old_id_set - new_id_set]
for item_id in old_id_set - new_id_set:
line = existing_by_item_id[item_id]
line.del_flag = 1
line.update_by = user_id
line.update_time = now
db.add(line)
_soft_delete_tasks_by_source_line_ids(db, remove_line_ids, tenant_no)
seq = seq_start
for lid in lab_item_ids:
seq += 1
if lid in existing_by_item_id and lid in new_id_set:
line = existing_by_item_id[lid]
line.order_num = seq
line.update_by = user_id
line.update_time = now
db.add(line)
task = _get_task_by_source_line_id(db, line.id, tenant_no)
if task:
task.order_num = seq
task.update_by = user_id
task.update_time = now
db.add(task)
else:
task = task_svc.create_task_for_line(
db,
work_order=wo,
task_type="sysz",
source_line_id=line.id,
item_code=line.item_code,
item_name=line.item_name,
tenant_no=tenant_no,
user_id=user_id,
create_dept_id=create_dept_id,
seq=seq,
order_source=order_source,
)
_apply_item_assignees(
db,
task,
lid,
lab_item_assignees,
tenant_no=tenant_no,
user_id=user_id,
create_dept_id=create_dept_id,
item_label="实验室自检项目",
)
continue
item = lab_item_svc.get_lab_item(db, lid, tenant_no=tenant_no)
if not item or item.status != "1":
raise ValueError("实验室项目无效或已停用")
line = BizQcWorkOrderLabItem(
id=generate_uuid(),
work_order_id=wo.id,
order_source=order_source,
lab_test_item_id=item.id,
item_code=item.item_code,
item_name=item.item_name,
tenant_no=tenant_no,
order_num=seq,
create_by=user_id,
create_dept_id=create_dept_id or "",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(line)
db.flush()
task = task_svc.create_task_for_line(
db,
work_order=wo,
task_type="sysz",
source_line_id=line.id,
item_code=line.item_code,
item_name=line.item_name,
tenant_no=tenant_no,
user_id=user_id,
create_dept_id=create_dept_id,
seq=seq,
order_source=order_source,
)
_apply_item_assignees(
db,
task,
lid,
lab_item_assignees,
tenant_no=tenant_no,
user_id=user_id,
create_dept_id=create_dept_id,
item_label="实验室自检项目",
)
return seq
def _sync_work_order_third_party_items(
db: Session,
wo: BizQcWorkOrder,
*,
order_source: str,
third_party_item_ids: List[str],
third_party_item_assignees: Optional[Dict[str, List[str]]] = None,
tenant_no: str,
user_id: Optional[str],
create_dept_id: str,
seq_start: int = 0,
) -> Tuple[int, Decimal]:
"""比对同步第三方项目明细:保留已有任务数据,仅增删变更项。"""
now = datetime.now()
existing_rows = (
db.query(BizQcWorkOrderThirdItem)
.filter(
BizQcWorkOrderThirdItem.work_order_id == wo.id,
BizQcWorkOrderThirdItem.order_source == order_source,
BizQcWorkOrderThirdItem.del_flag == 0,
)
.all()
)
existing_by_item_id = {r.third_party_item_id: r for r in existing_rows}
new_id_set = set(third_party_item_ids)
old_id_set = set(existing_by_item_id.keys())
remove_line_ids = [existing_by_item_id[i].id for i in old_id_set - new_id_set]
for item_id in old_id_set - new_id_set:
line = existing_by_item_id[item_id]
line.del_flag = 1
line.update_by = user_id
line.update_time = now
db.add(line)
_soft_delete_tasks_by_source_line_ids(db, remove_line_ids, tenant_no)
fee_total = Decimal("0")
seq = seq_start
for tid in third_party_item_ids:
seq += 1
if tid in existing_by_item_id and tid in new_id_set:
line = existing_by_item_id[tid]
line.order_num = seq
line.update_by = user_id
line.update_time = now
db.add(line)
fee_total += line.fee_amount or Decimal("0")
task = _get_task_by_source_line_id(db, line.id, tenant_no)
if task:
task.order_num = seq
task.update_by = user_id
task.update_time = now
db.add(task)
else:
task = task_svc.create_task_for_line(
db,
work_order=wo,
task_type="dsfjc",
source_line_id=line.id,
item_code=line.item_code,
item_name=line.item_name,
tenant_no=tenant_no,
user_id=user_id,
create_dept_id=create_dept_id,
seq=seq,
company_id=line.company_id,
company_name=line.company_name,
fee_amount=line.fee_amount,
order_source=order_source,
)
_apply_item_assignees(
db,
task,
tid,
third_party_item_assignees,
tenant_no=tenant_no,
user_id=user_id,
create_dept_id=create_dept_id,
item_label="第三方检测项目",
)
continue
item = third_item_svc.get_item(db, tid, tenant_no=tenant_no)
if not item or item.status != "1":
raise ValueError("第三方项目无效或已停用")
comp = get_company(db, item.company_id, tenant_no=tenant_no)
if not comp:
raise ValueError("第三方公司不存在")
fee = item.fee_amount or Decimal("0")
fee_total += fee
line = BizQcWorkOrderThirdItem(
id=generate_uuid(),
work_order_id=wo.id,
order_source=order_source,
third_party_item_id=item.id,
company_id=comp.id,
company_name=comp.company_name,
item_code=item.item_code,
item_name=item.item_name,
fee_amount=fee,
tenant_no=tenant_no,
order_num=seq,
create_by=user_id,
create_dept_id=create_dept_id or "",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(line)
db.flush()
task = task_svc.create_task_for_line(
db,
work_order=wo,
task_type="dsfjc",
source_line_id=line.id,
item_code=line.item_code,
item_name=line.item_name,
tenant_no=tenant_no,
user_id=user_id,
create_dept_id=create_dept_id,
seq=seq,
company_id=comp.id,
company_name=comp.company_name,
fee_amount=fee,
order_source=order_source,
)
_apply_item_assignees(
db,
task,
tid,
third_party_item_assignees,
tenant_no=tenant_no,
user_id=user_id,
create_dept_id=create_dept_id,
item_label="第三方检测项目",
)
return seq, fee_total
def configure_work_order(
db: Session,
work_order_id: str,
*,
has_lab: bool,
has_third_party: bool,
lab_item_ids: List[str],
third_party_item_ids: List[str],
lab_item_assignees: Optional[Dict[str, List[str]]] = None,
third_party_item_assignees: Optional[Dict[str, List[str]]] = None,
batches: Optional[List[Dict[str, Any]]] = None,
tenant_no: str,
user_id: Optional[str] = None,
create_dept_id: str = "",
) -> BizQcWorkOrder:
from app.crud import qc_inspection_batch as batch_svc
wo = get_work_order(db, work_order_id, tenant_no=tenant_no)
if not wo:
raise ValueError("工单不存在")
if batches is not None:
payload_batches = batches
else:
payload_batches = batch_svc.normalize_legacy_configure_to_batches(
{
"hasLab": has_lab,
"hasThirdParty": has_third_party,
"labItemIds": lab_item_ids,
"thirdPartyItemIds": third_party_item_ids,
"labItemAssignees": lab_item_assignees,
"thirdPartyItemAssignees": third_party_item_assignees,
}
)
return batch_svc.configure_work_order_batches(
db,
wo,
payload_batches,
tenant_no=tenant_no,
user_id=user_id,
create_dept_id=create_dept_id,
)
def try_complete_work_order(db: Session, work_order_id: str, tenant_no: str) -> None:
pending = (
db.query(BizQcTask)
.filter(
BizQcTask.work_order_id == work_order_id,
BizQcTask.tenant_no == tenant_no,
BizQcTask.del_flag == 0,
BizQcTask.task_status != "ywc",
)
.count()
)
if pending:
return
wo = get_work_order(db, work_order_id, tenant_no=tenant_no)
if wo and wo.work_order_status == "ypz":
wo.work_order_status = "ywc"
task_conclusions = [
c
for (c,) in db.query(BizQcTask.conclusion)
.filter(
BizQcTask.work_order_id == work_order_id,
BizQcTask.tenant_no == tenant_no,
BizQcTask.del_flag == 0,
)
.all()
]
derived = build_qc_conclusion_fields(task_conclusions)["qcConclusion"]
wo.conclusion = derived or None
wo.update_time = datetime.now()
db.add(wo)
from app.crud.pilot_outbound_status_sync import sync_schedule_from_qc_work_order
sync_schedule_from_qc_work_order(db, wo)
db.commit()
def try_reopen_work_order(db: Session, work_order_id: str, tenant_no: str) -> None:
"""任务从已完成回退时,若工单已完成则恢复为已配置。"""
wo = get_work_order(db, work_order_id, tenant_no=tenant_no)
if not wo or (wo.work_order_status or "").strip() != "ywc":
return
wo.work_order_status = "ypz"
wo.conclusion = None
wo.update_time = datetime.now()
db.add(wo)
from app.crud.pilot_outbound_status_sync import sync_schedule_from_qc_work_order
sync_schedule_from_qc_work_order(db, wo)
db.commit()
# ---------- 临时质检工单 ----------
def _temp_lab_lines(db: Session, work_order_id: str) -> List[Dict[str, Any]]:
rows = (
db.query(BizQcWorkOrderLabItem)
.filter(
BizQcWorkOrderLabItem.work_order_id == work_order_id,
BizQcWorkOrderLabItem.order_source == "temporary",
BizQcWorkOrderLabItem.del_flag == 0,
)
.order_by(BizQcWorkOrderLabItem.order_num.asc())
.all()
)
return [
{
"id": r.id,
"labTestItemId": r.lab_test_item_id,
"itemCode": r.item_code,
"itemName": r.item_name,
}
for r in rows
]
def _temp_third_lines(db: Session, work_order_id: str) -> List[Dict[str, Any]]:
rows = (
db.query(BizQcWorkOrderThirdItem)
.filter(
BizQcWorkOrderThirdItem.work_order_id == work_order_id,
BizQcWorkOrderThirdItem.order_source == "temporary",
BizQcWorkOrderThirdItem.del_flag == 0,
)
.order_by(BizQcWorkOrderThirdItem.order_num.asc())
.all()
)
return [
{
"id": r.id,
"thirdPartyItemId": r.third_party_item_id,
"companyId": r.company_id,
"companyName": r.company_name,
"itemCode": r.item_code,
"itemName": r.item_name,
"feeAmount": _dec(r.fee_amount),
}
for r in rows
]
def _temp_tasks(db: Session, work_order_id: str, tenant_no: str) -> List[Dict[str, Any]]:
rows = (
db.query(BizQcTask)
.filter(
BizQcTask.work_order_id == work_order_id,
BizQcTask.order_source == "temporary",
BizQcTask.tenant_no == tenant_no,
BizQcTask.del_flag == 0,
)
.order_by(BizQcTask.order_num.asc())
.all()
)
items = []
for t in rows:
d = task_svc._task_to_dict(t, "", db, tenant_no)
tt = (t.task_type or "").strip()
ts = (t.task_status or "").strip()
tc = (t.conclusion or "").strip()
d["taskTypeLabel"] = _TASK_TYPE_LABELS.get(tt, tt)
d["taskStatusLabel"] = _TASK_STATUS_LABELS.get(ts, ts)
d["conclusionLabel"] = _CONCLUSION_LABELS.get(tc, tc or "—")
items.append(d)
return items
def create_temp_qc_work_order(
db: Session,
*,
tenant_no: str,
user_id: Optional[str] = None,
create_dept_id: str = "",
cell_product_id: str = "",
remark: str = "",
) -> BizQcWorkOrder:
"""创建临时质检工单(work_order_type='temporary',LSZJ前缀编号)"""
cp_id = _validate_cell_product_id(db, cell_product_id, tenant_no)
wo = BizQcWorkOrder(
id=generate_uuid(),
work_order_no=_gen_daily_no(db, tenant_no, "LSZJ"),
work_order_type="temporary",
reception_id=None,
protocol_sample_id=None,
collection_protocol_id=None,
protocol_no=None,
sample_code=None,
reception_source=None,
qc_biz_type="temporary",
trigger_source="manual",
cell_product_id=cp_id,
work_order_status="dpz",
has_lab="0",
has_third_party="0",
tenant_no=tenant_no,
remark=remark,
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(wo)
db.flush()
return wo
def update_qc_work_order(
db: Session,
work_order_id: str,
*,
tenant_no: str,
user_id: Optional[str] = None,
cell_product_id: Optional[str] = None,
work_order_status: Optional[str] = None,
qc_conclusion: Optional[str] = None,
remark: Optional[str] = None,
) -> BizQcWorkOrder:
"""编辑质检工单:入库/出库可改备注、状态与质检结论;临时可改备注与质检对象"""
wo = get_work_order(db, work_order_id, tenant_no=tenant_no)
if not wo:
raise ValueError("工单不存在")
wo_type = (getattr(wo, "work_order_type", None) or "inbound").strip()
if wo_type == "temporary":
if cell_product_id is not None:
wo.cell_product_id = _validate_cell_product_id(db, cell_product_id, tenant_no)
elif cell_product_id is not None:
pass
if work_order_status is not None:
st = (work_order_status or "").strip()
if st not in _VALID_QC_WORK_ORDER_STATUSES:
raise ValueError("工单状态无效")
wo.work_order_status = st
if qc_conclusion is not None:
conc = (qc_conclusion or "").strip()
if conc and conc not in _VALID_QC_WORK_ORDER_CONCLUSIONS:
raise ValueError("质检结论无效")
wo.conclusion = conc or None
if remark is not None:
wo.remark = (remark or "").strip() or None
wo.update_by = user_id
wo.update_time = datetime.now()
db.add(wo)
db.flush()
return wo
def update_temp_qc_work_order(
db: Session,
work_order_id: str,
*,
tenant_no: str,
user_id: Optional[str] = None,
cell_product_id: Optional[str] = None,
work_order_status: Optional[str] = None,
qc_conclusion: Optional[str] = None,
remark: Optional[str] = None,
) -> BizQcWorkOrder:
"""编辑临时质检工单(备注、质检对象、状态、质检结论)"""
return update_qc_work_order(
db,
work_order_id,
tenant_no=tenant_no,
user_id=user_id,
cell_product_id=cell_product_id,
work_order_status=work_order_status,
qc_conclusion=qc_conclusion,
remark=remark,
)
def list_temp_qc_work_orders(
db: Session,
*,
page: int = 1,
page_size: int = 10,
keyword: Optional[str] = None,
work_order_status: Optional[str] = None,
tenant_no: Optional[str] = None,
user_info=None,
) -> Tuple[List[Dict[str, Any]], int]:
def _filtered():
q0 = db.query(BizQcWorkOrder).filter(
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.work_order_type == "temporary",
)
if tenant_no:
q0 = q0.filter(BizQcWorkOrder.tenant_no == tenant_no)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
q0 = q0.filter(BizQcWorkOrder.work_order_no.like(like))
st = (work_order_status or "").strip()
if st:
q0 = q0.filter(BizQcWorkOrder.work_order_status == st)
if user_info:
from app.core.data_scope import apply_data_scope
q0 = apply_data_scope(q0, BizQcWorkOrder, user_info, db, tenant_no)
return q0
total = _filtered().order_by(None).with_entities(func.count(BizQcWorkOrder.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
_filtered()
.order_by(BizQcWorkOrder.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
items = [_wo_to_dict(x) for x in rows]
if tenant_no:
items = _enrich_work_order_qc_targets(db, tenant_no, items)
items = _enrich_work_order_qc_conclusions(db, items)
return items, int(total)
def get_temp_qc_work_order(
db: Session, row_id: str, tenant_no: str
) -> Optional[BizQcWorkOrder]:
q = db.query(BizQcWorkOrder).filter(
BizQcWorkOrder.id == row_id,
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.work_order_type == "temporary",
)
if tenant_no:
q = q.filter(BizQcWorkOrder.tenant_no == tenant_no)
return q.first()
def get_temp_qc_work_order_detail(
db: Session, row_id: str, tenant_no: str
) -> Optional[Dict[str, Any]]:
wo = get_temp_qc_work_order(db, row_id, tenant_no)
if not wo:
return None
out = _wo_to_dict(wo)
enriched_list = _enrich_work_order_qc_targets(db, tenant_no, [out])
out = enriched_list[0]
out["labItems"] = _temp_lab_lines(db, wo.id)
out["thirdPartyItems"] = _temp_third_lines(db, wo.id)
out["tasks"] = _temp_tasks(db, wo.id, tenant_no)
return out
def configure_temp_qc_work_order(
db: Session,
work_order_id: str,
*,
has_lab: bool,
has_third_party: bool,
lab_item_ids: List[str],
third_party_item_ids: List[str],
batches: Optional[List[Dict[str, Any]]] = None,
tenant_no: str,
user_id: Optional[str] = None,
create_dept_id: str = "",
) -> BizQcWorkOrder:
wo = get_temp_qc_work_order(db, work_order_id, tenant_no)
if not wo:
raise ValueError("工单不存在")
from app.crud import qc_inspection_batch as batch_svc
if batches is not None:
payload_batches = batches
else:
payload_batches = batch_svc.normalize_legacy_configure_to_batches(
{
"hasLab": has_lab,
"hasThirdParty": has_third_party,
"labItemIds": lab_item_ids,
"thirdPartyItemIds": third_party_item_ids,
}
)
return batch_svc.configure_work_order_batches(
db,
wo,
payload_batches,
tenant_no=tenant_no,
user_id=user_id,
create_dept_id=create_dept_id,
)
def try_complete_temp_qc_work_order(
db: Session, work_order_id: str, tenant_no: str
) -> None:
pending = (
db.query(BizQcTask)
.filter(
BizQcTask.work_order_id == work_order_id,
BizQcTask.order_source == "temporary",
BizQcTask.tenant_no == tenant_no,
BizQcTask.del_flag == 0,
BizQcTask.task_status != "ywc",
)
.count()
)
if pending:
return
wo = get_temp_qc_work_order(db, work_order_id, tenant_no)
if wo and wo.work_order_status == "ypz":
wo.work_order_status = "ywc"
wo.update_time = datetime.now()
db.add(wo)
db.commit()
def delete_temp_qc_work_order(
db: Session, work_order_id: str, tenant_no: str, user_id: Optional[str]
) -> None:
wo = get_temp_qc_work_order(db, work_order_id, tenant_no)
if not wo:
raise ValueError("工单不存在")
if wo.work_order_status == "ywc":
raise ValueError("已完成的工单不能删除")
now = datetime.now()
tasks = (
db.query(BizQcTask)
.filter(
BizQcTask.work_order_id == work_order_id,
BizQcTask.order_source == "temporary",
BizQcTask.del_flag == 0,
)
.all()
)
for task in tasks:
results = (
db.query(BizQcTaskResult)
.filter(
BizQcTaskResult.task_id == task.id,
BizQcTaskResult.del_flag == 0,
)
.all()
)
for r in results:
r.del_flag = 1
r.update_by = user_id
r.update_time = now
task.del_flag = 1
task.update_by = user_id
task.update_time = now
lab_items = (
db.query(BizQcWorkOrderLabItem)
.filter(
BizQcWorkOrderLabItem.work_order_id == work_order_id,
BizQcWorkOrderLabItem.order_source == "temporary",
BizQcWorkOrderLabItem.del_flag == 0,
)
.all()
)
for item in lab_items:
item.del_flag = 1
item.update_by = user_id
item.update_time = now
third_items = (
db.query(BizQcWorkOrderThirdItem)
.filter(
BizQcWorkOrderThirdItem.work_order_id == work_order_id,
BizQcWorkOrderThirdItem.order_source == "temporary",
BizQcWorkOrderThirdItem.del_flag == 0,
)
.all()
)
for item in third_items:
item.del_flag = 1
item.update_by = user_id
item.update_time = now
wo.del_flag = 1
wo.update_by = user_id
wo.update_time = now
db.commit()
def build_outbound_pilot_context_by_schedule(
db: Session, schedule_id: str, tenant_no: str
) -> Dict[str, Any]:
"""按输注计划 ID 组装出库中试服务上下文(申请单/患者/方案/计划/产品),供质检与生产工单详情复用。"""
out: Dict[str, Any] = {
"application": None,
"patient": None,
"infusionPlan": None,
"infusionSchedule": None,
"products": [],
}
if not schedule_id:
return out
sched = (
db.query(BizPilotInfusionSchedule)
.filter(
BizPilotInfusionSchedule.id == schedule_id,
BizPilotInfusionSchedule.tenant_no == tenant_no,
BizPilotInfusionSchedule.del_flag == 0,
)
.first()
)
if sched:
out["infusionSchedule"] = {
"infusionTime": _fmt_dt(sched.infusion_time),
"infusionLocation": sched.infusion_location or "",
"infusionMethod": sched.infusion_method or "",
"cellType": sched.cell_type or "",
"specText": sched.spec_text or "",
"scheduleStatus": sched.schedule_status or "",
"infusionStatus": sched.infusion_status or "",
"qcStatus": sched.qc_status or "",
"cancelReason": sched.cancel_reason or "",
"amount": _dec(sched.amount) if sched.amount is not None else None,
}
plan = (
db.query(BizPilotInfusionPlan)
.filter(
BizPilotInfusionPlan.id == (sched.infusion_plan_id or ""),
BizPilotInfusionPlan.tenant_no == tenant_no,
BizPilotInfusionPlan.del_flag == 0,
)
.first()
)
if plan:
out["infusionPlan"] = {
"cellType": plan.cell_type or "",
"specText": plan.spec_text or "",
"totalCourseCount": plan.total_course_count,
"courseInterval": plan.course_interval or "",
"infusionRoute": plan.infusion_route or "",
"infusionStartDate": plan.infusion_start_date or "",
}
app = (
db.query(BizPilotApplication)
.filter(
BizPilotApplication.id == (plan.pilot_application_id or ""),
BizPilotApplication.tenant_no == tenant_no,
BizPilotApplication.del_flag == 0,
)
.first()
)
if app:
app_biz = (app.biz_type or "").strip() or "pilot_service"
out["application"] = {
"applicationNo": app.application_no or "",
"applicationName": app.application_name or "",
"applicationStatus": app.application_status or "",
"channelName": app.channel_name or "",
"bizType": app_biz,
"bizTypeLabel": _OUTBOUND_BIZ_TYPE_LABELS.get(app_biz, app_biz),
}
if app_biz == "basic_experiment":
out["application"]["projectTitle"] = app.project_title or ""
out["application"]["customerName"] = app.customer_name or ""
out["application"]["customerContact"] = app.customer_contact or ""
out["application"]["customerPhone"] = app.customer_phone or ""
else:
patient = (
db.query(BizPilotPatient)
.filter(
BizPilotPatient.pilot_application_id == app.id,
BizPilotPatient.tenant_no == tenant_no,
BizPilotPatient.del_flag == 0,
)
.first()
)
if patient:
out["patient"] = {
"patientName": patient.patient_name or "",
"sex": patient.sex or "",
"age": str(patient.age) if patient.age is not None else "",
"phoneNo": patient.phone_no or "",
"idCardNo": patient.id_card_no or "",
"emergencyContact": patient.emergency_contact or "",
"emergencyPhone": patient.emergency_phone or "",
"clinicalDiagnosis": patient.clinical_diagnosis or "",
"diagnosisAttachments": patient.diagnosis_attachments or "",
}
products = (
db.query(BizPilotInfusionScheduleProduct)
.filter(
BizPilotInfusionScheduleProduct.infusion_schedule_id == schedule_id,
BizPilotInfusionScheduleProduct.tenant_no == tenant_no,
BizPilotInfusionScheduleProduct.del_flag == 0,
)
.all()
)
out["products"] = [
{
"projectCode": p.project_code or "",
"projectName": p.project_name or "",
"cellTypeName": p.cell_type_name or "",
"specText": p.spec_text or "",
"agreementPrice": _dec(p.agreement_price),
"actualPrice": _dec(p.actual_price),
"quantity": p.quantity,
"productStatus": p.product_status or "",
}
for p in products
]
return out
def get_outbound_qc_detail(
db: Session, work_order_id: str, tenant_no: str
) -> Dict[str, Any]:
wo = get_work_order(db, work_order_id, tenant_no=tenant_no)
if not wo:
raise ValueError("工单不存在")
wo_type = getattr(wo, "work_order_type", None) or "inbound"
if wo_type != "outbound":
raise ValueError("该工单不是出库质检类型")
configured_by = wo.configured_by or ""
configured_by_name = ""
if configured_by:
configured_by_name = _user_nick_name_map(db, [configured_by]).get(configured_by, "")
schedule_id = getattr(wo, "infusion_schedule_id", None) or ""
pilot_ctx = build_outbound_pilot_context_by_schedule(db, schedule_id, tenant_no)
out: Dict[str, Any] = {
"workOrder": _wo_to_dict(wo, configured_by_name=configured_by_name),
**pilot_ctx,
"tasks": [],
}
from app.crud import qc_inspection_batch as batch_svc
out["tasks"] = task_svc.list_tasks_by_work_order(db, wo.id, tenant_no)
out["inspectionBatches"] = batch_svc.list_inspection_batches(db, wo.id, tenant_no)
enriched_fee = enrich_work_order_fee_summaries(db, [out["workOrder"]], tenant_no)
if enriched_fee:
out["workOrder"]["feeSummary"] = enriched_fee[0].get("feeSummary")
out["feeSummary"] = enriched_fee[0].get("feeSummary")
return out
def create_qc_work_order_manual(
db: Session,
*,
tenant_no: str,
user_id: Optional[str] = None,
create_dept_id: str = "",
protocol_no: str = "",
sample_code: str = "",
reception_source: str = "cell_storage",
qc_biz_type: str = "inbound",
remark: str = "",
) -> BizQcWorkOrder:
"""手动创建质检工单(不关联接收记录)"""
wo = BizQcWorkOrder(
id=generate_uuid(),
work_order_no=_gen_daily_no(db, tenant_no, "ZJ"),
work_order_type="inbound",
reception_id=None,
protocol_sample_id=None,
collection_protocol_id=None,
protocol_no=protocol_no or None,
sample_code=sample_code or None,
reception_source=reception_source,
qc_biz_type=qc_biz_type,
work_order_status="dpz",
has_lab="0",
has_third_party="0",
tenant_no=tenant_no,
remark=remark,
create_by=user_id,
create_dept_id=create_dept_id,
)
db.add(wo)
db.flush()
return wo
def create_outbound_qc_work_order(
db: Session,
*,
schedule_id: str,
cell_product_id: str = "",
tenant_no: str = "",
user_id: Optional[str] = None,
create_dept_id: str = "",
remark: str = "",
biz_type: Optional[str] = None,
) -> BizQcWorkOrder:
resolved_biz_type = (biz_type or "").strip() or resolve_outbound_biz_type_for_schedule(
db, schedule_id, tenant_no
)
if resolved_biz_type not in _OUTBOUND_BIZ_TYPE_LABELS:
resolved_biz_type = "pilot_service"
existing = (
db.query(BizQcWorkOrder)
.filter(
BizQcWorkOrder.infusion_schedule_id == schedule_id,
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.trigger_source == "schedule",
)
.first()
)
if existing:
if (existing.biz_type or "") != resolved_biz_type:
existing.biz_type = resolved_biz_type
db.flush()
return existing
wo = BizQcWorkOrder(
id=generate_uuid(),
work_order_no=_gen_daily_no(db, tenant_no, "CKZJ"),
work_order_type="outbound",
reception_id=None,
protocol_sample_id=None,
collection_protocol_id=None,
protocol_no=None,
sample_code=None,
reception_source="outbound",
qc_biz_type="outbound",
biz_type=resolved_biz_type,
infusion_schedule_id=schedule_id,
trigger_source="schedule",
cell_product_id=cell_product_id or None,
work_order_status="dpz",
has_lab="0",
has_third_party="0",
tenant_no=tenant_no,
remark=remark,
create_by=user_id,
create_dept_id=create_dept_id,
)
db.add(wo)
db.flush()
return wo
def delete_qc_work_order(db: Session, work_order_id: str, tenant_no: str, user_id: Optional[str]) -> None:
"""软删除质检工单及其关联的任务、任务结果、检测项目和第三方检测项目"""
wo = db.query(BizQcWorkOrder).filter(
and_(
BizQcWorkOrder.id == work_order_id,
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.del_flag == 0,
)
).first()
if not wo:
raise ValueError("工单不存在")
if wo.work_order_status == "ywc":
raise ValueError("已完成的工单不能删除")
now = datetime.now()
# 1. 软删除所有关联任务及其结果
tasks = db.query(BizQcTask).filter(
and_(BizQcTask.work_order_id == work_order_id, BizQcTask.del_flag == 0)
).all()
from app.models.qc_param import BizQcTaskResult
for task in tasks:
results = db.query(BizQcTaskResult).filter(
and_(BizQcTaskResult.task_id == task.id, BizQcTaskResult.del_flag == 0)
).all()
for r in results:
r.del_flag = 1
r.update_by = user_id
r.update_time = now
task.del_flag = 1
task.update_by = user_id
task.update_time = now
# 2. 软删除检测项目
lab_items = db.query(BizQcWorkOrderLabItem).filter(
and_(BizQcWorkOrderLabItem.work_order_id == work_order_id, BizQcWorkOrderLabItem.del_flag == 0)
).all()
for item in lab_items:
item.del_flag = 1
item.update_by = user_id
item.update_time = now
# 3. 软删除第三方检测项目
third_items = db.query(BizQcWorkOrderThirdItem).filter(
and_(BizQcWorkOrderThirdItem.work_order_id == work_order_id, BizQcWorkOrderThirdItem.del_flag == 0)
).all()
for item in third_items:
item.del_flag = 1
item.update_by = user_id
item.update_time = now
# 4. 软删除工单本体
wo.del_flag = 1
wo.update_by = user_id
wo.update_time = now
db.commit()