process_route.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  1. """工艺路线 CRUD(biz_process_route + biz_process_route_step + 4张步骤子表)"""
  2. from __future__ import annotations
  3. import math
  4. from datetime import datetime
  5. from typing import Any, Dict, List, Optional, Tuple
  6. from sqlalchemy import and_, func, or_
  7. from sqlalchemy.orm import Session
  8. from app.core.utils import generate_uuid
  9. from app.crud.equipment_snapshot import parse_equipment_ids, resolve_equipment_list_snapshot
  10. from app.models.models import (
  11. BizProductionProductType,
  12. BizProcess,
  13. BizProcessReagent,
  14. BizProcessConsumable,
  15. BizProcessOutput,
  16. BizProcessDataCollection,
  17. BizProcessRoute,
  18. BizProcessRouteStep,
  19. BizProcessRouteStepReagent,
  20. BizProcessRouteStepConsumable,
  21. BizProcessRouteStepOutput,
  22. BizProcessRouteStepDataCollection,
  23. )
  24. # ---------------------------------------------------------------------------
  25. # 序列化函数
  26. # ---------------------------------------------------------------------------
  27. def _route_row_to_dict(r: BizProcessRoute) -> Dict[str, Any]:
  28. return {
  29. "id": r.id,
  30. "routeCode": r.route_code,
  31. "routeName": r.route_name,
  32. "productTypeId": r.product_type_id or "",
  33. "version": r.version or "",
  34. "isCurrent": r.is_current if r.is_current is not None else "0",
  35. "equipmentIds": parse_equipment_ids(getattr(r, "equipment_ids", None)),
  36. "equipmentName": getattr(r, "equipment_name", None) or "",
  37. "tenantNo": r.tenant_no,
  38. "orderNum": r.order_num if r.order_num is not None else 0,
  39. "remark": r.remark or "",
  40. "isBuiltin": getattr(r, "is_builtin", None) or "0",
  41. "createTime": r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else "",
  42. "updateTime": r.update_time.strftime("%Y-%m-%d %H:%M:%S") if r.update_time else "",
  43. }
  44. def _step_row_to_dict(s: BizProcessRouteStep) -> Dict[str, Any]:
  45. return {
  46. "id": s.id,
  47. "routeId": s.route_id,
  48. "processId": s.process_id or "",
  49. "stepOrder": s.step_order if s.step_order is not None else 0,
  50. "processCode": s.process_code or "",
  51. "categoryId": s.category_id or "",
  52. "processName": s.process_name or "",
  53. "description": s.description or "",
  54. "estimatedDuration": s.estimated_duration or "",
  55. "sopIds": s.sop_ids or "",
  56. "sopContent": s.sop_content or "",
  57. "remark": s.remark or "",
  58. }
  59. def _step_reagent_row_to_dict(r: BizProcessRouteStepReagent) -> Dict[str, Any]:
  60. return {
  61. "id": r.id,
  62. "stepId": r.step_id,
  63. "reagentCode": r.reagent_code or "",
  64. "reagentName": r.reagent_name or "",
  65. "spec": r.spec or "",
  66. "standardUsage": r.standard_usage or "",
  67. "unit": r.unit or "",
  68. "isKeyMaterial": r.is_key_material if r.is_key_material is not None else "0",
  69. "orderNum": r.order_num if r.order_num is not None else 0,
  70. "remark": r.remark or "",
  71. }
  72. def _step_consumable_row_to_dict(c: BizProcessRouteStepConsumable) -> Dict[str, Any]:
  73. return {
  74. "id": c.id,
  75. "stepId": c.step_id,
  76. "consumableCode": c.consumable_code or "",
  77. "consumableName": c.consumable_name or "",
  78. "specModel": c.spec_model or "",
  79. "standardUsage": c.standard_usage or "",
  80. "unit": c.unit or "",
  81. "isRequired": c.is_required if c.is_required is not None else "1",
  82. "orderNum": c.order_num if c.order_num is not None else 0,
  83. "remark": c.remark or "",
  84. }
  85. def _step_output_row_to_dict(o: BizProcessRouteStepOutput) -> Dict[str, Any]:
  86. return {
  87. "id": o.id,
  88. "stepId": o.step_id,
  89. "outputTypeId": o.output_type_id or "",
  90. "outputName": o.output_name or "",
  91. "outputBarcode": o.output_barcode or "",
  92. "orderNum": o.order_num if o.order_num is not None else 0,
  93. "remark": o.remark or "",
  94. }
  95. def _step_data_collection_row_to_dict(d: BizProcessRouteStepDataCollection) -> Dict[str, Any]:
  96. return {
  97. "id": d.id,
  98. "stepId": d.step_id,
  99. "collectionName": d.collection_name or "",
  100. "unit": d.unit or "",
  101. "orderNum": d.order_num if d.order_num is not None else 0,
  102. "remark": d.remark or "",
  103. }
  104. # ---------------------------------------------------------------------------
  105. # 路线主表 CRUD
  106. # ---------------------------------------------------------------------------
  107. def list_routes(db: Session, *, page: int = 1, page_size: int = 10,
  108. route_name: Optional[str] = None, route_code: Optional[str] = None,
  109. product_type_id: Optional[str] = None,
  110. tenant_no: Optional[str] = None,
  111. user_info=None) -> Tuple[List[Dict], int]:
  112. def _filtered():
  113. q = db.query(BizProcessRoute).filter(BizProcessRoute.del_flag == 0)
  114. if tenant_no:
  115. q = q.filter(BizProcessRoute.tenant_no == tenant_no)
  116. if route_name and route_name.strip():
  117. q = q.filter(BizProcessRoute.route_name.like(f"%{route_name.strip()}%"))
  118. if route_code and route_code.strip():
  119. q = q.filter(BizProcessRoute.route_code == route_code.strip())
  120. if product_type_id and product_type_id.strip():
  121. q = q.filter(BizProcessRoute.product_type_id == product_type_id.strip())
  122. if user_info:
  123. from app.core.data_scope import apply_data_scope
  124. q = apply_data_scope(q, BizProcessRoute, user_info, db, tenant_no)
  125. return q
  126. total = _filtered().order_by(None).with_entities(func.count(BizProcessRoute.id)).scalar() or 0
  127. page = max(1, page)
  128. page_size = min(max(1, page_size), 100)
  129. rows = (
  130. _filtered()
  131. .outerjoin(
  132. BizProductionProductType,
  133. and_(
  134. BizProcessRoute.product_type_id == BizProductionProductType.id,
  135. BizProductionProductType.del_flag == 0,
  136. ),
  137. )
  138. .order_by(
  139. BizProductionProductType.product_type_name.is_(None),
  140. BizProductionProductType.product_type_name.asc(),
  141. BizProcessRoute.create_time.desc(),
  142. )
  143. .offset((page - 1) * page_size)
  144. .limit(page_size)
  145. .all()
  146. )
  147. return [_route_row_to_dict(x) for x in rows], int(total)
  148. def get_route(db: Session, route_id: str) -> Optional[BizProcessRoute]:
  149. return db.query(BizProcessRoute).filter(
  150. and_(BizProcessRoute.id == route_id, BizProcessRoute.del_flag == 0)
  151. ).first()
  152. def get_step(db: Session, step_id: str) -> Optional[BizProcessRouteStep]:
  153. return db.query(BizProcessRouteStep).filter(
  154. and_(BizProcessRouteStep.id == step_id, BizProcessRouteStep.del_flag == 0)
  155. ).first()
  156. def is_super_admin_user(user: Optional[Dict[str, Any]]) -> bool:
  157. return bool(user) and (user.get("role") or "") == "super_admin"
  158. def assert_route_writable(
  159. db: Session, route_id: str, user: Optional[Dict[str, Any]] = None
  160. ) -> BizProcessRoute:
  161. """内置模版仅超级管理员可写;返回路线对象。"""
  162. route = get_route(db, route_id)
  163. if not route:
  164. raise ValueError("路线不存在")
  165. if (getattr(route, "is_builtin", None) or "0") == "1" and not is_super_admin_user(user):
  166. raise ValueError("内置模版仅超级管理员可修改")
  167. return route
  168. def assert_step_route_writable(
  169. db: Session, step_id: str, user: Optional[Dict[str, Any]] = None
  170. ) -> BizProcessRouteStep:
  171. step = get_step(db, step_id)
  172. if not step:
  173. raise ValueError("步骤不存在")
  174. assert_route_writable(db, step.route_id, user)
  175. return step
  176. def get_route_detail(db: Session, route_id: str) -> Optional[Dict[str, Any]]:
  177. route = get_route(db, route_id)
  178. if not route:
  179. return None
  180. result = _route_row_to_dict(route)
  181. steps = (
  182. db.query(BizProcessRouteStep)
  183. .filter(and_(BizProcessRouteStep.route_id == route_id, BizProcessRouteStep.del_flag == 0))
  184. .order_by(BizProcessRouteStep.step_order.asc(), BizProcessRouteStep.create_time.asc())
  185. .all()
  186. )
  187. step_list = []
  188. for s in steps:
  189. sd = _step_row_to_dict(s)
  190. reagents = db.query(BizProcessRouteStepReagent).filter(
  191. and_(BizProcessRouteStepReagent.step_id == s.id, BizProcessRouteStepReagent.del_flag == 0)
  192. ).order_by(BizProcessRouteStepReagent.order_num.asc(), BizProcessRouteStepReagent.create_time.asc()).all()
  193. consumables = db.query(BizProcessRouteStepConsumable).filter(
  194. and_(BizProcessRouteStepConsumable.step_id == s.id, BizProcessRouteStepConsumable.del_flag == 0)
  195. ).order_by(BizProcessRouteStepConsumable.order_num.asc(), BizProcessRouteStepConsumable.create_time.asc()).all()
  196. outputs = db.query(BizProcessRouteStepOutput).filter(
  197. and_(BizProcessRouteStepOutput.step_id == s.id, BizProcessRouteStepOutput.del_flag == 0)
  198. ).order_by(BizProcessRouteStepOutput.order_num.asc(), BizProcessRouteStepOutput.create_time.asc()).all()
  199. data_collections = db.query(BizProcessRouteStepDataCollection).filter(
  200. and_(BizProcessRouteStepDataCollection.step_id == s.id, BizProcessRouteStepDataCollection.del_flag == 0)
  201. ).order_by(BizProcessRouteStepDataCollection.order_num.asc(), BizProcessRouteStepDataCollection.create_time.asc()).all()
  202. sd["reagents"] = [_step_reagent_row_to_dict(r) for r in reagents]
  203. sd["consumables"] = [_step_consumable_row_to_dict(c) for c in consumables]
  204. sd["outputs"] = [_step_output_row_to_dict(o) for o in outputs]
  205. sd["dataCollections"] = [_step_data_collection_row_to_dict(d) for d in data_collections]
  206. step_list.append(sd)
  207. result["steps"] = step_list
  208. return result
  209. def create_route(db: Session, *, route_code: str, route_name: str,
  210. product_type_id: Optional[str] = None, version: Optional[str] = None,
  211. remark: Optional[str] = None, tenant_no: str = "zkxw",
  212. user_id: Optional[str] = None) -> BizProcessRoute:
  213. rc = (route_code or "").strip()
  214. if not rc:
  215. raise ValueError("路线编码不能为空")
  216. now = datetime.now()
  217. obj = BizProcessRoute(
  218. id=generate_uuid(),
  219. route_code=rc,
  220. route_name=(route_name or "").strip() or rc,
  221. product_type_id=(product_type_id or "").strip() or None,
  222. version=(version or "").strip() or None,
  223. is_current="0",
  224. tenant_no=tenant_no,
  225. order_num=0,
  226. remark=(remark or "").strip() or None,
  227. is_builtin="0",
  228. create_by=user_id,
  229. create_dept_id="",
  230. create_time=now,
  231. update_by=user_id,
  232. update_time=now,
  233. del_flag=0,
  234. )
  235. db.add(obj)
  236. db.commit()
  237. db.refresh(obj)
  238. return obj
  239. def update_route(db: Session, route_id: str, *, user_id: Optional[str] = None, **kwargs) -> BizProcessRoute:
  240. obj = get_route(db, route_id)
  241. if not obj:
  242. raise ValueError("路线不存在")
  243. for field in ["route_code", "route_name", "product_type_id", "version", "remark"]:
  244. val = kwargs.get(field)
  245. if val is not None:
  246. setattr(obj, field, (val or "").strip() or None if field != "route_code" else (val or "").strip())
  247. if "equipment_ids" in kwargs:
  248. eids, enames = resolve_equipment_list_snapshot(
  249. db, kwargs.get("equipment_ids"), obj.tenant_no or "zkxw"
  250. )
  251. obj.equipment_ids = eids
  252. obj.equipment_name = enames
  253. obj.update_by = user_id
  254. obj.update_time = datetime.now()
  255. db.add(obj)
  256. db.commit()
  257. db.refresh(obj)
  258. return obj
  259. def set_current_route(db: Session, route_id: str, tenant_no: str) -> BizProcessRoute:
  260. obj = get_route(db, route_id)
  261. if not obj:
  262. raise ValueError("路线不存在")
  263. now = datetime.now()
  264. siblings = db.query(BizProcessRoute).filter(
  265. and_(BizProcessRoute.route_code == obj.route_code,
  266. BizProcessRoute.tenant_no == tenant_no,
  267. BizProcessRoute.del_flag == 0)
  268. ).all()
  269. for s in siblings:
  270. s.is_current = "0"
  271. s.update_time = now
  272. db.add(s)
  273. obj.is_current = "1"
  274. obj.update_time = now
  275. db.add(obj)
  276. db.commit()
  277. db.refresh(obj)
  278. return obj
  279. # ---------------------------------------------------------------------------
  280. # 步骤子表辅助
  281. # ---------------------------------------------------------------------------
  282. def _soft_delete_sub_records(db: Session, model_class, step_id: str, now: datetime) -> None:
  283. rows = db.query(model_class).filter(
  284. and_(model_class.step_id == step_id, model_class.del_flag == 0)
  285. ).all()
  286. for r in rows:
  287. r.del_flag = 1
  288. r.update_time = now
  289. db.add(r)
  290. def _create_step_reagent(db, step_id, tenant_no, user_id, now, data):
  291. db.add(BizProcessRouteStepReagent(
  292. id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
  293. reagent_code=(data.get("reagent_code") or "").strip() or None,
  294. reagent_name=(data.get("reagent_name") or "").strip() or None,
  295. spec=(data.get("spec") or "").strip() or None,
  296. standard_usage=(data.get("standard_usage") or "").strip() or None,
  297. unit=(data.get("unit") or "").strip() or None,
  298. is_key_material=data.get("is_key_material", "0"),
  299. remark=(data.get("remark") or "").strip() or None,
  300. order_num=data.get("order_num", 0) or 0,
  301. create_by=user_id, create_dept_id="",
  302. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  303. ))
  304. def _create_step_consumable(db, step_id, tenant_no, user_id, now, data):
  305. db.add(BizProcessRouteStepConsumable(
  306. id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
  307. consumable_code=(data.get("consumable_code") or "").strip() or None,
  308. consumable_name=(data.get("consumable_name") or "").strip() or None,
  309. spec_model=(data.get("spec_model") or "").strip() or None,
  310. standard_usage=(data.get("standard_usage") or "").strip() or None,
  311. unit=(data.get("unit") or "").strip() or None,
  312. is_required=data.get("is_required", "1"),
  313. remark=(data.get("remark") or "").strip() or None,
  314. order_num=data.get("order_num", 0) or 0,
  315. create_by=user_id, create_dept_id="",
  316. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  317. ))
  318. def _create_step_output(db, step_id, tenant_no, user_id, now, data):
  319. db.add(BizProcessRouteStepOutput(
  320. id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
  321. output_type_id=(data.get("output_type_id") or "").strip() or None,
  322. output_name=(data.get("output_name") or "").strip() or None,
  323. output_barcode=(data.get("output_barcode") or "").strip() or None,
  324. remark=(data.get("remark") or "").strip() or None,
  325. order_num=data.get("order_num", 0) or 0,
  326. create_by=user_id, create_dept_id="",
  327. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  328. ))
  329. def _create_step_data_collection(db, step_id, tenant_no, user_id, now, data):
  330. db.add(BizProcessRouteStepDataCollection(
  331. id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
  332. collection_name=(data.get("collection_name") or "").strip() or None,
  333. unit=(data.get("unit") or "").strip() or None,
  334. order_num=data.get("order_num", 0) or 0,
  335. remark=(data.get("remark") or "").strip() or None,
  336. create_by=user_id, create_dept_id="",
  337. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  338. ))
  339. # ---------------------------------------------------------------------------
  340. # 步骤 CRUD
  341. # ---------------------------------------------------------------------------
  342. def add_step(db: Session, *, route_id: str, process_id: str, step_order: Optional[int] = None,
  343. tenant_no: str = "zkxw", user_id: Optional[str] = None) -> Dict[str, Any]:
  344. route = get_route(db, route_id)
  345. if not route:
  346. raise ValueError("路线不存在")
  347. proc = db.query(BizProcess).filter(
  348. and_(BizProcess.id == process_id, BizProcess.del_flag == 0)
  349. ).first()
  350. if not proc:
  351. raise ValueError("工艺不存在")
  352. now = datetime.now()
  353. if step_order is None:
  354. max_order = db.query(func.max(BizProcessRouteStep.step_order)).filter(
  355. and_(BizProcessRouteStep.route_id == route_id, BizProcessRouteStep.del_flag == 0)
  356. ).scalar()
  357. step_order = (max_order or 0) + 1
  358. step = BizProcessRouteStep(
  359. id=generate_uuid(), route_id=route_id, process_id=process_id,
  360. step_order=step_order,
  361. process_code=proc.process_code, category_id=proc.category_id,
  362. process_name=proc.process_name, description=proc.description,
  363. estimated_duration=proc.estimated_duration,
  364. sop_ids=proc.sop_ids,
  365. sop_content=proc.sop_content,
  366. tenant_no=tenant_no, order_num=0,
  367. remark=None, create_by=user_id, create_dept_id="",
  368. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  369. )
  370. db.add(step)
  371. db.flush()
  372. src_reagents = db.query(BizProcessReagent).filter(
  373. and_(BizProcessReagent.process_id == process_id, BizProcessReagent.del_flag == 0)
  374. ).all()
  375. for r in src_reagents:
  376. _create_step_reagent(db, step.id, tenant_no, user_id, now, {
  377. "reagent_code": r.reagent_code, "reagent_name": r.reagent_name,
  378. "spec": r.spec, "standard_usage": r.standard_usage,
  379. "unit": r.unit, "is_key_material": r.is_key_material,
  380. })
  381. src_consumables = db.query(BizProcessConsumable).filter(
  382. and_(BizProcessConsumable.process_id == process_id, BizProcessConsumable.del_flag == 0)
  383. ).all()
  384. for c in src_consumables:
  385. _create_step_consumable(db, step.id, tenant_no, user_id, now, {
  386. "consumable_code": c.consumable_code, "consumable_name": c.consumable_name,
  387. "spec_model": c.spec_model, "standard_usage": c.standard_usage,
  388. "unit": c.unit, "is_required": c.is_required, "remark": c.remark,
  389. })
  390. src_outputs = db.query(BizProcessOutput).filter(
  391. and_(BizProcessOutput.process_id == process_id, BizProcessOutput.del_flag == 0)
  392. ).all()
  393. for o in src_outputs:
  394. _create_step_output(db, step.id, tenant_no, user_id, now, {
  395. "output_type_id": o.output_type_id, "output_name": o.output_name,
  396. "output_barcode": o.output_barcode,
  397. })
  398. src_collections = db.query(BizProcessDataCollection).filter(
  399. and_(BizProcessDataCollection.process_id == process_id, BizProcessDataCollection.del_flag == 0)
  400. ).all()
  401. for d in src_collections:
  402. _create_step_data_collection(db, step.id, tenant_no, user_id, now, {
  403. "collection_name": d.collection_name, "unit": d.unit, "order_num": d.order_num,
  404. })
  405. db.commit()
  406. detail = get_route_detail(db, route_id)
  407. return detail["steps"][-1] if detail and detail["steps"] else _step_row_to_dict(step)
  408. def update_step(db: Session, step_id: str, *, user_id: Optional[str] = None, **kwargs) -> BizProcessRouteStep:
  409. step = db.query(BizProcessRouteStep).filter(
  410. and_(BizProcessRouteStep.id == step_id, BizProcessRouteStep.del_flag == 0)
  411. ).first()
  412. if not step:
  413. raise ValueError("步骤不存在")
  414. for field in ["process_code", "category_id", "process_name", "description",
  415. "estimated_duration", "sop_ids", "sop_content"]:
  416. val = kwargs.get(field)
  417. if val is not None:
  418. if field == "sop_content":
  419. setattr(step, field, val if str(val).strip() else None)
  420. else:
  421. setattr(step, field, (val or "").strip() or None)
  422. step.update_by = user_id
  423. step.update_time = datetime.now()
  424. db.add(step)
  425. db.commit()
  426. db.refresh(step)
  427. return step
  428. def reorder_steps(db: Session, route_id: str, step_ids: List[str]) -> None:
  429. now = datetime.now()
  430. for idx, sid in enumerate(step_ids):
  431. step = db.query(BizProcessRouteStep).filter(
  432. and_(BizProcessRouteStep.id == sid, BizProcessRouteStep.route_id == route_id,
  433. BizProcessRouteStep.del_flag == 0)
  434. ).first()
  435. if step:
  436. step.step_order = idx + 1
  437. step.update_time = now
  438. db.add(step)
  439. db.commit()
  440. def copy_step(db: Session, step_id: str, *, tenant_no: str = "zkxw",
  441. user_id: Optional[str] = None) -> Dict[str, Any]:
  442. """复制步骤,插入到原步骤之后,名称追加(复制)"""
  443. src = db.query(BizProcessRouteStep).filter(
  444. and_(BizProcessRouteStep.id == step_id, BizProcessRouteStep.del_flag == 0)
  445. ).first()
  446. if not src:
  447. raise ValueError("步骤不存在")
  448. now = datetime.now()
  449. new_order = src.step_order + 1
  450. # 后续步骤 step_order +1
  451. later_steps = db.query(BizProcessRouteStep).filter(
  452. and_(
  453. BizProcessRouteStep.route_id == src.route_id,
  454. BizProcessRouteStep.del_flag == 0,
  455. BizProcessRouteStep.step_order > src.step_order,
  456. )
  457. ).order_by(BizProcessRouteStep.step_order.asc()).all()
  458. for ls in later_steps:
  459. ls.step_order += 1
  460. ls.update_time = now
  461. db.add(ls)
  462. # 创建新步骤
  463. new_name = (src.process_name or "").strip()
  464. if not new_name.endswith("(复制)"):
  465. new_name += "(复制)"
  466. new_step = BizProcessRouteStep(
  467. id=generate_uuid(), route_id=src.route_id, process_id=src.process_id,
  468. step_order=new_order,
  469. process_code=src.process_code, category_id=src.category_id,
  470. process_name=new_name, description=src.description,
  471. estimated_duration=src.estimated_duration,
  472. sop_ids=src.sop_ids,
  473. sop_content=src.sop_content,
  474. tenant_no=tenant_no, order_num=0,
  475. remark=src.remark, create_by=user_id, create_dept_id="",
  476. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  477. )
  478. db.add(new_step)
  479. db.flush()
  480. # 复制子表:试剂
  481. for r in db.query(BizProcessRouteStepReagent).filter(
  482. and_(BizProcessRouteStepReagent.step_id == step_id, BizProcessRouteStepReagent.del_flag == 0)
  483. ).order_by(
  484. BizProcessRouteStepReagent.order_num.asc(),
  485. BizProcessRouteStepReagent.create_time.asc(),
  486. ).all():
  487. _create_step_reagent(db, new_step.id, tenant_no, user_id, now, {
  488. "reagent_code": r.reagent_code, "reagent_name": r.reagent_name,
  489. "spec": r.spec, "standard_usage": r.standard_usage,
  490. "unit": r.unit, "is_key_material": r.is_key_material,
  491. "order_num": r.order_num,
  492. "remark": r.remark,
  493. })
  494. # 复制子表:耗材
  495. for c in db.query(BizProcessRouteStepConsumable).filter(
  496. and_(BizProcessRouteStepConsumable.step_id == step_id, BizProcessRouteStepConsumable.del_flag == 0)
  497. ).order_by(
  498. BizProcessRouteStepConsumable.order_num.asc(),
  499. BizProcessRouteStepConsumable.create_time.asc(),
  500. ).all():
  501. _create_step_consumable(db, new_step.id, tenant_no, user_id, now, {
  502. "consumable_code": c.consumable_code, "consumable_name": c.consumable_name,
  503. "spec_model": c.spec_model, "standard_usage": c.standard_usage,
  504. "unit": c.unit, "is_required": c.is_required,
  505. "order_num": c.order_num,
  506. "remark": c.remark,
  507. })
  508. # 复制子表:产出
  509. for o in db.query(BizProcessRouteStepOutput).filter(
  510. and_(BizProcessRouteStepOutput.step_id == step_id, BizProcessRouteStepOutput.del_flag == 0)
  511. ).order_by(
  512. BizProcessRouteStepOutput.order_num.asc(),
  513. BizProcessRouteStepOutput.create_time.asc(),
  514. ).all():
  515. _create_step_output(db, new_step.id, tenant_no, user_id, now, {
  516. "output_type_id": o.output_type_id, "output_name": o.output_name,
  517. "output_barcode": o.output_barcode,
  518. "order_num": o.order_num,
  519. "remark": o.remark,
  520. })
  521. # 复制子表:数据采集
  522. for d in db.query(BizProcessRouteStepDataCollection).filter(
  523. and_(BizProcessRouteStepDataCollection.step_id == step_id, BizProcessRouteStepDataCollection.del_flag == 0)
  524. ).order_by(
  525. BizProcessRouteStepDataCollection.order_num.asc(),
  526. BizProcessRouteStepDataCollection.create_time.asc(),
  527. ).all():
  528. _create_step_data_collection(db, new_step.id, tenant_no, user_id, now, {
  529. "collection_name": d.collection_name, "unit": d.unit,
  530. "order_num": d.order_num, "remark": d.remark,
  531. })
  532. db.commit()
  533. detail = get_route_detail(db, src.route_id)
  534. return detail["steps"][-1] if detail and detail["steps"] else _step_row_to_dict(new_step)
  535. def soft_delete_step(db: Session, step_id: str) -> None:
  536. step = db.query(BizProcessRouteStep).filter(
  537. and_(BizProcessRouteStep.id == step_id, BizProcessRouteStep.del_flag == 0)
  538. ).first()
  539. if not step:
  540. raise ValueError("步骤不存在")
  541. now = datetime.now()
  542. step.del_flag = 1
  543. step.update_time = now
  544. db.add(step)
  545. _soft_delete_sub_records(db, BizProcessRouteStepReagent, step_id, now)
  546. _soft_delete_sub_records(db, BizProcessRouteStepConsumable, step_id, now)
  547. _soft_delete_sub_records(db, BizProcessRouteStepOutput, step_id, now)
  548. _soft_delete_sub_records(db, BizProcessRouteStepDataCollection, step_id, now)
  549. db.commit()
  550. def soft_delete_route(db: Session, route_id: str) -> None:
  551. route = get_route(db, route_id)
  552. if not route:
  553. raise ValueError("路线不存在")
  554. now = datetime.now()
  555. route.del_flag = 1
  556. route.update_time = now
  557. db.add(route)
  558. steps = db.query(BizProcessRouteStep).filter(
  559. and_(BizProcessRouteStep.route_id == route_id, BizProcessRouteStep.del_flag == 0)
  560. ).all()
  561. for s in steps:
  562. s.del_flag = 1
  563. s.update_time = now
  564. db.add(s)
  565. _soft_delete_sub_records(db, BizProcessRouteStepReagent, s.id, now)
  566. _soft_delete_sub_records(db, BizProcessRouteStepConsumable, s.id, now)
  567. _soft_delete_sub_records(db, BizProcessRouteStepOutput, s.id, now)
  568. _soft_delete_sub_records(db, BizProcessRouteStepDataCollection, s.id, now)
  569. db.commit()
  570. # ---------------------------------------------------------------------------
  571. # 子表完整替换更新
  572. # ---------------------------------------------------------------------------
  573. def update_step_reagents(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None):
  574. now = datetime.now()
  575. _soft_delete_sub_records(db, BizProcessRouteStepReagent, step_id, now)
  576. for it in items:
  577. _create_step_reagent(db, step_id, tenant_no, user_id, now, it)
  578. db.commit()
  579. def update_step_consumables(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None):
  580. now = datetime.now()
  581. _soft_delete_sub_records(db, BizProcessRouteStepConsumable, step_id, now)
  582. for it in items:
  583. _create_step_consumable(db, step_id, tenant_no, user_id, now, it)
  584. db.commit()
  585. def update_step_outputs(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None):
  586. now = datetime.now()
  587. _soft_delete_sub_records(db, BizProcessRouteStepOutput, step_id, now)
  588. for it in items:
  589. _create_step_output(db, step_id, tenant_no, user_id, now, it)
  590. db.commit()
  591. def update_step_data_collections(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None):
  592. now = datetime.now()
  593. _soft_delete_sub_records(db, BizProcessRouteStepDataCollection, step_id, now)
  594. for it in items:
  595. _create_step_data_collection(db, step_id, tenant_no, user_id, now, it)
  596. db.commit()
  597. def total_pages(total: int, page_size: int) -> int:
  598. if total <= 0:
  599. return 0
  600. return int(math.ceil(total / float(page_size)))