"""工艺路线 CRUD(biz_process_route + biz_process_route_step + 4张步骤子表)""" from __future__ import annotations import math from datetime import datetime from typing import Any, Dict, List, Optional, Tuple from sqlalchemy import and_, func, or_ from sqlalchemy.orm import Session from app.core.utils import generate_uuid from app.crud.equipment_snapshot import parse_equipment_ids, resolve_equipment_list_snapshot from app.models.models import ( BizProductionProductType, BizProcess, BizProcessReagent, BizProcessConsumable, BizProcessOutput, BizProcessDataCollection, BizProcessRoute, BizProcessRouteStep, BizProcessRouteStepReagent, BizProcessRouteStepConsumable, BizProcessRouteStepOutput, BizProcessRouteStepDataCollection, ) # --------------------------------------------------------------------------- # 序列化函数 # --------------------------------------------------------------------------- def _route_row_to_dict(r: BizProcessRoute) -> Dict[str, Any]: return { "id": r.id, "routeCode": r.route_code, "routeName": r.route_name, "productTypeId": r.product_type_id or "", "version": r.version or "", "isCurrent": r.is_current if r.is_current is not None else "0", "equipmentIds": parse_equipment_ids(getattr(r, "equipment_ids", None)), "equipmentName": getattr(r, "equipment_name", None) or "", "tenantNo": r.tenant_no, "orderNum": r.order_num if r.order_num is not None else 0, "remark": r.remark or "", "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 "", } def _step_row_to_dict(s: BizProcessRouteStep) -> Dict[str, Any]: return { "id": s.id, "routeId": s.route_id, "processId": s.process_id or "", "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 "", "sopIds": s.sop_ids or "", "remark": s.remark or "", } def _step_reagent_row_to_dict(r: BizProcessRouteStepReagent) -> 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 "", "unit": r.unit or "", "isKeyMaterial": r.is_key_material if r.is_key_material is not None else "0", "orderNum": r.order_num if r.order_num is not None else 0, "remark": r.remark or "", } def _step_consumable_row_to_dict(c: BizProcessRouteStepConsumable) -> 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 "", "unit": c.unit or "", "isRequired": c.is_required if c.is_required is not None else "1", "orderNum": c.order_num if c.order_num is not None else 0, "remark": c.remark or "", } def _step_output_row_to_dict(o: BizProcessRouteStepOutput) -> 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_row_to_dict(d: BizProcessRouteStepDataCollection) -> Dict[str, Any]: return { "id": d.id, "stepId": d.step_id, "collectionName": d.collection_name or "", "unit": d.unit or "", "orderNum": d.order_num if d.order_num is not None else 0, "remark": d.remark or "", } # --------------------------------------------------------------------------- # 路线主表 CRUD # --------------------------------------------------------------------------- def list_routes(db: Session, *, page: int = 1, page_size: int = 10, route_name: Optional[str] = None, route_code: Optional[str] = None, product_type_id: Optional[str] = None, tenant_no: Optional[str] = None, user_info=None) -> Tuple[List[Dict], int]: def _filtered(): q = db.query(BizProcessRoute).filter(BizProcessRoute.del_flag == 0) if tenant_no: q = q.filter(BizProcessRoute.tenant_no == tenant_no) if route_name and route_name.strip(): q = q.filter(BizProcessRoute.route_name.like(f"%{route_name.strip()}%")) if route_code and route_code.strip(): q = q.filter(BizProcessRoute.route_code == route_code.strip()) if product_type_id and product_type_id.strip(): q = q.filter(BizProcessRoute.product_type_id == product_type_id.strip()) if user_info: from app.core.data_scope import apply_data_scope q = apply_data_scope(q, BizProcessRoute, user_info, db, tenant_no) return q total = _filtered().order_by(None).with_entities(func.count(BizProcessRoute.id)).scalar() or 0 page = max(1, page) page_size = min(max(1, page_size), 100) rows = ( _filtered() .outerjoin( BizProductionProductType, and_( BizProcessRoute.product_type_id == BizProductionProductType.id, BizProductionProductType.del_flag == 0, ), ) .order_by( BizProductionProductType.product_type_name.is_(None), BizProductionProductType.product_type_name.asc(), BizProcessRoute.create_time.desc(), ) .offset((page - 1) * page_size) .limit(page_size) .all() ) return [_route_row_to_dict(x) for x in rows], int(total) def get_route(db: Session, route_id: str) -> Optional[BizProcessRoute]: return db.query(BizProcessRoute).filter( and_(BizProcessRoute.id == route_id, BizProcessRoute.del_flag == 0) ).first() def get_route_detail(db: Session, route_id: str) -> Optional[Dict[str, Any]]: route = get_route(db, route_id) if not route: return None result = _route_row_to_dict(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() ) step_list = [] for s in steps: sd = _step_row_to_dict(s) reagents = db.query(BizProcessRouteStepReagent).filter( and_(BizProcessRouteStepReagent.step_id == s.id, BizProcessRouteStepReagent.del_flag == 0) ).order_by(BizProcessRouteStepReagent.order_num.asc(), BizProcessRouteStepReagent.create_time.asc()).all() consumables = db.query(BizProcessRouteStepConsumable).filter( and_(BizProcessRouteStepConsumable.step_id == s.id, BizProcessRouteStepConsumable.del_flag == 0) ).order_by(BizProcessRouteStepConsumable.order_num.asc(), BizProcessRouteStepConsumable.create_time.asc()).all() outputs = db.query(BizProcessRouteStepOutput).filter( and_(BizProcessRouteStepOutput.step_id == s.id, BizProcessRouteStepOutput.del_flag == 0) ).order_by(BizProcessRouteStepOutput.order_num.asc(), BizProcessRouteStepOutput.create_time.asc()).all() data_collections = db.query(BizProcessRouteStepDataCollection).filter( and_(BizProcessRouteStepDataCollection.step_id == s.id, BizProcessRouteStepDataCollection.del_flag == 0) ).order_by(BizProcessRouteStepDataCollection.order_num.asc(), BizProcessRouteStepDataCollection.create_time.asc()).all() sd["reagents"] = [_step_reagent_row_to_dict(r) for r in reagents] sd["consumables"] = [_step_consumable_row_to_dict(c) for c in consumables] sd["outputs"] = [_step_output_row_to_dict(o) for o in outputs] sd["dataCollections"] = [_step_data_collection_row_to_dict(d) for d in data_collections] step_list.append(sd) result["steps"] = step_list return result def create_route(db: Session, *, route_code: str, route_name: str, product_type_id: Optional[str] = None, version: Optional[str] = None, remark: Optional[str] = None, tenant_no: str = "zkxw", user_id: Optional[str] = None) -> BizProcessRoute: rc = (route_code or "").strip() if not rc: raise ValueError("路线编码不能为空") now = datetime.now() obj = BizProcessRoute( id=generate_uuid(), route_code=rc, route_name=(route_name or "").strip() or rc, product_type_id=(product_type_id or "").strip() or None, version=(version or "").strip() or None, is_current="0", tenant_no=tenant_no, order_num=0, remark=(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(obj) db.commit() db.refresh(obj) return obj def update_route(db: Session, route_id: str, *, user_id: Optional[str] = None, **kwargs) -> BizProcessRoute: obj = get_route(db, route_id) if not obj: raise ValueError("路线不存在") for field in ["route_code", "route_name", "product_type_id", "version", "remark"]: val = kwargs.get(field) if val is not None: setattr(obj, field, (val or "").strip() or None if field != "route_code" else (val or "").strip()) if "equipment_ids" in kwargs: eids, enames = resolve_equipment_list_snapshot( db, kwargs.get("equipment_ids"), obj.tenant_no or "zkxw" ) obj.equipment_ids = eids obj.equipment_name = enames obj.update_by = user_id obj.update_time = datetime.now() db.add(obj) db.commit() db.refresh(obj) return obj def set_current_route(db: Session, route_id: str, tenant_no: str) -> BizProcessRoute: obj = get_route(db, route_id) if not obj: raise ValueError("路线不存在") now = datetime.now() siblings = db.query(BizProcessRoute).filter( and_(BizProcessRoute.route_code == obj.route_code, BizProcessRoute.tenant_no == tenant_no, BizProcessRoute.del_flag == 0) ).all() for s in siblings: s.is_current = "0" s.update_time = now db.add(s) obj.is_current = "1" obj.update_time = now db.add(obj) db.commit() db.refresh(obj) return obj # --------------------------------------------------------------------------- # 步骤子表辅助 # --------------------------------------------------------------------------- def _soft_delete_sub_records(db: Session, model_class, step_id: str, now: datetime) -> None: rows = db.query(model_class).filter( and_(model_class.step_id == step_id, model_class.del_flag == 0) ).all() for r in rows: r.del_flag = 1 r.update_time = now db.add(r) def _create_step_reagent(db, step_id, tenant_no, user_id, now, data): db.add(BizProcessRouteStepReagent( id=generate_uuid(), step_id=step_id, tenant_no=tenant_no, reagent_code=(data.get("reagent_code") or "").strip() or None, reagent_name=(data.get("reagent_name") or "").strip() or None, spec=(data.get("spec") or "").strip() or None, standard_usage=(data.get("standard_usage") or "").strip() or None, unit=(data.get("unit") or "").strip() or None, is_key_material=data.get("is_key_material", "0"), remark=(data.get("remark") or "").strip() or None, order_num=data.get("order_num", 0) or 0, create_by=user_id, create_dept_id="", create_time=now, update_by=user_id, update_time=now, del_flag=0, )) def _create_step_consumable(db, step_id, tenant_no, user_id, now, data): db.add(BizProcessRouteStepConsumable( id=generate_uuid(), step_id=step_id, tenant_no=tenant_no, consumable_code=(data.get("consumable_code") or "").strip() or None, consumable_name=(data.get("consumable_name") or "").strip() or None, spec_model=(data.get("spec_model") or "").strip() or None, standard_usage=(data.get("standard_usage") or "").strip() or None, unit=(data.get("unit") or "").strip() or None, is_required=data.get("is_required", "1"), remark=(data.get("remark") or "").strip() or None, order_num=data.get("order_num", 0) or 0, create_by=user_id, create_dept_id="", create_time=now, update_by=user_id, update_time=now, del_flag=0, )) def _create_step_output(db, step_id, tenant_no, user_id, now, data): db.add(BizProcessRouteStepOutput( id=generate_uuid(), step_id=step_id, tenant_no=tenant_no, output_type_id=(data.get("output_type_id") or "").strip() or None, output_name=(data.get("output_name") or "").strip() or None, output_barcode=(data.get("output_barcode") or "").strip() or None, remark=(data.get("remark") or "").strip() or None, order_num=data.get("order_num", 0) or 0, create_by=user_id, create_dept_id="", create_time=now, update_by=user_id, update_time=now, del_flag=0, )) def _create_step_data_collection(db, step_id, tenant_no, user_id, now, data): db.add(BizProcessRouteStepDataCollection( id=generate_uuid(), step_id=step_id, tenant_no=tenant_no, collection_name=(data.get("collection_name") or "").strip() or None, unit=(data.get("unit") or "").strip() or None, order_num=data.get("order_num", 0) or 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, )) # --------------------------------------------------------------------------- # 步骤 CRUD # --------------------------------------------------------------------------- def add_step(db: Session, *, route_id: str, process_id: str, step_order: Optional[int] = None, tenant_no: str = "zkxw", user_id: Optional[str] = None) -> Dict[str, Any]: route = get_route(db, route_id) if not route: raise ValueError("路线不存在") proc = db.query(BizProcess).filter( and_(BizProcess.id == process_id, BizProcess.del_flag == 0) ).first() if not proc: raise ValueError("工艺不存在") now = datetime.now() if step_order is None: max_order = db.query(func.max(BizProcessRouteStep.step_order)).filter( and_(BizProcessRouteStep.route_id == route_id, BizProcessRouteStep.del_flag == 0) ).scalar() step_order = (max_order or 0) + 1 step = BizProcessRouteStep( id=generate_uuid(), route_id=route_id, process_id=process_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, 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() src_reagents = db.query(BizProcessReagent).filter( and_(BizProcessReagent.process_id == process_id, BizProcessReagent.del_flag == 0) ).all() for r in src_reagents: _create_step_reagent(db, step.id, tenant_no, user_id, now, { "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, }) src_consumables = db.query(BizProcessConsumable).filter( and_(BizProcessConsumable.process_id == process_id, BizProcessConsumable.del_flag == 0) ).all() for c in src_consumables: _create_step_consumable(db, step.id, tenant_no, user_id, now, { "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, }) src_outputs = db.query(BizProcessOutput).filter( and_(BizProcessOutput.process_id == process_id, BizProcessOutput.del_flag == 0) ).all() for o in src_outputs: _create_step_output(db, step.id, tenant_no, user_id, now, { "output_type_id": o.output_type_id, "output_name": o.output_name, "output_barcode": o.output_barcode, }) src_collections = db.query(BizProcessDataCollection).filter( and_(BizProcessDataCollection.process_id == process_id, BizProcessDataCollection.del_flag == 0) ).all() for d in src_collections: _create_step_data_collection(db, step.id, tenant_no, user_id, now, { "collection_name": d.collection_name, "unit": d.unit, "order_num": d.order_num, }) db.commit() detail = get_route_detail(db, route_id) return detail["steps"][-1] if detail and detail["steps"] else _step_row_to_dict(step) def update_step(db: Session, step_id: str, *, user_id: Optional[str] = None, **kwargs) -> BizProcessRouteStep: step = db.query(BizProcessRouteStep).filter( and_(BizProcessRouteStep.id == step_id, BizProcessRouteStep.del_flag == 0) ).first() if not step: raise ValueError("步骤不存在") for field in ["process_code", "category_id", "process_name", "description", "estimated_duration", "sop_ids"]: val = kwargs.get(field) if val is not None: setattr(step, field, (val or "").strip() or None) step.update_by = user_id step.update_time = datetime.now() db.add(step) db.commit() db.refresh(step) return step def reorder_steps(db: Session, route_id: str, step_ids: List[str]) -> None: now = datetime.now() for idx, sid in enumerate(step_ids): step = db.query(BizProcessRouteStep).filter( and_(BizProcessRouteStep.id == sid, BizProcessRouteStep.route_id == route_id, BizProcessRouteStep.del_flag == 0) ).first() if step: step.step_order = idx + 1 step.update_time = now db.add(step) db.commit() def copy_step(db: Session, step_id: str, *, tenant_no: str = "zkxw", user_id: Optional[str] = None) -> Dict[str, Any]: """复制步骤,插入到原步骤之后,名称追加(复制)""" src = db.query(BizProcessRouteStep).filter( and_(BizProcessRouteStep.id == step_id, BizProcessRouteStep.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(BizProcessRouteStep).filter( and_( BizProcessRouteStep.route_id == src.route_id, BizProcessRouteStep.del_flag == 0, BizProcessRouteStep.step_order > src.step_order, ) ).order_by(BizProcessRouteStep.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 = BizProcessRouteStep( id=generate_uuid(), route_id=src.route_id, process_id=src.process_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, sop_ids=src.sop_ids, 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() # 复制子表:试剂 for r in db.query(BizProcessRouteStepReagent).filter( and_(BizProcessRouteStepReagent.step_id == step_id, BizProcessRouteStepReagent.del_flag == 0) ).order_by( BizProcessRouteStepReagent.order_num.asc(), BizProcessRouteStepReagent.create_time.asc(), ).all(): _create_step_reagent(db, new_step.id, tenant_no, user_id, now, { "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, "order_num": r.order_num, "remark": r.remark, }) # 复制子表:耗材 for c in db.query(BizProcessRouteStepConsumable).filter( and_(BizProcessRouteStepConsumable.step_id == step_id, BizProcessRouteStepConsumable.del_flag == 0) ).order_by( BizProcessRouteStepConsumable.order_num.asc(), BizProcessRouteStepConsumable.create_time.asc(), ).all(): _create_step_consumable(db, new_step.id, tenant_no, user_id, now, { "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, "order_num": c.order_num, "remark": c.remark, }) # 复制子表:产出 for o in db.query(BizProcessRouteStepOutput).filter( and_(BizProcessRouteStepOutput.step_id == step_id, BizProcessRouteStepOutput.del_flag == 0) ).order_by( BizProcessRouteStepOutput.order_num.asc(), BizProcessRouteStepOutput.create_time.asc(), ).all(): _create_step_output(db, new_step.id, tenant_no, user_id, now, { "output_type_id": o.output_type_id, "output_name": o.output_name, "output_barcode": o.output_barcode, "order_num": o.order_num, "remark": o.remark, }) # 复制子表:数据采集 for d in db.query(BizProcessRouteStepDataCollection).filter( and_(BizProcessRouteStepDataCollection.step_id == step_id, BizProcessRouteStepDataCollection.del_flag == 0) ).order_by( BizProcessRouteStepDataCollection.order_num.asc(), BizProcessRouteStepDataCollection.create_time.asc(), ).all(): _create_step_data_collection(db, new_step.id, tenant_no, user_id, now, { "collection_name": d.collection_name, "unit": d.unit, "order_num": d.order_num, "remark": d.remark, }) db.commit() detail = get_route_detail(db, src.route_id) return detail["steps"][-1] if detail and detail["steps"] else _step_row_to_dict(new_step) def soft_delete_step(db: Session, step_id: str) -> None: step = db.query(BizProcessRouteStep).filter( and_(BizProcessRouteStep.id == step_id, BizProcessRouteStep.del_flag == 0) ).first() if not step: raise ValueError("步骤不存在") now = datetime.now() step.del_flag = 1 step.update_time = now db.add(step) _soft_delete_sub_records(db, BizProcessRouteStepReagent, step_id, now) _soft_delete_sub_records(db, BizProcessRouteStepConsumable, step_id, now) _soft_delete_sub_records(db, BizProcessRouteStepOutput, step_id, now) _soft_delete_sub_records(db, BizProcessRouteStepDataCollection, step_id, now) db.commit() def soft_delete_route(db: Session, route_id: str) -> None: route = get_route(db, route_id) if not route: raise ValueError("路线不存在") now = datetime.now() route.del_flag = 1 route.update_time = now db.add(route) steps = db.query(BizProcessRouteStep).filter( and_(BizProcessRouteStep.route_id == route_id, BizProcessRouteStep.del_flag == 0) ).all() for s in steps: s.del_flag = 1 s.update_time = now db.add(s) _soft_delete_sub_records(db, BizProcessRouteStepReagent, s.id, now) _soft_delete_sub_records(db, BizProcessRouteStepConsumable, s.id, now) _soft_delete_sub_records(db, BizProcessRouteStepOutput, s.id, now) _soft_delete_sub_records(db, BizProcessRouteStepDataCollection, s.id, now) db.commit() # --------------------------------------------------------------------------- # 子表完整替换更新 # --------------------------------------------------------------------------- def update_step_reagents(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None): now = datetime.now() _soft_delete_sub_records(db, BizProcessRouteStepReagent, step_id, now) for it in items: _create_step_reagent(db, step_id, tenant_no, user_id, now, it) db.commit() def update_step_consumables(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None): now = datetime.now() _soft_delete_sub_records(db, BizProcessRouteStepConsumable, step_id, now) for it in items: _create_step_consumable(db, step_id, tenant_no, user_id, now, it) db.commit() def update_step_outputs(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None): now = datetime.now() _soft_delete_sub_records(db, BizProcessRouteStepOutput, step_id, now) for it in items: _create_step_output(db, step_id, tenant_no, user_id, now, it) db.commit() def update_step_data_collections(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None): now = datetime.now() _soft_delete_sub_records(db, BizProcessRouteStepDataCollection, step_id, now) for it in items: _create_step_data_collection(db, step_id, tenant_no, user_id, now, it) db.commit() def total_pages(total: int, page_size: int) -> int: if total <= 0: return 0 return int(math.ceil(total / float(page_size)))