| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778 |
- """生产任务集合 CRUD"""
- from __future__ import annotations
- from datetime import datetime
- from typing import Any, Dict, List, Optional, Set, Tuple
- from sqlalchemy import func
- from sqlalchemy.orm import Session
- from app.core.utils import generate_uuid
- from app.models.models import (
- BizProcessRoute,
- BizProductionTask,
- BizProductionTaskSet,
- BizProductionTaskSetItem,
- BizProductionWorkOrder,
- )
- from app.utils.production_task_name_placeholders import (
- apply_task_name_placeholders,
- revert_task_name_placeholders,
- )
- def resolve_work_order_lab_storage_code(
- db: Session,
- *,
- tenant_no: str,
- wo: BizProductionWorkOrder,
- ) -> str:
- """解析工单对应实验室入库编码(样本接收优先,样本编码回退)。"""
- from app.crud import sample_reception as sample_reception_svc
- from app.models.models import BizSampleReception
- reception_id = (getattr(wo, "reception_id", None) or "").strip()
- if reception_id:
- rec = (
- db.query(BizSampleReception)
- .filter(
- BizSampleReception.id == reception_id,
- BizSampleReception.tenant_no == tenant_no,
- BizSampleReception.del_flag == 0,
- )
- .first()
- )
- if rec:
- code = (rec.lab_storage_code or "").strip()
- if code:
- return code
- sample_code = (getattr(wo, "sample_code", None) or "").strip()
- if not sample_code:
- return ""
- source = (getattr(wo, "reception_source", None) or "").strip() or None
- code_map = sample_reception_svc.get_lab_storage_code_map_by_sample_codes(
- db, tenant_no, [sample_code], reception_source=source
- )
- return (code_map.get(sample_code) or "").strip()
- def _strip_or_none(raw: Any) -> Optional[str]:
- text = str(raw or "").strip()
- return text or None
- def normalize_item_payload(raw: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
- data = raw or {}
- task_name = str(data.get("taskName") or data.get("task_name") or "").strip()
- if not task_name:
- return None
- route_raw = data.get("routeId") if "routeId" in data else data.get("route_id")
- return {
- "task_name": task_name,
- "route_id": _strip_or_none(route_raw),
- "description": _strip_or_none(data.get("description")),
- "remark": _strip_or_none(data.get("remark")),
- }
- def skip_empty_named_tasks(tasks: List[Dict[str, Any]]) -> Tuple[List[Dict[str, Any]], int]:
- kept: List[Dict[str, Any]] = []
- skipped = 0
- for row in tasks or []:
- name = str(row.get("task_name") or row.get("taskName") or "").strip()
- if not name:
- skipped += 1
- continue
- route_raw = row.get("route_id") if "route_id" in row else row.get("routeId")
- name_raw = row.get("route_name") if "route_name" in row else row.get("routeName")
- kept.append(
- {
- "task_name": name,
- "route_id": _strip_or_none(route_raw),
- "route_name": _strip_or_none(name_raw),
- "description": _strip_or_none(row.get("description")),
- "remark": _strip_or_none(row.get("remark")),
- }
- )
- return kept, skipped
- def collect_missing_route_ids(items: List[Dict[str, Any]], existing_ids: Set[str]) -> List[str]:
- missing: List[str] = []
- seen: Set[str] = set()
- alive = existing_ids or set()
- for item in items or []:
- rid = _strip_or_none(item.get("route_id"))
- if not rid or rid in alive or rid in seen:
- continue
- seen.add(rid)
- missing.append(rid)
- return missing
- def total_pages(total: int, page_size: int) -> int:
- if page_size <= 0:
- return 0
- return (int(total) + int(page_size) - 1) // int(page_size)
- def _item_to_dict(r: BizProductionTaskSetItem) -> Dict[str, Any]:
- return {
- "id": r.id,
- "setId": r.set_id,
- "taskName": r.task_name or "",
- "routeId": r.route_id or "",
- "routeName": r.route_name or "",
- "description": r.description or "",
- "remark": r.remark or "",
- "orderNum": r.order_num if r.order_num is not None else 0,
- }
- def _set_to_dict(
- r: BizProductionTaskSet,
- item_count: int = 0,
- items: Optional[List[Dict[str, Any]]] = None,
- ) -> Dict[str, Any]:
- data: Dict[str, Any] = {
- "id": r.id,
- "setName": r.set_name or "",
- "status": r.status or "1",
- "itemCount": item_count,
- "orderNum": r.order_num if r.order_num is not None else 0,
- "remark": r.remark or "",
- "isBuiltin": getattr(r, "is_builtin", None) or "0",
- "createTime": r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else "",
- "updateTime": r.update_time.strftime("%Y-%m-%d %H:%M:%S") if r.update_time else "",
- }
- if items is not None:
- data["items"] = items
- return data
- def set_name_exists(
- db: Session, name: str, tenant_no: str, exclude_id: Optional[str] = None
- ) -> bool:
- n = (name or "").strip()
- if not n:
- return False
- q = db.query(BizProductionTaskSet).filter(
- BizProductionTaskSet.set_name == n,
- BizProductionTaskSet.tenant_no == tenant_no,
- BizProductionTaskSet.del_flag == 0,
- )
- if exclude_id:
- q = q.filter(BizProductionTaskSet.id != exclude_id)
- return q.first() is not None
- def _require_status(status: Optional[str]) -> str:
- st = (status or "1").strip()
- if st not in ("0", "1"):
- raise ValueError("状态无效")
- return st
- def _load_route_name_map(db: Session, tenant_no: str, route_ids: List[str]) -> Dict[str, str]:
- ids = [rid for rid in route_ids if rid]
- if not ids:
- return {}
- rows = (
- db.query(BizProcessRoute)
- .filter(
- BizProcessRoute.tenant_no == tenant_no,
- BizProcessRoute.del_flag == 0,
- BizProcessRoute.id.in_(ids),
- )
- .all()
- )
- return {r.id: (r.route_name or "") for r in rows}
- def _normalize_items_or_raise(raw_items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
- out: List[Dict[str, Any]] = []
- for raw in raw_items or []:
- item = normalize_item_payload(raw)
- if item:
- out.append(item)
- if not out:
- raise ValueError("请至少添加一条任务名称非空的条目")
- return out
- def _assert_routes_exist(
- db: Session, tenant_no: str, items: List[Dict[str, Any]]
- ) -> Dict[str, str]:
- wanted = [i["route_id"] for i in items if i.get("route_id")]
- name_map = _load_route_name_map(db, tenant_no, wanted)
- missing = collect_missing_route_ids(items, set(name_map.keys()))
- if missing:
- raise ValueError("存在无效或已删除的工艺路线")
- return name_map
- def _replace_items(
- db: Session,
- *,
- set_id: str,
- tenant_no: str,
- items: List[Dict[str, Any]],
- route_names: Dict[str, str],
- user_id: Optional[str],
- create_dept_id: str,
- ) -> None:
- now = datetime.now()
- existing = (
- db.query(BizProductionTaskSetItem)
- .filter(
- BizProductionTaskSetItem.set_id == set_id,
- BizProductionTaskSetItem.tenant_no == tenant_no,
- BizProductionTaskSetItem.del_flag == 0,
- )
- .all()
- )
- for row in existing:
- row.del_flag = 1
- row.update_by = user_id
- row.update_time = now
- db.add(row)
- for idx, item in enumerate(items, start=1):
- rid = item.get("route_id")
- db.add(
- BizProductionTaskSetItem(
- id=generate_uuid(),
- set_id=set_id,
- task_name=item["task_name"],
- route_id=rid,
- route_name=route_names.get(rid or "") if rid else None,
- description=item.get("description"),
- remark=item.get("remark"),
- tenant_no=tenant_no,
- order_num=idx,
- create_by=user_id,
- create_dept_id=create_dept_id or "",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- )
- def _item_count_map(db: Session, tenant_no: str, set_ids: List[str]) -> Dict[str, int]:
- if not set_ids:
- return {}
- rows = (
- db.query(BizProductionTaskSetItem.set_id, func.count(BizProductionTaskSetItem.id))
- .filter(
- BizProductionTaskSetItem.tenant_no == tenant_no,
- BizProductionTaskSetItem.del_flag == 0,
- BizProductionTaskSetItem.set_id.in_(set_ids),
- )
- .group_by(BizProductionTaskSetItem.set_id)
- .all()
- )
- return {sid: int(cnt) for sid, cnt in rows}
- def list_sets(
- db: Session,
- *,
- tenant_no: str,
- page: int = 1,
- page_size: int = 10,
- keyword: Optional[str] = None,
- status: Optional[str] = None,
- ) -> Tuple[List[Dict[str, Any]], int]:
- q = db.query(BizProductionTaskSet).filter(
- BizProductionTaskSet.tenant_no == tenant_no,
- BizProductionTaskSet.del_flag == 0,
- )
- kw = (keyword or "").strip()
- if kw:
- q = q.filter(BizProductionTaskSet.set_name.like(f"%{kw}%"))
- st = (status or "").strip()
- if st in ("0", "1"):
- q = q.filter(BizProductionTaskSet.status == st)
- total = q.count()
- rows = (
- q.order_by(BizProductionTaskSet.create_time.desc())
- .offset((page - 1) * page_size)
- .limit(page_size)
- .all()
- )
- counts = _item_count_map(db, tenant_no, [r.id for r in rows])
- return [_set_to_dict(r, item_count=counts.get(r.id, 0)) for r in rows], int(total)
- def list_active_sets(db: Session, *, tenant_no: str) -> List[Dict[str, Any]]:
- rows = (
- db.query(BizProductionTaskSet)
- .filter(
- BizProductionTaskSet.tenant_no == tenant_no,
- BizProductionTaskSet.del_flag == 0,
- BizProductionTaskSet.status == "1",
- )
- .order_by(BizProductionTaskSet.create_time.desc())
- .all()
- )
- counts = _item_count_map(db, tenant_no, [r.id for r in rows])
- return [_set_to_dict(r, item_count=counts.get(r.id, 0)) for r in rows]
- def get_set(db: Session, set_id: str, tenant_no: str) -> Optional[Dict[str, Any]]:
- row = (
- db.query(BizProductionTaskSet)
- .filter(
- BizProductionTaskSet.id == set_id,
- BizProductionTaskSet.tenant_no == tenant_no,
- BizProductionTaskSet.del_flag == 0,
- )
- .first()
- )
- if not row:
- return None
- items = (
- db.query(BizProductionTaskSetItem)
- .filter(
- BizProductionTaskSetItem.set_id == set_id,
- BizProductionTaskSetItem.tenant_no == tenant_no,
- BizProductionTaskSetItem.del_flag == 0,
- )
- .order_by(
- BizProductionTaskSetItem.order_num.asc(),
- BizProductionTaskSetItem.create_time.asc(),
- )
- .all()
- )
- return _set_to_dict(row, item_count=len(items), items=[_item_to_dict(i) for i in items])
- def is_super_admin_user(user: Optional[Dict[str, Any]]) -> bool:
- return bool(user) and (user.get("role") or "") == "super_admin"
- def assert_set_writable(
- db: Session,
- set_id: str,
- tenant_no: str,
- user: Optional[Dict[str, Any]] = None,
- ) -> BizProductionTaskSet:
- """内置模版仅超级管理员可写。"""
- row = (
- db.query(BizProductionTaskSet)
- .filter(
- BizProductionTaskSet.id == set_id,
- BizProductionTaskSet.tenant_no == tenant_no,
- BizProductionTaskSet.del_flag == 0,
- )
- .first()
- )
- if not row:
- raise ValueError("集合不存在")
- if (getattr(row, "is_builtin", None) or "0") == "1" and not is_super_admin_user(user):
- raise ValueError("内置模版仅超级管理员可修改")
- return row
- def create_set(
- db: Session,
- *,
- tenant_no: str,
- user_id: Optional[str] = None,
- create_dept_id: str = "",
- set_name: str,
- status: str = "1",
- remark: Optional[str] = None,
- items: Optional[List[Dict[str, Any]]] = None,
- ) -> Dict[str, Any]:
- name = (set_name or "").strip()
- if not name:
- raise ValueError("集合名称不能为空")
- if set_name_exists(db, name, tenant_no):
- raise ValueError("集合名称已存在")
- normalized = _normalize_items_or_raise(items or [])
- route_names = _assert_routes_exist(db, tenant_no, normalized)
- now = datetime.now()
- obj = BizProductionTaskSet(
- id=generate_uuid(),
- set_name=name,
- status=_require_status(status),
- tenant_no=tenant_no,
- order_num=0,
- remark=(remark or "").strip() or None,
- is_builtin="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(obj)
- db.flush()
- _replace_items(
- db,
- set_id=obj.id,
- tenant_no=tenant_no,
- items=normalized,
- route_names=route_names,
- user_id=user_id,
- create_dept_id=create_dept_id,
- )
- db.flush()
- detail = get_set(db, obj.id, tenant_no)
- if not detail:
- raise ValueError("集合创建失败")
- return detail
- def update_set(
- db: Session,
- set_id: str,
- *,
- tenant_no: str,
- user_id: Optional[str] = None,
- set_name: Optional[str] = None,
- status: Optional[str] = None,
- remark: Optional[str] = None,
- items: Optional[List[Dict[str, Any]]] = None,
- ) -> Dict[str, Any]:
- obj = (
- db.query(BizProductionTaskSet)
- .filter(
- BizProductionTaskSet.id == set_id,
- BizProductionTaskSet.tenant_no == tenant_no,
- BizProductionTaskSet.del_flag == 0,
- )
- .first()
- )
- if not obj:
- raise ValueError("集合不存在")
- if set_name is not None:
- name = set_name.strip()
- if not name:
- raise ValueError("集合名称不能为空")
- if set_name_exists(db, name, tenant_no, exclude_id=set_id):
- raise ValueError("集合名称已存在")
- obj.set_name = name
- if status is not None:
- obj.status = _require_status(status)
- if remark is not None:
- obj.remark = remark.strip() or None
- if items is not None:
- normalized = _normalize_items_or_raise(items)
- route_names = _assert_routes_exist(db, tenant_no, normalized)
- _replace_items(
- db,
- set_id=obj.id,
- tenant_no=tenant_no,
- items=normalized,
- route_names=route_names,
- user_id=user_id,
- create_dept_id=obj.create_dept_id or "",
- )
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- db.flush()
- detail = get_set(db, obj.id, tenant_no)
- if not detail:
- raise ValueError("集合不存在")
- return detail
- def delete_set(db: Session, set_id: str, *, tenant_no: str, user_id: Optional[str] = None) -> None:
- obj = (
- db.query(BizProductionTaskSet)
- .filter(
- BizProductionTaskSet.id == set_id,
- BizProductionTaskSet.tenant_no == tenant_no,
- BizProductionTaskSet.del_flag == 0,
- )
- .first()
- )
- if not obj:
- raise ValueError("集合不存在")
- now = datetime.now()
- obj.del_flag = 1
- obj.update_by = user_id
- obj.update_time = now
- db.add(obj)
- items = (
- db.query(BizProductionTaskSetItem)
- .filter(
- BizProductionTaskSetItem.set_id == set_id,
- BizProductionTaskSetItem.del_flag == 0,
- )
- .all()
- )
- for row in items:
- row.del_flag = 1
- row.update_by = user_id
- row.update_time = now
- db.add(row)
- db.flush()
- def create_set_from_work_order(
- db: Session,
- *,
- tenant_no: str,
- user_id: Optional[str] = None,
- create_dept_id: str = "",
- work_order_id: str,
- set_name: str,
- remark: Optional[str] = None,
- items: Optional[List[Dict[str, Any]]] = None,
- ) -> Dict[str, Any]:
- wo_id = (work_order_id or "").strip()
- wo = (
- db.query(BizProductionWorkOrder)
- .filter(
- BizProductionWorkOrder.id == wo_id,
- BizProductionWorkOrder.tenant_no == tenant_no,
- BizProductionWorkOrder.del_flag == 0,
- )
- .first()
- )
- if not wo:
- raise ValueError("工单不存在")
- if items is not None:
- raw = items
- else:
- tasks = (
- db.query(BizProductionTask)
- .filter(
- BizProductionTask.work_order_id == wo_id,
- BizProductionTask.tenant_no == tenant_no,
- BizProductionTask.del_flag == 0,
- )
- .order_by(BizProductionTask.create_time.asc(), BizProductionTask.id.asc())
- .all()
- )
- raw = [
- {
- "task_name": t.task_name,
- "route_id": t.route_id,
- "description": t.description,
- "remark": t.remark,
- }
- for t in tasks
- ]
- kept, skipped = skip_empty_named_tasks(raw)
- if not kept:
- raise ValueError("无可保存的生产任务(任务名称均为空)")
- lab_storage_code = resolve_work_order_lab_storage_code(
- db, tenant_no=tenant_no, wo=wo
- )
- route_ids = [i["route_id"] for i in kept if i.get("route_id")]
- name_map = _load_route_name_map(db, tenant_no, route_ids)
- payload_items = []
- for i in kept:
- rid = i.get("route_id")
- if rid and rid not in name_map:
- rid = None
- payload_items.append(
- {
- "taskName": revert_task_name_placeholders(
- i["task_name"] or "",
- lab_storage_code=lab_storage_code,
- ),
- "routeId": rid,
- "description": i.get("description"),
- "remark": i.get("remark"),
- }
- )
- detail = create_set(
- db,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=create_dept_id,
- set_name=set_name,
- status="1",
- remark=remark,
- items=payload_items,
- )
- return {**detail, "skippedEmptyNameCount": skipped}
- def create_tasks_from_set(
- db: Session,
- *,
- tenant_no: str,
- user_id: Optional[str] = None,
- create_dept_id: str = "",
- work_order_id: str,
- set_id: str,
- items: Optional[List[Dict[str, Any]]] = None,
- ) -> Dict[str, Any]:
- from app.crud import production_task as production_task_svc
- wo_id = (work_order_id or "").strip()
- wo = (
- db.query(BizProductionWorkOrder)
- .filter(
- BizProductionWorkOrder.id == wo_id,
- BizProductionWorkOrder.tenant_no == tenant_no,
- BizProductionWorkOrder.del_flag == 0,
- )
- .first()
- )
- if not wo:
- raise ValueError("工单不存在")
- status = getattr(wo, "work_order_status", None) or ""
- if status not in ("dpz", "jxz"):
- raise ValueError("仅待配置或进行中的工单可派发任务")
- header = (
- db.query(BizProductionTaskSet)
- .filter(
- BizProductionTaskSet.id == (set_id or "").strip(),
- BizProductionTaskSet.tenant_no == tenant_no,
- BizProductionTaskSet.del_flag == 0,
- )
- .first()
- )
- if not header:
- raise ValueError("任务组合模版不存在")
- if (header.status or "1") != "1":
- raise ValueError("任务组合模版已停用")
- if items is not None:
- payloads = _normalize_items_or_raise(items)
- else:
- rows = (
- db.query(BizProductionTaskSetItem)
- .filter(
- BizProductionTaskSetItem.set_id == header.id,
- BizProductionTaskSetItem.tenant_no == tenant_no,
- BizProductionTaskSetItem.del_flag == 0,
- )
- .order_by(
- BizProductionTaskSetItem.order_num.asc(),
- BizProductionTaskSetItem.create_time.asc(),
- )
- .all()
- )
- if not rows:
- raise ValueError("任务组合模版没有可派发的任务")
- payloads = [
- {
- "route_id": row.route_id,
- "task_name": row.task_name,
- "description": row.description,
- "remark": row.remark,
- }
- for row in rows
- ]
- name_map = _load_route_name_map(
- db, tenant_no, [p["route_id"] for p in payloads if p.get("route_id")]
- )
- missing = collect_missing_route_ids(payloads, set(name_map.keys()))
- if missing:
- raise ValueError("工艺路线已删除,无法派发:" + "、".join(missing))
- lab_storage_code = resolve_work_order_lab_storage_code(
- db, tenant_no=tenant_no, wo=wo
- )
- created = []
- for row in payloads:
- resolved_name = apply_task_name_placeholders(
- row["task_name"] or "",
- lab_storage_code=lab_storage_code,
- )
- task = production_task_svc.create_production_task(
- db,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=create_dept_id,
- data={
- "work_order_id": wo_id,
- "task_name": resolved_name,
- "route_id": row.get("route_id"),
- "description": row.get("description"),
- "remark": row.get("remark"),
- },
- )
- created.append(production_task_svc._task_to_dict(task))
- return {"createdCount": len(created), "list": created}
- def try_auto_dispatch_builtin_tasks_after_reception(
- db: Session,
- *,
- tenant_no: str,
- user_id: Optional[str] = None,
- create_dept_id: str = "",
- work_order: BizProductionWorkOrder,
- lab_storage_code: Optional[str] = None,
- ) -> Optional[Dict[str, Any]]:
- """样本接收建工单后按入库编码尝试自动派发;失败不抛错。"""
- import logging
- logger = logging.getLogger(__name__)
- wo = work_order
- if not wo or not getattr(wo, "id", None):
- return None
- existing = (
- db.query(BizProductionTask.id)
- .filter(
- BizProductionTask.work_order_id == wo.id,
- BizProductionTask.tenant_no == tenant_no,
- BizProductionTask.del_flag == 0,
- )
- .first()
- )
- if existing:
- logger.info(
- "skip auto-dispatch: work_order %s already has tasks",
- wo.id,
- )
- return None
- code = (lab_storage_code or "").strip()
- if not code:
- code = resolve_work_order_lab_storage_code(db, tenant_no=tenant_no, wo=wo)
- from app.utils.lab_storage_code_task_set import (
- match_builtin_task_set_name_by_lab_storage_code,
- )
- set_name = match_builtin_task_set_name_by_lab_storage_code(code)
- if not set_name:
- logger.info(
- "skip auto-dispatch: lab_storage_code=%r no builtin set rule",
- code,
- )
- return None
- header = (
- db.query(BizProductionTaskSet)
- .filter(
- BizProductionTaskSet.tenant_no == tenant_no,
- BizProductionTaskSet.set_name == set_name,
- BizProductionTaskSet.is_builtin == "1",
- BizProductionTaskSet.status == "1",
- BizProductionTaskSet.del_flag == 0,
- )
- .first()
- )
- if not header:
- logger.warning(
- "skip auto-dispatch: builtin set %r not found/enabled for tenant %s",
- set_name,
- tenant_no,
- )
- return None
- try:
- return create_tasks_from_set(
- db,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=create_dept_id or "",
- work_order_id=wo.id,
- set_id=header.id,
- )
- except Exception:
- logger.exception(
- "auto-dispatch failed: work_order=%s set=%s lab_storage_code=%r",
- wo.id,
- set_name,
- code,
- )
- return None
|