Files
shanxia2.0/backend/app/crud/production_task.py
T
2026-05-28 08:37:37 +08:00

1898 lines
78 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(biz_production_task + 步骤 + 子表 + 投入/产出)"""
from __future__ import annotations
import math
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.utils import generate_uuid, parse_datetime
from app.models.models import (
BizCellProduct,
BizCollectionProtocol,
BizProcess,
BizProcessConsumable,
BizProcessDataCollection,
BizProcessOutput,
BizProcessReagent,
BizProcessRoute,
BizProcessRouteStep,
BizProcessRouteStepConsumable,
BizProcessRouteStepDataCollection,
BizProcessRouteStepOutput,
BizProcessRouteStepReagent,
BizProductionTask,
BizProductionTaskInput,
BizProductionTaskOutput,
BizProductionTaskStep,
BizProductionTaskStepConsumable,
BizProductionTaskStepDataCollection,
BizProductionTaskStepOutput,
BizProductionTaskStepReagent,
BizProductionWorkOrder,
BizProtocolSample,
BizSampleReception,
BizStorageSlotOccupancy,
)
from app.crud import storage_occupancy as occupancy_svc
from app.crud.cell_product import _row_to_dict as _cp_row_to_dict, _enrich_product_dicts as _cp_enrich
from app.models.research_donation import BizResearchDonation, BizResearchDonationSample
_PROTOCOL_STATUS_LABELS = {
"dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "zky": "转科研"
}
_DONATION_STATUS_LABELS = {
"dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "zckc": "转细胞存储"
}
_RECEPTION_SOURCE_LABELS = {
"cell_storage": "细胞存储",
"research_donation": "科研捐赠",
}
_OUTBOUND_BIZ_TYPE_LABELS = {
"pilot_service": "中试服务",
"clinical_application": "临床应用",
}
_APPLICATION_STATUS_LABELS = {
"dks": "待开始",
"jxz": "进行中",
"ywc": "已完成",
"yzf": "已作废",
}
_PRODUCTION_CONCLUSION_LABELS = {
"cg": "成功",
"sb": "失败",
}
def _production_conclusion_label(v: Optional[str]) -> str:
code = (v or "").strip()
if not code:
return ""
return _PRODUCTION_CONCLUSION_LABELS.get(code, code)
def _fmt_dt(t: Optional[datetime]) -> str:
return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
def _normalize_date_bound(value: str, *, end_of_day: bool = False) -> str:
val = (value or "").strip()
if len(val) == 10:
return f"{val} 23:59:59" if end_of_day else f"{val} 00:00:00"
return val
def _enrich_outbound_task_items(
db: Session,
tenant_no: str,
items: List[Dict[str, Any]],
wo_ctx_map: Dict[str, Dict[str, Any]],
) -> None:
"""出库生产任务:来源类型=业务类型,协议号=申请单号,协议状态=申请单状态。"""
from app.models.models import (
BizPilotApplication,
BizPilotInfusionPlan,
BizPilotInfusionSchedule,
)
schedule_ids = [
ctx.get("infusionScheduleId")
for ctx in wo_ctx_map.values()
if ctx.get("workOrderType") == "outbound" and ctx.get("infusionScheduleId")
]
if not schedule_ids:
return
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})
plan_map = {}
if plan_ids:
plan_map = {
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()
}
app_ids = list({p.pilot_application_id for p in plan_map.values() if p.pilot_application_id})
app_map = {}
if app_ids:
app_map = {
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()
}
schedule_app_map: Dict[str, BizPilotApplication] = {}
for sid, sched in schedules.items():
plan = plan_map.get(sched.infusion_plan_id) if sched.infusion_plan_id else None
app = app_map.get(plan.pilot_application_id) if plan and plan.pilot_application_id else None
if app:
schedule_app_map[sid] = app
for item in items:
ctx = wo_ctx_map.get(item.get("workOrderId") or "")
if not ctx or ctx.get("workOrderType") != "outbound":
continue
biz_type = ctx.get("bizType") or ""
biz_label = _OUTBOUND_BIZ_TYPE_LABELS.get(biz_type, biz_type)
item["bizType"] = biz_type
item["bizTypeLabel"] = biz_label
item["receptionSource"] = biz_type
item["receptionSourceLabel"] = biz_label
sched_id = ctx.get("infusionScheduleId") or ""
app = schedule_app_map.get(sched_id)
if app:
app_status = app.application_status or ""
item["protocolNo"] = app.application_no or ""
item["protocolStatus"] = app_status
item["protocolStatusLabel"] = _APPLICATION_STATUS_LABELS.get(app_status, app_status)
else:
item["protocolNo"] = ""
item["protocolStatus"] = ""
item["protocolStatusLabel"] = ""
def _generate_task_no(db: Session, tenant_no: str) -> str:
day = datetime.now().strftime("%Y%m%d")
like = f"RW{day}%"
cnt = (
db.query(func.count(BizProductionTask.id))
.filter(
BizProductionTask.tenant_no == tenant_no,
BizProductionTask.task_no.like(like),
)
.scalar()
)
return f"RW{day}{(cnt or 0) + 1:04d}"
def _task_to_dict(t: BizProductionTask, *, work_order_no: str = "") -> Dict[str, Any]:
return {
"id": t.id,
"taskNo": t.task_no,
"workOrderId": t.work_order_id,
"orderSource": getattr(t, "order_source", "inbound") or "inbound",
"workOrderNo": work_order_no,
"taskType": t.task_type or "cell_storage",
"taskName": t.task_name or "",
"description": t.description or "",
"routeId": t.route_id or "",
"taskStatus": t.task_status,
"startedTime": _fmt_dt(t.started_time),
"completedTime": _fmt_dt(t.completed_time),
"conclusion": (getattr(t, "conclusion", None) or "").strip(),
"conclusionLabel": _production_conclusion_label(getattr(t, "conclusion", None)),
"remark": t.remark or "",
"attachments": t.attachments or "",
"tenantNo": t.tenant_no,
"orderNum": t.order_num if t.order_num is not None else 0,
"createTime": _fmt_dt(t.create_time),
"updateTime": _fmt_dt(t.update_time),
}
def _step_to_dict(s: BizProductionTaskStep) -> Dict[str, Any]:
return {
"id": s.id,
"taskId": s.task_id,
"stepOrder": s.step_order if s.step_order is not None else 0,
"processCode": s.process_code or "",
"categoryId": s.category_id or "",
"processName": s.process_name or "",
"description": s.description or "",
"equipmentName": s.equipment_name or "",
"estimatedDuration": s.estimated_duration or "",
"actualDuration": s.actual_duration or "",
"sopIds": s.sop_ids or "",
"stepStatus": s.step_status or "dzx",
"startedTime": _fmt_dt(s.started_time),
"completedTime": _fmt_dt(s.completed_time),
"remark": s.remark or "",
}
def _step_reagent_to_dict(r: BizProductionTaskStepReagent) -> Dict[str, Any]:
return {
"id": r.id,
"stepId": r.step_id,
"reagentCode": r.reagent_code or "",
"reagentName": r.reagent_name or "",
"spec": r.spec or "",
"standardUsage": r.standard_usage or "",
"actualUsage": r.actual_usage or "",
"unit": r.unit or "",
"isKeyMaterial": r.is_key_material if r.is_key_material is not None else "0",
"orderNum": r.order_num if r.order_num is not None else 0,
"remark": r.remark or "",
}
def _step_consumable_to_dict(c: BizProductionTaskStepConsumable) -> Dict[str, Any]:
return {
"id": c.id,
"stepId": c.step_id,
"consumableCode": c.consumable_code or "",
"consumableName": c.consumable_name or "",
"specModel": c.spec_model or "",
"standardUsage": c.standard_usage or "",
"actualUsage": c.actual_usage or "",
"unit": c.unit or "",
"isRequired": c.is_required if c.is_required is not None else "1",
"orderNum": c.order_num if c.order_num is not None else 0,
"remark": c.remark or "",
}
def _step_output_to_dict(o: BizProductionTaskStepOutput) -> Dict[str, Any]:
return {
"id": o.id,
"stepId": o.step_id,
"outputTypeId": o.output_type_id or "",
"outputName": o.output_name or "",
"outputBarcode": o.output_barcode or "",
"orderNum": o.order_num if o.order_num is not None else 0,
"remark": o.remark or "",
}
def _step_data_collection_to_dict(d: BizProductionTaskStepDataCollection) -> Dict[str, Any]:
return {
"id": d.id,
"stepId": d.step_id,
"collectionName": d.collection_name or "",
"unit": d.unit or "",
"collectionValue": d.collection_value or "",
"orderNum": d.order_num if d.order_num is not None else 0,
"remark": d.remark or "",
}
def _task_input_to_dict(i: BizProductionTaskInput, *, product_dict: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
result = {
"id": i.id,
"taskId": i.task_id,
"inputType": i.input_type or "",
"inputId": i.input_id or "",
"inputCode": i.input_code or "",
"inputName": i.input_name or "",
"remark": i.remark or "",
}
if product_dict:
for key in [
"productCode", "productName", "productTypeId", "productTypeName",
"specId", "specName", "passage", "batchNo", "storageSlotName",
"expectedCellCount", "viability", "freezeDate", "expiryDate",
"productStatus", "productImages", "storageSlotId",
]:
if key in product_dict:
result[key] = product_dict[key]
return result
def _task_output_to_dict(o: BizProductionTaskOutput, *, product_dict: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
result = {
"id": o.id,
"taskId": o.task_id,
"cellProductId": o.cell_product_id or "",
"storageSlotId": o.storage_slot_id or "",
"outputName": o.output_name or "",
"outputBarcode": o.output_barcode or "",
"quantity": float(o.quantity) if o.quantity is not None else None,
"unit": o.unit or "",
"remark": o.remark or "",
"attachments": o.attachments or "",
}
if product_dict:
for key in [
"productCode", "productName", "productTypeId", "productTypeName",
"specId", "specName", "passage", "batchNo", "storageSlotName",
"expectedCellCount", "viability", "freezeDate", "expiryDate",
"productStatus", "productImages",
]:
if key in product_dict:
result[key] = product_dict[key]
return result
def _soft_delete_sub(db: Session, model_class, parent_field: str, parent_id: str, now: datetime) -> None:
rows = db.query(model_class).filter(
and_(
getattr(model_class, parent_field) == parent_id,
model_class.del_flag == 0,
)
).all()
for r in rows:
r.del_flag = 1
r.update_time = now
db.add(r)
def _resolve_sub_table_order_num(order_num: Optional[int], idx: int, rows: list) -> int:
"""复制/克隆子表排序号:保留源 order_num;若源记录均为 0 则按遍历顺序写入 idx。"""
if rows and len(rows) > 1 and all((getattr(r, "order_num", None) or 0) == 0 for r in rows):
return idx
if order_num is not None:
return order_num
return idx
def _step_sub_tables_to_dict(db: Session, step_id: str) -> Dict[str, Any]:
"""加载步骤四张子表并序列化(与 get_production_task_detail 一致)。"""
reagents = db.query(BizProductionTaskStepReagent).filter(
and_(BizProductionTaskStepReagent.step_id == step_id, BizProductionTaskStepReagent.del_flag == 0)
).order_by(BizProductionTaskStepReagent.order_num.asc(), BizProductionTaskStepReagent.create_time.asc()).all()
consumables = db.query(BizProductionTaskStepConsumable).filter(
and_(BizProductionTaskStepConsumable.step_id == step_id, BizProductionTaskStepConsumable.del_flag == 0)
).order_by(BizProductionTaskStepConsumable.order_num.asc(), BizProductionTaskStepConsumable.create_time.asc()).all()
outputs = db.query(BizProductionTaskStepOutput).filter(
and_(BizProductionTaskStepOutput.step_id == step_id, BizProductionTaskStepOutput.del_flag == 0)
).order_by(BizProductionTaskStepOutput.order_num.asc(), BizProductionTaskStepOutput.create_time.asc()).all()
data_collections = db.query(BizProductionTaskStepDataCollection).filter(
and_(BizProductionTaskStepDataCollection.step_id == step_id, BizProductionTaskStepDataCollection.del_flag == 0)
).order_by(BizProductionTaskStepDataCollection.order_num.asc(), BizProductionTaskStepDataCollection.create_time.asc()).all()
return {
"reagents": [_step_reagent_to_dict(r) for r in reagents],
"consumables": [_step_consumable_to_dict(c) for c in consumables],
"outputs": [_step_output_to_dict(o) for o in outputs],
"dataCollections": [_step_data_collection_to_dict(d) for d in data_collections],
}
def _clone_step_reagents(
db: Session, src_step_id: str, new_step_id: str,
tenant_no: str, user_id: Optional[str], now: datetime,
) -> None:
rows = db.query(BizProcessRouteStepReagent).filter(
and_(BizProcessRouteStepReagent.step_id == src_step_id, BizProcessRouteStepReagent.del_flag == 0)
).order_by(
BizProcessRouteStepReagent.order_num.asc(),
BizProcessRouteStepReagent.create_time.asc(),
).all()
for idx, r in enumerate(rows):
db.add(BizProductionTaskStepReagent(
id=generate_uuid(), step_id=new_step_id, tenant_no=tenant_no,
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,
remark=r.remark,
order_num=_resolve_sub_table_order_num(r.order_num, idx, rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
def _clone_step_consumables(
db: Session, src_step_id: str, new_step_id: str,
tenant_no: str, user_id: Optional[str], now: datetime,
) -> None:
rows = db.query(BizProcessRouteStepConsumable).filter(
and_(BizProcessRouteStepConsumable.step_id == src_step_id, BizProcessRouteStepConsumable.del_flag == 0)
).order_by(
BizProcessRouteStepConsumable.order_num.asc(),
BizProcessRouteStepConsumable.create_time.asc(),
).all()
for idx, c in enumerate(rows):
db.add(BizProductionTaskStepConsumable(
id=generate_uuid(), step_id=new_step_id, tenant_no=tenant_no,
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,
order_num=_resolve_sub_table_order_num(c.order_num, idx, rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
def _clone_step_outputs(
db: Session, src_step_id: str, new_step_id: str,
tenant_no: str, user_id: Optional[str], now: datetime,
) -> None:
rows = db.query(BizProcessRouteStepOutput).filter(
and_(BizProcessRouteStepOutput.step_id == src_step_id, BizProcessRouteStepOutput.del_flag == 0)
).order_by(
BizProcessRouteStepOutput.order_num.asc(),
BizProcessRouteStepOutput.create_time.asc(),
).all()
for idx, o in enumerate(rows):
db.add(BizProductionTaskStepOutput(
id=generate_uuid(), step_id=new_step_id, tenant_no=tenant_no,
output_type_id=o.output_type_id, output_name=o.output_name,
output_barcode=o.output_barcode,
remark=o.remark,
order_num=_resolve_sub_table_order_num(o.order_num, idx, rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
def _clone_step_data_collections(
db: Session, src_step_id: str, new_step_id: str,
tenant_no: str, user_id: Optional[str], now: datetime,
) -> None:
rows = db.query(BizProcessRouteStepDataCollection).filter(
and_(BizProcessRouteStepDataCollection.step_id == src_step_id, BizProcessRouteStepDataCollection.del_flag == 0)
).order_by(
BizProcessRouteStepDataCollection.order_num.asc(),
BizProcessRouteStepDataCollection.create_time.asc(),
).all()
for idx, d in enumerate(rows):
db.add(BizProductionTaskStepDataCollection(
id=generate_uuid(), step_id=new_step_id, tenant_no=tenant_no,
collection_name=d.collection_name, unit=d.unit,
remark=d.remark,
order_num=_resolve_sub_table_order_num(d.order_num, idx, rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
def _clone_route_to_task(
db: Session, task_id: str, route_id: str,
tenant_no: str, user_id: Optional[str], create_dept_id: str,
) -> None:
route_steps = (
db.query(BizProcessRouteStep)
.filter(and_(BizProcessRouteStep.route_id == route_id, BizProcessRouteStep.del_flag == 0))
.order_by(BizProcessRouteStep.step_order.asc(), BizProcessRouteStep.create_time.asc())
.all()
)
now = datetime.now()
for rs in route_steps:
new_step = BizProductionTaskStep(
id=generate_uuid(), task_id=task_id,
step_order=rs.step_order,
process_code=rs.process_code, category_id=rs.category_id,
process_name=rs.process_name, description=rs.description,
equipment_name=rs.equipment_name, estimated_duration=rs.estimated_duration,
sop_ids=rs.sop_ids,
step_status="dzx",
tenant_no=tenant_no, order_num=0,
remark=None, 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_reagents(db, rs.id, new_step.id, tenant_no, user_id, now)
_clone_step_consumables(db, rs.id, new_step.id, tenant_no, user_id, now)
_clone_step_outputs(db, rs.id, new_step.id, tenant_no, user_id, now)
_clone_step_data_collections(db, rs.id, new_step.id, tenant_no, user_id, now)
def create_production_task(
db: Session, *, tenant_no: str, user_id: Optional[str] = None,
create_dept_id: str = "", data: Dict[str, Any],
order_source: str = "inbound",
) -> BizProductionTask:
work_order_id = (data.get("work_order_id") or "").strip()
wo = None
if work_order_id:
wo = (
db.query(BizProductionWorkOrder)
.filter(
BizProductionWorkOrder.id == work_order_id,
BizProductionWorkOrder.tenant_no == tenant_no,
BizProductionWorkOrder.del_flag == 0,
)
.first()
)
if wo:
wo_type = getattr(wo, "work_order_type", None) or "inbound"
if wo_type == "temporary":
order_source = "temporary"
elif wo_type == "outbound":
order_source = "outbound"
task_type = (data.get("task_type") or "").strip() or "cell_storage"
if wo and (getattr(wo, "work_order_type", None) or "inbound") == "outbound":
if not (data.get("task_type") or "").strip():
task_type = "outbound"
task_no = _generate_task_no(db, tenant_no)
now = datetime.now()
task = BizProductionTask(
id=generate_uuid(),
task_no=task_no,
work_order_id=data.get("work_order_id", ""),
order_source=order_source,
task_type=task_type,
task_name=(data.get("task_name") or "").strip() or None,
description=(data.get("description") or "").strip() or None,
route_id=data.get("route_id") or None,
task_status="dpz",
remark=(data.get("remark") or "").strip() or None,
attachments=(data.get("attachments") or "").strip() or None,
tenant_no=tenant_no,
order_num=0,
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(task)
db.flush()
route_id = data.get("route_id")
if route_id:
_clone_route_to_task(db, task.id, route_id, tenant_no, user_id, create_dept_id)
from app.crud import production_work_order as production_work_order_svc
production_work_order_svc.sync_production_work_order_status_from_tasks(
db, task.work_order_id or "", tenant_no, user_id
)
if wo and (getattr(wo, "work_order_type", None) or "inbound") == "temporary":
if not getattr(wo, "configured_time", None):
wo.configured_time = now
wo.configured_by = user_id
wo.update_by = user_id
wo.update_time = now
db.add(wo)
db.flush()
return task
def list_production_tasks(
db: Session, *, tenant_no: str,
page: int = 1, page_size: int = 10,
keyword: Optional[str] = None,
work_order_id: Optional[str] = None,
task_no: Optional[str] = None,
task_status: Optional[str] = None,
started_time_start: Optional[str] = None,
started_time_end: Optional[str] = None,
user_info=None,
) -> Tuple[List[Dict[str, Any]], int]:
def _filtered():
q = db.query(BizProductionTask).filter(
BizProductionTask.tenant_no == tenant_no,
BizProductionTask.del_flag == 0,
)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
keyword_conditions = [
BizProductionTask.task_no.like(like),
BizProductionWorkOrder.work_order_no.like(like),
BizProductionWorkOrder.protocol_no.like(like),
BizProductionWorkOrder.sample_code.like(like),
]
if tenant_no:
from app.crud.production_work_order import _outbound_work_order_ids_by_keyword
outbound_ids = _outbound_work_order_ids_by_keyword(db, tenant_no, kw)
if outbound_ids:
keyword_conditions.append(BizProductionWorkOrder.id.in_(outbound_ids))
q = q.outerjoin(
BizProductionWorkOrder,
and_(
BizProductionWorkOrder.id == BizProductionTask.work_order_id,
BizProductionWorkOrder.del_flag == 0,
),
).filter(or_(*keyword_conditions))
elif task_no and task_no.strip():
q = q.filter(BizProductionTask.task_no.like(f"%{task_no.strip()}%"))
if work_order_id and work_order_id.strip():
q = q.filter(BizProductionTask.work_order_id == work_order_id.strip())
if task_status and task_status.strip():
q = q.filter(BizProductionTask.task_status == task_status.strip())
if started_time_start and started_time_start.strip():
q = q.filter(
BizProductionTask.started_time
>= _normalize_date_bound(started_time_start.strip())
)
if started_time_end and started_time_end.strip():
q = q.filter(
BizProductionTask.started_time
<= _normalize_date_bound(started_time_end.strip(), end_of_day=True)
)
if user_info:
from app.core.data_scope import apply_data_scope
q = apply_data_scope(q, BizProductionTask, user_info, db, tenant_no)
return q
total = _filtered().order_by(None).with_entities(func.count(BizProductionTask.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
_filtered()
.order_by(BizProductionTask.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
items = [_task_to_dict(x) for x in rows]
# 批量补充路线名称
route_ids = [x.route_id for x in rows if x.route_id]
if route_ids:
route_map = {
r.id: r.route_name
for r in db.query(BizProcessRoute.id, BizProcessRoute.route_name)
.filter(BizProcessRoute.id.in_(route_ids))
.all()
}
for item, row in zip(items, rows):
item["routeName"] = route_map.get(row.route_id, "")
# 批量补充工单号 + 协议号 + 样本编码
wo_ids = [x.work_order_id for x in rows if x.work_order_id]
wo_map: Dict[str, Any] = {}
wo_ctx_map: Dict[str, Dict[str, Any]] = {}
if wo_ids:
wo_rows = (
db.query(
BizProductionWorkOrder.id,
BizProductionWorkOrder.work_order_no,
BizProductionWorkOrder.work_order_type,
BizProductionWorkOrder.biz_type,
BizProductionWorkOrder.infusion_schedule_id,
BizProductionWorkOrder.protocol_no,
BizProductionWorkOrder.sample_code,
BizProductionWorkOrder.reception_id,
BizProductionWorkOrder.protocol_sample_id,
BizProductionWorkOrder.collection_protocol_id,
BizProductionWorkOrder.reception_source,
)
.filter(BizProductionWorkOrder.id.in_(wo_ids))
.all()
)
for w in wo_rows:
wo_map[w.id] = w
wo_type = getattr(w, "work_order_type", None) or "inbound"
wo_ctx_map[w.id] = {
"workOrderType": wo_type,
"bizType": getattr(w, "biz_type", None) or "",
"infusionScheduleId": getattr(w, "infusion_schedule_id", None) or "",
}
cell_wo_ids = [
w.collection_protocol_id
for w in wo_rows
if (w.reception_source or "cell_storage") != "research_donation"
and (getattr(w, "work_order_type", None) or "inbound") != "outbound"
and w.collection_protocol_id
]
donation_wo_ids = [
w.collection_protocol_id
for w in wo_rows
if (w.reception_source or "") == "research_donation"
and (getattr(w, "work_order_type", None) or "inbound") != "outbound"
and w.collection_protocol_id
]
proto_status_map: Dict[str, str] = {}
if cell_wo_ids:
for p in (
db.query(BizCollectionProtocol.id, BizCollectionProtocol.protocol_status)
.filter(BizCollectionProtocol.id.in_(cell_wo_ids), BizCollectionProtocol.del_flag == 0)
.all()
):
proto_status_map[p.id] = p.protocol_status or ""
donation_status_map: Dict[str, str] = {}
if donation_wo_ids:
for d in (
db.query(BizResearchDonation.id, BizResearchDonation.donation_status)
.filter(BizResearchDonation.id.in_(donation_wo_ids), BizResearchDonation.del_flag == 0)
.all()
):
donation_status_map[d.id] = d.donation_status or ""
for item, row in zip(items, rows):
wo = wo_map.get(row.work_order_id)
if wo:
item["workOrderNo"] = wo.work_order_no or ""
wo_type = getattr(wo, "work_order_type", None) or "inbound"
if wo_type != "outbound":
item["protocolNo"] = wo.protocol_no or ""
item["sampleCode"] = wo.sample_code or ""
_source = wo.reception_source or "cell_storage"
if _source == "research_donation":
_st = donation_status_map.get(wo.collection_protocol_id, "")
item["protocolStatus"] = _st
item["protocolStatusLabel"] = _DONATION_STATUS_LABELS.get(_st, _st)
else:
_st = proto_status_map.get(wo.collection_protocol_id, "")
item["protocolStatus"] = _st
item["protocolStatusLabel"] = _PROTOCOL_STATUS_LABELS.get(_st, _st)
else:
item["sampleCode"] = wo.sample_code or ""
_enrich_outbound_task_items(db, tenant_no, items, wo_ctx_map)
# 批量补充接收人、接收时间、样本类型(与质检任务/工单列表一致:优先接收记录样本 id)
from app.crud.qc_work_order import (
_effective_donation_sample_id,
_effective_protocol_sample_id,
_effective_reception_source,
_load_sample_type_name_map,
_sample_type_name_for_item,
)
from app.crud.sample_reception import _donation_sample_id_col
reception_ids = [wo.reception_id for wo in wo_map.values() if getattr(wo, "reception_id", None)]
reception_map: Dict[str, BizSampleReception] = {}
if reception_ids:
reception_map = {
r.id: r
for r in db.query(BizSampleReception)
.filter(BizSampleReception.id.in_(reception_ids), BizSampleReception.del_flag == 0)
.all()
}
wo_item_ctx: Dict[str, Dict[str, Any]] = {}
cell_sample_ids: List[str] = []
donation_sample_ids: List[str] = []
for wo in wo_map.values():
rec = reception_map.get(wo.reception_id) if wo.reception_id else None
ctx = {
"receptionSource": getattr(wo, "reception_source", "cell_storage") or "cell_storage",
"protocolSampleId": wo.protocol_sample_id or "",
"collectionProtocolId": wo.collection_protocol_id or "",
}
wo_item_ctx[wo.id] = ctx
source = _effective_reception_source(ctx, rec)
if source == "research_donation":
dsid = _effective_donation_sample_id(ctx, rec)
if dsid:
donation_sample_ids.append(dsid)
else:
psid = _effective_protocol_sample_id(ctx, rec)
if psid:
cell_sample_ids.append(psid)
cell_sample_ids = list(dict.fromkeys(cell_sample_ids))
donation_sample_ids = list(dict.fromkeys(donation_sample_ids))
protocol_samples: Dict[str, BizProtocolSample] = {}
if cell_sample_ids:
protocol_samples = {
s.id: s
for s in db.query(BizProtocolSample)
.filter(BizProtocolSample.id.in_(cell_sample_ids), BizProtocolSample.del_flag == 0)
.all()
}
donation_samples: Dict[str, BizResearchDonationSample] = {}
if donation_sample_ids:
for s in (
db.query(BizResearchDonationSample)
.filter(
_donation_sample_id_col(BizResearchDonationSample.id).in_(donation_sample_ids),
BizResearchDonationSample.del_flag == 0,
)
.all()
):
donation_samples[s.id] = s
donation_samples[(s.id or "").strip()] = s
type_ids = list(
{
s.sample_type_id
for s in list(protocol_samples.values()) + list(donation_samples.values())
if s.sample_type_id
}
)
type_map = _load_sample_type_name_map(db, type_ids)
# 将补充字段写入 items
for item, row in zip(items, rows):
wo = wo_map.get(row.work_order_id)
if not wo:
continue
wo_type = getattr(wo, "work_order_type", None) or "inbound"
if wo_type == "outbound":
continue
item["protocolNo"] = wo.protocol_no or ""
item["sampleCode"] = wo.sample_code or ""
rec = reception_map.get(wo.reception_id) if wo.reception_id else None
item["receiverName"] = rec.handler_name or "" if rec else ""
item["receptionTime"] = _fmt_dt(rec.reception_time) if rec else ""
ctx = wo_item_ctx.get(wo.id, {})
source = _effective_reception_source(ctx, rec)
item["sampleTypeName"] = _sample_type_name_for_item(
source=source,
donation_sample_id=_effective_donation_sample_id(ctx, rec),
protocol_sample_id=_effective_protocol_sample_id(ctx, rec),
type_map=type_map,
protocol_samples=protocol_samples,
donation_samples=donation_samples,
)
_rs = source
item["receptionSource"] = _rs
item["receptionSourceLabel"] = _RECEPTION_SOURCE_LABELS.get(_rs, _rs)
return items, int(total)
def get_production_task_detail(db: Session, task_id: str, tenant_no: str) -> Optional[Dict[str, Any]]:
task = db.query(BizProductionTask).filter(
and_(
BizProductionTask.id == task_id,
BizProductionTask.tenant_no == tenant_no,
BizProductionTask.del_flag == 0,
)
).first()
if not task:
return None
wo = db.query(BizProductionWorkOrder).filter(BizProductionWorkOrder.id == task.work_order_id).first()
result = _task_to_dict(task, work_order_no=wo.work_order_no if wo else "")
if wo:
wo_type = getattr(wo, "work_order_type", None) or "inbound"
result["sampleCode"] = wo.sample_code or ""
if wo_type == "outbound":
wo_ctx = {
wo.id: {
"workOrderType": wo_type,
"bizType": getattr(wo, "biz_type", None) or "",
"infusionScheduleId": getattr(wo, "infusion_schedule_id", None) or "",
}
}
_enrich_outbound_task_items(db, tenant_no, [result], wo_ctx)
else:
result["protocolNo"] = wo.protocol_no or ""
_src = getattr(wo, "reception_source", "cell_storage") or "cell_storage"
_cpid = wo.collection_protocol_id or ""
if _src == "research_donation" and _cpid:
d = db.query(BizResearchDonation.donation_status).filter(
BizResearchDonation.id == _cpid, BizResearchDonation.del_flag == 0
).first()
_st = d.donation_status or "" if d else ""
result["protocolStatus"] = _st
result["protocolStatusLabel"] = _DONATION_STATUS_LABELS.get(_st, _st)
elif _cpid:
p = db.query(BizCollectionProtocol.protocol_status).filter(
BizCollectionProtocol.id == _cpid, BizCollectionProtocol.del_flag == 0
).first()
_st = p.protocol_status or "" if p else ""
result["protocolStatus"] = _st
result["protocolStatusLabel"] = _PROTOCOL_STATUS_LABELS.get(_st, _st)
else:
result["protocolStatus"] = ""
result["protocolStatusLabel"] = ""
steps = (
db.query(BizProductionTaskStep)
.filter(and_(BizProductionTaskStep.task_id == task_id, BizProductionTaskStep.del_flag == 0))
.order_by(BizProductionTaskStep.step_order.asc(), BizProductionTaskStep.create_time.asc())
.all()
)
step_list = []
for s in steps:
sd = _step_to_dict(s)
reagents = db.query(BizProductionTaskStepReagent).filter(
and_(BizProductionTaskStepReagent.step_id == s.id, BizProductionTaskStepReagent.del_flag == 0)
).order_by(BizProductionTaskStepReagent.order_num.asc(), BizProductionTaskStepReagent.create_time.asc()).all()
consumables = db.query(BizProductionTaskStepConsumable).filter(
and_(BizProductionTaskStepConsumable.step_id == s.id, BizProductionTaskStepConsumable.del_flag == 0)
).order_by(BizProductionTaskStepConsumable.order_num.asc(), BizProductionTaskStepConsumable.create_time.asc()).all()
outputs = db.query(BizProductionTaskStepOutput).filter(
and_(BizProductionTaskStepOutput.step_id == s.id, BizProductionTaskStepOutput.del_flag == 0)
).order_by(BizProductionTaskStepOutput.order_num.asc(), BizProductionTaskStepOutput.create_time.asc()).all()
data_collections = db.query(BizProductionTaskStepDataCollection).filter(
and_(BizProductionTaskStepDataCollection.step_id == s.id, BizProductionTaskStepDataCollection.del_flag == 0)
).order_by(BizProductionTaskStepDataCollection.order_num.asc(), BizProductionTaskStepDataCollection.create_time.asc()).all()
sd["reagents"] = [_step_reagent_to_dict(r) for r in reagents]
sd["consumables"] = [_step_consumable_to_dict(c) for c in consumables]
sd["outputs"] = [_step_output_to_dict(o) for o in outputs]
sd["dataCollections"] = [_step_data_collection_to_dict(d) for d in data_collections]
step_list.append(sd)
inputs = db.query(BizProductionTaskInput).filter(
and_(BizProductionTaskInput.task_id == task_id, BizProductionTaskInput.del_flag == 0)
).order_by(BizProductionTaskInput.order_num.asc(), BizProductionTaskInput.create_time.asc()).all()
outputs = db.query(BizProductionTaskOutput).filter(
and_(BizProductionTaskOutput.task_id == task_id, BizProductionTaskOutput.del_flag == 0)
).order_by(BizProductionTaskOutput.order_num.asc(), BizProductionTaskOutput.create_time.asc()).all()
result["steps"] = step_list
# --- enrich inputs with cell product info ---
input_dicts = [_task_input_to_dict(i) for i in inputs]
cp_input_ids = [i.input_id for i in inputs if i.input_type == "cell_product" and i.input_id]
input_product_map: Dict[str, Dict[str, Any]] = {}
if cp_input_ids:
cp_rows = db.query(BizCellProduct).filter(
BizCellProduct.id.in_(set(cp_input_ids)), BizCellProduct.del_flag == 0
).all()
cp_dicts = [_cp_row_to_dict(c) for c in cp_rows]
cp_dicts = _cp_enrich(db, tenant_no, cp_dicts)
input_product_map = {d["id"]: d for d in cp_dicts}
for idx, inp in enumerate(inputs):
if inp.input_type == "cell_product" and inp.input_id and inp.input_id in input_product_map:
input_dicts[idx] = _task_input_to_dict(inp, product_dict=input_product_map[inp.input_id])
result["inputs"] = input_dicts
# --- enrich outputs with cell product info ---
output_dicts = [_task_output_to_dict(o) for o in outputs]
cp_output_ids = [o.cell_product_id for o in outputs if o.cell_product_id]
output_product_map: Dict[str, Dict[str, Any]] = {}
if cp_output_ids:
cp_rows = db.query(BizCellProduct).filter(
BizCellProduct.id.in_(set(cp_output_ids)), BizCellProduct.del_flag == 0
).all()
cp_dicts = [_cp_row_to_dict(c) for c in cp_rows]
cp_dicts = _cp_enrich(db, tenant_no, cp_dicts)
output_product_map = {d["id"]: d for d in cp_dicts}
for idx, out in enumerate(outputs):
if out.cell_product_id and out.cell_product_id in output_product_map:
output_dicts[idx] = _task_output_to_dict(out, product_dict=output_product_map[out.cell_product_id])
result["outputs"] = output_dicts
return result
def update_production_task(
db: Session, task_id: str, tenant_no: str,
user_id: Optional[str], data: Dict[str, Any],
) -> BizProductionTask:
task = db.query(BizProductionTask).filter(
and_(
BizProductionTask.id == task_id,
BizProductionTask.tenant_no == tenant_no,
BizProductionTask.del_flag == 0,
)
).first()
if not task:
raise ValueError("任务不存在")
for field in ["task_name", "description", "remark", "attachments"]:
val = data.get(field)
if val is not None:
setattr(task, field, (val or "").strip() or None)
task.update_by = user_id
task.update_time = datetime.now()
db.add(task)
db.commit()
db.refresh(task)
return task
def _consume_task_input_cell_products(
db: Session,
task_id: str,
user_id: Optional[str],
now: datetime,
) -> None:
"""任务首次完成时,将原材料中的细胞产品标记为已使用并释放库位占用。"""
input_rows = db.query(BizProductionTaskInput).filter(
and_(
BizProductionTaskInput.task_id == task_id,
BizProductionTaskInput.del_flag == 0,
BizProductionTaskInput.input_type == "cell_product",
BizProductionTaskInput.input_id.isnot(None),
BizProductionTaskInput.input_id != "",
)
).all()
seen_ids: set = set()
consumable_from = frozenset({"frozen", "thawed", "in_production"})
for row in input_rows:
cp_id = (row.input_id or "").strip()
if not cp_id or cp_id in seen_ids:
continue
seen_ids.add(cp_id)
product = db.query(BizCellProduct).filter(
BizCellProduct.id == cp_id,
BizCellProduct.del_flag == 0,
).first()
if not product:
continue
current = (product.product_status or "").strip()
if current in ("used", "voided", "sold"):
continue
if current not in consumable_from:
continue
product.product_status = "used"
product.update_by = user_id
product.update_time = now
db.add(product)
if product.storage_slot_id:
try:
occupancy_svc.remove_by_item(
db, "cell_product", product.id, user_id=user_id,
)
except Exception:
pass
def update_task_status(
db: Session,
task_id: str,
tenant_no: str,
user_id: Optional[str],
status: str,
*,
conclusion: Optional[str] = None,
remark: Optional[str] = None,
) -> BizProductionTask:
task = db.query(BizProductionTask).filter(
and_(
BizProductionTask.id == task_id,
BizProductionTask.tenant_no == tenant_no,
BizProductionTask.del_flag == 0,
)
).first()
if not task:
raise ValueError("任务不存在")
if task.task_status == "ywc" and status != "ywc":
raise ValueError("任务已完成,不可变更状态")
now = datetime.now()
previous_status = task.task_status
task.task_status = status
if status == "jxz" and not task.started_time:
task.started_time = now
if status == "ywc":
concl = (conclusion or "").strip()
if concl not in ("cg", "sb"):
raise ValueError("须选择生产结论(成功或失败)")
task.conclusion = concl
if remark is not None:
task.remark = (remark or "").strip() or None
if previous_status != "ywc" and not task.completed_time:
task.completed_time = now
if previous_status != "ywc":
_consume_task_input_cell_products(db, task.id, user_id, now)
task.update_by = user_id
task.update_time = now
db.add(task)
from app.crud import production_work_order as production_work_order_svc
production_work_order_svc.sync_production_work_order_status_from_tasks(
db, task.work_order_id or "", tenant_no, user_id
)
db.commit()
db.refresh(task)
return task
def add_task_step(
db: Session, task_id: str, tenant_no: str,
user_id: Optional[str], data: Dict[str, Any],
) -> Dict[str, Any]:
task = db.query(BizProductionTask).filter(
and_(
BizProductionTask.id == task_id,
BizProductionTask.tenant_no == tenant_no,
BizProductionTask.del_flag == 0,
)
).first()
if not task:
raise ValueError("任务不存在")
now = datetime.now()
step_order = data.get("step_order")
if step_order is None:
max_order = db.query(func.max(BizProductionTaskStep.step_order)).filter(
and_(BizProductionTaskStep.task_id == task_id, BizProductionTaskStep.del_flag == 0)
).scalar()
step_order = (max_order or 0) + 1
# 如果传入了 process_id,从工艺主表深拷贝基本信息和子表数据
process_id = data.get("process_id")
if process_id:
proc = db.query(BizProcess).filter(
and_(BizProcess.id == process_id, BizProcess.del_flag == 0)
).first()
if not proc:
raise ValueError("工艺不存在")
step = BizProductionTaskStep(
id=generate_uuid(), task_id=task_id,
step_order=step_order,
process_code=proc.process_code,
category_id=proc.category_id,
process_name=proc.process_name,
description=proc.description,
equipment_name=proc.equipment_name,
estimated_duration=proc.estimated_duration,
sop_ids=proc.sop_ids,
step_status="dzx",
tenant_no=tenant_no, order_num=0,
remark=None,
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
)
db.add(step)
db.flush()
# 深拷贝工艺子表 → 任务步骤子表
reagent_rows = db.query(BizProcessReagent).filter(
and_(BizProcessReagent.process_id == process_id, BizProcessReagent.del_flag == 0)
).order_by(BizProcessReagent.order_num.asc(), BizProcessReagent.create_time.asc()).all()
for idx, r in enumerate(reagent_rows):
db.add(BizProductionTaskStepReagent(
id=generate_uuid(), step_id=step.id, tenant_no=tenant_no,
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,
remark=r.remark,
order_num=_resolve_sub_table_order_num(r.order_num, idx, reagent_rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
consumable_rows = db.query(BizProcessConsumable).filter(
and_(BizProcessConsumable.process_id == process_id, BizProcessConsumable.del_flag == 0)
).order_by(BizProcessConsumable.order_num.asc(), BizProcessConsumable.create_time.asc()).all()
for idx, c in enumerate(consumable_rows):
db.add(BizProductionTaskStepConsumable(
id=generate_uuid(), step_id=step.id, tenant_no=tenant_no,
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,
order_num=_resolve_sub_table_order_num(c.order_num, idx, consumable_rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
output_rows = db.query(BizProcessOutput).filter(
and_(BizProcessOutput.process_id == process_id, BizProcessOutput.del_flag == 0)
).order_by(BizProcessOutput.order_num.asc(), BizProcessOutput.create_time.asc()).all()
for idx, o in enumerate(output_rows):
db.add(BizProductionTaskStepOutput(
id=generate_uuid(), step_id=step.id, tenant_no=tenant_no,
output_type_id=o.output_type_id, output_name=o.output_name,
output_barcode=o.output_barcode,
remark=o.remark,
order_num=_resolve_sub_table_order_num(o.order_num, idx, output_rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
collection_rows = db.query(BizProcessDataCollection).filter(
and_(BizProcessDataCollection.process_id == process_id, BizProcessDataCollection.del_flag == 0)
).order_by(BizProcessDataCollection.order_num.asc(), BizProcessDataCollection.create_time.asc()).all()
for idx, d in enumerate(collection_rows):
db.add(BizProductionTaskStepDataCollection(
id=generate_uuid(), step_id=step.id, tenant_no=tenant_no,
collection_name=d.collection_name, unit=d.unit,
remark=d.remark,
order_num=_resolve_sub_table_order_num(d.order_num, idx, collection_rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
db.commit()
else:
# 无 process_id 时,使用前端传来的字段直接创建(兼容旧逻辑)
step = BizProductionTaskStep(
id=generate_uuid(), task_id=task_id,
step_order=step_order,
process_code=(data.get("process_code") or "").strip() or None,
category_id=(data.get("category_id") or "").strip() or None,
process_name=(data.get("process_name") or "").strip() or None,
description=(data.get("description") or "").strip() or None,
equipment_name=(data.get("equipment_name") or "").strip() or None,
estimated_duration=(data.get("estimated_duration") or "").strip() or None,
actual_duration=(data.get("actual_duration") or "").strip() or None,
sop_ids=(data.get("sop_ids") or "").strip() or None,
step_status=(data.get("step_status") or "dzx"),
tenant_no=tenant_no, order_num=0,
remark=(data.get("remark") or "").strip() or None,
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
)
db.add(step)
db.commit()
# 返回带子表的完整步骤信息(与 get_production_task_detail 格式一致)
result = _step_to_dict(step)
reagents = db.query(BizProductionTaskStepReagent).filter(
and_(BizProductionTaskStepReagent.step_id == step.id, BizProductionTaskStepReagent.del_flag == 0)
).order_by(BizProductionTaskStepReagent.order_num.asc(), BizProductionTaskStepReagent.create_time.asc()).all()
consumables = db.query(BizProductionTaskStepConsumable).filter(
and_(BizProductionTaskStepConsumable.step_id == step.id, BizProductionTaskStepConsumable.del_flag == 0)
).order_by(BizProductionTaskStepConsumable.order_num.asc(), BizProductionTaskStepConsumable.create_time.asc()).all()
outputs = db.query(BizProductionTaskStepOutput).filter(
and_(BizProductionTaskStepOutput.step_id == step.id, BizProductionTaskStepOutput.del_flag == 0)
).order_by(BizProductionTaskStepOutput.order_num.asc(), BizProductionTaskStepOutput.create_time.asc()).all()
data_collections = db.query(BizProductionTaskStepDataCollection).filter(
and_(BizProductionTaskStepDataCollection.step_id == step.id, BizProductionTaskStepDataCollection.del_flag == 0)
).order_by(BizProductionTaskStepDataCollection.order_num.asc(), BizProductionTaskStepDataCollection.create_time.asc()).all()
result["reagents"] = [_step_reagent_to_dict(r) for r in reagents]
result["consumables"] = [_step_consumable_to_dict(c) for c in consumables]
result["outputs"] = [_step_output_to_dict(o) for o in outputs]
result["dataCollections"] = [_step_data_collection_to_dict(d) for d in data_collections]
return result
def update_task_step(
db: Session, step_id: str, tenant_no: str,
user_id: Optional[str], data: Dict[str, Any],
) -> BizProductionTaskStep:
step = db.query(BizProductionTaskStep).filter(
and_(
BizProductionTaskStep.id == step_id,
BizProductionTaskStep.tenant_no == tenant_no,
BizProductionTaskStep.del_flag == 0,
)
).first()
if not step:
raise ValueError("步骤不存在")
for field in [
"step_order", "process_code", "category_id", "process_name",
"description", "equipment_name", "estimated_duration", "actual_duration",
"sop_ids", "step_status", "remark",
]:
val = data.get(field)
if val is not None:
if field == "step_order":
setattr(step, field, int(val))
else:
setattr(step, field, (val or "").strip() or None)
if data.get("started_time"):
from dateutil.parser import parse as _parse_dt
step.started_time = _parse_dt(data["started_time"])
if data.get("completed_time"):
from dateutil.parser import parse as _parse_dt
step.completed_time = _parse_dt(data["completed_time"])
step.update_by = user_id
step.update_time = datetime.now()
db.add(step)
db.commit()
db.refresh(step)
return step
def delete_task_step(db: Session, step_id: str, tenant_no: str, user_id: Optional[str]) -> None:
step = db.query(BizProductionTaskStep).filter(
and_(
BizProductionTaskStep.id == step_id,
BizProductionTaskStep.tenant_no == tenant_no,
BizProductionTaskStep.del_flag == 0,
)
).first()
if not step:
raise ValueError("步骤不存在")
now = datetime.now()
step.del_flag = 1
step.update_by = user_id
step.update_time = now
db.add(step)
_soft_delete_sub(db, BizProductionTaskStepReagent, "step_id", step_id, now)
_soft_delete_sub(db, BizProductionTaskStepConsumable, "step_id", step_id, now)
_soft_delete_sub(db, BizProductionTaskStepOutput, "step_id", step_id, now)
_soft_delete_sub(db, BizProductionTaskStepDataCollection, "step_id", step_id, now)
db.commit()
def replace_step_reagents(
db: Session, step_id: str, tenant_no: str,
user_id: Optional[str], items: List[Dict[str, Any]],
) -> None:
now = datetime.now()
# Look up task info from step_id for stock log
step_row = db.query(BizProductionTaskStep).filter(
and_(BizProductionTaskStep.id == step_id, BizProductionTaskStep.del_flag == 0)
).first()
task_id = ""
task_no = ""
step_name = ""
if step_row:
step_name = step_row.process_name or ""
task_row = db.query(BizProductionTask).filter(
and_(BizProductionTask.id == step_row.task_id, BizProductionTask.del_flag == 0)
).first()
if task_row:
task_id = task_row.id
task_no = task_row.task_no or ""
try:
_soft_delete_sub(db, BizProductionTaskStepReagent, "step_id", step_id, now)
# Revert previous step_save stock deductions
from app.crud import reagent_stock_log as reagent_stock_log_crud
reagent_stock_log_crud.revert_step_stock_changes(db, step_id, tenant_no, user_id)
for idx, it in enumerate(items):
db.add(BizProductionTaskStepReagent(
id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
reagent_code=(it.get("reagent_code") or "").strip() or None,
reagent_name=(it.get("reagent_name") or "").strip() or None,
spec=(it.get("spec") or "").strip() or None,
standard_usage=(it.get("standard_usage") or "").strip() or None,
actual_usage=(it.get("actual_usage") or "").strip() or None,
unit=(it.get("unit") or "").strip() or None,
is_key_material=it.get("is_key_material", "0"),
remark=(it.get("remark") or "").strip() or None,
order_num=idx, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
# Deduct stock based on actual_usage
reagent_stock_log_crud.deduct_for_step(
db, step_id, step_name, task_id, task_no, items, tenant_no, user_id, "",
)
db.commit()
except Exception:
db.rollback()
raise
def replace_step_consumables(
db: Session, step_id: str, tenant_no: str,
user_id: Optional[str], items: List[Dict[str, Any]],
) -> None:
now = datetime.now()
# Look up task info from step_id for stock log
step_row = db.query(BizProductionTaskStep).filter(
and_(BizProductionTaskStep.id == step_id, BizProductionTaskStep.del_flag == 0)
).first()
task_id = ""
task_no = ""
step_name = ""
if step_row:
step_name = step_row.process_name or ""
task_row = db.query(BizProductionTask).filter(
and_(BizProductionTask.id == step_row.task_id, BizProductionTask.del_flag == 0)
).first()
if task_row:
task_id = task_row.id
task_no = task_row.task_no or ""
try:
_soft_delete_sub(db, BizProductionTaskStepConsumable, "step_id", step_id, now)
# Revert previous step_save stock deductions
from app.crud import consumable_stock_log as consumable_stock_log_crud
consumable_stock_log_crud.revert_step_stock_changes(db, step_id, tenant_no, user_id)
for idx, it in enumerate(items):
db.add(BizProductionTaskStepConsumable(
id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
consumable_code=(it.get("consumable_code") or "").strip() or None,
consumable_name=(it.get("consumable_name") or "").strip() or None,
spec_model=(it.get("spec_model") or "").strip() or None,
standard_usage=(it.get("standard_usage") or "").strip() or None,
actual_usage=(it.get("actual_usage") or "").strip() or None,
unit=(it.get("unit") or "").strip() or None,
is_required=it.get("is_required", "1"),
remark=(it.get("remark") or "").strip() or None,
order_num=idx, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
# Deduct stock based on actual_usage
consumable_stock_log_crud.deduct_for_step(
db, step_id, step_name, task_id, task_no, items, tenant_no, user_id, "",
)
db.commit()
except Exception:
db.rollback()
raise
def replace_step_outputs(
db: Session, step_id: str, tenant_no: str,
user_id: Optional[str], items: List[Dict[str, Any]],
) -> None:
now = datetime.now()
_soft_delete_sub(db, BizProductionTaskStepOutput, "step_id", step_id, now)
for idx, it in enumerate(items):
db.add(BizProductionTaskStepOutput(
id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
output_type_id=(it.get("output_type_id") or "").strip() or None,
output_name=(it.get("output_name") or "").strip() or None,
output_barcode=(it.get("output_barcode") or "").strip() or None,
remark=(it.get("remark") or "").strip() or None,
order_num=idx, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
db.commit()
def replace_step_data_collections(
db: Session, step_id: str, tenant_no: str,
user_id: Optional[str], items: List[Dict[str, Any]],
) -> None:
now = datetime.now()
_soft_delete_sub(db, BizProductionTaskStepDataCollection, "step_id", step_id, now)
for idx, it in enumerate(items):
db.add(BizProductionTaskStepDataCollection(
id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
collection_name=(it.get("collection_name") or "").strip() or None,
unit=(it.get("unit") or "").strip() or None,
collection_value=(it.get("collection_value") or "").strip() or None,
remark=(it.get("remark") or "").strip() or None,
order_num=idx, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
db.commit()
def set_task_inputs(
db: Session, task_id: str, tenant_no: str,
user_id: Optional[str], items: List[Dict[str, Any]],
) -> None:
now = datetime.now()
# --- 1. 加载旧 inputs 并按 cell_product 恢复状态 ---
old_rows = db.query(BizProductionTaskInput).filter(
and_(
BizProductionTaskInput.task_id == task_id,
BizProductionTaskInput.del_flag == 0,
)
).all()
for old_row in old_rows:
# 如果是细胞产品,恢复状态
if old_row.input_type == "cell_product" and old_row.input_id:
product = db.query(BizCellProduct).filter(
BizCellProduct.id == old_row.input_id, BizCellProduct.del_flag == 0
).first()
if product and product.product_status == "in_production":
product.product_status = "frozen"
product.update_by = user_id
product.update_time = now
db.add(product)
# --- 2. 全量软删除旧行 ---
_soft_delete_sub(db, BizProductionTaskInput, "task_id", task_id, now)
# --- 3. 按传入顺序批量新建 ---
for idx, it in enumerate(items):
inp_type = (it.get("input_type") or it.get("inputType") or "").strip()
inp_id = (it.get("input_id") or it.get("inputId") or "").strip()
# 如果是细胞产品,设置状态为 in_production
if inp_type == "cell_product" and inp_id:
product = db.query(BizCellProduct).filter(
BizCellProduct.id == inp_id, BizCellProduct.del_flag == 0
).first()
if product and product.product_status in ("frozen", "thawed"):
product.product_status = "in_production"
product.update_by = user_id
product.update_time = now
db.add(product)
db.add(BizProductionTaskInput(
id=generate_uuid(), task_id=task_id, tenant_no=tenant_no,
input_type=inp_type or None,
input_id=inp_id or None,
input_code=(it.get("input_code") or it.get("inputCode") or "").strip() or None,
input_name=(it.get("input_name") or it.get("inputName") or "").strip() or None,
remark=(it.get("remark") or "").strip() or None,
order_num=idx, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
db.flush()
db.commit()
def _parse_optional_date(val):
"""解析可选日期字符串(兼容 YYYY-MM-DD 与 ISO 8601)"""
if not val:
return None
if isinstance(val, datetime):
return val
return parse_datetime(str(val).strip())
def _create_occupancy_for_product(db: Session, product: BizCellProduct, tenant_no: str, user_id: Optional[str]):
"""为细胞产品创建库位占用记录"""
if not product.storage_slot_id:
return
now = datetime.now()
occ = BizStorageSlotOccupancy(
id=generate_uuid(),
slot_id=product.storage_slot_id,
item_type="cell_product",
item_id=product.id,
item_code=product.product_code or "",
item_name=product.product_name or "",
item_source="production",
stored_at=now,
occupancy_status="stored",
tenant_no=tenant_no,
order_num=0,
remark=None,
create_by=user_id,
create_dept_id="",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(occ)
db.flush()
occupancy_svc._refresh_slot_cache(db, product.storage_slot_id)
def _update_product_from_output(product: BizCellProduct, it: Dict[str, Any], user_id, now):
"""从 output item 更新细胞产品字段"""
field_map = [
("product_name", "product_name", "productName"),
("product_type_id", "product_type_id", "productTypeId"),
("spec_id", "spec_id", "specId"),
("batch_no", "batch_no", "batchNo"),
("storage_slot_id", "storage_slot_id", "storageSlotId"),
("unit", "unit", "unit"),
("product_images", "product_images", "productImages"),
("remark", "remark", "remark"),
]
for db_field, snake_key, camel_key in field_map:
val = it.get(snake_key) if it.get(snake_key) is not None else it.get(camel_key)
if val is not None:
setattr(product, db_field, (str(val) or "").strip() or None)
for num_field, snake_key, camel_key in [
("expected_cell_count", "expected_cell_count", "expectedCellCount"),
("viability", "viability", "viability"),
]:
val = it.get(snake_key)
if val is None:
val = it.get(camel_key)
if val is not None:
setattr(product, num_field, val)
for date_field, snake_key, camel_key in [
("freeze_date", "freeze_date", "freezeDate"),
("expiry_date", "expiry_date", "expiryDate"),
]:
val = it.get(snake_key) or it.get(camel_key)
if val is not None:
setattr(product, date_field, _parse_optional_date(val))
passage = it.get("passage")
if passage is not None:
product.passage = (str(passage) or "").strip() or None
quantity = it.get("quantity")
if quantity is not None:
product.quantity = quantity
product.update_by = user_id
product.update_time = now
def set_task_outputs(
db: Session, task_id: str, tenant_no: str,
user_id: Optional[str], items: List[Dict[str, Any]],
) -> None:
now = datetime.now()
# --- 1. 加载旧 outputs ---
old_rows = db.query(BizProductionTaskOutput).filter(
and_(
BizProductionTaskOutput.task_id == task_id,
BizProductionTaskOutput.del_flag == 0,
)
).all()
old_cp_map: Dict[str, BizProductionTaskOutput] = {}
for o in old_rows:
if o.cell_product_id:
old_cp_map[o.cell_product_id] = o
# --- 2. 构建新的 cp_id 集合 ---
new_cp_ids: set = set()
for it in items:
cp_id = (it.get("cell_product_id") or it.get("cellProductId") or "").strip()
if cp_id:
new_cp_ids.add(cp_id)
# --- 3. 处理删除(旧有新无) ---
for cp_id, old_row in old_cp_map.items():
if cp_id not in new_cp_ids:
product = db.query(BizCellProduct).filter(
BizCellProduct.id == cp_id, BizCellProduct.del_flag == 0
).first()
if product:
product.product_status = "voided"
product.update_by = user_id
product.update_time = now
db.add(product)
if product.storage_slot_id:
occupancy_svc.remove_by_item(db, "cell_product", cp_id, user_id=user_id)
old_row.del_flag = 1
old_row.update_by = user_id
old_row.update_time = now
db.add(old_row)
# --- 4. 处理新增和修改 ---
for idx, it in enumerate(items):
cp_id = (it.get("cell_product_id") or it.get("cellProductId") or "").strip()
if cp_id and cp_id in old_cp_map:
# === 修改 ===
product = db.query(BizCellProduct).filter(
BizCellProduct.id == cp_id, BizCellProduct.del_flag == 0
).first()
if product:
old_slot_id = product.storage_slot_id
_update_product_from_output(product, it, user_id, now)
new_slot_id = product.storage_slot_id
db.add(product)
if old_slot_id != new_slot_id:
if old_slot_id:
occupancy_svc.remove_by_item(db, "cell_product", cp_id, user_id=user_id)
if new_slot_id:
_create_occupancy_for_product(db, product, tenant_no, user_id)
old_row = old_cp_map[cp_id]
old_row.output_name = product.product_name if product else ""
old_row.output_barcode = product.product_code if product else ""
old_row.storage_slot_id = product.storage_slot_id if product else None
old_row.quantity = it.get("quantity")
old_row.unit = (it.get("unit") or "").strip() or None
old_row.remark = (it.get("remark") or "").strip() or None
old_row.order_num = idx
old_row.update_by = user_id
old_row.update_time = now
db.add(old_row)
else:
# === 新增 ===
product = BizCellProduct(
id=generate_uuid(),
product_code=(it.get("product_code") or it.get("productCode") or "").strip(),
product_name=(it.get("product_name") or it.get("productName") or "").strip(),
product_type_id=(it.get("product_type_id") or it.get("productTypeId") or "").strip() or None,
spec_id=(it.get("spec_id") or it.get("specId") or "").strip() or None,
passage=(it.get("passage") or "").strip() or None,
batch_no=(it.get("batch_no") or it.get("batchNo") or "").strip() or None,
storage_slot_id=(it.get("storage_slot_id") or it.get("storageSlotId") or "").strip() or None,
expected_cell_count=it.get("expected_cell_count") if it.get("expected_cell_count") is not None else it.get("expectedCellCount"),
viability=it.get("viability"),
freeze_date=_parse_optional_date(it.get("freeze_date") or it.get("freezeDate")),
product_status=it.get("product_status") or it.get("productStatus") or "frozen",
source_type="production",
production_task_id=task_id,
quantity=it.get("quantity"),
unit=(it.get("unit") or "").strip() or None,
expiry_date=_parse_optional_date(it.get("expiry_date") or it.get("expiryDate")),
product_images=(it.get("product_images") or it.get("productImages") or "").strip() or None,
tenant_no=tenant_no,
order_num=idx,
remark=(it.get("remark") or "").strip() or None,
create_by=user_id,
create_dept_id="",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(product)
db.flush()
if product.storage_slot_id:
_create_occupancy_for_product(db, product, tenant_no, user_id)
output_row = BizProductionTaskOutput(
id=generate_uuid(), task_id=task_id, tenant_no=tenant_no,
cell_product_id=product.id,
storage_slot_id=product.storage_slot_id,
output_name=product.product_name or "",
output_barcode=product.product_code or "",
quantity=product.quantity,
unit=product.unit,
remark=product.remark,
attachments=(it.get("attachments") or "").strip() or None,
order_num=idx, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
)
db.add(output_row)
db.flush()
db.commit()
def reorder_task_steps(db: Session, task_id: str, step_ids: List[str]) -> None:
now = datetime.now()
for idx, sid in enumerate(step_ids):
step = db.query(BizProductionTaskStep).filter(
and_(BizProductionTaskStep.id == sid, BizProductionTaskStep.task_id == task_id,
BizProductionTaskStep.del_flag == 0)
).first()
if step:
step.step_order = idx + 1
step.update_time = now
db.add(step)
db.commit()
def copy_task_step(db: Session, step_id: str, *, tenant_no: str = "zkxw",
user_id: Optional[str] = None) -> Dict[str, Any]:
"""复制生产任务步骤,插入到原步骤之后,名称追加(复制)"""
src = db.query(BizProductionTaskStep).filter(
and_(BizProductionTaskStep.id == step_id, BizProductionTaskStep.del_flag == 0)
).first()
if not src:
raise ValueError("步骤不存在")
now = datetime.now()
new_order = src.step_order + 1
# 后续步骤 step_order +1
later_steps = db.query(BizProductionTaskStep).filter(
and_(
BizProductionTaskStep.task_id == src.task_id,
BizProductionTaskStep.del_flag == 0,
BizProductionTaskStep.step_order > src.step_order,
)
).order_by(BizProductionTaskStep.step_order.asc()).all()
for ls in later_steps:
ls.step_order += 1
ls.update_time = now
db.add(ls)
# 创建新步骤
new_name = (src.process_name or "").strip()
if not new_name.endswith("(复制)"):
new_name += "(复制)"
new_step = BizProductionTaskStep(
id=generate_uuid(), task_id=src.task_id,
step_order=new_order,
process_code=src.process_code, category_id=src.category_id,
process_name=new_name, description=src.description,
equipment_name=src.equipment_name, estimated_duration=src.estimated_duration,
actual_duration=src.actual_duration, sop_ids=src.sop_ids,
step_status="dzx",
tenant_no=tenant_no, order_num=0,
remark=src.remark, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
)
db.add(new_step)
db.flush()
# 复制子表:试剂(从生产任务步骤试剂表复制)
reagent_rows = db.query(BizProductionTaskStepReagent).filter(
and_(BizProductionTaskStepReagent.step_id == step_id, BizProductionTaskStepReagent.del_flag == 0)
).order_by(
BizProductionTaskStepReagent.order_num.asc(),
BizProductionTaskStepReagent.create_time.asc(),
).all()
for idx, r in enumerate(reagent_rows):
db.add(BizProductionTaskStepReagent(
id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
reagent_code=r.reagent_code, reagent_name=r.reagent_name,
spec=r.spec, standard_usage=r.standard_usage,
actual_usage=r.actual_usage,
unit=r.unit, is_key_material=r.is_key_material,
remark=r.remark,
order_num=_resolve_sub_table_order_num(r.order_num, idx, reagent_rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
# 复制子表:耗材
consumable_rows = db.query(BizProductionTaskStepConsumable).filter(
and_(BizProductionTaskStepConsumable.step_id == step_id, BizProductionTaskStepConsumable.del_flag == 0)
).order_by(
BizProductionTaskStepConsumable.order_num.asc(),
BizProductionTaskStepConsumable.create_time.asc(),
).all()
for idx, c in enumerate(consumable_rows):
db.add(BizProductionTaskStepConsumable(
id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
consumable_code=c.consumable_code, consumable_name=c.consumable_name,
spec_model=c.spec_model, standard_usage=c.standard_usage,
actual_usage=c.actual_usage,
unit=c.unit, is_required=c.is_required,
remark=c.remark,
order_num=_resolve_sub_table_order_num(c.order_num, idx, consumable_rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
# 复制子表:产出
output_rows = db.query(BizProductionTaskStepOutput).filter(
and_(BizProductionTaskStepOutput.step_id == step_id, BizProductionTaskStepOutput.del_flag == 0)
).order_by(
BizProductionTaskStepOutput.order_num.asc(),
BizProductionTaskStepOutput.create_time.asc(),
).all()
for idx, o in enumerate(output_rows):
db.add(BizProductionTaskStepOutput(
id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
output_type_id=o.output_type_id, output_name=o.output_name,
output_barcode=o.output_barcode,
remark=o.remark,
order_num=_resolve_sub_table_order_num(o.order_num, idx, output_rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
# 复制子表:数据采集
collection_rows = db.query(BizProductionTaskStepDataCollection).filter(
and_(BizProductionTaskStepDataCollection.step_id == step_id, BizProductionTaskStepDataCollection.del_flag == 0)
).order_by(
BizProductionTaskStepDataCollection.order_num.asc(),
BizProductionTaskStepDataCollection.create_time.asc(),
).all()
for idx, d in enumerate(collection_rows):
db.add(BizProductionTaskStepDataCollection(
id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
collection_name=d.collection_name, unit=d.unit,
collection_value=d.collection_value,
remark=d.remark,
order_num=_resolve_sub_table_order_num(d.order_num, idx, collection_rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
db.commit()
result = _step_to_dict(new_step)
result.update(_step_sub_tables_to_dict(db, new_step.id))
return result
def delete_production_task(db: Session, task_id: str, tenant_no: str, user_id: Optional[str]) -> None:
"""软删除生产任务及其所有子表数据(步骤 + 投入 + 产出)"""
task = db.query(BizProductionTask).filter(
and_(
BizProductionTask.id == task_id,
BizProductionTask.tenant_no == tenant_no,
BizProductionTask.del_flag == 0,
)
).first()
if not task:
raise ValueError("任务不存在")
now = datetime.now()
# 1. 软删除所有步骤及其子表
steps = db.query(BizProductionTaskStep).filter(
and_(BizProductionTaskStep.task_id == task_id, BizProductionTaskStep.del_flag == 0)
).all()
for step in steps:
step.del_flag = 1
step.update_by = user_id
step.update_time = now
db.add(step)
_soft_delete_sub(db, BizProductionTaskStepReagent, "step_id", step.id, now)
_soft_delete_sub(db, BizProductionTaskStepConsumable, "step_id", step.id, now)
_soft_delete_sub(db, BizProductionTaskStepOutput, "step_id", step.id, now)
_soft_delete_sub(db, BizProductionTaskStepDataCollection, "step_id", step.id, now)
# 2. 软删除投入
_soft_delete_sub(db, BizProductionTaskInput, "task_id", task_id, now)
# 3. 软删除产出
_soft_delete_sub(db, BizProductionTaskOutput, "task_id", task_id, now)
# 4. 软删除任务本体
task.del_flag = 1
task.update_by = user_id
task.update_time = now
db.add(task)
work_order_id = task.work_order_id or ""
from app.crud import production_work_order as production_work_order_svc
production_work_order_svc.sync_production_work_order_status_from_tasks(
db, work_order_id, tenant_no, user_id
)
db.commit()
def total_pages(total: int, page_size: int) -> int:
if total <= 0:
return 0
return int(math.ceil(total / float(page_size)))