production_task_set.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778
  1. """生产任务集合 CRUD"""
  2. from __future__ import annotations
  3. from datetime import datetime
  4. from typing import Any, Dict, List, Optional, Set, Tuple
  5. from sqlalchemy import func
  6. from sqlalchemy.orm import Session
  7. from app.core.utils import generate_uuid
  8. from app.models.models import (
  9. BizProcessRoute,
  10. BizProductionTask,
  11. BizProductionTaskSet,
  12. BizProductionTaskSetItem,
  13. BizProductionWorkOrder,
  14. )
  15. from app.utils.production_task_name_placeholders import (
  16. apply_task_name_placeholders,
  17. revert_task_name_placeholders,
  18. )
  19. def resolve_work_order_lab_storage_code(
  20. db: Session,
  21. *,
  22. tenant_no: str,
  23. wo: BizProductionWorkOrder,
  24. ) -> str:
  25. """解析工单对应实验室入库编码(样本接收优先,样本编码回退)。"""
  26. from app.crud import sample_reception as sample_reception_svc
  27. from app.models.models import BizSampleReception
  28. reception_id = (getattr(wo, "reception_id", None) or "").strip()
  29. if reception_id:
  30. rec = (
  31. db.query(BizSampleReception)
  32. .filter(
  33. BizSampleReception.id == reception_id,
  34. BizSampleReception.tenant_no == tenant_no,
  35. BizSampleReception.del_flag == 0,
  36. )
  37. .first()
  38. )
  39. if rec:
  40. code = (rec.lab_storage_code or "").strip()
  41. if code:
  42. return code
  43. sample_code = (getattr(wo, "sample_code", None) or "").strip()
  44. if not sample_code:
  45. return ""
  46. source = (getattr(wo, "reception_source", None) or "").strip() or None
  47. code_map = sample_reception_svc.get_lab_storage_code_map_by_sample_codes(
  48. db, tenant_no, [sample_code], reception_source=source
  49. )
  50. return (code_map.get(sample_code) or "").strip()
  51. def _strip_or_none(raw: Any) -> Optional[str]:
  52. text = str(raw or "").strip()
  53. return text or None
  54. def normalize_item_payload(raw: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
  55. data = raw or {}
  56. task_name = str(data.get("taskName") or data.get("task_name") or "").strip()
  57. if not task_name:
  58. return None
  59. route_raw = data.get("routeId") if "routeId" in data else data.get("route_id")
  60. return {
  61. "task_name": task_name,
  62. "route_id": _strip_or_none(route_raw),
  63. "description": _strip_or_none(data.get("description")),
  64. "remark": _strip_or_none(data.get("remark")),
  65. }
  66. def skip_empty_named_tasks(tasks: List[Dict[str, Any]]) -> Tuple[List[Dict[str, Any]], int]:
  67. kept: List[Dict[str, Any]] = []
  68. skipped = 0
  69. for row in tasks or []:
  70. name = str(row.get("task_name") or row.get("taskName") or "").strip()
  71. if not name:
  72. skipped += 1
  73. continue
  74. route_raw = row.get("route_id") if "route_id" in row else row.get("routeId")
  75. name_raw = row.get("route_name") if "route_name" in row else row.get("routeName")
  76. kept.append(
  77. {
  78. "task_name": name,
  79. "route_id": _strip_or_none(route_raw),
  80. "route_name": _strip_or_none(name_raw),
  81. "description": _strip_or_none(row.get("description")),
  82. "remark": _strip_or_none(row.get("remark")),
  83. }
  84. )
  85. return kept, skipped
  86. def collect_missing_route_ids(items: List[Dict[str, Any]], existing_ids: Set[str]) -> List[str]:
  87. missing: List[str] = []
  88. seen: Set[str] = set()
  89. alive = existing_ids or set()
  90. for item in items or []:
  91. rid = _strip_or_none(item.get("route_id"))
  92. if not rid or rid in alive or rid in seen:
  93. continue
  94. seen.add(rid)
  95. missing.append(rid)
  96. return missing
  97. def total_pages(total: int, page_size: int) -> int:
  98. if page_size <= 0:
  99. return 0
  100. return (int(total) + int(page_size) - 1) // int(page_size)
  101. def _item_to_dict(r: BizProductionTaskSetItem) -> Dict[str, Any]:
  102. return {
  103. "id": r.id,
  104. "setId": r.set_id,
  105. "taskName": r.task_name or "",
  106. "routeId": r.route_id or "",
  107. "routeName": r.route_name or "",
  108. "description": r.description or "",
  109. "remark": r.remark or "",
  110. "orderNum": r.order_num if r.order_num is not None else 0,
  111. }
  112. def _set_to_dict(
  113. r: BizProductionTaskSet,
  114. item_count: int = 0,
  115. items: Optional[List[Dict[str, Any]]] = None,
  116. ) -> Dict[str, Any]:
  117. data: Dict[str, Any] = {
  118. "id": r.id,
  119. "setName": r.set_name or "",
  120. "status": r.status or "1",
  121. "itemCount": item_count,
  122. "orderNum": r.order_num if r.order_num is not None else 0,
  123. "remark": r.remark or "",
  124. "isBuiltin": getattr(r, "is_builtin", None) or "0",
  125. "createTime": r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else "",
  126. "updateTime": r.update_time.strftime("%Y-%m-%d %H:%M:%S") if r.update_time else "",
  127. }
  128. if items is not None:
  129. data["items"] = items
  130. return data
  131. def set_name_exists(
  132. db: Session, name: str, tenant_no: str, exclude_id: Optional[str] = None
  133. ) -> bool:
  134. n = (name or "").strip()
  135. if not n:
  136. return False
  137. q = db.query(BizProductionTaskSet).filter(
  138. BizProductionTaskSet.set_name == n,
  139. BizProductionTaskSet.tenant_no == tenant_no,
  140. BizProductionTaskSet.del_flag == 0,
  141. )
  142. if exclude_id:
  143. q = q.filter(BizProductionTaskSet.id != exclude_id)
  144. return q.first() is not None
  145. def _require_status(status: Optional[str]) -> str:
  146. st = (status or "1").strip()
  147. if st not in ("0", "1"):
  148. raise ValueError("状态无效")
  149. return st
  150. def _load_route_name_map(db: Session, tenant_no: str, route_ids: List[str]) -> Dict[str, str]:
  151. ids = [rid for rid in route_ids if rid]
  152. if not ids:
  153. return {}
  154. rows = (
  155. db.query(BizProcessRoute)
  156. .filter(
  157. BizProcessRoute.tenant_no == tenant_no,
  158. BizProcessRoute.del_flag == 0,
  159. BizProcessRoute.id.in_(ids),
  160. )
  161. .all()
  162. )
  163. return {r.id: (r.route_name or "") for r in rows}
  164. def _normalize_items_or_raise(raw_items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
  165. out: List[Dict[str, Any]] = []
  166. for raw in raw_items or []:
  167. item = normalize_item_payload(raw)
  168. if item:
  169. out.append(item)
  170. if not out:
  171. raise ValueError("请至少添加一条任务名称非空的条目")
  172. return out
  173. def _assert_routes_exist(
  174. db: Session, tenant_no: str, items: List[Dict[str, Any]]
  175. ) -> Dict[str, str]:
  176. wanted = [i["route_id"] for i in items if i.get("route_id")]
  177. name_map = _load_route_name_map(db, tenant_no, wanted)
  178. missing = collect_missing_route_ids(items, set(name_map.keys()))
  179. if missing:
  180. raise ValueError("存在无效或已删除的工艺路线")
  181. return name_map
  182. def _replace_items(
  183. db: Session,
  184. *,
  185. set_id: str,
  186. tenant_no: str,
  187. items: List[Dict[str, Any]],
  188. route_names: Dict[str, str],
  189. user_id: Optional[str],
  190. create_dept_id: str,
  191. ) -> None:
  192. now = datetime.now()
  193. existing = (
  194. db.query(BizProductionTaskSetItem)
  195. .filter(
  196. BizProductionTaskSetItem.set_id == set_id,
  197. BizProductionTaskSetItem.tenant_no == tenant_no,
  198. BizProductionTaskSetItem.del_flag == 0,
  199. )
  200. .all()
  201. )
  202. for row in existing:
  203. row.del_flag = 1
  204. row.update_by = user_id
  205. row.update_time = now
  206. db.add(row)
  207. for idx, item in enumerate(items, start=1):
  208. rid = item.get("route_id")
  209. db.add(
  210. BizProductionTaskSetItem(
  211. id=generate_uuid(),
  212. set_id=set_id,
  213. task_name=item["task_name"],
  214. route_id=rid,
  215. route_name=route_names.get(rid or "") if rid else None,
  216. description=item.get("description"),
  217. remark=item.get("remark"),
  218. tenant_no=tenant_no,
  219. order_num=idx,
  220. create_by=user_id,
  221. create_dept_id=create_dept_id or "",
  222. create_time=now,
  223. update_by=user_id,
  224. update_time=now,
  225. del_flag=0,
  226. )
  227. )
  228. def _item_count_map(db: Session, tenant_no: str, set_ids: List[str]) -> Dict[str, int]:
  229. if not set_ids:
  230. return {}
  231. rows = (
  232. db.query(BizProductionTaskSetItem.set_id, func.count(BizProductionTaskSetItem.id))
  233. .filter(
  234. BizProductionTaskSetItem.tenant_no == tenant_no,
  235. BizProductionTaskSetItem.del_flag == 0,
  236. BizProductionTaskSetItem.set_id.in_(set_ids),
  237. )
  238. .group_by(BizProductionTaskSetItem.set_id)
  239. .all()
  240. )
  241. return {sid: int(cnt) for sid, cnt in rows}
  242. def list_sets(
  243. db: Session,
  244. *,
  245. tenant_no: str,
  246. page: int = 1,
  247. page_size: int = 10,
  248. keyword: Optional[str] = None,
  249. status: Optional[str] = None,
  250. ) -> Tuple[List[Dict[str, Any]], int]:
  251. q = db.query(BizProductionTaskSet).filter(
  252. BizProductionTaskSet.tenant_no == tenant_no,
  253. BizProductionTaskSet.del_flag == 0,
  254. )
  255. kw = (keyword or "").strip()
  256. if kw:
  257. q = q.filter(BizProductionTaskSet.set_name.like(f"%{kw}%"))
  258. st = (status or "").strip()
  259. if st in ("0", "1"):
  260. q = q.filter(BizProductionTaskSet.status == st)
  261. total = q.count()
  262. rows = (
  263. q.order_by(BizProductionTaskSet.create_time.desc())
  264. .offset((page - 1) * page_size)
  265. .limit(page_size)
  266. .all()
  267. )
  268. counts = _item_count_map(db, tenant_no, [r.id for r in rows])
  269. return [_set_to_dict(r, item_count=counts.get(r.id, 0)) for r in rows], int(total)
  270. def list_active_sets(db: Session, *, tenant_no: str) -> List[Dict[str, Any]]:
  271. rows = (
  272. db.query(BizProductionTaskSet)
  273. .filter(
  274. BizProductionTaskSet.tenant_no == tenant_no,
  275. BizProductionTaskSet.del_flag == 0,
  276. BizProductionTaskSet.status == "1",
  277. )
  278. .order_by(BizProductionTaskSet.create_time.desc())
  279. .all()
  280. )
  281. counts = _item_count_map(db, tenant_no, [r.id for r in rows])
  282. return [_set_to_dict(r, item_count=counts.get(r.id, 0)) for r in rows]
  283. def get_set(db: Session, set_id: str, tenant_no: str) -> Optional[Dict[str, Any]]:
  284. row = (
  285. db.query(BizProductionTaskSet)
  286. .filter(
  287. BizProductionTaskSet.id == set_id,
  288. BizProductionTaskSet.tenant_no == tenant_no,
  289. BizProductionTaskSet.del_flag == 0,
  290. )
  291. .first()
  292. )
  293. if not row:
  294. return None
  295. items = (
  296. db.query(BizProductionTaskSetItem)
  297. .filter(
  298. BizProductionTaskSetItem.set_id == set_id,
  299. BizProductionTaskSetItem.tenant_no == tenant_no,
  300. BizProductionTaskSetItem.del_flag == 0,
  301. )
  302. .order_by(
  303. BizProductionTaskSetItem.order_num.asc(),
  304. BizProductionTaskSetItem.create_time.asc(),
  305. )
  306. .all()
  307. )
  308. return _set_to_dict(row, item_count=len(items), items=[_item_to_dict(i) for i in items])
  309. def is_super_admin_user(user: Optional[Dict[str, Any]]) -> bool:
  310. return bool(user) and (user.get("role") or "") == "super_admin"
  311. def assert_set_writable(
  312. db: Session,
  313. set_id: str,
  314. tenant_no: str,
  315. user: Optional[Dict[str, Any]] = None,
  316. ) -> BizProductionTaskSet:
  317. """内置模版仅超级管理员可写。"""
  318. row = (
  319. db.query(BizProductionTaskSet)
  320. .filter(
  321. BizProductionTaskSet.id == set_id,
  322. BizProductionTaskSet.tenant_no == tenant_no,
  323. BizProductionTaskSet.del_flag == 0,
  324. )
  325. .first()
  326. )
  327. if not row:
  328. raise ValueError("集合不存在")
  329. if (getattr(row, "is_builtin", None) or "0") == "1" and not is_super_admin_user(user):
  330. raise ValueError("内置模版仅超级管理员可修改")
  331. return row
  332. def create_set(
  333. db: Session,
  334. *,
  335. tenant_no: str,
  336. user_id: Optional[str] = None,
  337. create_dept_id: str = "",
  338. set_name: str,
  339. status: str = "1",
  340. remark: Optional[str] = None,
  341. items: Optional[List[Dict[str, Any]]] = None,
  342. ) -> Dict[str, Any]:
  343. name = (set_name or "").strip()
  344. if not name:
  345. raise ValueError("集合名称不能为空")
  346. if set_name_exists(db, name, tenant_no):
  347. raise ValueError("集合名称已存在")
  348. normalized = _normalize_items_or_raise(items or [])
  349. route_names = _assert_routes_exist(db, tenant_no, normalized)
  350. now = datetime.now()
  351. obj = BizProductionTaskSet(
  352. id=generate_uuid(),
  353. set_name=name,
  354. status=_require_status(status),
  355. tenant_no=tenant_no,
  356. order_num=0,
  357. remark=(remark or "").strip() or None,
  358. is_builtin="0",
  359. create_by=user_id,
  360. create_dept_id=create_dept_id or "",
  361. create_time=now,
  362. update_by=user_id,
  363. update_time=now,
  364. del_flag=0,
  365. )
  366. db.add(obj)
  367. db.flush()
  368. _replace_items(
  369. db,
  370. set_id=obj.id,
  371. tenant_no=tenant_no,
  372. items=normalized,
  373. route_names=route_names,
  374. user_id=user_id,
  375. create_dept_id=create_dept_id,
  376. )
  377. db.flush()
  378. detail = get_set(db, obj.id, tenant_no)
  379. if not detail:
  380. raise ValueError("集合创建失败")
  381. return detail
  382. def update_set(
  383. db: Session,
  384. set_id: str,
  385. *,
  386. tenant_no: str,
  387. user_id: Optional[str] = None,
  388. set_name: Optional[str] = None,
  389. status: Optional[str] = None,
  390. remark: Optional[str] = None,
  391. items: Optional[List[Dict[str, Any]]] = None,
  392. ) -> Dict[str, Any]:
  393. obj = (
  394. db.query(BizProductionTaskSet)
  395. .filter(
  396. BizProductionTaskSet.id == set_id,
  397. BizProductionTaskSet.tenant_no == tenant_no,
  398. BizProductionTaskSet.del_flag == 0,
  399. )
  400. .first()
  401. )
  402. if not obj:
  403. raise ValueError("集合不存在")
  404. if set_name is not None:
  405. name = set_name.strip()
  406. if not name:
  407. raise ValueError("集合名称不能为空")
  408. if set_name_exists(db, name, tenant_no, exclude_id=set_id):
  409. raise ValueError("集合名称已存在")
  410. obj.set_name = name
  411. if status is not None:
  412. obj.status = _require_status(status)
  413. if remark is not None:
  414. obj.remark = remark.strip() or None
  415. if items is not None:
  416. normalized = _normalize_items_or_raise(items)
  417. route_names = _assert_routes_exist(db, tenant_no, normalized)
  418. _replace_items(
  419. db,
  420. set_id=obj.id,
  421. tenant_no=tenant_no,
  422. items=normalized,
  423. route_names=route_names,
  424. user_id=user_id,
  425. create_dept_id=obj.create_dept_id or "",
  426. )
  427. obj.update_by = user_id
  428. obj.update_time = datetime.now()
  429. db.add(obj)
  430. db.flush()
  431. detail = get_set(db, obj.id, tenant_no)
  432. if not detail:
  433. raise ValueError("集合不存在")
  434. return detail
  435. def delete_set(db: Session, set_id: str, *, tenant_no: str, user_id: Optional[str] = None) -> None:
  436. obj = (
  437. db.query(BizProductionTaskSet)
  438. .filter(
  439. BizProductionTaskSet.id == set_id,
  440. BizProductionTaskSet.tenant_no == tenant_no,
  441. BizProductionTaskSet.del_flag == 0,
  442. )
  443. .first()
  444. )
  445. if not obj:
  446. raise ValueError("集合不存在")
  447. now = datetime.now()
  448. obj.del_flag = 1
  449. obj.update_by = user_id
  450. obj.update_time = now
  451. db.add(obj)
  452. items = (
  453. db.query(BizProductionTaskSetItem)
  454. .filter(
  455. BizProductionTaskSetItem.set_id == set_id,
  456. BizProductionTaskSetItem.del_flag == 0,
  457. )
  458. .all()
  459. )
  460. for row in items:
  461. row.del_flag = 1
  462. row.update_by = user_id
  463. row.update_time = now
  464. db.add(row)
  465. db.flush()
  466. def create_set_from_work_order(
  467. db: Session,
  468. *,
  469. tenant_no: str,
  470. user_id: Optional[str] = None,
  471. create_dept_id: str = "",
  472. work_order_id: str,
  473. set_name: str,
  474. remark: Optional[str] = None,
  475. items: Optional[List[Dict[str, Any]]] = None,
  476. ) -> Dict[str, Any]:
  477. wo_id = (work_order_id or "").strip()
  478. wo = (
  479. db.query(BizProductionWorkOrder)
  480. .filter(
  481. BizProductionWorkOrder.id == wo_id,
  482. BizProductionWorkOrder.tenant_no == tenant_no,
  483. BizProductionWorkOrder.del_flag == 0,
  484. )
  485. .first()
  486. )
  487. if not wo:
  488. raise ValueError("工单不存在")
  489. if items is not None:
  490. raw = items
  491. else:
  492. tasks = (
  493. db.query(BizProductionTask)
  494. .filter(
  495. BizProductionTask.work_order_id == wo_id,
  496. BizProductionTask.tenant_no == tenant_no,
  497. BizProductionTask.del_flag == 0,
  498. )
  499. .order_by(BizProductionTask.create_time.asc(), BizProductionTask.id.asc())
  500. .all()
  501. )
  502. raw = [
  503. {
  504. "task_name": t.task_name,
  505. "route_id": t.route_id,
  506. "description": t.description,
  507. "remark": t.remark,
  508. }
  509. for t in tasks
  510. ]
  511. kept, skipped = skip_empty_named_tasks(raw)
  512. if not kept:
  513. raise ValueError("无可保存的生产任务(任务名称均为空)")
  514. lab_storage_code = resolve_work_order_lab_storage_code(
  515. db, tenant_no=tenant_no, wo=wo
  516. )
  517. route_ids = [i["route_id"] for i in kept if i.get("route_id")]
  518. name_map = _load_route_name_map(db, tenant_no, route_ids)
  519. payload_items = []
  520. for i in kept:
  521. rid = i.get("route_id")
  522. if rid and rid not in name_map:
  523. rid = None
  524. payload_items.append(
  525. {
  526. "taskName": revert_task_name_placeholders(
  527. i["task_name"] or "",
  528. lab_storage_code=lab_storage_code,
  529. ),
  530. "routeId": rid,
  531. "description": i.get("description"),
  532. "remark": i.get("remark"),
  533. }
  534. )
  535. detail = create_set(
  536. db,
  537. tenant_no=tenant_no,
  538. user_id=user_id,
  539. create_dept_id=create_dept_id,
  540. set_name=set_name,
  541. status="1",
  542. remark=remark,
  543. items=payload_items,
  544. )
  545. return {**detail, "skippedEmptyNameCount": skipped}
  546. def create_tasks_from_set(
  547. db: Session,
  548. *,
  549. tenant_no: str,
  550. user_id: Optional[str] = None,
  551. create_dept_id: str = "",
  552. work_order_id: str,
  553. set_id: str,
  554. items: Optional[List[Dict[str, Any]]] = None,
  555. ) -> Dict[str, Any]:
  556. from app.crud import production_task as production_task_svc
  557. wo_id = (work_order_id or "").strip()
  558. wo = (
  559. db.query(BizProductionWorkOrder)
  560. .filter(
  561. BizProductionWorkOrder.id == wo_id,
  562. BizProductionWorkOrder.tenant_no == tenant_no,
  563. BizProductionWorkOrder.del_flag == 0,
  564. )
  565. .first()
  566. )
  567. if not wo:
  568. raise ValueError("工单不存在")
  569. status = getattr(wo, "work_order_status", None) or ""
  570. if status not in ("dpz", "jxz"):
  571. raise ValueError("仅待配置或进行中的工单可派发任务")
  572. header = (
  573. db.query(BizProductionTaskSet)
  574. .filter(
  575. BizProductionTaskSet.id == (set_id or "").strip(),
  576. BizProductionTaskSet.tenant_no == tenant_no,
  577. BizProductionTaskSet.del_flag == 0,
  578. )
  579. .first()
  580. )
  581. if not header:
  582. raise ValueError("任务组合模版不存在")
  583. if (header.status or "1") != "1":
  584. raise ValueError("任务组合模版已停用")
  585. if items is not None:
  586. payloads = _normalize_items_or_raise(items)
  587. else:
  588. rows = (
  589. db.query(BizProductionTaskSetItem)
  590. .filter(
  591. BizProductionTaskSetItem.set_id == header.id,
  592. BizProductionTaskSetItem.tenant_no == tenant_no,
  593. BizProductionTaskSetItem.del_flag == 0,
  594. )
  595. .order_by(
  596. BizProductionTaskSetItem.order_num.asc(),
  597. BizProductionTaskSetItem.create_time.asc(),
  598. )
  599. .all()
  600. )
  601. if not rows:
  602. raise ValueError("任务组合模版没有可派发的任务")
  603. payloads = [
  604. {
  605. "route_id": row.route_id,
  606. "task_name": row.task_name,
  607. "description": row.description,
  608. "remark": row.remark,
  609. }
  610. for row in rows
  611. ]
  612. name_map = _load_route_name_map(
  613. db, tenant_no, [p["route_id"] for p in payloads if p.get("route_id")]
  614. )
  615. missing = collect_missing_route_ids(payloads, set(name_map.keys()))
  616. if missing:
  617. raise ValueError("工艺路线已删除,无法派发:" + "、".join(missing))
  618. lab_storage_code = resolve_work_order_lab_storage_code(
  619. db, tenant_no=tenant_no, wo=wo
  620. )
  621. created = []
  622. for row in payloads:
  623. resolved_name = apply_task_name_placeholders(
  624. row["task_name"] or "",
  625. lab_storage_code=lab_storage_code,
  626. )
  627. task = production_task_svc.create_production_task(
  628. db,
  629. tenant_no=tenant_no,
  630. user_id=user_id,
  631. create_dept_id=create_dept_id,
  632. data={
  633. "work_order_id": wo_id,
  634. "task_name": resolved_name,
  635. "route_id": row.get("route_id"),
  636. "description": row.get("description"),
  637. "remark": row.get("remark"),
  638. },
  639. )
  640. created.append(production_task_svc._task_to_dict(task))
  641. return {"createdCount": len(created), "list": created}
  642. def try_auto_dispatch_builtin_tasks_after_reception(
  643. db: Session,
  644. *,
  645. tenant_no: str,
  646. user_id: Optional[str] = None,
  647. create_dept_id: str = "",
  648. work_order: BizProductionWorkOrder,
  649. lab_storage_code: Optional[str] = None,
  650. ) -> Optional[Dict[str, Any]]:
  651. """样本接收建工单后按入库编码尝试自动派发;失败不抛错。"""
  652. import logging
  653. logger = logging.getLogger(__name__)
  654. wo = work_order
  655. if not wo or not getattr(wo, "id", None):
  656. return None
  657. existing = (
  658. db.query(BizProductionTask.id)
  659. .filter(
  660. BizProductionTask.work_order_id == wo.id,
  661. BizProductionTask.tenant_no == tenant_no,
  662. BizProductionTask.del_flag == 0,
  663. )
  664. .first()
  665. )
  666. if existing:
  667. logger.info(
  668. "skip auto-dispatch: work_order %s already has tasks",
  669. wo.id,
  670. )
  671. return None
  672. code = (lab_storage_code or "").strip()
  673. if not code:
  674. code = resolve_work_order_lab_storage_code(db, tenant_no=tenant_no, wo=wo)
  675. from app.utils.lab_storage_code_task_set import (
  676. match_builtin_task_set_name_by_lab_storage_code,
  677. )
  678. set_name = match_builtin_task_set_name_by_lab_storage_code(code)
  679. if not set_name:
  680. logger.info(
  681. "skip auto-dispatch: lab_storage_code=%r no builtin set rule",
  682. code,
  683. )
  684. return None
  685. header = (
  686. db.query(BizProductionTaskSet)
  687. .filter(
  688. BizProductionTaskSet.tenant_no == tenant_no,
  689. BizProductionTaskSet.set_name == set_name,
  690. BizProductionTaskSet.is_builtin == "1",
  691. BizProductionTaskSet.status == "1",
  692. BizProductionTaskSet.del_flag == 0,
  693. )
  694. .first()
  695. )
  696. if not header:
  697. logger.warning(
  698. "skip auto-dispatch: builtin set %r not found/enabled for tenant %s",
  699. set_name,
  700. tenant_no,
  701. )
  702. return None
  703. try:
  704. return create_tasks_from_set(
  705. db,
  706. tenant_no=tenant_no,
  707. user_id=user_id,
  708. create_dept_id=create_dept_id or "",
  709. work_order_id=wo.id,
  710. set_id=header.id,
  711. )
  712. except Exception:
  713. logger.exception(
  714. "auto-dispatch failed: work_order=%s set=%s lab_storage_code=%r",
  715. wo.id,
  716. set_name,
  717. code,
  718. )
  719. return None