"""生产管理 API 路由(7 个子模块:耗材、试剂、SOP文档、工艺工序分类、产出物类型、耗材包、工艺库)""" from fastapi import APIRouter, Depends, Query, HTTPException from pydantic import BaseModel, ConfigDict, Field from sqlalchemy.orm import Session from typing import Any, Optional, List from app.core.database import get_db from app.core.menu_guard import ( require_admin_or_super_admin, require_any_menu_code, require_menu_code, ) from app.core.response import success_response, error_response from app.crud import consumable as consumable_svc from app.crud import reagent as reagent_svc from app.crud import consumable_stock_log as consumable_stock_log_svc from app.crud import reagent_stock_log as reagent_stock_log_svc from app.crud import sop_document as sop_document_svc from app.crud import process_category as process_category_svc from app.crud import equipment as equipment_svc from app.crud import output_type as output_type_svc from app.crud import production_product_type as production_product_type_svc from app.crud import consumable_pack as consumable_pack_svc from app.crud import process as process_svc from app.crud import process_route as route_svc from app.schemas.process_route import ( ProcessRouteCreateBody, ProcessRouteUpdateBody, SetCurrentBody, StepAddBody, StepCopyBody, StepUpdateBody, StepReorderBody, StepReagentUpdateBody, StepConsumableUpdateBody, StepOutputUpdateBody, StepDataCollectionUpdateBody, ) from app.models.models import SysUser from app.crud import production_work_order as production_work_order_svc from app.crud import production_task as production_task_svc from app.crud import production_task_set as production_task_set_svc from app.crud import production_waste_output as production_waste_output_svc from app.crud import cell_product as cell_product_svc from app.crud import qc_work_order as qc_work_order_svc from app.schemas.production_work_order import ( ProductionWorkOrderListQuery, ProductionWorkOrderComplete, ProductionWorkOrderCreate, ProductionWorkOrderConfigureBody, ProductionWorkOrderUpdate, ) from app.schemas.production_task_set import ( ProductionTaskSetCreateBody, ProductionTaskSetFromWorkOrderBody, ProductionTaskSetUpdateBody, ProductionTasksFromSetBody, ) from app.schemas.production_task import ( ProductionTaskCreate, ProductionTaskUpdate, ProductionTaskStatusUpdate, ProductionTaskForwardBody, ProductionTaskAssigneeUpdate, ProductionTaskStepDispatchBody, SpawnTemporaryWorkOrderBody, TaskStepAdd, TaskStepUpdate, TaskStepMaterialItem, TaskStepOutputItem, TaskStepDataCollectionItem, TaskInputItem, TaskOutputItem, TaskStepReorderBody, ProductionTaskListByWorkOrdersBody, ) from app.schemas.cell_product import CellProductCreate, CellProductUpdate from app.schemas.qc_work_order import ProductionQcWorkOrderCreateBody router = APIRouter(prefix="/web/admin", tags=["生产管理"]) _LEGACY_MATERIAL_GONE_MSG = "耗材/试剂维护已迁移至【生产物料库】,请使用 production-material 接口" def _legacy_material_write_gone(): return error_response(410, _LEGACY_MATERIAL_GONE_MSG) def _get_tenant_no(db: Session, user: dict) -> str: user_id = user.get("user_id") if not user_id: return "zkxw" sys_user = db.query(SysUser).filter(SysUser.id == user_id).first() return (sys_user.tenant_no if sys_user else None) or "zkxw" def _get_current_user_id(user: dict) -> str: return (user.get("user_id") or "").strip() # --------------------------------------------------------------------------- # 1. 耗材 # --------------------------------------------------------------------------- class ConsumableCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) consumable_code: str = Field(..., alias="consumableCode") consumable_name: str = Field(..., alias="consumableName") spec_model: Optional[str] = Field(None, alias="specModel") standard_usage: Optional[str] = Field(None, alias="standardUsage") unit: Optional[str] = None description: Optional[str] = None warning_qty: Optional[float] = Field(0, alias="warningQty") status: Optional[str] = Field("1", alias="status") order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None class ConsumableUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) consumable_code: Optional[str] = Field(None, alias="consumableCode") consumable_name: Optional[str] = Field(None, alias="consumableName") spec_model: Optional[str] = Field(None, alias="specModel") standard_usage: Optional[str] = Field(None, alias="standardUsage") unit: Optional[str] = None description: Optional[str] = None warning_qty: Optional[float] = Field(None, alias="warningQty") status: Optional[str] = Field(None, alias="status") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None @router.get("/consumables") async def list_consumables( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, user: dict = Depends(require_menu_code("production:consumable:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = consumable_svc.list_consumables(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user) tp = consumable_svc.total_pages(total, pageSize) return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.get("/consumables/all") async def list_all_consumables( user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, _ = consumable_svc.list_consumables(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user) return success_response(items) @router.post("/consumables") async def create_consumable( body: ConsumableCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): return _legacy_material_write_gone() @router.put("/consumables/{consumable_id}") async def update_consumable( consumable_id: str, body: ConsumableUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): return _legacy_material_write_gone() @router.delete("/consumables/{consumable_id}") async def delete_consumable( consumable_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): return _legacy_material_write_gone() class StockInBody(BaseModel): model_config = ConfigDict(populate_by_name=True) qty: float = Field(..., gt=0) remark: Optional[str] = None class StockOutBody(BaseModel): model_config = ConfigDict(populate_by_name=True) qty: float = Field(..., gt=0) receiver: str = Field(..., min_length=1) purpose: Optional[str] = None remark: Optional[str] = None class BatchStockInItem(BaseModel): model_config = ConfigDict(populate_by_name=True) materialId: str qty: float = Field(..., gt=0) remark: Optional[str] = None class BatchStockInBody(BaseModel): model_config = ConfigDict(populate_by_name=True) items: List[BatchStockInItem] remark: Optional[str] = None class BatchStockOutItem(BaseModel): model_config = ConfigDict(populate_by_name=True) materialId: str qty: float = Field(..., gt=0) class BatchStockOutBody(BaseModel): model_config = ConfigDict(populate_by_name=True) items: List[BatchStockOutItem] receiver: str = Field(..., min_length=1) purpose: Optional[str] = None remark: Optional[str] = None @router.post("/consumables/{consumable_id}/stock-in") async def consumable_stock_in( consumable_id: str, body: StockInBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) from decimal import Decimal obj = consumable_svc.stock_in( db, consumable_id, qty=Decimal(str(body.qty)), remark=body.remark, user_id=user.get("user_id"), tenant_no=tenant_no, ) return success_response(consumable_svc._row_to_dict(obj), message="入库成功") except ValueError as e: return error_response(400, str(e)) @router.post("/consumables/{consumable_id}/stock-out") async def consumable_stock_out( consumable_id: str, body: StockOutBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) from decimal import Decimal obj = consumable_svc.stock_out( db, consumable_id, qty=Decimal(str(body.qty)), receiver=body.receiver, purpose=body.purpose, remark=body.remark, user_id=user.get("user_id"), tenant_no=tenant_no, ) return success_response(consumable_svc._row_to_dict(obj), message="出库成功") except ValueError as e: return error_response(400, str(e)) @router.post("/consumables/batch-stock-in") async def consumable_batch_stock_in( body: BatchStockInBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) items_data = [it.model_dump() for it in body.items] count = consumable_svc.batch_stock_in( db, items=items_data, remark=body.remark, tenant_no=tenant_no, user_id=user.get("user_id"), ) return success_response({"count": count}, message=f"成功入库 {count} 条") except ValueError as e: return error_response(400, str(e)) @router.post("/consumables/batch-stock-out") async def consumable_batch_stock_out( body: BatchStockOutBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) items_data = [it.model_dump() for it in body.items] count = consumable_svc.batch_stock_out( db, items=items_data, receiver=body.receiver, purpose=body.purpose, remark=body.remark, tenant_no=tenant_no, user_id=user.get("user_id"), ) return success_response({"count": count}, message=f"成功出库 {count} 条") except ValueError as e: return error_response(400, str(e)) @router.get("/consumables/{consumable_id}/stock-logs") async def list_consumable_stock_logs( consumable_id: str, page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), changeType: Optional[str] = None, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = consumable_stock_log_svc.list_stock_logs( db, tenant_no, page=page, page_size=pageSize, material_id=consumable_id, change_type=changeType, user_info=user, ) tp = (total + pageSize - 1) // pageSize if total > 0 else 0 return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.get("/consumables/stock-logs") async def list_all_consumable_stock_logs( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, changeType: Optional[str] = None, startTime: Optional[str] = None, endTime: Optional[str] = None, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = consumable_stock_log_svc.list_stock_logs( db, tenant_no, page=page, page_size=pageSize, keyword=keyword, change_type=changeType, start_time=startTime, end_time=endTime, user_info=user, ) tp = (total + pageSize - 1) // pageSize if total > 0 else 0 return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) # --------------------------------------------------------------------------- # 2. 试剂 # --------------------------------------------------------------------------- class ReagentCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) reagent_code: str = Field(..., alias="reagentCode") reagent_name: str = Field(..., alias="reagentName") spec: Optional[str] = None standard_usage: Optional[str] = Field(None, alias="standardUsage") unit: Optional[str] = None description: Optional[str] = None warning_qty: Optional[float] = Field(0, alias="warningQty") status: Optional[str] = Field("1", alias="status") order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None class ReagentUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) reagent_code: Optional[str] = Field(None, alias="reagentCode") reagent_name: Optional[str] = Field(None, alias="reagentName") spec: Optional[str] = None standard_usage: Optional[str] = Field(None, alias="standardUsage") unit: Optional[str] = None description: Optional[str] = None warning_qty: Optional[float] = Field(None, alias="warningQty") status: Optional[str] = Field(None, alias="status") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None @router.get("/reagents") async def list_reagents( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, user: dict = Depends(require_menu_code("production:reagent:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = reagent_svc.list_reagents(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user) tp = reagent_svc.total_pages(total, pageSize) return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.get("/reagents/all") async def list_all_reagents( user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, _ = reagent_svc.list_reagents(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user) return success_response(items) @router.post("/reagents") async def create_reagent( body: ReagentCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): return _legacy_material_write_gone() @router.put("/reagents/{reagent_id}") async def update_reagent( reagent_id: str, body: ReagentUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): return _legacy_material_write_gone() @router.delete("/reagents/{reagent_id}") async def delete_reagent( reagent_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): return _legacy_material_write_gone() @router.post("/reagents/{reagent_id}/stock-in") async def reagent_stock_in( reagent_id: str, body: StockInBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) from decimal import Decimal obj = reagent_svc.stock_in( db, reagent_id, qty=Decimal(str(body.qty)), remark=body.remark, user_id=user.get("user_id"), tenant_no=tenant_no, ) return success_response(reagent_svc._row_to_dict(obj), message="入库成功") except ValueError as e: return error_response(400, str(e)) @router.post("/reagents/{reagent_id}/stock-out") async def reagent_stock_out( reagent_id: str, body: StockOutBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) from decimal import Decimal obj = reagent_svc.stock_out( db, reagent_id, qty=Decimal(str(body.qty)), receiver=body.receiver, purpose=body.purpose, remark=body.remark, user_id=user.get("user_id"), tenant_no=tenant_no, ) return success_response(reagent_svc._row_to_dict(obj), message="出库成功") except ValueError as e: return error_response(400, str(e)) @router.post("/reagents/batch-stock-in") async def reagent_batch_stock_in( body: BatchStockInBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) items_data = [it.model_dump() for it in body.items] count = reagent_svc.batch_stock_in( db, items=items_data, remark=body.remark, tenant_no=tenant_no, user_id=user.get("user_id"), ) return success_response({"count": count}, message=f"成功入库 {count} 条") except ValueError as e: return error_response(400, str(e)) @router.post("/reagents/batch-stock-out") async def reagent_batch_stock_out( body: BatchStockOutBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) items_data = [it.model_dump() for it in body.items] count = reagent_svc.batch_stock_out( db, items=items_data, receiver=body.receiver, purpose=body.purpose, remark=body.remark, tenant_no=tenant_no, user_id=user.get("user_id"), ) return success_response({"count": count}, message=f"成功出库 {count} 条") except ValueError as e: return error_response(400, str(e)) @router.get("/reagents/{reagent_id}/stock-logs") async def list_reagent_stock_logs( reagent_id: str, page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), changeType: Optional[str] = None, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = reagent_stock_log_svc.list_stock_logs( db, tenant_no, page=page, page_size=pageSize, material_id=reagent_id, change_type=changeType, user_info=user, ) tp = (total + pageSize - 1) // pageSize if total > 0 else 0 return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.get("/reagents/stock-logs") async def list_all_reagent_stock_logs( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, changeType: Optional[str] = None, startTime: Optional[str] = None, endTime: Optional[str] = None, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = reagent_stock_log_svc.list_stock_logs( db, tenant_no, page=page, page_size=pageSize, keyword=keyword, change_type=changeType, start_time=startTime, end_time=endTime, user_info=user, ) tp = (total + pageSize - 1) // pageSize if total > 0 else 0 return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) # --------------------------------------------------------------------------- # 3. SOP文档 # --------------------------------------------------------------------------- class SopDocumentCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) sop_code: str = Field(..., alias="sopCode") sop_name: str = Field(..., alias="sopName") version_no: Optional[str] = Field(None, alias="versionNo") sop_attachments: Optional[str] = Field(None, alias="sopAttachments") order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None class SopDocumentUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) sop_code: Optional[str] = Field(None, alias="sopCode") sop_name: Optional[str] = Field(None, alias="sopName") version_no: Optional[str] = Field(None, alias="versionNo") sop_attachments: Optional[str] = Field(None, alias="sopAttachments") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None @router.get("/sop-documents") async def list_sop_documents( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, user: dict = Depends(require_menu_code("production:sop-document:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = sop_document_svc.list_sop_documents(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user) tp = sop_document_svc.total_pages(total, pageSize) return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.get("/sop-documents/all") async def list_all_sop_documents( user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, _ = sop_document_svc.list_sop_documents(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user) return success_response(items) @router.post("/sop-documents") async def create_sop_document( body: SopDocumentCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = sop_document_svc.create_sop_document( db, sop_code=body.sop_code, sop_name=body.sop_name, version_no=body.version_no, sop_attachments=body.sop_attachments, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response( sop_document_svc._row_to_dict(obj, db, tenant_no), message="创建成功", ) except ValueError as e: return error_response(400, str(e)) @router.put("/sop-documents/{sop_id}") async def update_sop_document( sop_id: str, body: SopDocumentUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = sop_document_svc.update_sop_document( db, sop_id, sop_code=payload.get("sop_code"), sop_name=payload.get("sop_name"), version_no=payload.get("version_no"), sop_attachments=payload.get("sop_attachments"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), ) tenant_no = _get_tenant_no(db, user) return success_response( sop_document_svc._row_to_dict(obj, db, tenant_no), message="更新成功", ) except ValueError as e: return error_response(400, str(e)) @router.delete("/sop-documents/{sop_id}") async def delete_sop_document( sop_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: sop_document_svc.soft_delete_sop_document(db, sop_id) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 4. 工艺工序分类 # --------------------------------------------------------------------------- class ProcessCategoryCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) category_code: str = Field(..., alias="categoryCode") category_name: str = Field(..., alias="categoryName") order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None class ProcessCategoryUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) category_code: Optional[str] = Field(None, alias="categoryCode") category_name: Optional[str] = Field(None, alias="categoryName") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None @router.get("/process-categories") async def list_process_categories( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, user: dict = Depends(require_menu_code("production:process-category:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = process_category_svc.list_process_categories(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user) tp = process_category_svc.total_pages(total, pageSize) return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.get("/process-categories/all") async def list_all_process_categories( user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, _ = process_category_svc.list_process_categories(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user) return success_response(items) @router.post("/process-categories") async def create_process_category( body: ProcessCategoryCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = process_category_svc.create_process_category( db, category_code=body.category_code, category_name=body.category_name, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response(process_category_svc._row_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/process-categories/{category_id}") async def update_process_category( category_id: str, body: ProcessCategoryUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = process_category_svc.update_process_category( db, category_id, category_code=payload.get("category_code"), category_name=payload.get("category_name"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), ) return success_response(process_category_svc._row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/process-categories/{category_id}") async def delete_process_category( category_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: process_category_svc.soft_delete_process_category(db, category_id) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 设备管理 # --------------------------------------------------------------------------- class EquipmentCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) equipment_code: str = Field(..., alias="equipmentCode") equipment_name: str = Field(..., alias="equipmentName") equipment_model: Optional[str] = Field(None, alias="equipmentModel") manufacturer: Optional[str] = None purchase_date: Optional[str] = Field(None, alias="purchaseDate") purchase_amount: Optional[float] = Field(None, alias="purchaseAmount") location: Optional[str] = None serial_no: Optional[str] = Field(None, alias="serialNo") status: Optional[str] = "1" order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None class EquipmentUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) equipment_code: Optional[str] = Field(None, alias="equipmentCode") equipment_name: Optional[str] = Field(None, alias="equipmentName") equipment_model: Optional[str] = Field(None, alias="equipmentModel") manufacturer: Optional[str] = None purchase_date: Optional[str] = Field(None, alias="purchaseDate") purchase_amount: Optional[float] = Field(None, alias="purchaseAmount") location: Optional[str] = None serial_no: Optional[str] = Field(None, alias="serialNo") status: Optional[str] = None order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None @router.get("/equipments") async def list_equipments( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, status: Optional[str] = None, user: dict = Depends(require_menu_code("production:equipment:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = equipment_svc.list_equipments( db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no, user_info=user, ) tp = equipment_svc.total_pages(total, pageSize) return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.get("/equipments/all") async def list_all_equipments( user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, _ = equipment_svc.list_equipments( db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user ) return success_response(items) @router.post("/equipments") async def create_equipment( body: EquipmentCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = equipment_svc.create_equipment( db, equipment_code=body.equipment_code, equipment_name=body.equipment_name, equipment_model=body.equipment_model, manufacturer=body.manufacturer, purchase_date=body.purchase_date, purchase_amount=body.purchase_amount, location=body.location, serial_no=body.serial_no, status=body.status, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response(equipment_svc._row_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/equipments/{equipment_id}") async def update_equipment( equipment_id: str, body: EquipmentUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: payload: dict[str, Any] = body.model_dump(by_alias=False) obj = equipment_svc.update_equipment( db, equipment_id, equipment_code=payload.get("equipment_code"), equipment_name=payload.get("equipment_name"), equipment_model=payload.get("equipment_model"), manufacturer=payload.get("manufacturer"), purchase_date=payload.get("purchase_date"), purchase_amount=payload.get("purchase_amount"), location=payload.get("location"), serial_no=payload.get("serial_no"), status=payload.get("status"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), ) return success_response(equipment_svc._row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/equipments/{equipment_id}") async def delete_equipment( equipment_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: equipment_svc.soft_delete_equipment(db, equipment_id) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 5. 工艺产出物类型 # --------------------------------------------------------------------------- class OutputTypeCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) type_code: str = Field(..., alias="typeCode") type_name: str = Field(..., alias="typeName") order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None class OutputTypeUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) type_code: Optional[str] = Field(None, alias="typeCode") type_name: Optional[str] = Field(None, alias="typeName") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None @router.get("/output-types") async def list_output_types( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, user: dict = Depends(require_menu_code("production:output-type:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = output_type_svc.list_output_types(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user) tp = output_type_svc.total_pages(total, pageSize) return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.get("/output-types/all") async def list_all_output_types( user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, _ = output_type_svc.list_output_types(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user) return success_response(items) @router.post("/output-types") async def create_output_type( body: OutputTypeCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = output_type_svc.create_output_type( db, type_code=body.type_code, type_name=body.type_name, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response(output_type_svc._row_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/output-types/{type_id}") async def update_output_type( type_id: str, body: OutputTypeUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = output_type_svc.update_output_type( db, type_id, type_code=payload.get("type_code"), type_name=payload.get("type_name"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), ) return success_response(output_type_svc._row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/output-types/{type_id}") async def delete_output_type( type_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: output_type_svc.soft_delete_output_type(db, type_id) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 5b. 生产产品类型(生产专用,解耦中试 biz_cell_product_type) # --------------------------------------------------------------------------- class ProductionProductTypeCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) product_type_code: str = Field(..., alias="productTypeCode") product_type_name: str = Field(..., alias="productTypeName") order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None class ProductionProductTypeUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) product_type_code: Optional[str] = Field(None, alias="productTypeCode") product_type_name: Optional[str] = Field(None, alias="productTypeName") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None @router.get("/production-product-types") async def list_production_product_types( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = production_product_type_svc.list_production_product_types( db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user ) tp = production_product_type_svc.total_pages(total, pageSize) return success_response( {"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp} ) @router.post("/production-product-types") async def create_production_product_type( body: ProductionProductTypeCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = production_product_type_svc.create_production_product_type( db, product_type_code=body.product_type_code, product_type_name=body.product_type_name, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response(production_product_type_svc._row_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/production-product-types/{row_id}") async def update_production_product_type( row_id: str, body: ProductionProductTypeUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = production_product_type_svc.update_production_product_type( db, row_id, product_type_code=payload.get("product_type_code"), product_type_name=payload.get("product_type_name"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), tenant_no=tenant_no, ) return success_response(production_product_type_svc._row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/production-product-types/{row_id}") async def delete_production_product_type( row_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) production_product_type_svc.soft_delete_production_product_type( db, row_id, tenant_no=tenant_no ) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 6. 耗材包(主 + 子项) # --------------------------------------------------------------------------- class PackItemInput(BaseModel): model_config = ConfigDict(populate_by_name=True) consumable_code: Optional[str] = Field(None, alias="consumableCode") consumable_name: Optional[str] = Field(None, alias="consumableName") spec_model: Optional[str] = Field(None, alias="specModel") standard_usage: Optional[str] = Field(None, alias="standardUsage") unit: Optional[str] = None remark: Optional[str] = None class ConsumablePackCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) pack_code: str = Field(..., alias="packCode") pack_name: str = Field(..., alias="packName") usage_scene: Optional[str] = Field(None, alias="usageScene") pack_type: Optional[str] = Field(None, alias="packType") status: Optional[int] = 1 order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None items: List[PackItemInput] = [] class ConsumablePackUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) pack_code: Optional[str] = Field(None, alias="packCode") pack_name: Optional[str] = Field(None, alias="packName") usage_scene: Optional[str] = Field(None, alias="usageScene") pack_type: Optional[str] = Field(None, alias="packType") status: Optional[int] = None order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None items: Optional[List[PackItemInput]] = None @router.get("/consumable-packs") async def list_consumable_packs( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, user: dict = Depends(require_menu_code("production:consumable-pack:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = consumable_pack_svc.list_consumable_packs(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user) tp = consumable_pack_svc.total_pages(total, pageSize) return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.get("/consumable-packs/all") async def list_all_consumable_packs( user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items = consumable_pack_svc.list_all_packs(db, tenant_no, user_info=user) return success_response(items) @router.get("/consumable-packs/{pack_id}") async def get_consumable_pack_detail( pack_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): detail = consumable_pack_svc.get_pack_detail(db, pack_id) if not detail: return error_response(404, "耗材包不存在") return success_response(detail) @router.post("/consumable-packs") async def create_consumable_pack( body: ConsumablePackCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) items_data = [it.model_dump(by_alias=False) for it in body.items] if body.items else None obj = consumable_pack_svc.create_consumable_pack( db, pack_code=body.pack_code, pack_name=body.pack_name, usage_scene=body.usage_scene, pack_type=body.pack_type, status=body.status, items=items_data, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response(consumable_pack_svc._row_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/consumable-packs/{pack_id}") async def update_consumable_pack( pack_id: str, body: ConsumablePackUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) items_data = [it.model_dump(by_alias=False) for it in body.items] if body.items is not None else None obj = consumable_pack_svc.update_consumable_pack( db, pack_id, pack_code=payload.get("pack_code"), pack_name=payload.get("pack_name"), usage_scene=payload.get("usage_scene"), pack_type=payload.get("pack_type"), status=payload.get("status"), items=items_data, order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), ) return success_response(consumable_pack_svc._row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/consumable-packs/{pack_id}") async def delete_consumable_pack( pack_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: consumable_pack_svc.soft_delete_consumable_pack(db, pack_id) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 7. 工艺库(主 + 3 个子表:试剂、耗材、产出物) # --------------------------------------------------------------------------- class ProcessReagentInput(BaseModel): model_config = ConfigDict(populate_by_name=True) id: Optional[str] = None reagent_code: Optional[str] = Field(None, alias="reagentCode") reagent_name: Optional[str] = Field(None, alias="reagentName") spec: Optional[str] = None standard_usage: Optional[str] = Field(None, alias="standardUsage") unit: Optional[str] = None is_key_material: Optional[str] = Field("0", alias="isKeyMaterial") remark: Optional[str] = None class ProcessConsumableInput(BaseModel): model_config = ConfigDict(populate_by_name=True) id: Optional[str] = None consumable_code: Optional[str] = Field(None, alias="consumableCode") consumable_name: Optional[str] = Field(None, alias="consumableName") spec_model: Optional[str] = Field(None, alias="specModel") standard_usage: Optional[str] = Field(None, alias="standardUsage") unit: Optional[str] = None is_required: Optional[str] = Field("1", alias="isRequired") remark: Optional[str] = None class ProcessOutputInput(BaseModel): model_config = ConfigDict(populate_by_name=True) id: Optional[str] = None output_type_id: Optional[str] = Field(None, alias="outputTypeId") output_name: Optional[str] = Field(None, alias="outputName") output_barcode: Optional[str] = Field(None, alias="outputBarcode") remark: Optional[str] = None class ProcessDataCollectionInput(BaseModel): model_config = ConfigDict(populate_by_name=True) id: Optional[str] = None collection_name: str = Field(..., alias="collectionName") unit: Optional[str] = None order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None class ProcessCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) process_code: str = Field(..., alias="processCode") category_id: Optional[str] = Field(None, alias="categoryId") process_name: str = Field(..., alias="processName") description: Optional[str] = None equipment_ids: Optional[List[str]] = Field(None, alias="equipmentIds") estimated_duration: Optional[str] = Field(None, alias="estimatedDuration") sop_ids: Optional[str] = Field(None, alias="sopIds") sop_content: Optional[str] = Field(None, alias="sopContent") order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None reagents: List[ProcessReagentInput] = [] consumables: List[ProcessConsumableInput] = [] outputs: List[ProcessOutputInput] = [] data_collections: List[ProcessDataCollectionInput] = Field([], alias="dataCollections") class ProcessUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) process_code: Optional[str] = Field(None, alias="processCode") category_id: Optional[str] = Field(None, alias="categoryId") process_name: Optional[str] = Field(None, alias="processName") description: Optional[str] = None equipment_ids: Optional[List[str]] = Field(None, alias="equipmentIds") estimated_duration: Optional[str] = Field(None, alias="estimatedDuration") sop_ids: Optional[str] = Field(None, alias="sopIds") sop_content: Optional[str] = Field(None, alias="sopContent") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None reagents: Optional[List[ProcessReagentInput]] = None consumables: Optional[List[ProcessConsumableInput]] = None outputs: Optional[List[ProcessOutputInput]] = None data_collections: Optional[List[ProcessDataCollectionInput]] = Field(None, alias="dataCollections") @router.get("/processes") async def list_processes( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, user: dict = Depends(require_menu_code("production:process:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = process_svc.list_processes(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user) tp = process_svc.total_pages(total, pageSize) return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.get("/processes/all") async def list_all_processes( user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items = process_svc.list_all_processes(db, tenant_no, user_info=user) return success_response(items) @router.get("/processes/{process_id}") async def get_process_detail( process_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): detail = process_svc.get_process_detail(db, process_id) if not detail: return error_response(404, "工艺不存在") return success_response(detail) @router.post("/processes") async def create_process( body: ProcessCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) reagents_data = [r.model_dump(by_alias=False) for r in body.reagents] if body.reagents else None consumables_data = [c.model_dump(by_alias=False) for c in body.consumables] if body.consumables else None outputs_data = [o.model_dump(by_alias=False) for o in body.outputs] if body.outputs else None data_collections_data = [d.model_dump(by_alias=False) for d in body.data_collections] if body.data_collections else None # Convert string fields to appropriate types for sub-records for rd in (reagents_data or []): if "is_key_material" in rd and isinstance(rd["is_key_material"], str): rd["is_key_material"] = int(rd["is_key_material"]) for cd in (consumables_data or []): if "is_required" in cd and isinstance(cd["is_required"], str): cd["is_required"] = int(cd["is_required"]) obj = process_svc.create_process( db, process_code=body.process_code, category_id=body.category_id, process_name=body.process_name, description=body.description, equipment_ids=body.equipment_ids, estimated_duration=body.estimated_duration, sop_ids=body.sop_ids, sop_content=body.sop_content, reagents=reagents_data, consumables=consumables_data, outputs=outputs_data, data_collections=data_collections_data, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response(process_svc._row_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/processes/{process_id}") async def update_process( process_id: str, body: ProcessUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) reagents_data = [r.model_dump(by_alias=False) for r in body.reagents] if body.reagents is not None else None consumables_data = [c.model_dump(by_alias=False) for c in body.consumables] if body.consumables is not None else None outputs_data = [o.model_dump(by_alias=False) for o in body.outputs] if body.outputs is not None else None data_collections_data = [d.model_dump(by_alias=False) for d in body.data_collections] if body.data_collections is not None else None # Convert string fields to appropriate types for sub-records for rd in (reagents_data or []): if "is_key_material" in rd and isinstance(rd["is_key_material"], str): rd["is_key_material"] = int(rd["is_key_material"]) for cd in (consumables_data or []): if "is_required" in cd and isinstance(cd["is_required"], str): cd["is_required"] = int(cd["is_required"]) update_kwargs: dict[str, Any] = { "process_code": payload.get("process_code"), "category_id": payload.get("category_id"), "process_name": payload.get("process_name"), "description": payload.get("description"), "estimated_duration": payload.get("estimated_duration"), "sop_ids": payload.get("sop_ids"), "sop_content": payload.get("sop_content"), "reagents": reagents_data, "consumables": consumables_data, "outputs": outputs_data, "data_collections": data_collections_data, "order_num": payload.get("order_num"), "remark": payload.get("remark"), "user_id": user.get("user_id"), } if "equipment_ids" in payload: update_kwargs["equipment_ids"] = payload.get("equipment_ids") obj = process_svc.update_process(db, process_id, **update_kwargs) return success_response(process_svc._row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/processes/{process_id}") async def delete_process( process_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: process_svc.soft_delete_process(db, process_id) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 8. 工艺路线管理 # --------------------------------------------------------------------------- @router.get("/process-routes") async def list_process_routes( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), routeName: Optional[str] = None, routeCode: Optional[str] = None, productTypeId: Optional[str] = None, user: dict = Depends(require_menu_code("production:process-route:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = route_svc.list_routes( db, page=page, page_size=pageSize, route_name=routeName, route_code=routeCode, product_type_id=productTypeId, tenant_no=tenant_no, user_info=user, ) tp = route_svc.total_pages(total, pageSize) return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.get("/process-routes/{route_id}") async def get_process_route_detail( route_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): detail = route_svc.get_route_detail(db, route_id) if not detail: return error_response(404, "路线不存在") return success_response(detail) @router.post("/process-routes") async def create_process_route( body: ProcessRouteCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = route_svc.create_route( db, route_code=body.route_code, route_name=body.route_name, product_type_id=body.product_type_id, version=body.version, remark=body.remark, tenant_no=tenant_no, user_id=user.get("user_id"), ) return success_response(route_svc._route_row_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/process-routes/{route_id}") async def update_process_route( route_id: str, body: ProcessRouteUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: route_svc.assert_route_writable(db, route_id, user) payload = body.model_dump(exclude_unset=True, by_alias=False) update_kwargs = { "user_id": user.get("user_id"), "route_code": payload.get("route_code"), "route_name": payload.get("route_name"), "product_type_id": payload.get("product_type_id"), "version": payload.get("version"), "remark": payload.get("remark"), } if "equipment_ids" in payload: update_kwargs["equipment_ids"] = payload.get("equipment_ids") obj = route_svc.update_route(db, route_id, **update_kwargs) return success_response(route_svc._route_row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.put("/process-routes/set-current") async def set_current_process_route( body: SetCurrentBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: route_svc.assert_route_writable(db, body.route_id, user) tenant_no = _get_tenant_no(db, user) obj = route_svc.set_current_route(db, body.route_id, tenant_no) return success_response(route_svc._route_row_to_dict(obj), message="设置成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/process-routes/{route_id}") async def delete_process_route( route_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: route_svc.assert_route_writable(db, route_id, user) route_svc.soft_delete_route(db, route_id) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # ----- 步骤操作 ----- @router.post("/process-routes/step/add") async def add_route_step( body: StepAddBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: route_svc.assert_route_writable(db, body.route_id, user) tenant_no = _get_tenant_no(db, user) step_data = route_svc.add_step( db, route_id=body.route_id, process_id=body.process_id, step_order=body.step_order, tenant_no=tenant_no, user_id=user.get("user_id"), ) return success_response(step_data, message="添加成功") except ValueError as e: return error_response(400, str(e)) @router.post("/process-routes/step/copy") async def copy_route_step( body: StepCopyBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: route_svc.assert_step_route_writable(db, body.step_id, user) tenant_no = _get_tenant_no(db, user) step_data = route_svc.copy_step( db, step_id=body.step_id, tenant_no=tenant_no, user_id=user.get("user_id"), ) return success_response(step_data, message="复制成功") except ValueError as e: return error_response(400, str(e)) @router.put("/process-routes/step/update") async def update_route_step( body: StepUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: route_svc.assert_step_route_writable(db, body.step_id, user) payload = body.model_dump(exclude_unset=True, by_alias=False) payload.pop("step_id", None) obj = route_svc.update_step(db, body.step_id, user_id=user.get("user_id"), **payload) return success_response(route_svc._step_row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.put("/process-routes/step/reorder") async def reorder_route_steps( body: StepReorderBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: route_svc.assert_route_writable(db, body.route_id, user) route_svc.reorder_steps(db, body.route_id, body.step_ids) return success_response(None, message="排序成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/process-routes/step/{step_id}") async def delete_route_step( step_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: route_svc.assert_step_route_writable(db, step_id, user) route_svc.soft_delete_step(db, step_id) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # ----- 步骤子表 ----- @router.put("/process-routes/step/reagent/update") async def update_step_reagents( body: StepReagentUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: route_svc.assert_step_route_writable(db, body.step_id, user) tenant_no = _get_tenant_no(db, user) items_data = [it.model_dump(by_alias=False) for it in body.items] route_svc.update_step_reagents(db, body.step_id, items_data, tenant_no, user.get("user_id")) return success_response(None, message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.put("/process-routes/step/consumable/update") async def update_step_consumables( body: StepConsumableUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: route_svc.assert_step_route_writable(db, body.step_id, user) tenant_no = _get_tenant_no(db, user) items_data = [it.model_dump(by_alias=False) for it in body.items] route_svc.update_step_consumables(db, body.step_id, items_data, tenant_no, user.get("user_id")) return success_response(None, message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.put("/process-routes/step/output/update") async def update_step_outputs( body: StepOutputUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: route_svc.assert_step_route_writable(db, body.step_id, user) tenant_no = _get_tenant_no(db, user) items_data = [it.model_dump(by_alias=False) for it in body.items] route_svc.update_step_outputs(db, body.step_id, items_data, tenant_no, user.get("user_id")) return success_response(None, message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.put("/process-routes/step/data-collection/update") async def update_step_data_collections( body: StepDataCollectionUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: route_svc.assert_step_route_writable(db, body.step_id, user) tenant_no = _get_tenant_no(db, user) items_data = [it.model_dump(by_alias=False) for it in body.items] route_svc.update_step_data_collections(db, body.step_id, items_data, tenant_no, user.get("user_id")) return success_response(None, message="更新成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 9. 入库生产工单 # --------------------------------------------------------------------------- @router.post("/production-work-orders") async def create_production_work_order( body: ProductionWorkOrderCreate, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) wo_type = (body.work_order_type or "inbound").strip() if wo_type == "temporary": wo = production_work_order_svc.create_temp_production_work_order( db, tenant_no=tenant_no, user_id=user.get("user_id"), create_dept_id=user.get("dept_id", ""), remark=body.remark or "", ) else: wo = production_work_order_svc.create_production_work_order_manual( db, tenant_no=tenant_no, user_id=user.get("user_id"), create_dept_id=user.get("dept_id", ""), protocol_no=body.protocol_no or "", sample_code=body.sample_code or "", reception_source=body.reception_source or "cell_storage", remark=body.remark or "", ) db.commit() return success_response(production_work_order_svc._wo_to_dict(wo), message="创建成功") except ValueError as e: return error_response(400, str(e)) except Exception as e: db.rollback() return error_response(400, str(e)) @router.get("/production-work-orders") async def list_production_work_orders( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, workOrderNo: Optional[str] = None, workOrderStatus: Optional[str] = Query(None, description="工单状态,多个用英文逗号分隔"), workOrderType: Optional[str] = None, receptionSource: Optional[str] = Query(None, alias="receptionSource"), bizType: Optional[str] = Query(None, alias="bizType"), protocolNo: Optional[str] = None, sampleCode: Optional[str] = None, createTimeStart: Optional[str] = Query(None, alias="createTimeStart"), createTimeEnd: Optional[str] = Query(None, alias="createTimeEnd"), infusionTimeStart: Optional[str] = Query(None, alias="infusionTimeStart"), infusionTimeEnd: Optional[str] = Query(None, alias="infusionTimeEnd"), plannedStartTimeStart: Optional[str] = Query(None, alias="plannedStartTimeStart"), plannedStartTimeEnd: Optional[str] = Query(None, alias="plannedStartTimeEnd"), excludeVoidProtocol: bool = Query(True, alias="excludeVoidProtocol"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) query = ProductionWorkOrderListQuery( page=page, page_size=pageSize, keyword=keyword, work_order_no=workOrderNo, work_order_status=workOrderStatus, work_order_type=workOrderType, reception_source=receptionSource, biz_type=bizType, protocol_no=protocolNo, sample_code=sampleCode, create_time_start=createTimeStart, create_time_end=createTimeEnd, infusion_time_start=infusionTimeStart, infusion_time_end=infusionTimeEnd, planned_start_time_start=plannedStartTimeStart, planned_start_time_end=plannedStartTimeEnd, exclude_void_protocol=excludeVoidProtocol, ) result = production_work_order_svc.list_production_work_orders( db, tenant_no=tenant_no, query=query, page=page, page_size=pageSize, user_info=user, ) items = result.get("items", []) total = result.get("total", 0) tp = production_task_svc.total_pages(total, pageSize) return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.get("/production-work-orders/tab-step-counts") async def production_work_order_tab_step_counts( user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) counts = production_task_svc.count_production_work_order_tab_step_badges( db, tenant_no=tenant_no, user_info=user ) return success_response(counts) except PermissionError as e: return error_response(403, str(e)) except Exception as e: return error_response(400, str(e)) @router.get("/production-work-orders/mine-tab-step-counts") async def production_work_order_mine_tab_step_counts( user: dict = Depends(require_menu_code("production:my-task:list")), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) counts = production_task_svc.count_mine_production_work_order_tab_step_badges( db, tenant_no=tenant_no, assignee_user_id=_get_current_user_id(user), ) return success_response(counts) except PermissionError as e: return error_response(403, str(e)) except Exception as e: return error_response(400, str(e)) @router.get("/production-work-orders/{wo_id}") async def get_production_work_order( wo_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) wo = production_work_order_svc.get_production_work_order(db, wo_id, tenant_no) if not wo: return error_response(404, "工单不存在") return success_response(production_work_order_svc._wo_to_response_dict(db, wo)) @router.put("/production-work-orders/{wo_id}") async def update_production_work_order_api( wo_id: str, body: ProductionWorkOrderUpdate, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) wo = production_work_order_svc.update_production_work_order( db, wo_id, tenant_no, user.get("user_id"), remark=body.remark, ) db.commit() return success_response( production_work_order_svc._wo_to_response_dict(db, wo), message="保存成功", ) except ValueError as e: db.rollback() return error_response(400, str(e)) except Exception as e: db.rollback() return error_response(400, str(e)) @router.put("/production-work-orders/{wo_id}/complete") async def complete_production_work_order( wo_id: str, body: ProductionWorkOrderComplete, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) wo = production_work_order_svc.complete_production_work_order( db, wo_id, tenant_no, user.get("user_id"), body, ) return success_response(production_work_order_svc._wo_to_response_dict(db, wo), message="操作成功") except ValueError as e: return error_response(400, str(e)) @router.put("/production-work-orders/{wo_id}/void") async def void_production_work_order_api( wo_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) wo = production_work_order_svc.void_production_work_order( db, wo_id, tenant_no, user.get("user_id"), ) return success_response( production_work_order_svc._wo_to_response_dict(db, wo), message="工单已作废", ) except ValueError as e: return error_response(400, str(e)) @router.delete("/production-work-orders/{wo_id}") async def delete_production_work_order_api( wo_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) production_work_order_svc.delete_production_work_order(db, wo_id, tenant_no, user.get("user_id")) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) @router.post("/production-work-orders/{wo_id}/configure") async def configure_production_work_order( wo_id: str, body: ProductionWorkOrderConfigureBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) wo = production_work_order_svc.configure_temp_production_work_order( db, wo_id, task_name=body.task_name or "", task_type=body.task_type or "cell_storage", route_id=body.route_id, description=body.description or "", remark=body.remark or "", assignee_user_id=body.assignee_user_id, tenant_no=tenant_no, user_id=user.get("user_id"), create_dept_id=user.get("dept_id", ""), ) db.commit() return success_response(production_work_order_svc._wo_to_dict(wo), message="配置成功") except ValueError as e: db.rollback() return error_response(400, str(e)) except Exception as e: db.rollback() return error_response(400, str(e)) @router.get("/production-work-orders/{wo_id}/cell-product-options") async def list_production_work_order_cell_product_options( wo_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) items = qc_work_order_svc.list_step_output_dispatch_options_for_production_work_order( db, production_work_order_id=wo_id, tenant_no=tenant_no ) return success_response(items) except ValueError as e: return error_response(400, str(e)) @router.get("/production-work-orders/{wo_id}/qc-info") async def get_production_work_order_qc_info_api( wo_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) data = qc_work_order_svc.get_production_work_order_qc_info( db, production_work_order_id=wo_id, tenant_no=tenant_no ) return success_response(data) except ValueError as e: return error_response(400, str(e)) @router.post("/production-work-orders/{wo_id}/qc-work-orders") async def create_qc_work_order_from_production_api( wo_id: str, body: ProductionQcWorkOrderCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) wo = qc_work_order_svc.create_qc_work_order_from_production( db, production_work_order_id=wo_id, production_step_output_id=body.production_step_output_id, remark=body.remark or "", tenant_no=tenant_no, user_id=user.get("user_id"), create_dept_id=user.get("dept_id", ""), ) db.commit() return success_response(qc_work_order_svc._wo_to_dict(wo), message="派发成功") except ValueError as e: db.rollback() return error_response(400, str(e)) except Exception as e: db.rollback() return error_response(400, str(e)) # --------------------------------------------------------------------------- # 生产任务集合 # --------------------------------------------------------------------------- @router.get("/production-task-sets") async def list_production_task_sets( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, status: Optional[str] = None, user: dict = Depends(require_menu_code("production:task-set:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = production_task_set_svc.list_sets( db, tenant_no=tenant_no, page=page, page_size=pageSize, keyword=keyword, status=status, ) tp = production_task_set_svc.total_pages(total, pageSize) return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.get("/production-task-sets/active") async def list_active_production_task_sets( user: dict = Depends(require_any_menu_code("production:task-set:list", "production:work-order:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items = production_task_set_svc.list_active_sets(db, tenant_no=tenant_no) return success_response({"list": items}) @router.post("/production-task-sets/from-work-order") async def create_production_task_set_from_work_order( body: ProductionTaskSetFromWorkOrderBody, user: dict = Depends( require_any_menu_code("production:work-order:list", "production:task-set:add") ), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) detail = production_task_set_svc.create_set_from_work_order( db, tenant_no=tenant_no, user_id=user.get("user_id"), work_order_id=body.work_order_id, set_name=body.set_name, remark=body.remark, items=( [x.model_dump(by_alias=False) for x in body.items] if body.items is not None else None ), ) db.commit() return success_response(detail, message="保存成功") except ValueError as e: db.rollback() return error_response(400, str(e)) @router.get("/production-task-sets/{set_id}") async def get_production_task_set( set_id: str, user: dict = Depends(require_any_menu_code("production:task-set:list", "production:work-order:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) detail = production_task_set_svc.get_set(db, set_id, tenant_no) if not detail: return error_response(404, "集合不存在") return success_response(detail) @router.post("/production-task-sets") async def create_production_task_set( body: ProductionTaskSetCreateBody, user: dict = Depends(require_menu_code("production:task-set:add")), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) detail = production_task_set_svc.create_set( db, tenant_no=tenant_no, user_id=user.get("user_id"), set_name=body.set_name, status=body.status or "1", remark=body.remark, items=[x.model_dump(by_alias=False) for x in body.items], ) db.commit() return success_response(detail, message="创建成功") except ValueError as e: db.rollback() return error_response(400, str(e)) @router.put("/production-task-sets/{set_id}") async def update_production_task_set( set_id: str, body: ProductionTaskSetUpdateBody, user: dict = Depends(require_menu_code("production:task-set:edit")), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) production_task_set_svc.assert_set_writable(db, set_id, tenant_no, user) data = body.model_dump(by_alias=False, exclude_unset=True) items = None if "items" in data: items = [x.model_dump(by_alias=False) for x in (body.items or [])] detail = production_task_set_svc.update_set( db, set_id, tenant_no=tenant_no, user_id=user.get("user_id"), set_name=data.get("set_name"), status=data.get("status"), remark=data.get("remark"), items=items, ) db.commit() return success_response(detail, message="更新成功") except ValueError as e: db.rollback() return error_response(400, str(e)) @router.delete("/production-task-sets/{set_id}") async def delete_production_task_set( set_id: str, user: dict = Depends(require_menu_code("production:task-set:remove")), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) production_task_set_svc.assert_set_writable(db, set_id, tenant_no, user) production_task_set_svc.delete_set( db, set_id, tenant_no=tenant_no, user_id=user.get("user_id") ) db.commit() return success_response(None, message="删除成功") except ValueError as e: db.rollback() return error_response(400, str(e)) # --------------------------------------------------------------------------- # 10. 生产任务 # --------------------------------------------------------------------------- @router.post("/production-tasks") async def create_production_task( body: ProductionTaskCreate, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) data = body.model_dump(by_alias=False) task = production_task_svc.create_production_task( db, tenant_no=tenant_no, user_id=user.get("user_id"), create_dept_id="", data=data, ) db.commit() result = production_task_svc._task_to_dict(task) if data.get("route_id"): production_task_svc._enrich_tasks_with_step_summaries(db, [result], tenant_no=tenant_no) return success_response(result, message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.post("/production-tasks/from-set") async def create_production_tasks_from_set( body: ProductionTasksFromSetBody, user: dict = Depends(require_menu_code("production:work-order:list")), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) result = production_task_set_svc.create_tasks_from_set( db, tenant_no=tenant_no, user_id=user.get("user_id"), create_dept_id="", work_order_id=body.work_order_id, set_id=body.set_id, items=( [x.model_dump(by_alias=False) for x in body.items] if body.items is not None else None ), ) db.commit() return success_response(result, message="创建成功") except ValueError as e: db.rollback() return error_response(400, str(e)) @router.get("/production-tasks") async def list_production_tasks( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, workOrderId: Optional[str] = None, taskNo: Optional[str] = None, taskStatus: Optional[str] = None, startedTimeStart: Optional[str] = Query(None, alias="startedTimeStart"), startedTimeEnd: Optional[str] = Query(None, alias="startedTimeEnd"), includeStepSummary: bool = Query(False, alias="includeStepSummary"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = production_task_svc.list_production_tasks( db, tenant_no=tenant_no, page=page, page_size=pageSize, keyword=keyword, work_order_id=workOrderId, task_no=taskNo, task_status=taskStatus, started_time_start=startedTimeStart, started_time_end=startedTimeEnd, user_info=user, include_step_summary=includeStepSummary, ) tp = production_task_svc.total_pages(total, pageSize) return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.get("/production-waste-outputs") async def list_production_waste_outputs( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, createTimeStart: Optional[str] = Query(None, alias="createTimeStart"), createTimeEnd: Optional[str] = Query(None, alias="createTimeEnd"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = production_waste_output_svc.list_production_waste_outputs( db, tenant_no=tenant_no, page=page, page_size=pageSize, keyword=keyword, create_time_start=createTimeStart, create_time_end=createTimeEnd, user_info=user, ) tp = production_waste_output_svc.total_pages(total, pageSize) return success_response( {"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp} ) @router.post("/production-tasks/list-by-work-orders") async def list_production_tasks_by_work_orders( body: ProductionTaskListByWorkOrdersBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) wo_ids = [x.strip() for x in body.work_order_ids if x and str(x).strip()] if not wo_ids: return success_response( {"list": [], "total": 0, "page": 1, "pageSize": body.page_size, "totalPages": 0} ) effective_page_size = min(max(body.page_size, len(wo_ids) * 100), 500) items, total = production_task_svc.list_production_tasks( db, tenant_no=tenant_no, page=1, page_size=effective_page_size, work_order_ids=wo_ids, user_info=user, include_step_summary=body.include_step_summary, ) tp = production_task_svc.total_pages(total, effective_page_size) return success_response( { "list": items, "total": total, "page": 1, "pageSize": effective_page_size, "totalPages": tp, } ) @router.get("/production-tasks/mine") async def list_my_production_tasks( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, taskStatus: Optional[str] = None, workOrderType: Optional[str] = None, receptionSource: Optional[str] = Query(None, alias="receptionSource"), bizType: Optional[str] = Query(None, alias="bizType"), scope: str = Query("current", description="current|history"), includeStepSummary: bool = Query(False, alias="includeStepSummary"), excludeVoidProtocol: bool = Query(True, alias="excludeVoidProtocol"), plannedStartTimeStart: Optional[str] = Query(None, alias="plannedStartTimeStart"), plannedStartTimeEnd: Optional[str] = Query(None, alias="plannedStartTimeEnd"), user: dict = Depends(require_menu_code("production:my-task:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = production_task_svc.list_mine_production_tasks( db, assignee_user_id=_get_current_user_id(user), tenant_no=tenant_no, scope=scope, page=page, page_size=pageSize, keyword=keyword, task_status=taskStatus, work_order_type=workOrderType, reception_source=receptionSource, biz_type=bizType, include_step_summary=includeStepSummary, exclude_void_protocol=excludeVoidProtocol, planned_start_time_start=plannedStartTimeStart, planned_start_time_end=plannedStartTimeEnd, ) tp = production_task_svc.total_pages(total, pageSize) return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.post("/production-tasks/{task_id}/forward") async def forward_production_task_api( task_id: str, body: ProductionTaskForwardBody, operateScope: Optional[str] = Query(None, alias="operateScope"), user: dict = Depends(require_any_menu_code("production:my-task:list", "production:work-order:list")), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) scope = (operateScope or "mine").strip().lower() require_assignee = scope == "mine" from_uid = _get_current_user_id(user) if require_assignee else (user.get("user_id") or "") task = production_task_svc.forward_production_task( db, task_id, from_user_id=from_uid, to_user_id=body.to_user_id, tenant_no=tenant_no, operator_id=user.get("user_id"), create_dept_id=user.get("dept_id", ""), remark=body.remark, require_assignee=require_assignee, ) db.commit() item = production_task_svc._task_to_dict(task) production_task_svc._enrich_assignee_names(db, [item]) return success_response(item, message="转发成功") except PermissionError as e: db.rollback() return error_response(403, str(e)) except ValueError as e: db.rollback() return error_response(400, str(e)) @router.post("/production-tasks/{task_id}/spawn-temporary-work-order") async def spawn_temporary_work_order_from_task_api( task_id: str, body: SpawnTemporaryWorkOrderBody, operateScope: Optional[str] = Query(None, alias="operateScope"), user: dict = Depends( require_any_menu_code("production:my-task:list", "production:work-order:list") ), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) result = production_task_svc.spawn_temporary_work_order_from_task( db, task_id, tenant_no=tenant_no, user_id=user.get("user_id"), create_dept_id=user.get("dept_id", ""), remark=body.remark or "", ) db.commit() return success_response(result, message="临时工单已创建") except ValueError as e: db.rollback() return error_response(400, str(e)) @router.put("/production-tasks/{task_id}/assignee") async def reassign_production_task_api( task_id: str, body: ProductionTaskAssigneeUpdate, user: dict = Depends(require_menu_code("production:work-order:list")), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) task = production_task_svc.reassign_production_task( db, task_id, assignee_user_id=body.assignee_user_id, tenant_no=tenant_no, operator_id=user.get("user_id"), create_dept_id=user.get("dept_id", ""), remark=body.remark, ) db.commit() item = production_task_svc._task_to_dict(task) production_task_svc._enrich_assignee_names(db, [item]) return success_response(item, message="改派成功") except ValueError as e: db.rollback() return error_response(400, str(e)) @router.put("/production-tasks/{task_id}/steps/dispatch") async def dispatch_production_task_steps_api( task_id: str, body: ProductionTaskStepDispatchBody, operateScope: Optional[str] = Query(None, alias="operateScope"), user: dict = Depends(require_any_menu_code("production:my-task:list", "production:work-order:list")), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) payload = [ { "step_id": it.step_id, "assignee_user_ids": it.assignee_user_ids, "planned_start_time": it.planned_start_time, } for it in body.steps ] detail = production_task_svc.dispatch_task_steps( db, task_id, tenant_no=tenant_no, steps_payload=payload, operator_id=user.get("user_id"), create_dept_id=user.get("dept_id", ""), ) db.commit() return success_response(detail, message="步骤派发成功") except ValueError as e: db.rollback() return error_response(400, str(e)) @router.get("/production-tasks/{task_id}/editor-init") async def get_production_task_editor_init( task_id: str, includeReference: bool = Query(True, alias="includeReference"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): """编辑生产任务弹窗初始化:任务详情 + 工艺库 + 步骤配置参考数据(聚合接口)。""" tenant_no = _get_tenant_no(db, user) payload = production_task_svc.get_production_task_editor_init( db, task_id, tenant_no, user_info=user, include_reference=includeReference, ) if not payload: return error_response(404, "任务不存在") return success_response(payload) @router.get("/production-tasks/{task_id}") async def get_production_task_detail( task_id: str, includeStepSubTables: bool = Query(True, alias="includeStepSubTables"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) detail = production_task_svc.get_production_task_detail( db, task_id, tenant_no, include_step_sub_tables=includeStepSubTables, ) if not detail: return error_response(404, "任务不存在") return success_response(detail) @router.put("/production-tasks/{task_id}") async def update_production_task( task_id: str, body: ProductionTaskUpdate, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) data = body.model_dump(exclude_unset=True, by_alias=False) task = production_task_svc.update_production_task( db, task_id, tenant_no, user.get("user_id"), data, ) return success_response(production_task_svc._task_to_dict(task), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/production-tasks/{task_id}") async def delete_production_task( task_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) production_task_svc.delete_production_task(db, task_id, tenant_no, user.get("user_id")) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) @router.put("/production-tasks/{task_id}/status") async def update_production_task_status( task_id: str, body: ProductionTaskStatusUpdate, operateScope: Optional[str] = Query(None, alias="operateScope"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) task = production_task_svc.update_task_status( db, task_id, tenant_no, user.get("user_id"), body.task_status, conclusion=body.conclusion, remark=body.remark, ) return success_response(production_task_svc._task_to_dict(task), message="操作成功") except PermissionError as e: return error_response(403, str(e)) except ValueError as e: return error_response(400, str(e)) @router.post("/production-tasks/{task_id}/steps") async def add_production_task_step( task_id: str, body: TaskStepAdd, operateScope: Optional[str] = Query(None, alias="operateScope"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) data = body.model_dump(by_alias=False) step_data = production_task_svc.add_task_step( db, task_id, tenant_no, user.get("user_id"), data, ) return success_response(step_data, message="添加成功") except PermissionError as e: return error_response(403, str(e)) except ValueError as e: return error_response(400, str(e)) @router.put("/production-tasks/{task_id}/steps/{step_id}") async def update_production_task_step( task_id: str, step_id: str, body: TaskStepUpdate, operateScope: Optional[str] = Query(None, alias="operateScope"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) data = body.model_dump(exclude_unset=True, by_alias=False) step = production_task_svc.update_task_step( db, step_id, tenant_no, user.get("user_id"), data, ) return success_response(production_task_svc._step_to_dict(step), message="更新成功") except PermissionError as e: return error_response(403, str(e)) except ValueError as e: return error_response(400, str(e)) @router.delete("/production-tasks/{task_id}/steps/{step_id}") async def delete_production_task_step( task_id: str, step_id: str, operateScope: Optional[str] = Query(None, alias="operateScope"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) production_task_svc.delete_task_step(db, step_id, tenant_no, user.get("user_id")) return success_response(None, message="删除成功") except PermissionError as e: return error_response(403, str(e)) except ValueError as e: return error_response(400, str(e)) @router.put("/production-tasks/step/reorder") async def reorder_production_task_steps( body: TaskStepReorderBody, operateScope: Optional[str] = Query(None, alias="operateScope"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) production_task_svc.reorder_task_steps(db, body.task_id, body.step_ids) return success_response(None, message="排序成功") except PermissionError as e: return error_response(403, str(e)) except ValueError as e: return error_response(400, str(e)) @router.post("/production-tasks/{task_id}/steps/{step_id}/copy") async def copy_production_task_step( task_id: str, step_id: str, operateScope: Optional[str] = Query(None, alias="operateScope"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) step_data = production_task_svc.copy_task_step( db, step_id=step_id, tenant_no=tenant_no, user_id=user.get("user_id"), ) return success_response(step_data, message="复制成功") except PermissionError as e: return error_response(403, str(e)) except ValueError as e: return error_response(400, str(e)) @router.put("/production-tasks/{task_id}/steps/{step_id}/reagents") async def replace_task_step_reagents( task_id: str, step_id: str, body: List[TaskStepMaterialItem], operateScope: Optional[str] = Query(None, alias="operateScope"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) items_data = [] for it in body: d = it.model_dump(by_alias=False) items_data.append({ "id": d.get("id"), "reagent_code": d.get("material_code"), "reagent_name": d.get("material_name"), "spec": d.get("spec"), "standard_usage": d.get("standard_usage"), "actual_usage": d.get("actual_usage"), "unit": d.get("unit"), "requisition_usage_id": d.get("requisition_usage_id"), "is_key_material": d.get("is_key_material", "0"), "remark": d.get("remark"), }) production_task_svc.replace_step_reagents(db, step_id, tenant_no, user.get("user_id"), items_data) return success_response(None, message="更新成功") except PermissionError as e: return error_response(403, str(e)) except ValueError as e: return error_response(400, str(e)) @router.put("/production-tasks/{task_id}/steps/{step_id}/consumables") async def replace_task_step_consumables( task_id: str, step_id: str, body: List[TaskStepMaterialItem], operateScope: Optional[str] = Query(None, alias="operateScope"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) items_data = [] for it in body: d = it.model_dump(by_alias=False) items_data.append({ "id": d.get("id"), "consumable_code": d.get("material_code"), "consumable_name": d.get("material_name"), "spec_model": d.get("spec_model"), "standard_usage": d.get("standard_usage"), "actual_usage": d.get("actual_usage"), "unit": d.get("unit"), "requisition_usage_id": d.get("requisition_usage_id"), "is_required": d.get("is_required", "1"), "remark": d.get("remark"), }) production_task_svc.replace_step_consumables(db, step_id, tenant_no, user.get("user_id"), items_data) return success_response(None, message="更新成功") except PermissionError as e: return error_response(403, str(e)) except ValueError as e: return error_response(400, str(e)) @router.put("/production-tasks/{task_id}/steps/{step_id}/configured-reagents") async def replace_task_step_configured_reagents( task_id: str, step_id: str, body: List[TaskStepMaterialItem], operateScope: Optional[str] = Query(None, alias="operateScope"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) items_data = [] for it in body: d = it.model_dump(by_alias=False) items_data.append({ "id": d.get("id"), "configured_reagent_code": d.get("material_code"), "configured_reagent_name": d.get("material_name"), "spec": d.get("spec"), "standard_usage": d.get("standard_usage"), "actual_usage": d.get("actual_usage"), "unit": d.get("unit"), "requisition_usage_id": d.get("requisition_usage_id"), "remark": d.get("remark"), }) production_task_svc.replace_step_configured_reagents( db, step_id, tenant_no, user.get("user_id"), items_data ) return success_response(None, message="更新成功") except PermissionError as e: return error_response(403, str(e)) except ValueError as e: return error_response(400, str(e)) @router.put("/production-tasks/{task_id}/steps/{step_id}/data-collection") async def replace_task_step_data_collections( task_id: str, step_id: str, body: List[TaskStepDataCollectionItem], operateScope: Optional[str] = Query(None, alias="operateScope"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) items_data = [it.model_dump(by_alias=False) for it in body] production_task_svc.replace_step_data_collections(db, step_id, tenant_no, user.get("user_id"), items_data) return success_response(None, message="更新成功") except PermissionError as e: return error_response(403, str(e)) except ValueError as e: return error_response(400, str(e)) @router.put("/production-tasks/{task_id}/steps/{step_id}/outputs") async def replace_task_step_outputs( task_id: str, step_id: str, body: List[TaskStepOutputItem], operateScope: Optional[str] = Query(None, alias="operateScope"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) items_data = [it.model_dump(by_alias=False) for it in body] saved = production_task_svc.replace_step_outputs( db, step_id, tenant_no, user.get("user_id"), items_data ) return success_response(saved, message="更新成功") except PermissionError as e: return error_response(403, str(e)) except ValueError as e: return error_response(400, str(e)) @router.post("/production-tasks/{task_id}/inputs") async def set_production_task_inputs( task_id: str, body: List[TaskInputItem], operateScope: Optional[str] = Query(None, alias="operateScope"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) items_data = [it.model_dump(by_alias=False) for it in body] production_task_svc.set_task_inputs(db, task_id, tenant_no, user.get("user_id"), items_data) return success_response(None, message="设置成功") except PermissionError as e: return error_response(403, str(e)) except ValueError as e: return error_response(400, str(e)) @router.post("/production-tasks/{task_id}/outputs") async def set_production_task_outputs( task_id: str, body: List[TaskOutputItem], operateScope: Optional[str] = Query(None, alias="operateScope"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) items_data = [it.model_dump(by_alias=False) for it in body] production_task_svc.set_task_outputs(db, task_id, tenant_no, user.get("user_id"), items_data) return success_response(None, message="设置成功") except PermissionError as e: return error_response(403, str(e)) except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 11. 细胞产品库 # --------------------------------------------------------------------------- @router.get("/cell-products/next-code") async def get_next_cell_product_code( count: int = Query(1, ge=1, le=50), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) if count <= 1: code = cell_product_svc.get_next_code(db, tenant_no) return success_response({"code": code}) codes = cell_product_svc.reserve_next_codes(db, tenant_no, count) return success_response({"codes": codes, "code": codes[0] if codes else ""}) @router.get("/cell-products/max-passage-seq") async def get_cell_product_max_passage_seq( productCode: str = Query(..., alias="productCode"), passage: Optional[str] = None, passages: Optional[str] = None, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): """查询 `{productCode}-{代次}-XX` 在细胞产品库中的最大两位序号。""" tenant_no = _get_tenant_no(db, user) base = (productCode or "").strip() if not base: return error_response(400, "产品编码不能为空") passage_list: List[str] = [] if passages: passage_list.extend(p.strip() for p in passages.split(",") if p and p.strip()) if passage and passage.strip(): passage_list.append(passage.strip()) if not passage_list: return error_response(400, "请指定代次") max_seq_map = cell_product_svc.get_max_passage_product_seq_map( db, tenant_no, base, passage_list ) next_seq_map = {p: max_seq_map.get(p, 0) + 1 for p in max_seq_map} if len(max_seq_map) == 1: only_passage = next(iter(max_seq_map)) only_max = max_seq_map[only_passage] only_next = next_seq_map[only_passage] return success_response( { "passage": only_passage, "maxSeq": only_max, "nextSeq": only_next, "maxSeqByPassage": max_seq_map, "nextSeqByPassage": next_seq_map, } ) return success_response( {"maxSeqByPassage": max_seq_map, "nextSeqByPassage": next_seq_map} ) @router.get("/cell-products/max-suffix-seq") async def get_cell_product_max_suffix_seq( productCode: str = Query(..., alias="productCode"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): """查询 `{productCode}-XX` 在细胞产品库中的最大两位序号。""" tenant_no = _get_tenant_no(db, user) base = (productCode or "").strip() if not base: return error_response(400, "产品编码不能为空") max_seq = cell_product_svc.get_max_product_suffix_seq(db, tenant_no, base) return success_response({"maxSeq": max_seq, "nextSeq": max_seq + 1}) @router.get("/cell-products/check-code") async def check_cell_product_code( productCode: str = Query(..., alias="productCode"), excludeId: Optional[str] = Query(None, alias="excludeId"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): """检查产品编码是否已被占用(编辑时可排除当前记录)。""" tenant_no = _get_tenant_no(db, user) code = (productCode or "").strip() if not code: return error_response(400, "产品编码不能为空") exists = cell_product_svc.product_code_exists( db, tenant_no, code, exclude_id=excludeId ) return success_response({"exists": exists, "productCode": code}) @router.get("/cell-products") async def list_cell_products( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, productCode: Optional[str] = None, productName: Optional[str] = None, productTypeId: Optional[str] = None, productStatus: Optional[str] = None, passage: Optional[str] = None, sourceType: Optional[str] = None, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) result = cell_product_svc.list_cell_products( db, tenant_no=tenant_no, page=page, page_size=pageSize, keyword=keyword, product_code=productCode, product_name=productName, product_type_id=productTypeId, product_status=productStatus, passage=passage, source_type=sourceType, user_info=user, ) items = result.get("items", []) total = result.get("total", 0) tp = cell_product_svc.total_pages(total, pageSize) return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.get("/cell-products/{product_id}") async def get_cell_product_detail( product_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) obj = cell_product_svc.get_cell_product(db, product_id, tenant_no) if not obj: return error_response(404, "细胞产品不存在") return success_response(cell_product_svc.product_to_response(db, tenant_no, obj)) @router.get("/cell-products/{product_id}/chain") async def get_cell_product_chain( product_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) chain = cell_product_svc.get_product_chain(db, product_id, tenant_no) if not chain: raise HTTPException(status_code=404, detail="细胞产品不存在") return success_response(chain) @router.get("/cell-products/{product_id}/lineage-qc") async def get_cell_product_lineage_qc( product_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) data = cell_product_svc.get_product_lineage_qc(db, product_id, tenant_no) if not data: raise HTTPException(status_code=404, detail="细胞产品不存在") return success_response(data) @router.post("/cell-products") async def create_cell_product( body: CellProductCreate, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) items = body.items or [] qty = int(body.quantity) if body.quantity is not None else 1 qty = max(1, min(qty, 50)) if items: created = cell_product_svc.create_cell_products_batch( db, tenant_no=tenant_no, user_id=user.get("user_id"), create_dept_id="", data=body, items=items, require_storage_slot=False, ) elif qty > 1: created = cell_product_svc.create_cell_products_by_quantity( db, tenant_no=tenant_no, user_id=user.get("user_id"), create_dept_id="", data=body, count=qty, ) else: obj = cell_product_svc.create_cell_product( db, tenant_no=tenant_no, user_id=user.get("user_id"), create_dept_id="", data=body, ) return success_response( cell_product_svc.product_to_response(db, tenant_no, obj), message="创建成功" ) resp_list = [ cell_product_svc.product_to_response(db, tenant_no, obj) for obj in created ] n = len(created) return success_response( {"list": resp_list, "count": n}, message=f"成功创建 {n} 份细胞产品", ) except ValueError as e: return error_response(400, str(e)) @router.put("/cell-products/{product_id}") async def update_cell_product( product_id: str, body: CellProductUpdate, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = cell_product_svc.update_cell_product( db, product_id, tenant_no, user.get("user_id"), data=body, ) return success_response( cell_product_svc.product_to_response(db, tenant_no, obj), message="更新成功" ) except ValueError as e: return error_response(400, str(e)) @router.delete("/cell-products/{product_id}") async def delete_cell_product( product_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) cell_product_svc.delete_cell_product(db, product_id, tenant_no, user.get("user_id")) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) from app.api.production_material import router as production_material_router router.include_router(production_material_router)