Files
zkxw/backend/app/api/production.py
T
2026-09-01 13:43:14 +08:00

3148 lines
114 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""生产管理 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)
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)
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)
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)
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({
"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({
"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({
"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]
production_task_svc.replace_step_outputs(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.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)