3287 lines
126 KiB
Python
3287 lines
126 KiB
Python
"""生产任务 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 format_date, generate_uuid, parse_datetime
|
||
from app.crud.equipment_snapshot import parse_equipment_ids, resolve_equipment_list_snapshot
|
||
from app.models.models import (
|
||
BizCellProduct,
|
||
BizCollectionProtocol,
|
||
BizProcess,
|
||
BizProcessConsumable,
|
||
BizProcessDataCollection,
|
||
BizProcessOutput,
|
||
BizProcessReagent,
|
||
BizProcessRoute,
|
||
BizProcessRouteStep,
|
||
BizProcessRouteStepConsumable,
|
||
BizProcessRouteStepDataCollection,
|
||
BizProcessRouteStepOutput,
|
||
BizProcessRouteStepReagent,
|
||
BizProductionTask,
|
||
BizProductionTaskHandover,
|
||
BizProductionTaskInput,
|
||
BizProductionTaskOutput,
|
||
BizProductionTaskStep,
|
||
BizProductionTaskStepAssignee,
|
||
BizProductionTaskStepConsumable,
|
||
BizProductionTaskStepConfiguredReagent,
|
||
BizProductionTaskStepDataCollection,
|
||
BizProductionTaskStepOutput,
|
||
BizProductionTaskStepReagent,
|
||
BizProductionWorkOrder,
|
||
BizProtocolSample,
|
||
BizSampleReception,
|
||
BizStorageSlotOccupancy,
|
||
SysUser,
|
||
)
|
||
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,
|
||
product_code_exists as _cp_product_code_exists,
|
||
)
|
||
from app.models.research_donation import BizResearchDonation, BizResearchDonationSample
|
||
|
||
|
||
def _require_requisition_for_catalog_material(
|
||
db: Session,
|
||
tenant_no: str,
|
||
material_code: Optional[str],
|
||
material_type: str,
|
||
actual_usage: Optional[str],
|
||
requisition_usage_id: Optional[str],
|
||
) -> None:
|
||
code = (material_code or "").strip()
|
||
actual = (actual_usage or "").strip()
|
||
req_id = (requisition_usage_id or "").strip()
|
||
if not code or not actual or req_id:
|
||
return
|
||
from app.models.production_material import BizProductionMaterial
|
||
catalog_row = db.query(BizProductionMaterial).filter(
|
||
and_(
|
||
BizProductionMaterial.material_code == code,
|
||
BizProductionMaterial.material_type == material_type,
|
||
BizProductionMaterial.tenant_no == tenant_no,
|
||
BizProductionMaterial.del_flag == 0,
|
||
)
|
||
).first()
|
||
if catalog_row:
|
||
raise ValueError(f"物料「{code}」需选择领用记录")
|
||
|
||
|
||
_PROTOCOL_STATUS_LABELS = {
|
||
"dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "sdfq": "属地废弃", "zky": "转科研"
|
||
}
|
||
_DONATION_STATUS_LABELS = {
|
||
"dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "sdfq": "属地废弃", "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.crud.pilot_infusion_schedule_mgmt import (
|
||
_get_plan_schedule_course_meta,
|
||
_resolve_course_display,
|
||
)
|
||
from app.models.models import (
|
||
BizPilotApplication,
|
||
BizPilotInfusionPlan,
|
||
BizPilotInfusionSchedule,
|
||
BizPilotPatient,
|
||
)
|
||
|
||
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 = {}
|
||
patient_map: Dict[str, BizPilotPatient] = {}
|
||
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()
|
||
}
|
||
patients = (
|
||
db.query(BizPilotPatient)
|
||
.filter(
|
||
BizPilotPatient.pilot_application_id.in_(app_ids),
|
||
BizPilotPatient.tenant_no == tenant_no,
|
||
BizPilotPatient.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
patient_map = {p.pilot_application_id: p for p in patients}
|
||
|
||
plan_course_meta = _get_plan_schedule_course_meta(db, plan_ids, tenant_no)
|
||
|
||
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 ""
|
||
sched = schedules.get(sched_id)
|
||
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)
|
||
item["applicationNo"] = app.application_no or ""
|
||
item["applicationName"] = app.application_name or ""
|
||
item["channelName"] = app.channel_name or ""
|
||
item["projectTitle"] = app.project_title or ""
|
||
item["customerName"] = app.customer_name or ""
|
||
else:
|
||
item["protocolNo"] = ""
|
||
item["protocolStatus"] = ""
|
||
item["protocolStatusLabel"] = ""
|
||
item["applicationNo"] = ""
|
||
item["applicationName"] = ""
|
||
item["channelName"] = ""
|
||
item["projectTitle"] = ""
|
||
item["customerName"] = ""
|
||
|
||
if sched:
|
||
plan = plan_map.get(sched.infusion_plan_id) if sched.infusion_plan_id else None
|
||
patient = (
|
||
patient_map.get(plan.pilot_application_id)
|
||
if plan and plan.pilot_application_id
|
||
else None
|
||
)
|
||
plan_meta = (
|
||
plan_course_meta.get(sched.infusion_plan_id) if sched.infusion_plan_id else None
|
||
)
|
||
course = _resolve_course_display(sched, plan, plan_meta)
|
||
item["patientName"] = patient.patient_name if patient else ""
|
||
item["sortNo"] = course.get("sortNo")
|
||
item["totalCourseCount"] = course.get("totalCourseCount")
|
||
item["infusionTime"] = _fmt_dt(sched.infusion_time) if sched.infusion_time else ""
|
||
|
||
|
||
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,
|
||
"currentAssigneeUserId": t.current_assignee_user_id or "",
|
||
"currentAssigneeUserName": "",
|
||
"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 "",
|
||
"equipmentIds": parse_equipment_ids(getattr(t, "equipment_ids", None)),
|
||
"equipmentName": getattr(t, "equipment_name", None) 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 "",
|
||
"estimatedDuration": s.estimated_duration or "",
|
||
"actualDuration": s.actual_duration or "",
|
||
"sopIds": s.sop_ids or "",
|
||
"sopContent": s.sop_content or "",
|
||
"stepStatus": s.step_status or "dzx",
|
||
"plannedStartTime": format_date(getattr(s, "planned_start_time", None)) or "",
|
||
"startedTime": _fmt_dt(s.started_time),
|
||
"completedTime": _fmt_dt(s.completed_time),
|
||
"completedBy": getattr(s, "completed_by", None) or "",
|
||
"completedByName": "",
|
||
"assigneeUserIds": [],
|
||
"assigneeNames": [],
|
||
"assignees": [],
|
||
"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 "",
|
||
"requisitionUsageId": (r.requisition_usage_id or "").strip(),
|
||
"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_configured_reagent_to_dict(r: BizProductionTaskStepConfiguredReagent) -> Dict[str, Any]:
|
||
return {
|
||
"id": r.id,
|
||
"stepId": r.step_id,
|
||
"configuredReagentCode": r.configured_reagent_code or "",
|
||
"configuredReagentName": r.configured_reagent_name or "",
|
||
"spec": r.spec or "",
|
||
"standardUsage": r.standard_usage or "",
|
||
"actualUsage": r.actual_usage or "",
|
||
"unit": r.unit or "",
|
||
"requisitionUsageId": (r.requisition_usage_id or "").strip(),
|
||
"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 "",
|
||
"requisitionUsageId": (c.requisition_usage_id or "").strip(),
|
||
"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", "specText", "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", "specText", "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 _empty_step_sub_tables() -> Dict[str, Any]:
|
||
return {
|
||
"reagents": [],
|
||
"consumables": [],
|
||
"configuredReagents": [],
|
||
"outputs": [],
|
||
"dataCollections": [],
|
||
}
|
||
|
||
|
||
def _batch_load_step_sub_tables(db: Session, step_ids: List[str]) -> Dict[str, Dict[str, Any]]:
|
||
"""批量加载步骤子表,避免 get_production_task_detail 逐步骤 N+1 查询。"""
|
||
ids = [sid for sid in step_ids if (sid or "").strip()]
|
||
if not ids:
|
||
return {}
|
||
result: Dict[str, Dict[str, Any]] = {sid: _empty_step_sub_tables() for sid in ids}
|
||
|
||
reagents = (
|
||
db.query(BizProductionTaskStepReagent)
|
||
.filter(
|
||
and_(
|
||
BizProductionTaskStepReagent.step_id.in_(ids),
|
||
BizProductionTaskStepReagent.del_flag == 0,
|
||
)
|
||
)
|
||
.order_by(
|
||
BizProductionTaskStepReagent.step_id.asc(),
|
||
BizProductionTaskStepReagent.order_num.asc(),
|
||
BizProductionTaskStepReagent.create_time.asc(),
|
||
)
|
||
.all()
|
||
)
|
||
for row in reagents:
|
||
result[row.step_id]["reagents"].append(_step_reagent_to_dict(row))
|
||
|
||
consumables = (
|
||
db.query(BizProductionTaskStepConsumable)
|
||
.filter(
|
||
and_(
|
||
BizProductionTaskStepConsumable.step_id.in_(ids),
|
||
BizProductionTaskStepConsumable.del_flag == 0,
|
||
)
|
||
)
|
||
.order_by(
|
||
BizProductionTaskStepConsumable.step_id.asc(),
|
||
BizProductionTaskStepConsumable.order_num.asc(),
|
||
BizProductionTaskStepConsumable.create_time.asc(),
|
||
)
|
||
.all()
|
||
)
|
||
for row in consumables:
|
||
result[row.step_id]["consumables"].append(_step_consumable_to_dict(row))
|
||
|
||
outputs = (
|
||
db.query(BizProductionTaskStepOutput)
|
||
.filter(
|
||
and_(
|
||
BizProductionTaskStepOutput.step_id.in_(ids),
|
||
BizProductionTaskStepOutput.del_flag == 0,
|
||
)
|
||
)
|
||
.order_by(
|
||
BizProductionTaskStepOutput.step_id.asc(),
|
||
BizProductionTaskStepOutput.order_num.asc(),
|
||
BizProductionTaskStepOutput.create_time.asc(),
|
||
)
|
||
.all()
|
||
)
|
||
for row in outputs:
|
||
result[row.step_id]["outputs"].append(_step_output_to_dict(row))
|
||
|
||
data_collections = (
|
||
db.query(BizProductionTaskStepDataCollection)
|
||
.filter(
|
||
and_(
|
||
BizProductionTaskStepDataCollection.step_id.in_(ids),
|
||
BizProductionTaskStepDataCollection.del_flag == 0,
|
||
)
|
||
)
|
||
.order_by(
|
||
BizProductionTaskStepDataCollection.step_id.asc(),
|
||
BizProductionTaskStepDataCollection.order_num.asc(),
|
||
BizProductionTaskStepDataCollection.create_time.asc(),
|
||
)
|
||
.all()
|
||
)
|
||
for row in data_collections:
|
||
result[row.step_id]["dataCollections"].append(_step_data_collection_to_dict(row))
|
||
|
||
configured_reagents = (
|
||
db.query(BizProductionTaskStepConfiguredReagent)
|
||
.filter(
|
||
and_(
|
||
BizProductionTaskStepConfiguredReagent.step_id.in_(ids),
|
||
BizProductionTaskStepConfiguredReagent.del_flag == 0,
|
||
)
|
||
)
|
||
.order_by(
|
||
BizProductionTaskStepConfiguredReagent.step_id.asc(),
|
||
BizProductionTaskStepConfiguredReagent.order_num.asc(),
|
||
BizProductionTaskStepConfiguredReagent.create_time.asc(),
|
||
)
|
||
.all()
|
||
)
|
||
for row in configured_reagents:
|
||
result[row.step_id]["configuredReagents"].append(_step_configured_reagent_to_dict(row))
|
||
|
||
return result
|
||
|
||
|
||
def _step_sub_tables_to_dict(db: Session, step_id: str) -> Dict[str, Any]:
|
||
"""加载步骤四张子表并序列化(与 get_production_task_detail 一致)。"""
|
||
return _batch_load_step_sub_tables(db, [step_id]).get(step_id) or _empty_step_sub_tables()
|
||
|
||
|
||
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,
|
||
estimated_duration=rs.estimated_duration,
|
||
sop_ids=rs.sop_ids,
|
||
sop_content=rs.sop_content,
|
||
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 _validate_assignee_user_id(db: Session, user_id: str, tenant_no: str) -> str:
|
||
uid = (user_id or "").strip()
|
||
if not uid:
|
||
raise ValueError("请选择负责人")
|
||
row = (
|
||
db.query(SysUser.id)
|
||
.filter(
|
||
SysUser.id == uid,
|
||
SysUser.tenant_no == tenant_no,
|
||
SysUser.del_flag == 0,
|
||
SysUser.status == "1",
|
||
)
|
||
.first()
|
||
)
|
||
if not row:
|
||
raise ValueError("负责人用户无效或已停用")
|
||
return uid
|
||
|
||
|
||
def _enrich_assignee_names(db: Session, items: List[Dict[str, Any]]) -> None:
|
||
user_ids = list({(x.get("currentAssigneeUserId") or "").strip() for x in items if (x.get("currentAssigneeUserId") or "").strip()})
|
||
if not user_ids:
|
||
return
|
||
rows = (
|
||
db.query(SysUser.id, SysUser.nick_name)
|
||
.filter(SysUser.id.in_(user_ids), SysUser.del_flag == 0)
|
||
.all()
|
||
)
|
||
name_map = {r.id: (r.nick_name or "").strip() for r in rows}
|
||
for item in items:
|
||
uid = (item.get("currentAssigneeUserId") or "").strip()
|
||
if uid:
|
||
item["currentAssigneeUserName"] = name_map.get(uid, "")
|
||
|
||
|
||
def _enrich_handover_times(
|
||
db: Session,
|
||
items: List[Dict[str, Any]],
|
||
*,
|
||
from_user_id: str,
|
||
tenant_no: str,
|
||
) -> None:
|
||
task_ids = [x.get("id") for x in items if x.get("id")]
|
||
if not task_ids:
|
||
return
|
||
rows = (
|
||
db.query(
|
||
BizProductionTaskHandover.task_id,
|
||
func.max(BizProductionTaskHandover.handover_time),
|
||
)
|
||
.filter(
|
||
BizProductionTaskHandover.task_id.in_(task_ids),
|
||
BizProductionTaskHandover.from_user_id == from_user_id,
|
||
BizProductionTaskHandover.tenant_no == tenant_no,
|
||
BizProductionTaskHandover.del_flag == 0,
|
||
)
|
||
.group_by(BizProductionTaskHandover.task_id)
|
||
.all()
|
||
)
|
||
time_map = {r[0]: _fmt_dt(r[1]) for r in rows}
|
||
for item in items:
|
||
item["handoverTime"] = time_map.get(item.get("id"), "")
|
||
|
||
|
||
def get_production_task_row(
|
||
db: Session, task_id: str, tenant_no: str
|
||
) -> Optional[BizProductionTask]:
|
||
return (
|
||
db.query(BizProductionTask)
|
||
.filter(
|
||
BizProductionTask.id == task_id,
|
||
BizProductionTask.tenant_no == tenant_no,
|
||
BizProductionTask.del_flag == 0,
|
||
)
|
||
.first()
|
||
)
|
||
|
||
|
||
def assert_task_assignee(
|
||
db: Session, task_id: str, user_id: str, tenant_no: str
|
||
) -> BizProductionTask:
|
||
"""Deprecated:优先使用 assert_task_step_assignee_or_legacy。"""
|
||
return assert_task_step_assignee_or_legacy(db, task_id, user_id, tenant_no)
|
||
|
||
|
||
def assert_task_step_assignee_or_legacy(
|
||
db: Session, task_id: str, user_id: str, tenant_no: str
|
||
) -> BizProductionTask:
|
||
from app.crud import production_task_step_assignee as step_assignee_svc
|
||
|
||
task = get_production_task_row(db, task_id, tenant_no)
|
||
if not task:
|
||
raise ValueError("任务不存在")
|
||
if step_assignee_svc.user_is_any_step_assignee_of_task(db, task_id, user_id, tenant_no):
|
||
return task
|
||
if (task.current_assignee_user_id or "") == (user_id or ""):
|
||
return task
|
||
raise PermissionError("非任务步骤负责人,无权操作")
|
||
|
||
|
||
def assert_step_assignee_for_write(
|
||
db: Session,
|
||
step_id: str,
|
||
user_id: str,
|
||
tenant_no: str,
|
||
*,
|
||
manage: bool = False,
|
||
) -> BizProductionTaskStep:
|
||
if manage:
|
||
step = db.query(BizProductionTaskStep).filter(
|
||
BizProductionTaskStep.id == step_id,
|
||
BizProductionTaskStep.tenant_no == tenant_no,
|
||
BizProductionTaskStep.del_flag == 0,
|
||
).first()
|
||
if not step:
|
||
raise ValueError("步骤不存在")
|
||
return step
|
||
from app.crud import production_task_step_assignee as step_assignee_svc
|
||
|
||
step = db.query(BizProductionTaskStep).filter(
|
||
BizProductionTaskStep.id == step_id,
|
||
BizProductionTaskStep.tenant_no == tenant_no,
|
||
BizProductionTaskStep.del_flag == 0,
|
||
).first()
|
||
if not step:
|
||
raise ValueError("步骤不存在")
|
||
if not step_assignee_svc.user_is_step_assignee(db, step_id, user_id, tenant_no):
|
||
raise PermissionError("非当前步骤负责人,无权操作")
|
||
return step
|
||
|
||
|
||
def _record_task_handover(
|
||
db: Session,
|
||
*,
|
||
task_id: str,
|
||
from_user_id: str,
|
||
to_user_id: str,
|
||
tenant_no: str,
|
||
operator_id: Optional[str],
|
||
create_dept_id: str,
|
||
remark: Optional[str] = None,
|
||
) -> None:
|
||
now = datetime.now()
|
||
db.add(
|
||
BizProductionTaskHandover(
|
||
id=generate_uuid(),
|
||
task_id=task_id,
|
||
from_user_id=from_user_id,
|
||
to_user_id=to_user_id,
|
||
handover_time=now,
|
||
remark=(remark or "").strip() or None,
|
||
tenant_no=tenant_no,
|
||
order_num=0,
|
||
create_by=operator_id,
|
||
create_dept_id=create_dept_id or "",
|
||
create_time=now,
|
||
update_by=operator_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
|
||
|
||
def forward_production_task(
|
||
db: Session,
|
||
task_id: str,
|
||
*,
|
||
from_user_id: str,
|
||
to_user_id: str,
|
||
tenant_no: str,
|
||
operator_id: Optional[str] = None,
|
||
create_dept_id: str = "",
|
||
remark: Optional[str] = None,
|
||
require_assignee: bool = True,
|
||
) -> BizProductionTask:
|
||
to_uid = _validate_assignee_user_id(db, to_user_id, tenant_no)
|
||
from_uid = (from_user_id or "").strip()
|
||
if not from_uid:
|
||
raise ValueError("转出用户无效")
|
||
if from_uid == to_uid:
|
||
raise ValueError("不能转发给自己")
|
||
if require_assignee:
|
||
task = assert_task_assignee(db, task_id, from_uid, tenant_no)
|
||
else:
|
||
task = get_production_task_row(db, task_id, tenant_no)
|
||
if not task:
|
||
raise ValueError("任务不存在")
|
||
handover_from = (task.current_assignee_user_id or from_uid).strip() or from_uid
|
||
_record_task_handover(
|
||
db,
|
||
task_id=task_id,
|
||
from_user_id=handover_from,
|
||
to_user_id=to_uid,
|
||
tenant_no=tenant_no,
|
||
operator_id=operator_id,
|
||
create_dept_id=create_dept_id,
|
||
remark=remark,
|
||
)
|
||
task.current_assignee_user_id = to_uid
|
||
task.update_by = operator_id
|
||
task.update_time = datetime.now()
|
||
db.add(task)
|
||
db.flush()
|
||
return task
|
||
|
||
|
||
def reassign_production_task(
|
||
db: Session,
|
||
task_id: str,
|
||
*,
|
||
assignee_user_id: str,
|
||
tenant_no: str,
|
||
operator_id: Optional[str] = None,
|
||
create_dept_id: str = "",
|
||
remark: Optional[str] = None,
|
||
) -> BizProductionTask:
|
||
task = get_production_task_row(db, task_id, tenant_no)
|
||
if not task:
|
||
raise ValueError("任务不存在")
|
||
to_uid = _validate_assignee_user_id(db, assignee_user_id, tenant_no)
|
||
from_uid = (task.current_assignee_user_id or operator_id or "").strip()
|
||
if from_uid and from_uid != to_uid:
|
||
_record_task_handover(
|
||
db,
|
||
task_id=task_id,
|
||
from_user_id=from_uid,
|
||
to_user_id=to_uid,
|
||
tenant_no=tenant_no,
|
||
operator_id=operator_id,
|
||
create_dept_id=create_dept_id,
|
||
remark=(remark or "").strip() or "管理员改派",
|
||
)
|
||
elif not from_uid:
|
||
_record_task_handover(
|
||
db,
|
||
task_id=task_id,
|
||
from_user_id=operator_id or to_uid,
|
||
to_user_id=to_uid,
|
||
tenant_no=tenant_no,
|
||
operator_id=operator_id,
|
||
create_dept_id=create_dept_id,
|
||
remark=(remark or "").strip() or "管理员改派",
|
||
)
|
||
task.current_assignee_user_id = to_uid
|
||
task.update_by = operator_id
|
||
task.update_time = datetime.now()
|
||
db.add(task)
|
||
db.flush()
|
||
return task
|
||
|
||
|
||
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"
|
||
|
||
assignee_raw = (data.get("assignee_user_id") or data.get("assigneeUserId") or "").strip()
|
||
assignee_user_id = None
|
||
if assignee_raw:
|
||
assignee_user_id = _validate_assignee_user_id(db, assignee_raw, tenant_no)
|
||
|
||
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",
|
||
current_assignee_user_id=assignee_user_id,
|
||
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)
|
||
route = (
|
||
db.query(BizProcessRoute)
|
||
.filter(
|
||
BizProcessRoute.id == route_id,
|
||
BizProcessRoute.tenant_no == tenant_no,
|
||
BizProcessRoute.del_flag == 0,
|
||
)
|
||
.first()
|
||
)
|
||
if route and (getattr(route, "equipment_ids", None) or getattr(route, "equipment_name", None)):
|
||
task.equipment_ids = route.equipment_ids
|
||
task.equipment_name = route.equipment_name
|
||
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
|
||
)
|
||
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 dispatch_task_steps(
|
||
db: Session,
|
||
task_id: str,
|
||
*,
|
||
tenant_no: str,
|
||
steps_payload: List[Dict[str, Any]],
|
||
operator_id: Optional[str] = None,
|
||
create_dept_id: str = "",
|
||
) -> Dict[str, Any]:
|
||
from app.crud import production_task_step_assignee as step_assignee_svc
|
||
|
||
task = get_production_task_row(db, task_id, tenant_no)
|
||
if not task:
|
||
raise ValueError("任务不存在")
|
||
if not steps_payload:
|
||
raise ValueError("请至少指定一个步骤的负责人")
|
||
|
||
step_rows = {
|
||
s.id: s
|
||
for s in db.query(BizProductionTaskStep).filter(
|
||
BizProductionTaskStep.task_id == task_id,
|
||
BizProductionTaskStep.tenant_no == tenant_no,
|
||
BizProductionTaskStep.del_flag == 0,
|
||
).all()
|
||
}
|
||
|
||
for item in steps_payload:
|
||
sid = (item.get("step_id") or item.get("stepId") or "").strip()
|
||
if sid not in step_rows:
|
||
raise ValueError(f"步骤不存在或不属于该任务: {sid}")
|
||
user_ids = item.get("assignee_user_ids") or item.get("assigneeUserIds") or []
|
||
step_assignee_svc.sync_assignees_for_step(
|
||
db,
|
||
sid,
|
||
user_ids,
|
||
tenant_no=tenant_no,
|
||
operator_id=operator_id,
|
||
create_dept_id=create_dept_id,
|
||
)
|
||
step = step_rows[sid]
|
||
if "planned_start_time" in item or "plannedStartTime" in item:
|
||
pst = item.get("planned_start_time")
|
||
if pst is None and "plannedStartTime" in item:
|
||
pst = item.get("plannedStartTime")
|
||
if pst is None or pst == "":
|
||
step.planned_start_time = None
|
||
else:
|
||
if isinstance(pst, datetime):
|
||
step.planned_start_time = pst
|
||
else:
|
||
parsed = parse_datetime(str(pst).strip())
|
||
if parsed is None:
|
||
raise ValueError("计划开始时间格式无效")
|
||
step.planned_start_time = parsed
|
||
step.update_by = operator_id
|
||
step.update_time = datetime.now()
|
||
db.add(step)
|
||
|
||
db.flush()
|
||
task_dict = _task_to_dict(task)
|
||
_enrich_tasks_with_step_summaries(db, [task_dict], tenant_no=tenant_no)
|
||
return {"taskId": task_id, "steps": task_dict.get("steps") or []}
|
||
|
||
|
||
def _enrich_tasks_with_step_summaries(
|
||
db: Session, items: List[Dict[str, Any]], *, tenant_no: str
|
||
) -> None:
|
||
"""批量附加工艺步骤摘要(无试剂/耗材/产出等子表),供列表卡片展示。"""
|
||
if not items:
|
||
return
|
||
task_ids = [it["id"] for it in items if (it.get("id") or "").strip()]
|
||
if not task_ids:
|
||
return
|
||
rows = (
|
||
db.query(BizProductionTaskStep)
|
||
.filter(
|
||
BizProductionTaskStep.task_id.in_(task_ids),
|
||
BizProductionTaskStep.tenant_no == tenant_no,
|
||
BizProductionTaskStep.del_flag == 0,
|
||
)
|
||
.order_by(
|
||
BizProductionTaskStep.task_id.asc(),
|
||
BizProductionTaskStep.step_order.asc(),
|
||
BizProductionTaskStep.create_time.asc(),
|
||
)
|
||
.all()
|
||
)
|
||
step_dicts = [_step_to_dict(s) for s in rows]
|
||
if step_dicts:
|
||
from app.crud.production_task_step_assignee import enrich_step_assignees
|
||
|
||
enrich_step_assignees(db, step_dicts, tenant_no=tenant_no)
|
||
by_task: Dict[str, List[Dict[str, Any]]] = {tid: [] for tid in task_ids}
|
||
for sd in step_dicts:
|
||
tid = (sd.get("taskId") or "").strip()
|
||
if tid in by_task:
|
||
by_task[tid].append(sd)
|
||
for item in items:
|
||
tid = (item.get("id") or "").strip()
|
||
item["steps"] = by_task.get(tid, [])
|
||
|
||
|
||
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,
|
||
work_order_ids: Optional[List[str]] = None,
|
||
task_no: Optional[str] = None,
|
||
task_status: Optional[str] = None,
|
||
started_time_start: Optional[str] = None,
|
||
started_time_end: Optional[str] = None,
|
||
planned_start_time_start: Optional[str] = None,
|
||
planned_start_time_end: Optional[str] = None,
|
||
planned_start_time_mine_user_id: Optional[str] = None,
|
||
current_assignee_user_id: Optional[str] = None,
|
||
step_assignee_user_id: Optional[str] = None,
|
||
mine_history_user_id: Optional[str] = None,
|
||
work_order_type: Optional[str] = None,
|
||
reception_source: Optional[str] = None,
|
||
biz_type: Optional[str] = None,
|
||
user_info=None,
|
||
include_step_summary: bool = False,
|
||
exclude_void_protocol: bool = True,
|
||
) -> Tuple[List[Dict[str, Any]], int]:
|
||
def _filtered():
|
||
q = db.query(BizProductionTask).filter(
|
||
BizProductionTask.tenant_no == tenant_no,
|
||
BizProductionTask.del_flag == 0,
|
||
)
|
||
joined_wo = False
|
||
kw = (keyword or "").strip()
|
||
if kw:
|
||
like = f"%{kw}%"
|
||
wo_type = (work_order_type or "").strip()
|
||
if wo_type == "temporary":
|
||
keyword_conditions = [BizProductionWorkOrder.remark.like(like)]
|
||
else:
|
||
keyword_conditions = [
|
||
BizProductionTask.task_no.like(like),
|
||
BizProductionWorkOrder.work_order_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))
|
||
joined_wo = True
|
||
elif task_no and task_no.strip():
|
||
q = q.filter(BizProductionTask.task_no.like(f"%{task_no.strip()}%"))
|
||
wo_ids = [x.strip() for x in (work_order_ids or []) if x and str(x).strip()]
|
||
if wo_ids:
|
||
q = q.filter(BizProductionTask.work_order_id.in_(wo_ids))
|
||
elif 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)
|
||
)
|
||
pst_start = (planned_start_time_start or "").strip()
|
||
pst_end = (planned_start_time_end or "").strip()
|
||
if pst_start or pst_end:
|
||
if not joined_wo:
|
||
q = q.join(
|
||
BizProductionWorkOrder,
|
||
and_(
|
||
BizProductionWorkOrder.id == BizProductionTask.work_order_id,
|
||
BizProductionWorkOrder.del_flag == 0,
|
||
),
|
||
)
|
||
joined_wo = True
|
||
from app.crud.production_work_order import apply_planned_start_time_step_filter
|
||
|
||
q = apply_planned_start_time_step_filter(
|
||
q,
|
||
tenant_no=tenant_no,
|
||
work_order_id_column=BizProductionTask.work_order_id,
|
||
planned_start_time_start=planned_start_time_start,
|
||
planned_start_time_end=planned_start_time_end,
|
||
mine_user_id=planned_start_time_mine_user_id,
|
||
)
|
||
ca_uid = (current_assignee_user_id or "").strip()
|
||
if ca_uid:
|
||
q = q.filter(BizProductionTask.current_assignee_user_id == ca_uid)
|
||
sa_uid = (step_assignee_user_id or "").strip()
|
||
if sa_uid:
|
||
step_task_ids = (
|
||
db.query(BizProductionTaskStep.task_id)
|
||
.join(
|
||
BizProductionTaskStepAssignee,
|
||
BizProductionTaskStepAssignee.step_id == BizProductionTaskStep.id,
|
||
)
|
||
.filter(
|
||
BizProductionTaskStepAssignee.user_id == sa_uid,
|
||
BizProductionTaskStepAssignee.tenant_no == tenant_no,
|
||
BizProductionTaskStepAssignee.del_flag == 0,
|
||
BizProductionTaskStep.del_flag == 0,
|
||
)
|
||
.distinct()
|
||
.subquery()
|
||
)
|
||
q = q.filter(
|
||
or_(
|
||
BizProductionTask.id.in_(step_task_ids),
|
||
BizProductionTask.current_assignee_user_id == sa_uid,
|
||
)
|
||
)
|
||
mh_uid = (mine_history_user_id or "").strip()
|
||
if mh_uid:
|
||
handover_task_ids = (
|
||
db.query(BizProductionTaskHandover.task_id)
|
||
.filter(
|
||
BizProductionTaskHandover.from_user_id == mh_uid,
|
||
BizProductionTaskHandover.tenant_no == tenant_no,
|
||
BizProductionTaskHandover.del_flag == 0,
|
||
)
|
||
.distinct()
|
||
.subquery()
|
||
)
|
||
q = q.filter(
|
||
BizProductionTask.current_assignee_user_id != mh_uid,
|
||
BizProductionTask.id.in_(handover_task_ids),
|
||
)
|
||
wo_type = (work_order_type or "").strip()
|
||
if wo_type:
|
||
if not joined_wo:
|
||
q = q.join(
|
||
BizProductionWorkOrder,
|
||
and_(
|
||
BizProductionWorkOrder.id == BizProductionTask.work_order_id,
|
||
BizProductionWorkOrder.del_flag == 0,
|
||
),
|
||
)
|
||
joined_wo = True
|
||
q = q.filter(BizProductionWorkOrder.work_order_type == wo_type)
|
||
rs = (reception_source or "").strip()
|
||
if rs:
|
||
if not joined_wo:
|
||
q = q.join(
|
||
BizProductionWorkOrder,
|
||
and_(
|
||
BizProductionWorkOrder.id == BizProductionTask.work_order_id,
|
||
BizProductionWorkOrder.del_flag == 0,
|
||
),
|
||
)
|
||
joined_wo = True
|
||
q = q.filter(BizProductionWorkOrder.reception_source == rs)
|
||
bt = (biz_type or "").strip()
|
||
if bt:
|
||
if not joined_wo:
|
||
q = q.join(
|
||
BizProductionWorkOrder,
|
||
and_(
|
||
BizProductionWorkOrder.id == BizProductionTask.work_order_id,
|
||
BizProductionWorkOrder.del_flag == 0,
|
||
),
|
||
)
|
||
joined_wo = True
|
||
q = q.filter(BizProductionWorkOrder.biz_type == bt)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
q = apply_data_scope(q, BizProductionTask, user_info, db, tenant_no)
|
||
if exclude_void_protocol:
|
||
if not joined_wo:
|
||
q = q.join(
|
||
BizProductionWorkOrder,
|
||
and_(
|
||
BizProductionWorkOrder.id == BizProductionTask.work_order_id,
|
||
BizProductionWorkOrder.del_flag == 0,
|
||
),
|
||
)
|
||
from app.crud.production_work_order import apply_exclude_inactive_protocol_filter
|
||
|
||
q = apply_exclude_inactive_protocol_filter(q, exclude=True)
|
||
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] = {}
|
||
proto_void_type_map: Dict[str, str] = {}
|
||
if cell_wo_ids:
|
||
for p in (
|
||
db.query(
|
||
BizCollectionProtocol.id,
|
||
BizCollectionProtocol.protocol_status,
|
||
BizCollectionProtocol.void_type,
|
||
)
|
||
.filter(BizCollectionProtocol.id.in_(cell_wo_ids), BizCollectionProtocol.del_flag == 0)
|
||
.all()
|
||
):
|
||
proto_status_map[p.id] = p.protocol_status or ""
|
||
proto_void_type_map[p.id] = p.void_type or ""
|
||
donation_status_map: Dict[str, str] = {}
|
||
donation_void_type_map: Dict[str, str] = {}
|
||
if donation_wo_ids:
|
||
for d in (
|
||
db.query(
|
||
BizResearchDonation.id,
|
||
BizResearchDonation.donation_status,
|
||
BizResearchDonation.void_type,
|
||
)
|
||
.filter(BizResearchDonation.id.in_(donation_wo_ids), BizResearchDonation.del_flag == 0)
|
||
.all()
|
||
):
|
||
donation_status_map[d.id] = d.donation_status or ""
|
||
donation_void_type_map[d.id] = d.void_type or ""
|
||
from app.core.protocol_status_display import format_protocol_status_label
|
||
|
||
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, "")
|
||
_vt = donation_void_type_map.get(wo.collection_protocol_id, "")
|
||
item["protocolStatus"] = _st
|
||
item["voidType"] = _vt
|
||
item["protocolStatusLabel"] = format_protocol_status_label(
|
||
_st, _vt, kind="donation", db=db, tenant_no=tenant_no
|
||
)
|
||
else:
|
||
_st = proto_status_map.get(wo.collection_protocol_id, "")
|
||
_vt = proto_void_type_map.get(wo.collection_protocol_id, "")
|
||
item["protocolStatus"] = _st
|
||
item["voidType"] = _vt
|
||
item["protocolStatusLabel"] = format_protocol_status_label(
|
||
_st, _vt, kind="protocol", db=db, tenant_no=tenant_no
|
||
)
|
||
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)
|
||
_enrich_assignee_names(db, items)
|
||
if include_step_summary and items:
|
||
_enrich_tasks_with_step_summaries(db, items, tenant_no=tenant_no)
|
||
return items, int(total)
|
||
|
||
|
||
def list_mine_production_tasks(
|
||
db: Session,
|
||
*,
|
||
assignee_user_id: str,
|
||
tenant_no: str,
|
||
scope: str = "current",
|
||
page: int = 1,
|
||
page_size: int = 10,
|
||
keyword: Optional[str] = None,
|
||
task_status: Optional[str] = None,
|
||
work_order_type: Optional[str] = None,
|
||
reception_source: Optional[str] = None,
|
||
biz_type: Optional[str] = None,
|
||
include_step_summary: bool = False,
|
||
exclude_void_protocol: bool = True,
|
||
planned_start_time_start: Optional[str] = None,
|
||
planned_start_time_end: Optional[str] = None,
|
||
) -> Tuple[List[Dict[str, Any]], int]:
|
||
uid = (assignee_user_id or "").strip()
|
||
if not uid:
|
||
return [], 0
|
||
pst_start = (planned_start_time_start or "").strip()
|
||
pst_end = (planned_start_time_end or "").strip()
|
||
mine_planned_user_id = uid if (pst_start or pst_end) else None
|
||
sc = (scope or "current").strip().lower()
|
||
if sc == "history":
|
||
items, total = list_production_tasks(
|
||
db,
|
||
tenant_no=tenant_no,
|
||
page=page,
|
||
page_size=page_size,
|
||
keyword=keyword,
|
||
task_status=task_status,
|
||
mine_history_user_id=uid,
|
||
work_order_type=work_order_type,
|
||
reception_source=reception_source,
|
||
biz_type=biz_type,
|
||
include_step_summary=include_step_summary,
|
||
exclude_void_protocol=exclude_void_protocol,
|
||
planned_start_time_start=planned_start_time_start,
|
||
planned_start_time_end=planned_start_time_end,
|
||
planned_start_time_mine_user_id=mine_planned_user_id,
|
||
)
|
||
_enrich_handover_times(db, items, from_user_id=uid, tenant_no=tenant_no)
|
||
return items, total
|
||
return list_production_tasks(
|
||
db,
|
||
tenant_no=tenant_no,
|
||
page=page,
|
||
page_size=page_size,
|
||
keyword=keyword,
|
||
task_status=task_status,
|
||
step_assignee_user_id=uid,
|
||
work_order_type=work_order_type,
|
||
reception_source=reception_source,
|
||
biz_type=biz_type,
|
||
include_step_summary=include_step_summary,
|
||
exclude_void_protocol=exclude_void_protocol,
|
||
planned_start_time_start=planned_start_time_start,
|
||
planned_start_time_end=planned_start_time_end,
|
||
planned_start_time_mine_user_id=mine_planned_user_id,
|
||
)
|
||
|
||
|
||
def get_production_task_detail(
|
||
db: Session,
|
||
task_id: str,
|
||
tenant_no: str,
|
||
*,
|
||
include_step_sub_tables: bool = True,
|
||
) -> 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["workOrderType"] = wo_type
|
||
if wo_type == "outbound":
|
||
result["bizType"] = getattr(wo, "biz_type", None) or ""
|
||
elif wo_type == "inbound":
|
||
result["receptionSource"] = (
|
||
getattr(wo, "reception_source", "cell_storage") or "cell_storage"
|
||
)
|
||
result["sampleCode"] = wo.sample_code or ""
|
||
if wo.reception_id:
|
||
rec = (
|
||
db.query(BizSampleReception)
|
||
.filter(
|
||
BizSampleReception.id == wo.reception_id,
|
||
BizSampleReception.tenant_no == tenant_no,
|
||
BizSampleReception.del_flag == 0,
|
||
)
|
||
.first()
|
||
)
|
||
if rec and (rec.lab_storage_code or "").strip():
|
||
result["labStorageCode"] = (rec.lab_storage_code or "").strip()
|
||
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, BizResearchDonation.void_type)
|
||
.filter(BizResearchDonation.id == _cpid, BizResearchDonation.del_flag == 0)
|
||
.first()
|
||
)
|
||
_st = d.donation_status or "" if d else ""
|
||
_vt = d.void_type or "" if d else ""
|
||
result["protocolStatus"] = _st
|
||
result["voidType"] = _vt
|
||
from app.core.protocol_status_display import format_protocol_status_label
|
||
|
||
result["protocolStatusLabel"] = format_protocol_status_label(
|
||
_st, _vt, kind="donation", db=db, tenant_no=tenant_no
|
||
)
|
||
elif _cpid:
|
||
p = (
|
||
db.query(BizCollectionProtocol.protocol_status, BizCollectionProtocol.void_type)
|
||
.filter(BizCollectionProtocol.id == _cpid, BizCollectionProtocol.del_flag == 0)
|
||
.first()
|
||
)
|
||
_st = p.protocol_status or "" if p else ""
|
||
_vt = p.void_type or "" if p else ""
|
||
result["protocolStatus"] = _st
|
||
result["voidType"] = _vt
|
||
from app.core.protocol_status_display import format_protocol_status_label
|
||
|
||
result["protocolStatusLabel"] = format_protocol_status_label(
|
||
_st, _vt, kind="protocol", db=db, tenant_no=tenant_no
|
||
)
|
||
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_ids = [s.id for s in steps]
|
||
sub_tables_by_step: Dict[str, Dict[str, Any]] = {}
|
||
if include_step_sub_tables and step_ids:
|
||
sub_tables_by_step = _batch_load_step_sub_tables(db, step_ids)
|
||
|
||
step_list = []
|
||
for s in steps:
|
||
sd = _step_to_dict(s)
|
||
sd.update(sub_tables_by_step.get(s.id) or _empty_step_sub_tables())
|
||
step_list.append(sd)
|
||
|
||
from app.crud.production_task_step_assignee import enrich_step_assignees, enrich_step_completed_by_names
|
||
|
||
enrich_step_assignees(db, step_list, tenant_no=tenant_no)
|
||
enrich_step_completed_by_names(db, step_list)
|
||
|
||
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])
|
||
|
||
sample_codes: List[str] = []
|
||
task_sample_code = (result.get("sampleCode") or "").strip()
|
||
if task_sample_code:
|
||
sample_codes.append(task_sample_code)
|
||
for item in input_dicts:
|
||
if item.get("inputType") == "sample":
|
||
sc = (item.get("inputCode") or "").strip()
|
||
if sc:
|
||
sample_codes.append(sc)
|
||
if sample_codes:
|
||
from app.crud import sample_reception as reception_svc
|
||
|
||
code_map = reception_svc.get_lab_storage_code_map_by_sample_codes(
|
||
db, tenant_no, sample_codes
|
||
)
|
||
if task_sample_code and code_map.get(task_sample_code) and not result.get("labStorageCode"):
|
||
result["labStorageCode"] = code_map[task_sample_code]
|
||
for item in input_dicts:
|
||
if item.get("inputType") == "sample":
|
||
sc = (item.get("inputCode") or "").strip()
|
||
item["labStorageCode"] = code_map.get(sc, "")
|
||
|
||
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 _build_process_library_for_editor(
|
||
processes: List[Dict[str, Any]],
|
||
categories: List[Dict[str, Any]],
|
||
) -> List[Dict[str, Any]]:
|
||
"""按分类分组工艺库,结构与前端 route-config 左栏一致。"""
|
||
cat_map: Dict[str, Dict[str, Any]] = {}
|
||
u_key = "__uncategorized__"
|
||
cat_map[u_key] = {"categoryId": u_key, "categoryName": "未分类", "processes": []}
|
||
for cat in categories or []:
|
||
cat_id = (cat.get("id") or "").strip()
|
||
if not cat_id:
|
||
continue
|
||
cat_map[cat_id] = {
|
||
"categoryId": cat_id,
|
||
"categoryName": cat.get("categoryName") or cat.get("name") or "未命名分类",
|
||
"processes": [],
|
||
}
|
||
for proc in processes or []:
|
||
cat_id = (proc.get("categoryId") or "").strip() or u_key
|
||
if cat_id not in cat_map:
|
||
cat_map[cat_id] = {"categoryId": cat_id, "categoryName": "未知分类", "processes": []}
|
||
cat_map[cat_id]["processes"].append(proc)
|
||
return list(cat_map.values())
|
||
|
||
|
||
def get_production_task_editor_init(
|
||
db: Session,
|
||
task_id: str,
|
||
tenant_no: str,
|
||
*,
|
||
user_info=None,
|
||
include_reference: bool = True,
|
||
) -> Optional[Dict[str, Any]]:
|
||
"""编辑生产任务弹窗初始化:任务详情 + 工艺库 + 步骤配置参考数据(单次聚合)。"""
|
||
task = get_production_task_detail(
|
||
db, task_id, tenant_no, include_step_sub_tables=True
|
||
)
|
||
if not task:
|
||
return None
|
||
|
||
result: Dict[str, Any] = {"task": task}
|
||
if not include_reference:
|
||
return result
|
||
|
||
from app.crud import consumable as consumable_svc
|
||
from app.crud import equipment as equipment_svc
|
||
from app.crud import item_unit as item_unit_svc
|
||
from app.crud import output_type as output_type_svc
|
||
from app.crud import process as process_svc
|
||
from app.crud import process_category as process_category_svc
|
||
from app.crud import reagent as reagent_svc
|
||
from app.crud import sop_document as sop_document_svc
|
||
|
||
processes = process_svc.list_all_processes(db, tenant_no, user_info=user_info)
|
||
categories, _ = process_category_svc.list_process_categories(
|
||
db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
|
||
)
|
||
sops, _ = sop_document_svc.list_sop_documents(
|
||
db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
|
||
)
|
||
output_types, _ = output_type_svc.list_output_types(
|
||
db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
|
||
)
|
||
equipments, _ = equipment_svc.list_equipments(
|
||
db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
|
||
)
|
||
reagents, _ = reagent_svc.list_reagents(
|
||
db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
|
||
)
|
||
consumables, _ = consumable_svc.list_consumables(
|
||
db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
|
||
)
|
||
item_units, _ = item_unit_svc.list_item_units(
|
||
db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
|
||
)
|
||
|
||
result["processLibrary"] = _build_process_library_for_editor(processes, categories)
|
||
result["options"] = {
|
||
"processCategories": categories,
|
||
"sops": sops,
|
||
"outputTypes": output_types,
|
||
"equipments": equipments,
|
||
"reagents": reagents,
|
||
"consumables": consumables,
|
||
"itemUnits": item_units,
|
||
}
|
||
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)
|
||
if "equipment_ids" in data:
|
||
eids, enames = resolve_equipment_list_snapshot(db, data.get("equipment_ids"), tenant_no)
|
||
task.equipment_ids = eids
|
||
task.equipment_name = enames
|
||
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 _force_complete_task_steps(
|
||
db: Session,
|
||
task_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
now: datetime,
|
||
) -> int:
|
||
"""将任务下未完成步骤置为 ywc;不 commit。返回更新条数。"""
|
||
steps = (
|
||
db.query(BizProductionTaskStep)
|
||
.filter(
|
||
and_(
|
||
BizProductionTaskStep.task_id == task_id,
|
||
BizProductionTaskStep.tenant_no == tenant_no,
|
||
BizProductionTaskStep.del_flag == 0,
|
||
BizProductionTaskStep.step_status != "ywc",
|
||
)
|
||
)
|
||
.all()
|
||
)
|
||
for step in steps:
|
||
step.step_status = "ywc"
|
||
if user_id:
|
||
step.completed_by = user_id
|
||
step.completed_time = now
|
||
step.update_by = user_id
|
||
step.update_time = now
|
||
db.add(step)
|
||
return len(steps)
|
||
|
||
|
||
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("须选择生产结论(成功或失败)")
|
||
if previous_status != "ywc":
|
||
_force_complete_task_steps(db, task_id, tenant_no, user_id, now)
|
||
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,
|
||
estimated_duration=proc.estimated_duration,
|
||
sop_ids=proc.sop_ids,
|
||
sop_content=proc.sop_content,
|
||
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,
|
||
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,
|
||
sop_content=(data.get("sop_content") if str(data.get("sop_content") or "").strip() else 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()
|
||
configured_reagents = db.query(BizProductionTaskStepConfiguredReagent).filter(
|
||
and_(BizProductionTaskStepConfiguredReagent.step_id == step.id, BizProductionTaskStepConfiguredReagent.del_flag == 0)
|
||
).order_by(BizProductionTaskStepConfiguredReagent.order_num.asc(), BizProductionTaskStepConfiguredReagent.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["configuredReagents"] = [_step_configured_reagent_to_dict(r) for r in configured_reagents]
|
||
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", "estimated_duration", "actual_duration",
|
||
"sop_ids", "sop_content", "step_status", "remark",
|
||
]:
|
||
val = data.get(field)
|
||
if val is not None:
|
||
if field == "step_order":
|
||
setattr(step, field, int(val))
|
||
elif field == "sop_content":
|
||
setattr(step, field, val if str(val).strip() else None)
|
||
else:
|
||
setattr(step, field, (val or "").strip() or None)
|
||
if data.get("started_time"):
|
||
parsed = parse_datetime(str(data["started_time"]).strip())
|
||
if parsed is None:
|
||
raise ValueError("开始时间格式无效")
|
||
step.started_time = parsed
|
||
if data.get("completed_time"):
|
||
parsed = parse_datetime(str(data["completed_time"]).strip())
|
||
if parsed is None:
|
||
raise ValueError("完成时间格式无效")
|
||
step.completed_time = parsed
|
||
new_status = (data.get("step_status") or "").strip()
|
||
if new_status == "ywc" and user_id:
|
||
step.completed_by = user_id
|
||
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, BizProductionTaskStepConfiguredReagent, "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)
|
||
|
||
usage_items: List[Dict[str, Any]] = []
|
||
for idx, it in enumerate(items):
|
||
req_id = (
|
||
(it.get("requisition_usage_id") or it.get("requisitionUsageId") or "").strip()
|
||
)
|
||
_require_requisition_for_catalog_material(
|
||
db,
|
||
tenant_no,
|
||
it.get("reagent_code"),
|
||
"reagent",
|
||
it.get("actual_usage"),
|
||
req_id,
|
||
)
|
||
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,
|
||
requisition_usage_id=req_id 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,
|
||
))
|
||
if req_id:
|
||
usage_items.append({
|
||
"requisitionUsageId": req_id,
|
||
"qty": it.get("actual_usage"),
|
||
"usageUnit": it.get("unit"),
|
||
"remark": it.get("remark"),
|
||
})
|
||
|
||
from app.crud import production_material_usage as pm_usage_crud
|
||
pm_usage_crud.register_step_usage_from_requisition(
|
||
db, step_id, step_name, task_id, task_no, usage_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)
|
||
|
||
usage_items: List[Dict[str, Any]] = []
|
||
for idx, it in enumerate(items):
|
||
req_id = (
|
||
(it.get("requisition_usage_id") or it.get("requisitionUsageId") or "").strip()
|
||
)
|
||
_require_requisition_for_catalog_material(
|
||
db,
|
||
tenant_no,
|
||
it.get("consumable_code"),
|
||
"consumable",
|
||
it.get("actual_usage"),
|
||
req_id,
|
||
)
|
||
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,
|
||
requisition_usage_id=req_id 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,
|
||
))
|
||
if req_id:
|
||
usage_items.append({
|
||
"requisitionUsageId": req_id,
|
||
"qty": it.get("actual_usage"),
|
||
"usageUnit": it.get("unit"),
|
||
"remark": it.get("remark"),
|
||
})
|
||
|
||
from app.crud import production_material_usage as pm_usage_crud
|
||
pm_usage_crud.register_step_usage_from_requisition(
|
||
db, step_id, step_name, task_id, task_no, usage_items, tenant_no, user_id, "",
|
||
)
|
||
|
||
db.commit()
|
||
except Exception:
|
||
db.rollback()
|
||
raise
|
||
|
||
|
||
def replace_step_configured_reagents(
|
||
db: Session, step_id: str, tenant_no: str,
|
||
user_id: Optional[str], items: List[Dict[str, Any]],
|
||
) -> None:
|
||
now = datetime.now()
|
||
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, BizProductionTaskStepConfiguredReagent, "step_id", step_id, now)
|
||
|
||
usage_items: List[Dict[str, Any]] = []
|
||
for idx, it in enumerate(items):
|
||
req_id = (
|
||
(it.get("requisition_usage_id") or it.get("requisitionUsageId") or "").strip()
|
||
)
|
||
_require_requisition_for_catalog_material(
|
||
db,
|
||
tenant_no,
|
||
it.get("configured_reagent_code"),
|
||
"configured_reagent",
|
||
it.get("actual_usage"),
|
||
req_id,
|
||
)
|
||
db.add(BizProductionTaskStepConfiguredReagent(
|
||
id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
|
||
configured_reagent_code=(it.get("configured_reagent_code") or "").strip() or None,
|
||
configured_reagent_name=(it.get("configured_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,
|
||
requisition_usage_id=req_id 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,
|
||
))
|
||
if req_id:
|
||
usage_items.append({
|
||
"requisitionUsageId": req_id,
|
||
"qty": it.get("actual_usage"),
|
||
"usageUnit": it.get("unit"),
|
||
"remark": it.get("remark"),
|
||
})
|
||
|
||
from app.crud import production_material_usage as pm_usage_crud
|
||
pm_usage_crud.register_step_usage_from_requisition(
|
||
db, step_id, step_name, task_id, task_no, usage_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 spawn_temporary_work_order_from_task(
|
||
db: Session,
|
||
source_task_id: str,
|
||
*,
|
||
tenant_no: str,
|
||
user_id: Optional[str] = None,
|
||
create_dept_id: str = "",
|
||
remark: str = "",
|
||
) -> Dict[str, Any]:
|
||
"""从生产任务创建临时生产工单,并返回待复制到新任务的原材料载荷(不含产出)。"""
|
||
task = (
|
||
db.query(BizProductionTask)
|
||
.filter(
|
||
and_(
|
||
BizProductionTask.id == source_task_id,
|
||
BizProductionTask.tenant_no == tenant_no,
|
||
BizProductionTask.del_flag == 0,
|
||
)
|
||
)
|
||
.first()
|
||
)
|
||
if not task:
|
||
raise ValueError("任务不存在")
|
||
|
||
inputs = (
|
||
db.query(BizProductionTaskInput)
|
||
.filter(
|
||
and_(
|
||
BizProductionTaskInput.task_id == source_task_id,
|
||
BizProductionTaskInput.del_flag == 0,
|
||
)
|
||
)
|
||
.order_by(BizProductionTaskInput.order_num.asc(), BizProductionTaskInput.create_time.asc())
|
||
.all()
|
||
)
|
||
if not inputs:
|
||
raise ValueError("请先在原材料中填写并保存后再派发临时工单")
|
||
|
||
copy_items: List[Dict[str, Any]] = []
|
||
for inp in inputs:
|
||
inp_type = (inp.input_type or "").strip()
|
||
inp_id = (inp.input_id or "").strip()
|
||
inp_code = (inp.input_code or "").strip()
|
||
inp_name = (inp.input_name or "").strip()
|
||
if not inp_type or (not inp_id and not inp_code and not inp_name):
|
||
raise ValueError("原材料数据不完整,请保存后再派发")
|
||
copy_items.append({
|
||
"inputType": inp_type,
|
||
"inputId": inp_id or None,
|
||
"inputCode": inp_code or None,
|
||
"inputName": inp_name or None,
|
||
"remark": (inp.remark or "").strip() or None,
|
||
})
|
||
|
||
user_remark = (remark or "").strip()
|
||
full_remark = user_remark[:512] if user_remark else ""
|
||
|
||
from app.crud import production_work_order as production_work_order_svc
|
||
|
||
new_wo = production_work_order_svc.create_temp_production_work_order(
|
||
db,
|
||
tenant_no=tenant_no,
|
||
user_id=user_id,
|
||
create_dept_id=create_dept_id,
|
||
remark=full_remark,
|
||
)
|
||
return {
|
||
"workOrder": production_work_order_svc._temp_wo_to_dict(new_wo),
|
||
"sourceTaskId": task.id,
|
||
"sourceTaskNo": task.task_no or "",
|
||
"inputItems": copy_items,
|
||
}
|
||
|
||
|
||
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]):
|
||
"""为细胞产品创建库位占用记录"""
|
||
slot_id = (product.storage_slot_id or "").strip()
|
||
if not slot_id:
|
||
return
|
||
occupancy_svc.assert_slot_vacant(
|
||
db, slot_id, tenant_no=tenant_no, exclude_product_id=product.id,
|
||
)
|
||
occupancy_svc._add_stored_occupancy_no_commit(
|
||
db,
|
||
slot_id=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",
|
||
tenant_no=tenant_no,
|
||
user_id=user_id,
|
||
create_dept_id=product.create_dept_id or "",
|
||
)
|
||
|
||
|
||
def _output_item_has_spec(it: Dict[str, Any]) -> bool:
|
||
return any(k in it for k in ("spec_text", "specText", "spec_name", "specName"))
|
||
|
||
|
||
def _output_item_spec_text(it: Dict[str, Any]) -> Optional[str]:
|
||
for key in ("spec_text", "specText", "spec_name", "specName"):
|
||
if key not in it:
|
||
continue
|
||
val = it[key]
|
||
if val is None:
|
||
return None
|
||
s = str(val).strip()
|
||
return s or None
|
||
val = it.get("spec_text") or it.get("specText") or it.get("spec_name") or it.get("specName")
|
||
if val is None:
|
||
return None
|
||
s = str(val).strip()
|
||
return s or None
|
||
|
||
|
||
def _update_product_from_output(
|
||
db: Session,
|
||
product: BizCellProduct,
|
||
it: Dict[str, Any],
|
||
tenant_no: str,
|
||
user_id,
|
||
now,
|
||
):
|
||
"""从 output item 更新细胞产品字段"""
|
||
new_code = it.get("product_code") if it.get("product_code") is not None else it.get("productCode")
|
||
if new_code is not None:
|
||
code = (str(new_code) or "").strip()
|
||
if code and code != (product.product_code or "").strip():
|
||
if _cp_product_code_exists(db, tenant_no, code, exclude_id=product.id):
|
||
raise ValueError(f"产品编码 {code} 已存在")
|
||
product.product_code = code
|
||
field_map = [
|
||
("product_name", "product_name", "productName"),
|
||
("product_type_id", "product_type_id", "productTypeId"),
|
||
("spec_id", "spec_id", "specId"),
|
||
("spec_text", "spec_text", "specText"),
|
||
("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)
|
||
spec_text = _output_item_spec_text(it)
|
||
if _output_item_has_spec(it):
|
||
product.spec_text = spec_text
|
||
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()
|
||
|
||
batch_slot_ids: List[str] = []
|
||
for it in items:
|
||
sid = (it.get("storage_slot_id") or it.get("storageSlotId") or "").strip()
|
||
if sid:
|
||
batch_slot_ids.append(sid)
|
||
occupancy_svc.assert_batch_slots_unique(batch_slot_ids)
|
||
|
||
# --- 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:
|
||
# 作废并软删:避免同编码再入库时留下 voided+frozen 双行,污染各类选择器
|
||
product.product_status = "voided"
|
||
product.del_flag = 1
|
||
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(db, product, it, tenant_no, 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,
|
||
spec_text=_output_item_spec_text(it),
|
||
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,
|
||
estimated_duration=src.estimated_duration,
|
||
actual_duration=src.actual_duration, sop_ids=src.sop_ids,
|
||
sop_content=src.sop_content,
|
||
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)))
|
||
|
||
|
||
_PRODUCTION_TAB_STEP_COUNT_KEYS = (
|
||
"inbound_cell_storage",
|
||
"inbound_research_donation",
|
||
"outbound_pilot_service",
|
||
"outbound_basic_experiment",
|
||
"temporary",
|
||
)
|
||
|
||
|
||
def _empty_production_tab_step_counts() -> Dict[str, Dict[str, int]]:
|
||
return {
|
||
k: {"overdueIncomplete": 0, "futureIncomplete": 0}
|
||
for k in _PRODUCTION_TAB_STEP_COUNT_KEYS
|
||
}
|
||
|
||
|
||
def _resolve_production_tab_key(
|
||
wo_type: Optional[str],
|
||
reception_source: Optional[str],
|
||
biz_type: Optional[str],
|
||
) -> Optional[str]:
|
||
wt = (wo_type or "inbound").strip() or "inbound"
|
||
if wt == "temporary":
|
||
return "temporary"
|
||
if wt == "outbound":
|
||
bt = (biz_type or "pilot_service").strip() or "pilot_service"
|
||
if bt == "basic_experiment":
|
||
return "outbound_basic_experiment"
|
||
if bt == "clinical_application":
|
||
return None
|
||
return "outbound_pilot_service"
|
||
rs = (reception_source or "cell_storage").strip() or "cell_storage"
|
||
return "inbound_research_donation" if rs == "research_donation" else "inbound_cell_storage"
|
||
|
||
|
||
def _aggregate_production_tab_step_field(
|
||
counts: Dict[str, Dict[str, int]],
|
||
rows,
|
||
field: str,
|
||
) -> None:
|
||
for wo_type, rs, bt, cnt in rows:
|
||
tab_key = _resolve_production_tab_key(wo_type, rs, bt)
|
||
if not tab_key or tab_key not in counts:
|
||
continue
|
||
counts[tab_key][field] += int(cnt or 0)
|
||
|
||
|
||
def _base_active_step_count_query(
|
||
db: Session,
|
||
*,
|
||
tenant_no: str,
|
||
user_info=None,
|
||
mine_user_id: Optional[str] = None,
|
||
):
|
||
q = (
|
||
db.query(
|
||
BizProductionWorkOrder.work_order_type,
|
||
BizProductionWorkOrder.reception_source,
|
||
BizProductionWorkOrder.biz_type,
|
||
func.count(BizProductionTaskStep.id),
|
||
)
|
||
.select_from(BizProductionTaskStep)
|
||
.join(
|
||
BizProductionTask,
|
||
and_(
|
||
BizProductionTask.id == BizProductionTaskStep.task_id,
|
||
BizProductionTask.del_flag == 0,
|
||
BizProductionTask.tenant_no == tenant_no,
|
||
),
|
||
)
|
||
.join(
|
||
BizProductionWorkOrder,
|
||
and_(
|
||
BizProductionWorkOrder.id == BizProductionTask.work_order_id,
|
||
BizProductionWorkOrder.del_flag == 0,
|
||
BizProductionWorkOrder.tenant_no == tenant_no,
|
||
),
|
||
)
|
||
.filter(
|
||
BizProductionTaskStep.del_flag == 0,
|
||
BizProductionTaskStep.tenant_no == tenant_no,
|
||
BizProductionWorkOrder.work_order_status.in_(("dpz", "jxz")),
|
||
BizProductionTask.task_status.in_(("dpz", "jxz")),
|
||
BizProductionTaskStep.step_status != "ywc",
|
||
)
|
||
)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
|
||
q = apply_data_scope(q, BizProductionWorkOrder, user_info, db, tenant_no)
|
||
|
||
uid = (mine_user_id or "").strip()
|
||
if uid:
|
||
step_task_ids = (
|
||
db.query(BizProductionTaskStep.task_id)
|
||
.join(
|
||
BizProductionTaskStepAssignee,
|
||
BizProductionTaskStepAssignee.step_id == BizProductionTaskStep.id,
|
||
)
|
||
.filter(
|
||
BizProductionTaskStepAssignee.user_id == uid,
|
||
BizProductionTaskStepAssignee.tenant_no == tenant_no,
|
||
BizProductionTaskStepAssignee.del_flag == 0,
|
||
BizProductionTaskStep.del_flag == 0,
|
||
)
|
||
.distinct()
|
||
.subquery()
|
||
)
|
||
q = q.filter(
|
||
or_(
|
||
BizProductionTask.id.in_(step_task_ids),
|
||
BizProductionTask.current_assignee_user_id == uid,
|
||
)
|
||
)
|
||
from app.crud.production_work_order import apply_exclude_inactive_protocol_filter
|
||
|
||
q = apply_exclude_inactive_protocol_filter(q, exclude=True)
|
||
return q
|
||
|
||
|
||
def count_step_tab_badges_by_tab(
|
||
db: Session,
|
||
*,
|
||
tenant_no: str,
|
||
user_info=None,
|
||
mine_user_id: Optional[str] = None,
|
||
) -> Dict[str, Dict[str, int]]:
|
||
"""按 Tab 统计工艺步骤角标:计划开始已到期未完成数 + 计划开始在今日之后未完成数。"""
|
||
counts = _empty_production_tab_step_counts()
|
||
today_end = datetime.now().replace(hour=23, minute=59, second=59, microsecond=0)
|
||
|
||
overdue_rows = (
|
||
_base_active_step_count_query(
|
||
db, tenant_no=tenant_no, user_info=user_info, mine_user_id=mine_user_id
|
||
)
|
||
.filter(
|
||
BizProductionTaskStep.planned_start_time.isnot(None),
|
||
BizProductionTaskStep.planned_start_time <= today_end,
|
||
)
|
||
.group_by(
|
||
BizProductionWorkOrder.work_order_type,
|
||
BizProductionWorkOrder.reception_source,
|
||
BizProductionWorkOrder.biz_type,
|
||
)
|
||
.all()
|
||
)
|
||
_aggregate_production_tab_step_field(counts, overdue_rows, "overdueIncomplete")
|
||
|
||
future_rows = (
|
||
_base_active_step_count_query(
|
||
db, tenant_no=tenant_no, user_info=user_info, mine_user_id=mine_user_id
|
||
)
|
||
.filter(
|
||
BizProductionTaskStep.planned_start_time.isnot(None),
|
||
BizProductionTaskStep.planned_start_time > today_end,
|
||
)
|
||
.group_by(
|
||
BizProductionWorkOrder.work_order_type,
|
||
BizProductionWorkOrder.reception_source,
|
||
BizProductionWorkOrder.biz_type,
|
||
)
|
||
.all()
|
||
)
|
||
_aggregate_production_tab_step_field(counts, future_rows, "futureIncomplete")
|
||
return counts
|
||
|
||
|
||
def count_production_work_order_tab_step_badges(
|
||
db: Session, *, tenant_no: str, user_info=None
|
||
) -> Dict[str, Dict[str, int]]:
|
||
return count_step_tab_badges_by_tab(db, tenant_no=tenant_no, user_info=user_info)
|
||
|
||
|
||
def count_mine_production_work_order_tab_step_badges(
|
||
db: Session, *, tenant_no: str, assignee_user_id: str
|
||
) -> Dict[str, Dict[str, int]]:
|
||
return count_step_tab_badges_by_tab(
|
||
db, tenant_no=tenant_no, mine_user_id=assignee_user_id
|
||
)
|