"""市场物料库 API""" from typing import Optional from fastapi import APIRouter, Depends, Query from pydantic import BaseModel, ConfigDict, Field from sqlalchemy.orm import Session from app.api.sample_reception import _get_current_user_id, _get_tenant_no, _page_payload from app.core.database import get_db from app.core.menu_guard import require_admin_or_super_admin, require_menu_code from app.core.response import error_response, success_response from app.crud import material as mat_svc from app.crud import material_stock_log as msl_svc router = APIRouter(prefix="/web/admin", tags=["市场物料"]) # ---------- Body models ---------- class MaterialBody(BaseModel): model_config = ConfigDict(populate_by_name=True) material_code: str = Field(..., alias="materialCode") material_name: str = Field(..., alias="materialName") category: Optional[str] = None spec_model: Optional[str] = Field(None, alias="specModel") unit: Optional[str] = None status: str = "1" order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None image_urls: Optional[str] = Field(None, alias="imageUrls") class MaterialUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) material_code: Optional[str] = Field(None, alias="materialCode") material_name: Optional[str] = Field(None, alias="materialName") category: Optional[str] = None spec_model: Optional[str] = Field(None, alias="specModel") unit: Optional[str] = None status: Optional[str] = None order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None image_urls: Optional[str] = Field(None, alias="imageUrls") 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: Optional[str] = None purpose: Optional[str] = None remark: Optional[str] = None # ---------- 物料 CRUD ---------- @router.get("/materials") async def list_materials( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100), keyword: Optional[str] = None, category: Optional[str] = None, status: Optional[str] = None, db: Session = Depends(get_db), user: dict = Depends(require_menu_code("biz:material:list")), ): try: tenant_no = _get_tenant_no(db, user) items, total = mat_svc.list_materials( db, tenant_no, page=page, page_size=pageSize, keyword=keyword, category=category, status=status, ) return success_response(_page_payload(items, total, page, pageSize)) except PermissionError as e: return error_response(str(e), code=403) except Exception as e: return error_response(str(e)) @router.get("/materials/options") async def material_options( db: Session = Depends(get_db), user: dict = Depends(require_menu_code("biz:material:list")), ): try: tenant_no = _get_tenant_no(db, user) return success_response(mat_svc.options_list(db, tenant_no=tenant_no)) except Exception as e: return error_response(str(e)) @router.post("/materials") async def create_material( body: MaterialBody, db: Session = Depends(get_db), user: dict = Depends(require_menu_code("biz:material:list")), ): try: tenant_no = _get_tenant_no(db, user) user_id = _get_current_user_id(user) obj = mat_svc.create_material( db, body.model_dump(by_alias=True, exclude_none=True), tenant_no=tenant_no, user_id=user_id, ) return success_response(obj, message="创建成功") except ValueError as e: return error_response(str(e)) except Exception as e: return error_response(str(e)) @router.put("/materials/{material_id}") async def update_material( material_id: str, body: MaterialUpdateBody, db: Session = Depends(get_db), user: dict = Depends(require_menu_code("biz:material:list")), ): try: tenant_no = _get_tenant_no(db, user) user_id = _get_current_user_id(user) obj = mat_svc.update_material( db, material_id, body.model_dump(by_alias=True, exclude_none=True), tenant_no=tenant_no, user_id=user_id, ) return success_response(obj, message="更新成功") except ValueError as e: return error_response(str(e)) except Exception as e: return error_response(str(e)) @router.delete("/materials/{material_id}") async def delete_material( material_id: str, db: Session = Depends(get_db), user: dict = Depends(require_menu_code("biz:material:list")), ): try: tenant_no = _get_tenant_no(db, user) user_id = _get_current_user_id(user) mat_svc.soft_delete_material(db, material_id, tenant_no=tenant_no, user_id=user_id) return success_response(None, message="删除成功") except ValueError as e: return error_response(str(e)) except Exception as e: return error_response(str(e)) @router.post("/materials/{material_id}/stock-in") async def stock_in_material( material_id: str, body: StockInBody, db: Session = Depends(get_db), user: dict = Depends(require_menu_code("biz:material:list")), ): try: tenant_no = _get_tenant_no(db, user) user_id = _get_current_user_id(user) obj = mat_svc.stock_in( db, material_id, qty=body.qty, remark=body.remark, tenant_no=tenant_no, user_id=user_id, ) return success_response(obj, message="入库成功") except ValueError as e: return error_response(str(e)) except Exception as e: return error_response(str(e)) @router.post("/materials/{material_id}/stock-out") async def stock_out_material( material_id: str, body: StockOutBody, db: Session = Depends(get_db), user: dict = Depends(require_menu_code("biz:material:list")), ): try: tenant_no = _get_tenant_no(db, user) user_id = _get_current_user_id(user) obj = mat_svc.stock_out( db, material_id, qty=body.qty, receiver=body.receiver, purpose=body.purpose, remark=body.remark, tenant_no=tenant_no, user_id=user_id, ) return success_response(obj, message="出库成功") except ValueError as e: return error_response(str(e)) except Exception as e: return error_response(str(e)) # ---------- 库存变动记录 ---------- @router.get("/material-stock-logs") async def list_stock_logs( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100), materialId: Optional[str] = None, changeType: Optional[str] = None, startTime: Optional[str] = None, endTime: Optional[str] = None, keyword: Optional[str] = None, db: Session = Depends(get_db), user: dict = Depends(require_menu_code("biz:materialStockLog:list")), ): try: tenant_no = _get_tenant_no(db, user) items, total = msl_svc.list_stock_logs( db, tenant_no, page=page, page_size=pageSize, material_id=materialId, change_type=changeType, start_time=startTime, end_time=endTime, keyword=keyword, ) return success_response(_page_payload(items, total, page, pageSize)) except PermissionError as e: return error_response(str(e), code=403) except Exception as e: return error_response(str(e))