Files
zkxw/backend/app/services/uc_msc_builtin_template.py
zlwuandCursor 50679fa75e fix(production): UC 入库编码自动派发脐带MSC五步任务
样本接收 UC… 此前无内置匹配规则导致工单空任务;新增 5 步原代培养模版与回补脚本,且不影响 BC 六步模版。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-13 11:01:48 +08:00

473 lines
14 KiB
Python

"""确保 UC 入库编码对应的 5 步内置工艺路线 / 任务组合存在。
步骤顺序(业务确认):
1. 脐带MSC原代细胞培养
2. 脐带MSC原代细胞培养半换液
3. 脐带MSC原代二次贴壁
4. 脐带MSC原代重铺操作
5. 干细胞冻存
内容从既有「干细胞培养(内置模版)」按工序代码拷贝,排除传代。
"""
from __future__ import annotations
from datetime import datetime
from typing import Any, Dict, List, Optional
from sqlalchemy.orm import Session
from app.models.models import (
BizProcessRoute,
BizProcessRouteStep,
BizProcessRouteStepConsumable,
BizProcessRouteStepDataCollection,
BizProcessRouteStepOutput,
BizProcessRouteStepReagent,
BizProductionTaskSet,
BizProductionTaskSetItem,
)
from app.core.utils import generate_uuid
from app.utils.lab_storage_code_task_set import (
BC_MSC_BUILTIN_SET_NAME,
UC_MSC_BUILTIN_SET_NAME,
)
UC_MSC_ROUTE_CODE = "MSC原代-UC-BUILTIN"
UC_MSC_ROUTE_NAME = UC_MSC_BUILTIN_SET_NAME
SOURCE_MSC_ROUTE_CODE = "MSC培养-BUILTIN"
SOURCE_MSC_ROUTE_NAME = BC_MSC_BUILTIN_SET_NAME
# process_code → 目标 step_order
UC_MSC_STEP_ORDER: List[str] = [
"SOP-SC-003-E01-01", # 原代细胞培养
"SOP-SC-004-E01-01", # 半换液
"SOP-SC-006-E01-01", # 二次贴壁
"SOP-SC-005-E01-01", # 重铺
"SOP-SC-008-E01-01", # 冻存
]
def _now() -> datetime:
return datetime.now()
def get_uc_msc_builtin_set(
db: Session, *, tenant_no: str
) -> Optional[BizProductionTaskSet]:
return (
db.query(BizProductionTaskSet)
.filter(
BizProductionTaskSet.tenant_no == tenant_no,
BizProductionTaskSet.set_name == UC_MSC_BUILTIN_SET_NAME,
BizProductionTaskSet.is_builtin == "1",
BizProductionTaskSet.del_flag == 0,
)
.first()
)
def _find_source_route(db: Session, *, tenant_no: str) -> BizProcessRoute:
route = (
db.query(BizProcessRoute)
.filter(
BizProcessRoute.tenant_no == tenant_no,
BizProcessRoute.route_code == SOURCE_MSC_ROUTE_CODE,
BizProcessRoute.del_flag == 0,
)
.first()
)
if route:
return route
route = (
db.query(BizProcessRoute)
.filter(
BizProcessRoute.tenant_no == tenant_no,
BizProcessRoute.route_name == SOURCE_MSC_ROUTE_NAME,
BizProcessRoute.is_builtin == "1",
BizProcessRoute.del_flag == 0,
)
.first()
)
if not route:
raise ValueError(
f"缺少源工艺路线 {SOURCE_MSC_ROUTE_CODE}/{SOURCE_MSC_ROUTE_NAME},无法创建 UC 模版"
)
return route
def _clone_step_children(
db: Session,
*,
src_step_id: str,
dst_step_id: str,
tenant_no: str,
user_id: Optional[str],
create_dept_id: str,
now: datetime,
) -> None:
reagent_rows = (
db.query(BizProcessRouteStepReagent)
.filter(
BizProcessRouteStepReagent.step_id == src_step_id,
BizProcessRouteStepReagent.tenant_no == tenant_no,
BizProcessRouteStepReagent.del_flag == 0,
)
.all()
)
for r in reagent_rows:
db.add(
BizProcessRouteStepReagent(
id=generate_uuid(),
step_id=dst_step_id,
reagent_code=r.reagent_code,
reagent_name=r.reagent_name,
spec=r.spec,
standard_usage=r.standard_usage,
unit=r.unit,
is_key_material=r.is_key_material,
tenant_no=tenant_no,
order_num=r.order_num,
remark=r.remark,
create_by=user_id,
create_dept_id=create_dept_id or "",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
)
consumable_rows = (
db.query(BizProcessRouteStepConsumable)
.filter(
BizProcessRouteStepConsumable.step_id == src_step_id,
BizProcessRouteStepConsumable.tenant_no == tenant_no,
BizProcessRouteStepConsumable.del_flag == 0,
)
.all()
)
for c in consumable_rows:
db.add(
BizProcessRouteStepConsumable(
id=generate_uuid(),
step_id=dst_step_id,
consumable_code=c.consumable_code,
consumable_name=c.consumable_name,
spec_model=c.spec_model,
standard_usage=c.standard_usage,
unit=c.unit,
is_required=c.is_required,
remark=c.remark,
tenant_no=tenant_no,
order_num=c.order_num,
create_by=user_id,
create_dept_id=create_dept_id or "",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
)
output_rows = (
db.query(BizProcessRouteStepOutput)
.filter(
BizProcessRouteStepOutput.step_id == src_step_id,
BizProcessRouteStepOutput.tenant_no == tenant_no,
BizProcessRouteStepOutput.del_flag == 0,
)
.all()
)
for o in output_rows:
db.add(
BizProcessRouteStepOutput(
id=generate_uuid(),
step_id=dst_step_id,
output_type_id=o.output_type_id,
output_name=o.output_name,
output_barcode=o.output_barcode,
tenant_no=tenant_no,
order_num=o.order_num,
remark=o.remark,
create_by=user_id,
create_dept_id=create_dept_id or "",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
)
dc_rows = (
db.query(BizProcessRouteStepDataCollection)
.filter(
BizProcessRouteStepDataCollection.step_id == src_step_id,
BizProcessRouteStepDataCollection.tenant_no == tenant_no,
BizProcessRouteStepDataCollection.del_flag == 0,
)
.all()
)
for d in dc_rows:
db.add(
BizProcessRouteStepDataCollection(
id=generate_uuid(),
step_id=dst_step_id,
collection_name=d.collection_name,
unit=d.unit,
tenant_no=tenant_no,
order_num=d.order_num,
remark=d.remark,
create_by=user_id,
create_dept_id=create_dept_id or "",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
)
def _source_steps_by_code(
db: Session, *, route_id: str, tenant_no: str
) -> Dict[str, BizProcessRouteStep]:
rows = (
db.query(BizProcessRouteStep)
.filter(
BizProcessRouteStep.route_id == route_id,
BizProcessRouteStep.tenant_no == tenant_no,
BizProcessRouteStep.del_flag == 0,
)
.all()
)
out: Dict[str, BizProcessRouteStep] = {}
for row in rows:
code = (row.process_code or "").strip()
if code and code not in out:
out[code] = row
return out
def ensure_uc_msc_builtin_template(
db: Session,
*,
tenant_no: str,
user_id: Optional[str] = None,
create_dept_id: str = "",
) -> Dict[str, Any]:
"""幂等创建 UC 5 步内置路线与任务组合;已存在则直接返回。"""
existing = get_uc_msc_builtin_set(db, tenant_no=tenant_no)
if existing and (existing.status or "1") == "1":
item = (
db.query(BizProductionTaskSetItem)
.filter(
BizProductionTaskSetItem.set_id == existing.id,
BizProductionTaskSetItem.tenant_no == tenant_no,
BizProductionTaskSetItem.del_flag == 0,
)
.order_by(
BizProductionTaskSetItem.order_num.asc(),
BizProductionTaskSetItem.create_time.asc(),
)
.first()
)
if item and item.route_id:
route = (
db.query(BizProcessRoute)
.filter(
BizProcessRoute.id == item.route_id,
BizProcessRoute.tenant_no == tenant_no,
BizProcessRoute.del_flag == 0,
)
.first()
)
step_count = (
db.query(BizProcessRouteStep.id)
.filter(
BizProcessRouteStep.route_id == item.route_id,
BizProcessRouteStep.tenant_no == tenant_no,
BizProcessRouteStep.del_flag == 0,
)
.count()
)
if route and step_count >= len(UC_MSC_STEP_ORDER):
return {
"created": False,
"setId": existing.id,
"setName": existing.set_name,
"routeId": route.id,
"routeCode": route.route_code,
"stepCount": step_count,
}
source = _find_source_route(db, tenant_no=tenant_no)
by_code = _source_steps_by_code(db, route_id=source.id, tenant_no=tenant_no)
missing = [c for c in UC_MSC_STEP_ORDER if c not in by_code]
if missing:
raise ValueError(
"源工艺路线缺少工序,无法创建 UC 模版:" + "、".join(missing)
)
now = _now()
route = (
db.query(BizProcessRoute)
.filter(
BizProcessRoute.tenant_no == tenant_no,
BizProcessRoute.route_code == UC_MSC_ROUTE_CODE,
BizProcessRoute.del_flag == 0,
)
.first()
)
if not route:
route = BizProcessRoute(
id=generate_uuid(),
route_code=UC_MSC_ROUTE_CODE,
route_name=UC_MSC_ROUTE_NAME,
product_type_id=source.product_type_id,
version=source.version,
is_current="0",
equipment_ids=source.equipment_ids,
equipment_name=source.equipment_name,
tenant_no=tenant_no,
order_num=0,
remark="内置模版:UC 入库编码自动派发(5 步,不含传代)",
is_builtin="1",
create_by=user_id,
create_dept_id=create_dept_id or "",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(route)
db.flush()
else:
route.route_name = UC_MSC_ROUTE_NAME
route.is_builtin = "1"
route.update_by = user_id
route.update_time = now
db.add(route)
# 重建步骤(幂等:软删旧步骤后按目标顺序插入)
old_steps = (
db.query(BizProcessRouteStep)
.filter(
BizProcessRouteStep.route_id == route.id,
BizProcessRouteStep.tenant_no == tenant_no,
BizProcessRouteStep.del_flag == 0,
)
.all()
)
for s in old_steps:
s.del_flag = 1
s.update_by = user_id
s.update_time = now
db.add(s)
db.flush()
for order, code in enumerate(UC_MSC_STEP_ORDER, start=1):
src = by_code[code]
new_step = BizProcessRouteStep(
id=generate_uuid(),
route_id=route.id,
process_id=src.process_id,
step_order=order,
process_code=src.process_code,
category_id=src.category_id,
process_name=src.process_name,
description=src.description,
equipment_ids=src.equipment_ids,
equipment_name=src.equipment_name,
estimated_duration=src.estimated_duration,
sop_ids=src.sop_ids,
sop_content=src.sop_content,
tenant_no=tenant_no,
order_num=order,
remark=src.remark,
create_by=user_id,
create_dept_id=create_dept_id or "",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(new_step)
db.flush()
_clone_step_children(
db,
src_step_id=src.id,
dst_step_id=new_step.id,
tenant_no=tenant_no,
user_id=user_id,
create_dept_id=create_dept_id or "",
now=now,
)
header = existing or BizProductionTaskSet(
id=generate_uuid(),
set_name=UC_MSC_BUILTIN_SET_NAME,
status="1",
tenant_no=tenant_no,
order_num=0,
remark="内置模版:UC 入库编码自动派发",
is_builtin="1",
create_by=user_id,
create_dept_id=create_dept_id or "",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
if existing:
header.status = "1"
header.is_builtin = "1"
header.update_by = user_id
header.update_time = now
db.add(header)
db.flush()
items = (
db.query(BizProductionTaskSetItem)
.filter(
BizProductionTaskSetItem.set_id == header.id,
BizProductionTaskSetItem.tenant_no == tenant_no,
BizProductionTaskSetItem.del_flag == 0,
)
.all()
)
for it in items:
it.del_flag = 1
it.update_by = user_id
it.update_time = now
db.add(it)
db.flush()
item = BizProductionTaskSetItem(
id=generate_uuid(),
set_id=header.id,
task_name="{lab_storage_code}样本处理",
route_id=route.id,
route_name=route.route_name,
description=UC_MSC_BUILTIN_SET_NAME,
remark=None,
tenant_no=tenant_no,
order_num=1,
create_by=user_id,
create_dept_id=create_dept_id or "",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(item)
db.flush()
return {
"created": True,
"setId": header.id,
"setName": header.set_name,
"routeId": route.id,
"routeCode": route.route_code,
"stepCount": len(UC_MSC_STEP_ORDER),
}