落库并展示质检工单级结论(可编辑/任务派生),库位树支持批量新增子节点与货架格子检索,协议/捐赠列表补充接收与门禁质检摘要,并同步生产物料与工单卡片展示优化。 Co-authored-by: Cursor <cursoragent@cursor.com>
3674 lines
127 KiB
Python
3674 lines
127 KiB
Python
"""质检工单 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()
|