diff --git a/backend/app/api/qc.py b/backend/app/api/qc.py index b151f86e..cee17f80 100644 --- a/backend/app/api/qc.py +++ b/backend/app/api/qc.py @@ -596,6 +596,7 @@ async def list_qc_work_orders( keyword: Optional[str] = None, workOrderStatus: Optional[str] = None, workOrderType: Optional[str] = None, + bizType: Optional[str] = Query(None, alias="bizType"), createTimeStart: Optional[str] = Query(None, alias="createTimeStart"), createTimeEnd: Optional[str] = Query(None, alias="createTimeEnd"), db: Session = Depends(get_db), @@ -610,6 +611,7 @@ async def list_qc_work_orders( keyword=keyword, work_order_status=workOrderStatus, work_order_type=workOrderType, + biz_type=bizType, create_time_start=createTimeStart, create_time_end=createTimeEnd, tenant_no=tenant_no, diff --git a/backend/app/crud/qc_work_order.py b/backend/app/crud/qc_work_order.py index 89ef29e4..29f9c110 100644 --- a/backend/app/crud/qc_work_order.py +++ b/backend/app/crud/qc_work_order.py @@ -21,6 +21,9 @@ from app.models.models import ( BizCellProduct, BizCellSampleType, BizCollectionProtocol, + BizPilotApplication, + BizPilotInfusionPlan, + BizPilotInfusionSchedule, BizProtocolSample, BizQcLabTestItem, BizQcTask, @@ -53,6 +56,7 @@ _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]: @@ -130,6 +134,63 @@ def _enrich_work_order_cell_products( return items +def _enrich_outbound_work_orders( + db: Session, tenant_no: str, items: List[Dict[str, Any]] +) -> List[Dict[str, Any]]: + """为 outbound 类型工单批量补充申请单/输注计划信息。""" + 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 {} + 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 + 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["infusionTime"] = _fmt_dt(sched.infusion_time) + return items + + def _accept_conclusion_label(v: Optional[str]) -> str: key = (v or "").strip() return _ACCEPT_CONCLUSION_LABELS.get(key, key) @@ -290,13 +351,20 @@ def _gen_daily_no(db: Session, tenant_no: str, prefix: str) -> str: return f"{prefix}{day}{cnt + 1:04d}" -def _wo_to_dict(wo: BizQcWorkOrder) -> Dict[str, Any]: +def _wo_to_dict( + wo: BizQcWorkOrder, + *, + outbound_info: Optional[Dict[str, Any]] = None, +) -> 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, @@ -311,6 +379,12 @@ def _wo_to_dict(wo: BizQcWorkOrder) -> Dict[str, Any]: "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": "", @@ -458,6 +532,7 @@ def list_work_orders( 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, tenant_no: Optional[str] = None, @@ -470,6 +545,9 @@ def list_work_orders( 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}%" @@ -511,6 +589,7 @@ def list_work_orders( 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) @@ -1500,6 +1579,7 @@ def create_outbound_qc_work_order( 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",