"""出库放行审核 CRUD""" from __future__ import annotations import math from collections import defaultdict from datetime import datetime from typing import Any, Dict, List, Optional from sqlalchemy import and_, exists, func, or_ from sqlalchemy.orm import Session from app.core.db_collation import eq_unicode from app.core.utils import generate_uuid from app.models.models import ( BizOutboundReleaseReview, BizPilotApplication, BizPilotInfusionPlan, BizPilotInfusionSchedule, BizPilotInfusionScheduleProduct, BizPilotPatient, BizProductionTask, BizProductionWorkOrder, BizQcTask, BizQcWorkOrder, SysUser, ) from app.models.qc_param import BizQcTaskResult _REVIEW_CONCLUSION_LABELS = {"tg": "通过", "btg": "不通过"} _SCHEDULE_STATUS_LABELS = { "dsc": "待生产", "ysc": "已生产", "yjf": "已交付", "yqx": "已取消", } _QC_STATUS_LABELS = { "dzc": "待质检", "yzj": "已质检", "yqx": "已取消", } _BIZ_TYPE_LABELS = { "pilot_service": "中试服务", "clinical_application": "临床应用", "basic_experiment": "基础实验", } # ──────────────────────────────────────────── # Helpers # ──────────────────────────────────────────── def _fmt_dt(t: Optional[datetime]) -> str: return t.strftime("%Y-%m-%d %H:%M:%S") if t else "" def _dec(v: Any) -> Optional[float]: if v is None: return None try: return float(v) except (TypeError, ValueError): return None def _total_pages(total: int, page_size: int) -> int: if total <= 0: return 0 return int(math.ceil(total / float(page_size))) def normalize_outbound_review_phase(review_phase: Optional[str]) -> Optional[str]: p = (review_phase or "").strip().lower() if p in ("pending", "reviewed"): return p return None def normalize_outbound_ready_for_review_only(raw) -> Optional[bool]: """Return True when ready-only filter should apply; None means no filter.""" if raw is None: return None if isinstance(raw, bool): return True if raw else None s = str(raw).strip().lower() if s in ("true", "1", "yes"): return True return None def outbound_work_orders_all_void(statuses) -> bool: """True when ≥1 non-empty status and every one equals yzf.""" cleaned = [(s or "").strip() for s in (statuses or [])] if not cleaned or not any(s for s in cleaned): return False return all(s.lower() == "yzf" for s in cleaned) def _outbound_qc_unfinished_exists_for_schedule(tenant_no: str): """Any unfinished outbound schedule QC (dpz/ypz) for this infusion schedule.""" return ( exists() .where( and_( BizQcWorkOrder.tenant_no == tenant_no, BizQcWorkOrder.del_flag == 0, BizQcWorkOrder.qc_biz_type == "outbound", BizQcWorkOrder.trigger_source == "schedule", # 跨表 id 排序规则可能不同(general_ci vs unicode_ci) eq_unicode( BizQcWorkOrder.infusion_schedule_id, BizPilotInfusionSchedule.id ), BizQcWorkOrder.work_order_status.in_(("dpz", "ypz")), ) ) .correlate(BizPilotInfusionSchedule) ) def _outbound_production_unfinished_exists_for_schedule(tenant_no: str): return ( exists() .where( and_( BizProductionWorkOrder.tenant_no == tenant_no, BizProductionWorkOrder.del_flag == 0, BizProductionWorkOrder.work_order_type == "outbound", eq_unicode( BizProductionWorkOrder.infusion_schedule_id, BizPilotInfusionSchedule.id, ), BizProductionWorkOrder.work_order_status.in_(("dpz", "jxz")), ) ) .correlate(BizPilotInfusionSchedule) ) def _outbound_qc_all_void_for_schedule(tenant_no: str): base = [ BizQcWorkOrder.tenant_no == tenant_no, BizQcWorkOrder.del_flag == 0, BizQcWorkOrder.qc_biz_type == "outbound", BizQcWorkOrder.trigger_source == "schedule", eq_unicode(BizQcWorkOrder.infusion_schedule_id, BizPilotInfusionSchedule.id), ] any_wo = exists().where(and_(*base)).correlate(BizPilotInfusionSchedule) any_non_void = ( exists() .where( and_( *base, or_( BizQcWorkOrder.work_order_status.is_(None), BizQcWorkOrder.work_order_status != "yzf", ), ) ) .correlate(BizPilotInfusionSchedule) ) return and_(any_wo, ~any_non_void) def _outbound_production_all_void_for_schedule(tenant_no: str): base = [ BizProductionWorkOrder.tenant_no == tenant_no, BizProductionWorkOrder.del_flag == 0, BizProductionWorkOrder.work_order_type == "outbound", eq_unicode( BizProductionWorkOrder.infusion_schedule_id, BizPilotInfusionSchedule.id ), ] any_wo = exists().where(and_(*base)).correlate(BizPilotInfusionSchedule) any_non_void = ( exists() .where( and_( *base, or_( BizProductionWorkOrder.work_order_status.is_(None), BizProductionWorkOrder.work_order_status != "yzf", ), ) ) .correlate(BizPilotInfusionSchedule) ) return and_(any_wo, ~any_non_void) def _get_user_name(db: Session, user_id: str) -> str: u = db.query(SysUser).filter(SysUser.id == user_id).first() return (u.nick_name or "") if u else "" def _resolve_schedule_biz_type( app: Optional[BizPilotApplication], qwo: Optional[BizQcWorkOrder], pwo: Optional[BizProductionWorkOrder], ) -> str: """输注计划业务类型:优先出库工单 biz_type,否则取申请单 biz_type。""" if qwo and (qwo.biz_type or "").strip(): return (qwo.biz_type or "").strip() if pwo and (pwo.biz_type or "").strip(): return (pwo.biz_type or "").strip() if app and (app.biz_type or "").strip(): return (app.biz_type or "").strip() if app: return "pilot_service" return "" def _row_to_dict(row) -> Dict[str, Any]: """将 BizOutboundReleaseReview ORM 对象转为前端字典。""" return { "id": row.id, "infusionScheduleId": row.infusion_schedule_id or "", "productionWorkOrderId": row.production_work_order_id or "", "qcWorkOrderId": row.qc_work_order_id or "", "reviewConclusion": row.review_conclusion or "", "reviewConclusionLabel": _REVIEW_CONCLUSION_LABELS.get( row.review_conclusion or "", row.review_conclusion or "" ), "reviewerUserId": row.reviewer_user_id or "", "reviewByName": row.review_by_name or "", "reviewAttachments": row.review_attachments or "", "reviewRemark": row.review_remark or "", "reviewTime": _fmt_dt(row.review_time), "tenantNo": row.tenant_no or "", "orderNum": row.order_num if row.order_num is not None else 0, "remark": row.remark or "", "createBy": row.create_by or "", "createTime": _fmt_dt(row.create_time), "updateBy": row.update_by or "", "updateTime": _fmt_dt(row.update_time), } # ──────────────────────────────────────────── # Paginated list # ──────────────────────────────────────────── def list_reviews_paginated( db: Session, *, tenant_no: str, keyword: Optional[str] = None, review_conclusion: Optional[str] = None, schedule_status: Optional[str] = None, qc_status: Optional[str] = None, biz_type: Optional[str] = None, infusion_time_start: Optional[str] = None, infusion_time_end: Optional[str] = None, review_phase: Optional[str] = None, ready_for_review_only=None, page: int = 1, page_size: int = 10, user_info=None, ) -> Dict[str, Any]: """分页列出出库放行审核数据,以输注计划为主表 LEFT JOIN 审核/生产/质检工单。""" # ── 基础查询:输注计划 ── q0 = db.query(BizPilotInfusionSchedule).filter( BizPilotInfusionSchedule.tenant_no == tenant_no, BizPilotInfusionSchedule.del_flag == 0, ) bt_filter = (biz_type or "").strip() if bt_filter: app_ids_for_biz = ( db.query(BizPilotApplication.id) .filter( BizPilotApplication.tenant_no == tenant_no, BizPilotApplication.del_flag == 0, BizPilotApplication.biz_type == bt_filter, ) .subquery() ) plan_ids_for_biz = ( db.query(BizPilotInfusionPlan.id) .filter( BizPilotInfusionPlan.tenant_no == tenant_no, BizPilotInfusionPlan.del_flag == 0, BizPilotInfusionPlan.pilot_application_id.in_(app_ids_for_biz), ) .subquery() ) q0 = q0.filter(BizPilotInfusionSchedule.infusion_plan_id.in_(plan_ids_for_biz)) # ── 关键字过滤(application_no, patient_name)需要子查询 ── kw = (keyword or "").strip() if kw: like = f"%{kw}%" # 通过 infusion_plan_id 关联 plan → application → patient plan_ids = ( db.query(BizPilotInfusionPlan.id) .filter( BizPilotInfusionPlan.tenant_no == tenant_no, BizPilotInfusionPlan.del_flag == 0, ) .subquery() ) app_ids = ( db.query(BizPilotApplication.id) .filter( BizPilotApplication.tenant_no == tenant_no, BizPilotApplication.del_flag == 0, or_( BizPilotApplication.application_no.like(like), BizPilotApplication.application_name.like(like), BizPilotApplication.project_title.like(like), BizPilotApplication.customer_name.like(like), ), ) .subquery() ) patient_app_ids = ( db.query(BizPilotPatient.pilot_application_id) .filter( BizPilotPatient.tenant_no == tenant_no, BizPilotPatient.del_flag == 0, BizPilotPatient.patient_name.like(like), ) .subquery() ) # 找出满足条件的 infusion_plan_id matched_plan_ids = ( db.query(BizPilotInfusionPlan.id) .filter( BizPilotInfusionPlan.tenant_no == tenant_no, BizPilotInfusionPlan.del_flag == 0, or_( BizPilotInfusionPlan.pilot_application_id.in_(app_ids), BizPilotInfusionPlan.pilot_application_id.in_(patient_app_ids), ), ) .subquery() ) q0 = q0.filter( BizPilotInfusionSchedule.infusion_plan_id.in_(matched_plan_ids) ) # ── schedule_status 过滤 ── ss = (schedule_status or "").strip() if ss: q0 = q0.filter(BizPilotInfusionSchedule.schedule_status == ss) # ── qc_status 过滤 ── qs = (qc_status or "").strip() if qs: q0 = q0.filter(BizPilotInfusionSchedule.qc_status == qs) # ── 输注时间范围 ── time_bounds = [] if (infusion_time_start or "").strip(): try: start_dt = datetime.strptime(infusion_time_start.strip()[:10], "%Y-%m-%d") time_bounds.append(BizPilotInfusionSchedule.infusion_time >= start_dt) except ValueError: pass if (infusion_time_end or "").strip(): try: end_dt = datetime.strptime(infusion_time_end.strip()[:10], "%Y-%m-%d") end_dt = end_dt.replace(hour=23, minute=59, second=59) time_bounds.append(BizPilotInfusionSchedule.infusion_time <= end_dt) except ValueError: pass if time_bounds: q0 = q0.filter(or_(and_(*time_bounds), BizPilotInfusionSchedule.infusion_time.is_(None))) # ── reviewPhase / review_conclusion 过滤 ── phase = normalize_outbound_review_phase(review_phase) rc = (review_conclusion or "").strip() if phase == "pending" or (phase is None and rc == "pending"): reviewed_schedule_ids = ( db.query(BizOutboundReleaseReview.infusion_schedule_id) .filter( BizOutboundReleaseReview.tenant_no == tenant_no, BizOutboundReleaseReview.del_flag == 0, BizOutboundReleaseReview.review_conclusion.isnot(None), BizOutboundReleaseReview.review_conclusion != "", ) .subquery() ) q0 = q0.filter(~BizPilotInfusionSchedule.id.in_(reviewed_schedule_ids)) elif phase == "reviewed": reviewed_schedule_ids = ( db.query(BizOutboundReleaseReview.infusion_schedule_id) .filter( BizOutboundReleaseReview.tenant_no == tenant_no, BizOutboundReleaseReview.del_flag == 0, BizOutboundReleaseReview.review_conclusion.isnot(None), BizOutboundReleaseReview.review_conclusion != "", ) .subquery() ) q0 = q0.filter(BizPilotInfusionSchedule.id.in_(reviewed_schedule_ids)) if rc in ("tg", "btg"): conclusion_ids = ( db.query(BizOutboundReleaseReview.infusion_schedule_id) .filter( BizOutboundReleaseReview.tenant_no == tenant_no, BizOutboundReleaseReview.del_flag == 0, BizOutboundReleaseReview.review_conclusion == rc, ) .subquery() ) q0 = q0.filter(BizPilotInfusionSchedule.id.in_(conclusion_ids)) elif rc in ("tg", "btg"): reviewed_schedule_ids = ( db.query(BizOutboundReleaseReview.infusion_schedule_id) .filter( BizOutboundReleaseReview.tenant_no == tenant_no, BizOutboundReleaseReview.del_flag == 0, BizOutboundReleaseReview.review_conclusion == rc, ) .subquery() ) q0 = q0.filter(BizPilotInfusionSchedule.id.in_(reviewed_schedule_ids)) if normalize_outbound_ready_for_review_only(ready_for_review_only) is True: q0 = q0.filter( ~_outbound_qc_unfinished_exists_for_schedule(tenant_no), ~_outbound_production_unfinished_exists_for_schedule(tenant_no), ) # 出库质检/生产「有工单且全部 yzf」→ 列表隐藏(两 Tab 常驻) q0 = q0.filter( ~or_( _outbound_qc_all_void_for_schedule(tenant_no), _outbound_production_all_void_for_schedule(tenant_no), ) ) # ── 数据权限 ── if user_info: from app.core.data_scope import apply_data_scope q0 = apply_data_scope(q0, BizPilotInfusionSchedule, user_info, db, tenant_no) # ── 分页 ── total = ( q0.order_by(None) .with_entities(func.count(BizPilotInfusionSchedule.id)) .scalar() or 0 ) page = max(1, page) page_size = min(max(1, page_size), 100) schedules = ( q0.order_by(BizPilotInfusionSchedule.create_time.desc()) .offset((page - 1) * page_size) .limit(page_size) .all() ) schedule_ids = [s.id for s in schedules] # ── 批量加载关联数据 ── # 审核记录 review_map: Dict[str, BizOutboundReleaseReview] = {} if schedule_ids: for rev in ( db.query(BizOutboundReleaseReview) .filter( BizOutboundReleaseReview.infusion_schedule_id.in_(schedule_ids), BizOutboundReleaseReview.tenant_no == tenant_no, BizOutboundReleaseReview.del_flag == 0, ) .all() ): review_map[rev.infusion_schedule_id] = rev # 生产工单(出库类型) production_map: Dict[str, BizProductionWorkOrder] = {} if schedule_ids: for pwo in ( db.query(BizProductionWorkOrder) .filter( BizProductionWorkOrder.infusion_schedule_id.in_(schedule_ids), BizProductionWorkOrder.work_order_type == "outbound", BizProductionWorkOrder.biz_type.in_( list(_BIZ_TYPE_LABELS.keys()) ), BizProductionWorkOrder.tenant_no == tenant_no, BizProductionWorkOrder.del_flag == 0, ) .all() ): production_map[pwo.infusion_schedule_id] = pwo # 质检工单(出库类型) qc_map: Dict[str, BizQcWorkOrder] = {} if schedule_ids: for qwo in ( db.query(BizQcWorkOrder) .filter( BizQcWorkOrder.infusion_schedule_id.in_(schedule_ids), BizQcWorkOrder.qc_biz_type == "outbound", BizQcWorkOrder.biz_type.in_(list(_BIZ_TYPE_LABELS.keys())), BizQcWorkOrder.tenant_no == tenant_no, BizQcWorkOrder.del_flag == 0, BizQcWorkOrder.trigger_source == "schedule", ) .all() ): qc_map[qwo.infusion_schedule_id] = qwo # 输注计划产品明细(用户需求细胞) products_map: Dict[str, List[Dict[str, Any]]] = defaultdict(list) if schedule_ids: for product in ( db.query(BizPilotInfusionScheduleProduct) .filter( BizPilotInfusionScheduleProduct.infusion_schedule_id.in_(schedule_ids), BizPilotInfusionScheduleProduct.tenant_no == tenant_no, BizPilotInfusionScheduleProduct.del_flag == 0, ) .order_by(BizPilotInfusionScheduleProduct.order_num.asc()) .all() ): products_map[product.infusion_schedule_id].append( { "projectCode": product.project_code or "", "projectName": product.project_name or "", "cellTypeName": product.cell_type_name or "", "specText": product.spec_text or "", "packageId": getattr(product, "package_id", None) or "", "packageText": getattr(product, "package_text", None) or "", "agreementPrice": _dec(product.agreement_price), "actualPrice": _dec(product.actual_price), "quantity": product.quantity, "productStatus": product.product_status or "", } ) # 输注计划 → 申请单 → 患者 plan_ids_for_schedules = { s.id: s.infusion_plan_id for s in schedules if s.infusion_plan_id } unique_plan_ids = list(set(plan_ids_for_schedules.values())) plan_map: Dict[str, BizPilotInfusionPlan] = {} if unique_plan_ids: for plan in ( db.query(BizPilotInfusionPlan) .filter( BizPilotInfusionPlan.id.in_(unique_plan_ids), BizPilotInfusionPlan.tenant_no == tenant_no, BizPilotInfusionPlan.del_flag == 0, ) .all() ): plan_map[plan.id] = plan unique_app_ids = list( {p.pilot_application_id for p in plan_map.values() if p.pilot_application_id} ) app_map: Dict[str, BizPilotApplication] = {} if unique_app_ids: for app in ( db.query(BizPilotApplication) .filter( BizPilotApplication.id.in_(unique_app_ids), BizPilotApplication.tenant_no == tenant_no, BizPilotApplication.del_flag == 0, ) .all() ): app_map[app.id] = app # 患者 patient_map: Dict[str, BizPilotPatient] = {} if unique_app_ids: for patient in ( db.query(BizPilotPatient) .filter( BizPilotPatient.pilot_application_id.in_(unique_app_ids), BizPilotPatient.tenant_no == tenant_no, BizPilotPatient.del_flag == 0, ) .all() ): patient_map[patient.pilot_application_id] = patient # ── 组装列表项 ── items: List[Dict[str, Any]] = [] for s in schedules: plan = plan_map.get(s.infusion_plan_id) app = app_map.get(plan.pilot_application_id) if plan else None patient = patient_map.get(app.id) if app else None rev = review_map.get(s.id) pwo = production_map.get(s.id) qwo = qc_map.get(s.id) biz_type = _resolve_schedule_biz_type(app, qwo, pwo) item: Dict[str, Any] = { "id": s.id, "scheduleId": s.id, "bizType": biz_type, "bizTypeLabel": _BIZ_TYPE_LABELS.get(biz_type, biz_type), "applicationNo": (app.application_no or "") if app else "", "applicationName": (app.application_name or "") if app else "", "channelName": (app.channel_name or "") if app else "", "projectTitle": (app.project_title or "") if app else "", "customerName": (app.customer_name or "") if app else "", "patientName": (patient.patient_name or "") if patient else "", "cellType": s.cell_type or (plan.cell_type or "") if plan else (s.cell_type or ""), "specText": s.spec_text or (plan.spec_text or "") if plan else (s.spec_text or ""), "packageId": getattr(s, "package_id", None) or "", "packageText": getattr(s, "package_text", None) or "", "infusionTime": _fmt_dt(s.infusion_time), "scheduleStatus": s.schedule_status or "", "scheduleStatusLabel": _SCHEDULE_STATUS_LABELS.get( s.schedule_status or "", s.schedule_status or "" ), "qcStatus": s.qc_status or "", "qcStatusLabel": _QC_STATUS_LABELS.get( s.qc_status or "", s.qc_status or "" ), "products": products_map.get(s.id, []), "reviewConclusion": "", "reviewConclusionLabel": "", "reviewByName": "", "reviewTime": "", "reviewRemark": "", "reviewAttachments": "", "reviewId": "", "productionWorkOrderNo": "", "productionWorkOrderStatus": "", "qcWorkOrderNo": "", "qcWorkOrderStatus": "", } if rev: rc_val = rev.review_conclusion or "" item["reviewConclusion"] = rc_val item["reviewConclusionLabel"] = _REVIEW_CONCLUSION_LABELS.get(rc_val, rc_val) item["reviewByName"] = rev.review_by_name or "" item["reviewTime"] = _fmt_dt(rev.review_time) item["reviewRemark"] = rev.review_remark or "" item["reviewAttachments"] = rev.review_attachments or "" item["reviewId"] = rev.id else: item["reviewConclusionLabel"] = "待审核" if pwo: item["productionWorkOrderNo"] = pwo.work_order_no or "" item["productionWorkOrderStatus"] = pwo.work_order_status or "" if qwo: item["qcWorkOrderNo"] = qwo.work_order_no or "" item["qcWorkOrderStatus"] = qwo.work_order_status or "" items.append(item) return { "list": items, "total": int(total), "page": page, "pageSize": page_size, "totalPages": _total_pages(int(total), page_size), } # ──────────────────────────────────────────── # Detail aggregation # ──────────────────────────────────────────── def get_schedule_detail( db: Session, schedule_id: str, tenant_no: str ) -> Optional[Dict[str, Any]]: """按输注计划 ID 聚合出库放行审核详情(中试上下文 + 生产 + 质检 + 审核)。""" from app.crud.qc_work_order import build_outbound_pilot_context_by_schedule pilot_context = build_outbound_pilot_context_by_schedule(db, schedule_id, tenant_no) # ── 生产工单 + 任务 ── production_info: Dict[str, Any] = {} pwo = ( db.query(BizProductionWorkOrder) .filter( BizProductionWorkOrder.infusion_schedule_id == schedule_id, BizProductionWorkOrder.work_order_type == "outbound", BizProductionWorkOrder.biz_type.in_(list(_BIZ_TYPE_LABELS.keys())), BizProductionWorkOrder.tenant_no == tenant_no, BizProductionWorkOrder.del_flag == 0, ) .first() ) if pwo: from app.crud.production_work_order import ( _production_info_item, ) production_info = _production_info_item(db, pwo) # ── 质检工单 + 任务 + 结果 ── from app.crud import qc_work_order as qc_work_order_svc qc_info = qc_work_order_svc.get_schedule_gate_qc_info( db, schedule_id=schedule_id, tenant_no=tenant_no ) or {} process_qc_info_list = qc_work_order_svc.list_process_qc_info_for_schedule( db, schedule_id=schedule_id, tenant_no=tenant_no ) # ── 审核记录 ── review_info: Dict[str, Any] = {} rev = ( db.query(BizOutboundReleaseReview) .filter( BizOutboundReleaseReview.infusion_schedule_id == schedule_id, BizOutboundReleaseReview.tenant_no == tenant_no, BizOutboundReleaseReview.del_flag == 0, ) .first() ) if rev: review_info = _row_to_dict(rev) outbound_material: Dict[str, Any] = {} try: from app.crud import outbound_release_material as orm_svc outbound_material = orm_svc.get_outbound_material(db, schedule_id, tenant_no) except Exception: outbound_material = {} # 与 OutboundReleaseReviewDetail / 出库质检详情一致:中试字段展平到顶层,勿嵌套 pilotContext return { **(pilot_context or {}), "productionInfo": production_info, "qcInfo": qc_info, "processQcInfoList": process_qc_info_list, "reviewInfo": review_info, "outboundMaterial": outbound_material, } # ──────────────────────────────────────────── # Review operations # ──────────────────────────────────────────── def review_schedule( db: Session, schedule_id: str, *, conclusion: str, remark: Optional[str] = None, attachments: Optional[str] = None, tenant_no: str, user_id: str, ) -> Dict[str, Any]: """对指定输注计划执行出库放行审核(upsert)。""" now = datetime.now() user_name = _get_user_name(db, user_id) # 查找已有审核记录 rev = ( db.query(BizOutboundReleaseReview) .filter( BizOutboundReleaseReview.infusion_schedule_id == schedule_id, BizOutboundReleaseReview.tenant_no == tenant_no, BizOutboundReleaseReview.del_flag == 0, ) .first() ) # 自动填充生产工单 ID pwo = ( db.query(BizProductionWorkOrder) .filter( BizProductionWorkOrder.infusion_schedule_id == schedule_id, BizProductionWorkOrder.work_order_type == "outbound", BizProductionWorkOrder.tenant_no == tenant_no, BizProductionWorkOrder.del_flag == 0, ) .first() ) # 自动填充质检工单 ID qwo = ( db.query(BizQcWorkOrder) .filter( BizQcWorkOrder.infusion_schedule_id == schedule_id, BizQcWorkOrder.qc_biz_type == "outbound", BizQcWorkOrder.tenant_no == tenant_no, BizQcWorkOrder.del_flag == 0, BizQcWorkOrder.trigger_source == "schedule", ) .first() ) if rev: # update rev.review_conclusion = conclusion rev.review_remark = (remark or "").strip() or None rev.review_attachments = (attachments or "").strip() or None rev.reviewer_user_id = user_id rev.review_by_name = user_name rev.review_time = now rev.production_work_order_id = pwo.id if pwo else (rev.production_work_order_id or "") rev.qc_work_order_id = qwo.id if qwo else (rev.qc_work_order_id or "") rev.update_by = user_id rev.update_time = now else: # create rev = BizOutboundReleaseReview( id=generate_uuid(), infusion_schedule_id=schedule_id, production_work_order_id=pwo.id if pwo else "", qc_work_order_id=qwo.id if qwo else "", review_conclusion=conclusion, reviewer_user_id=user_id, review_by_name=user_name, review_time=now, review_remark=(remark or "").strip() or None, review_attachments=(attachments or "").strip() or None, tenant_no=tenant_no, order_num=0, remark="", create_by=user_id, create_dept_id="", create_time=now, update_by=user_id, update_time=now, del_flag=0, ) db.add(rev) db.flush() return _row_to_dict(rev) def batch_review( db: Session, schedule_ids: List[str], *, conclusion: str, remark: Optional[str] = None, attachments: Optional[str] = None, tenant_no: str, user_id: str, ) -> Dict[str, Any]: """批量审核多个输注计划。""" success = 0 fail = 0 for sid in schedule_ids: try: review_schedule( db, sid, conclusion=conclusion, remark=remark, attachments=attachments, tenant_no=tenant_no, user_id=user_id, ) success += 1 except Exception: fail += 1 return {"success": success, "fail": fail}