Files
shanxia2.0/backend/app/crud/qc_work_order.py
T
2026-05-28 07:37:16 +08:00

2007 lines
68 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""质检工单 CRUD"""
from __future__ import annotations
from 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
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_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.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.models.models import (
BizCellProduct,
BizCellSampleType,
BizCollectionProtocol,
BizPilotApplication,
BizPilotInfusionPlan,
BizPilotInfusionSchedule,
BizPilotInfusionScheduleProduct,
BizPilotPatient,
BizProtocolSample,
BizQcLabTestItem,
BizQcTask,
BizQcThirdPartyItem,
BizQcWorkOrder,
BizQcWorkOrderLabItem,
BizQcWorkOrderThirdItem,
BizSampleReception,
SysUser,
)
from app.models.qc_param import BizQcTaskResult
from app.models.research_donation import BizResearchDonation, BizResearchDonationSample
_RECEPTION_SOURCE_LABELS = {
"cell_storage": "细胞存储",
"research_donation": "科研捐赠",
"outbound": "出库",
}
_QC_BIZ_TYPE_LABELS = {
"inbound": "入库质检",
"outbound": "出库质检",
"temporary": "临时质检",
}
_ACCEPT_CONCLUSION_LABELS = {"js": "接收", "bj": "拒收"}
_WORK_ORDER_STATUS_LABELS = {"dpz": "待配置", "ypz": "已配置", "ywc": "已完成"}
_TASK_TYPE_LABELS = {"sysz": "实验室自检", "dsfjc": "第三方检测"}
_TASK_STATUS_LABELS = {"dzx": "待执行", "jxz": "进行中", "ywc": "已完成"}
_PROTOCOL_STATUS_LABELS = {
"dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "zky": "转科研"
}
_DONATION_STATUS_LABELS = {
"dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "zckc": "转细胞存储"
}
_CONCLUSION_LABELS = {"tg": "通过", "btg": "不通过"}
_OUTBOUND_BIZ_TYPE_LABELS = {"pilot_service": "中试服务", "clinical_application": "临床应用"}
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_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
# 查输注计划 → 输注方案 → 申请单 → 患者
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 ""
item["patientName"] = patient.patient_name if patient else ""
item["sortNo"] = course.get("sortNo")
item["totalCourseCount"] = course.get("totalCourseCount")
item["infusionTime"] = _fmt_dt(sched.infusion_time)
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),
),
)
.all()
)
return [r[0] for r in rows]
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")]
if not rec_ids:
return items
rec_map = {
r.id: r
for r in db.query(BizSampleReception)
.filter(BizSampleReception.id.in_(rec_ids), BizSampleReception.del_flag == 0)
.all()
}
cell_sample_ids: List[str] = []
donation_sample_ids: List[str] = []
cell_proto_ids: List[str] = []
donation_ids: List[str] = []
for item in items:
rec = rec_map.get(item.get("receptionId") or "")
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] = {}
if cell_proto_ids:
for p in (
db.query(BizCollectionProtocol.id, BizCollectionProtocol.protocol_status)
.filter(BizCollectionProtocol.id.in_(cell_proto_ids), BizCollectionProtocol.del_flag == 0)
.all()
):
proto_status_map[p.id] = p.protocol_status or ""
donation_status_map: Dict[str, str] = {}
if donation_ids:
for d in (
db.query(BizResearchDonation.id, BizResearchDonation.donation_status)
.filter(BizResearchDonation.id.in_(donation_ids), BizResearchDonation.del_flag == 0)
.all()
):
donation_status_map[d.id] = d.donation_status or ""
for item in items:
if item.get("workOrderType") == "temporary":
item["receptionSource"] = ""
item["receptionSourceLabel"] = ""
continue
rec = rec_map.get(item.get("receptionId") or "")
source = _effective_reception_source(item, rec)
item["receptionSource"] = source
item["receptionSourceLabel"] = _reception_source_label(source)
if not rec:
item.setdefault("receptionTime", "")
item.setdefault("handlerName", "")
item.setdefault("sampleTypeName", "")
continue
item["receptionTime"] = _fmt_dt(rec.reception_time)
item["handlerName"] = rec.handler_name 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,
)
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 "", "")
item["protocolStatus"] = _st
item["protocolStatusLabel"] = _DONATION_STATUS_LABELS.get(_st, _st)
else:
_pid = (
(getattr(rec, "collection_protocol_id", None) or "").strip()
or (item.get("collectionProtocolId") or "").strip()
)
_st = proto_status_map.get(_pid or "", "")
item["protocolStatus"] = _st
item["protocolStatusLabel"] = _PROTOCOL_STATUS_LABELS.get(_st, _st)
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": "",
"receptionTime": "",
"handlerName": "",
"sampleTypeName": "",
"workOrderStatus": wo.work_order_status,
"workOrderStatusLabel": _WORK_ORDER_STATUS_LABELS.get(
wo.work_order_status or "", wo.work_order_status 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",
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",
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 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,
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)
bt = (biz_type or "").strip()
if bt:
q0 = q0.filter(BizQcWorkOrder.biz_type == bt)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
keyword_conditions = [
BizQcWorkOrder.work_order_no.like(like),
BizQcWorkOrder.protocol_no.like(like),
BizQcWorkOrder.sample_code.like(like),
]
if tenant_no and wt == "outbound":
outbound_ids = _outbound_qc_work_order_ids_by_keyword(db, tenant_no, kw)
if outbound_ids:
keyword_conditions.append(BizQcWorkOrder.id.in_(outbound_ids))
q0 = q0.filter(or_(*keyword_conditions))
st = (work_order_status or "").strip()
if st:
q0 = q0.filter(BizQcWorkOrder.work_order_status == st)
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)
)
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_cell_products(db, tenant_no, items)
items = _enrich_outbound_work_orders(db, tenant_no, items)
return items, int(total)
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]:
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),
"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]]:
"""按样本批量返回质检工单摘要,供协议/捐赠查看弹窗使用。
细胞存储:传入 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.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,
)
.all()
)
out = {}
for wo in wos:
sid = (wo.protocol_sample_id or "").strip()
if sid:
out[sid] = _qc_info_item(db, wo, tenant_no)
return out
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_cell_products(db, tenant_no, enriched_list)
enriched = enriched_list[0]
out.update(
{
"receptionTime": enriched.get("receptionTime", ""),
"handlerName": enriched.get("handlerName", ""),
"sampleTypeName": enriched.get("sampleTypeName", ""),
"receptionSourceLabel": enriched.get("receptionSourceLabel", ""),
"cellProductCode": enriched.get("cellProductCode", ""),
"cellProductName": enriched.get("cellProductName", ""),
"cellProductTypeName": enriched.get("cellProductTypeName", ""),
"cellProductPassage": enriched.get("cellProductPassage", ""),
"cellProductSpecName": enriched.get("cellProductSpecName", ""),
}
)
reception_detail = sample_reception_svc.get_reception_by_id(db, wo.reception_id, tenant_no)
if reception_detail:
out["reception"] = reception_detail
else:
rec = (
db.query(BizSampleReception)
.filter(BizSampleReception.id == wo.reception_id, BizSampleReception.del_flag == 0)
.first()
)
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 "",
"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)
return out
def _soft_delete_pending_work_order_tasks(
db: Session, work_order_id: str, tenant_no: str
) -> None:
"""调整检测前软删待执行任务;存在进行中/已完成任务时不允许调整。"""
tasks = (
db.query(BizQcTask)
.filter(
BizQcTask.work_order_id == work_order_id,
BizQcTask.tenant_no == tenant_no,
BizQcTask.del_flag == 0,
)
.all()
)
if any(t.task_status in ("jxz", "ywc") for t in tasks):
raise ValueError("存在进行中或已完成的质检任务,无法调整检测项目")
if not tasks:
return
now = datetime.now()
task_ids = [t.id for t in tasks]
if task_ids:
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 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],
tenant_no: str,
user_id: Optional[str] = None,
create_dept_id: str = "",
) -> BizQcWorkOrder:
wo = get_work_order(db, work_order_id, tenant_no=tenant_no)
if not wo:
raise ValueError("工单不存在")
if wo.work_order_status == "ywc":
raise ValueError("工单已完成,无法调整检测项目")
if wo.work_order_status not in ("dpz", "ypz"):
raise ValueError("当前工单状态不可配置检测项目")
wo_type = getattr(wo, "work_order_type", None) or "inbound"
if wo_type == "temporary":
order_source = "temporary"
elif wo_type == "outbound":
order_source = "outbound"
else:
order_source = "inbound"
if wo.work_order_status == "ypz":
_soft_delete_pending_work_order_tasks(db, wo.id, tenant_no)
if not has_lab and not has_third_party:
raise ValueError("请至少选择一种检测方式")
if has_lab and not lab_item_ids:
raise ValueError("请选择实验室自检项目")
if has_third_party and not third_party_item_ids:
raise ValueError("请选择第三方检测项目")
if not has_lab:
lab_item_ids = []
if not has_third_party:
third_party_item_ids = []
# 清除旧明细
for row in (
db.query(BizQcWorkOrderLabItem)
.filter(BizQcWorkOrderLabItem.work_order_id == wo.id, BizQcWorkOrderLabItem.del_flag == 0)
.all()
):
row.del_flag = 1
for row in (
db.query(BizQcWorkOrderThirdItem)
.filter(BizQcWorkOrderThirdItem.work_order_id == wo.id, BizQcWorkOrderThirdItem.del_flag == 0)
.all()
):
row.del_flag = 1
fee_total = Decimal("0")
seq = 0
for lid in lab_item_ids:
item = lab_item_svc.get_lab_item(db, lid, tenant_no=tenant_no)
if not item or item.status != "1":
raise ValueError("实验室项目无效或已停用")
seq += 1
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=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(line)
db.flush()
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,
)
for tid in third_party_item_ids:
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("第三方公司不存在")
seq += 1
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=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(line)
db.flush()
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,
)
wo.has_lab = "1" if has_lab else "0"
wo.has_third_party = "1" if has_third_party else "0"
wo.work_order_status = "ypz"
wo.configured_time = datetime.now()
wo.configured_by = user_id
wo.third_party_fee_total = fee_total if has_third_party else None
wo.update_by = user_id
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, user_id=user_id)
db.commit()
db.refresh(wo)
return wo
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"
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",
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,
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 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,
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,
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_cell_products(db, tenant_no, 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_cell_products(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],
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("工单不存在")
if wo.work_order_status == "ywc":
raise ValueError("工单已完成,无法调整检测项目")
if wo.work_order_status not in ("dpz", "ypz"):
raise ValueError("当前工单状态不可配置检测项目")
if wo.work_order_status == "ypz":
tasks = (
db.query(BizQcTask)
.filter(
BizQcTask.work_order_id == wo.id,
BizQcTask.order_source == "temporary",
BizQcTask.tenant_no == tenant_no,
BizQcTask.del_flag == 0,
)
.all()
)
if any(t.task_status in ("jxz", "ywc") for t in tasks):
raise ValueError("存在进行中或已完成的质检任务,无法调整检测项目")
now = datetime.now()
task_ids = [t.id for t in tasks]
if task_ids:
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)
if not has_lab and not has_third_party:
raise ValueError("请至少选择一种检测方式")
if has_lab and not lab_item_ids:
raise ValueError("请选择实验室自检项目")
if has_third_party and not third_party_item_ids:
raise ValueError("请选择第三方检测项目")
if not has_lab:
lab_item_ids = []
if not has_third_party:
third_party_item_ids = []
for row in (
db.query(BizQcWorkOrderLabItem)
.filter(
BizQcWorkOrderLabItem.work_order_id == wo.id,
BizQcWorkOrderLabItem.order_source == "temporary",
BizQcWorkOrderLabItem.del_flag == 0,
)
.all()
):
row.del_flag = 1
for row in (
db.query(BizQcWorkOrderThirdItem)
.filter(
BizQcWorkOrderThirdItem.work_order_id == wo.id,
BizQcWorkOrderThirdItem.order_source == "temporary",
BizQcWorkOrderThirdItem.del_flag == 0,
)
.all()
):
row.del_flag = 1
fee_total = Decimal("0")
seq = 0
for lid in lab_item_ids:
item = lab_item_svc.get_lab_item(db, lid, tenant_no=tenant_no)
if not item or item.status != "1":
raise ValueError("实验室项目无效或已停用")
seq += 1
line = BizQcWorkOrderLabItem(
id=generate_uuid(),
work_order_id=wo.id,
order_source="temporary",
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=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(line)
db.flush()
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="temporary",
)
for tid in third_party_item_ids:
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("第三方公司不存在")
seq += 1
fee = item.fee_amount or Decimal("0")
fee_total += fee
line = BizQcWorkOrderThirdItem(
id=generate_uuid(),
work_order_id=wo.id,
order_source="temporary",
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=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(line)
db.flush()
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="temporary",
)
wo.has_lab = "1" if has_lab else "0"
wo.has_third_party = "1" if has_third_party else "0"
wo.work_order_status = "ypz"
wo.configured_time = datetime.now()
wo.configured_by = user_id
wo.third_party_fee_total = fee_total if has_third_party else None
wo.update_by = user_id
wo.update_time = datetime.now()
db.add(wo)
db.commit()
db.refresh(wo)
return wo
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 "",
"scheduleStatus": sched.schedule_status or "",
"infusionStatus": sched.infusion_status or "",
"qcStatus": sched.qc_status or "",
"cancelReason": sched.cancel_reason or "",
}
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:
out["application"] = {
"applicationNo": app.application_no or "",
"applicationName": app.application_name or "",
"applicationStatus": app.application_status or "",
"channelName": app.channel_name or "",
}
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": [],
}
if not schedule_id:
out["tasks"] = task_svc.list_tasks_by_work_order(db, wo.id, tenant_no)
return out
out["tasks"] = task_svc.list_tasks_by_work_order(db, wo.id, tenant_no)
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 = "",
) -> BizQcWorkOrder:
existing = (
db.query(BizQcWorkOrder)
.filter(
BizQcWorkOrder.infusion_schedule_id == schedule_id,
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.del_flag == 0,
)
.first()
)
if existing:
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="pilot_service",
infusion_schedule_id=schedule_id,
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()