Files
shanxia2.0/backend/app/crud/outbound_release_review.py
T
2026-05-28 20:04:27 +08:00

665 lines
23 KiB
Python

"""出库放行审核 CRUD"""
from __future__ import annotations
import math
from datetime import datetime
from typing import Any, Dict, List, Optional
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.utils import generate_uuid
from app.models.models import (
BizOutboundReleaseReview,
BizPilotApplication,
BizPilotInfusionPlan,
BizPilotInfusionSchedule,
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": "临床应用",
}
# ────────────────────────────────────────────
# Helpers
# ────────────────────────────────────────────
def _fmt_dt(t: Optional[datetime]) -> str:
return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
def _total_pages(total: int, page_size: int) -> int:
if total <= 0:
return 0
return int(math.ceil(total / float(page_size)))
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,
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),
),
)
.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)))
# ── review_conclusion 过滤 ──
rc = (review_conclusion or "").strip()
if rc == "pending":
# 待审核:不存在审核记录
reviewed_schedule_ids = (
db.query(BizOutboundReleaseReview.infusion_schedule_id)
.filter(
BizOutboundReleaseReview.tenant_no == tenant_no,
BizOutboundReleaseReview.del_flag == 0,
BizOutboundReleaseReview.review_conclusion != "",
)
.subquery()
)
q0 = q0.filter(
~BizPilotInfusionSchedule.id.in_(reviewed_schedule_ids)
)
elif rc:
# 有具体结论
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 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,
)
.all()
):
qc_map[qwo.infusion_schedule_id] = qwo
# 输注计划 → 申请单 → 患者
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] = {
"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 "",
"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 ""),
"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 ""
),
"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)
# ── 质检工单 + 任务 + 结果 ──
qc_info: Dict[str, Any] = {}
qwo = (
db.query(BizQcWorkOrder)
.filter(
BizQcWorkOrder.infusion_schedule_id == schedule_id,
BizQcWorkOrder.qc_biz_type == "outbound",
BizQcWorkOrder.biz_type.in_(list(_BIZ_TYPE_LABELS.keys())),
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.del_flag == 0,
)
.first()
)
if qwo:
from app.crud.qc_work_order import _qc_info_item
qc_info = _qc_info_item(db, qwo, 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,
"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,
)
.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}