Files
zkxw/backend/app/crud/outbound_release_review.py
T
zlwuandCursor 5dad1bb0e3 fix: 协议子资源单事务保存,并补齐样本编码/OA/质检等近期能力
锁定缴费校验失败时整单回滚,避免样本半成功落库;前端禁止删除锁定缴费样本,改码走更正编码。同步落地样本更正、采集微信通知、OA提及未读、细胞产品套餐与相关迁移文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-26 11:02:25 +08:00

862 lines
30 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
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}