Files
zkxw/backend/app/crud/qc_work_order.py
T
2026-05-24 16:58:51 +08:00

1488 lines
49 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.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 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,
BizProtocolSample,
BizQcLabTestItem,
BizQcTask,
BizQcThirdPartyItem,
BizQcWorkOrder,
BizQcWorkOrderLabItem,
BizQcWorkOrderThirdItem,
BizSampleReception,
)
from app.models.qc_param import BizQcTaskResult
from app.models.research_donation import BizResearchDonation, BizResearchDonationSample
_RECEPTION_SOURCE_LABELS = {
"cell_storage": "细胞存储",
"research_donation": "科研捐赠",
}
_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": "不通过"}
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 _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 _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 _sample_type_name_for_reception(
db: Session,
rec: BizSampleReception,
type_map: Dict[str, str],
*,
protocol_samples: Dict[str, BizProtocolSample],
donation_samples: Dict[str, BizResearchDonationSample],
) -> str:
if (rec.reception_source or "cell_storage") == "research_donation":
sample = donation_samples.get(rec.donation_sample_id or "")
tid = sample.sample_type_id if sample else ""
else:
sample = protocol_samples.get(rec.protocol_sample_id or "")
tid = sample.sample_type_id if sample else ""
return type_map.get(tid or "", "")
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 = [
r.protocol_sample_id
for r in rec_map.values()
if (r.reception_source or "cell_storage") != "research_donation" and r.protocol_sample_id
]
donation_sample_ids = [
r.donation_sample_id
for r in rec_map.values()
if (r.reception_source or "") == "research_donation" and r.donation_sample_id
]
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 = {
s.id: s
for s in db.query(BizResearchDonationSample)
.filter(
BizResearchDonationSample.id.in_(donation_sample_ids),
BizResearchDonationSample.del_flag == 0,
)
.all()
}
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)
cell_proto_ids = [
rec.collection_protocol_id
for rec in rec_map.values()
if (rec.reception_source or "cell_storage") != "research_donation" and rec.collection_protocol_id
]
donation_ids = [
rec.research_donation_id
for rec in rec_map.values()
if (rec.reception_source or "") == "research_donation" and rec.research_donation_id
]
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 = item.get("receptionSource") or (rec.reception_source if rec else "cell_storage")
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_reception(
db,
rec,
type_map,
protocol_samples=protocol_samples,
donation_samples=donation_samples,
)
if source == "research_donation":
_did = rec.research_donation_id if rec else ""
_st = donation_status_map.get(_did or "", "")
item["protocolStatus"] = _st
item["protocolStatusLabel"] = _DONATION_STATUS_LABELS.get(_st, _st)
else:
_pid = rec.collection_protocol_id if rec else ""
_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 _wo_to_dict(wo: BizQcWorkOrder) -> 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"
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)),
"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 "",
"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,
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)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
q0 = q0.filter(
or_(
BizQcWorkOrder.work_order_no.like(like),
BizQcWorkOrder.protocol_no.like(like),
BizQcWorkOrder.sample_code.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]
items = _enrich_work_order_list_items(db, items)
if tenant_no:
items = _enrich_work_order_cell_products(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("当前工单状态不可配置检测项目")
order_source = "temporary" if (getattr(wo, "work_order_type", None) or "inbound") == "temporary" else "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)
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)
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_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:
"""编辑临时质检工单(备注、质检对象)"""
wo = get_work_order(db, work_order_id, tenant_no=tenant_no)
if not wo:
raise ValueError("工单不存在")
if (getattr(wo, "work_order_type", None) or "inbound") != "temporary":
raise ValueError("仅临时质检工单可编辑")
if cell_product_id is not None:
wo.cell_product_id = _validate_cell_product_id(db, cell_product_id, tenant_no)
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 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 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 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()