| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- """市场物料库 API"""
- from datetime import datetime
- from typing import List, 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.utils import parse_datetime
- 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
- warning_qty: Optional[float] = Field(0, alias="warningQty")
- 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
- warning_qty: Optional[float] = Field(None, alias="warningQty")
- 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
- stock_in_time: Optional[str] = Field(None, alias="stockInTime")
- 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
- stock_out_time: Optional[str] = Field(None, alias="stockOutTime")
- class BatchStockInItem(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- material_id: str = Field(..., alias="materialId")
- qty: float = Field(..., gt=0)
- remark: Optional[str] = None
- class BatchStockInBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- items: List[BatchStockInItem] = Field(..., min_length=1)
- remark: Optional[str] = None
- stock_in_time: Optional[str] = Field(None, alias="stockInTime")
- class BatchStockOutItem(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- material_id: str = Field(..., alias="materialId")
- qty: float = Field(..., gt=0)
- remark: Optional[str] = None
- class BatchStockOutBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- items: List[BatchStockOutItem] = Field(..., min_length=1)
- receiver: Optional[str] = None
- purpose: Optional[str] = None
- remark: Optional[str] = None
- stock_out_time: Optional[str] = Field(None, alias="stockOutTime")
- # ---------- 物料 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, user_info=user,
- )
- return success_response(_page_payload(items, total, page, pageSize))
- except PermissionError as e:
- return error_response(403, str(e))
- except Exception as e:
- return error_response(400, 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(400, 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(400, str(e))
- except Exception as e:
- return error_response(400, 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(400, str(e))
- except Exception as e:
- return error_response(400, 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(400, str(e))
- except Exception as e:
- return error_response(400, str(e))
- @router.post("/materials/batch-stock-in")
- async def batch_stock_in_material(
- body: BatchStockInBody,
- 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)
- operate_time = datetime.now()
- if body.stock_in_time:
- rt = parse_datetime(body.stock_in_time)
- if not rt:
- return error_response(400, "入库时间格式无效")
- operate_time = rt
- items = [
- {"material_id": x.material_id, "qty": x.qty, "remark": x.remark}
- for x in body.items
- ]
- count = mat_svc.batch_stock_in(
- db,
- items=items,
- remark=body.remark,
- tenant_no=tenant_no,
- user_id=user_id,
- operate_time=operate_time,
- )
- return success_response({"count": count}, message=f"批量入库成功,共 {count} 项")
- except ValueError as e:
- return error_response(400, str(e))
- except Exception as e:
- return error_response(400, str(e))
- @router.post("/materials/batch-stock-out")
- async def batch_stock_out_material(
- body: BatchStockOutBody,
- 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)
- operate_time = datetime.now()
- if body.stock_out_time:
- rt = parse_datetime(body.stock_out_time)
- if not rt:
- return error_response(400, "出库时间格式无效")
- operate_time = rt
- items = [
- {"material_id": x.material_id, "qty": x.qty, "remark": x.remark}
- for x in body.items
- ]
- count = mat_svc.batch_stock_out(
- db,
- items=items,
- receiver=body.receiver,
- purpose=body.purpose,
- remark=body.remark,
- tenant_no=tenant_no,
- user_id=user_id,
- operate_time=operate_time,
- )
- return success_response({"count": count}, message=f"批量出库成功,共 {count} 项")
- except ValueError as e:
- return error_response(400, str(e))
- except Exception as e:
- return error_response(400, 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)
- operate_time = datetime.now()
- if body.stock_in_time:
- rt = parse_datetime(body.stock_in_time)
- if not rt:
- return error_response(400, "入库时间格式无效")
- operate_time = rt
- obj = mat_svc.stock_in(
- db, material_id, qty=body.qty, remark=body.remark,
- tenant_no=tenant_no, user_id=user_id, operate_time=operate_time,
- )
- return success_response(obj, message="入库成功")
- except ValueError as e:
- return error_response(400, str(e))
- except Exception as e:
- return error_response(400, 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)
- operate_time = datetime.now()
- if body.stock_out_time:
- rt = parse_datetime(body.stock_out_time)
- if not rt:
- return error_response(400, "出库时间格式无效")
- operate_time = rt
- 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, operate_time=operate_time,
- )
- return success_response(obj, message="出库成功")
- except ValueError as e:
- return error_response(400, str(e))
- except Exception as e:
- return error_response(400, 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, user_info=user,
- )
- return success_response(_page_payload(items, total, page, pageSize))
- except PermissionError as e:
- return error_response(403, str(e))
- except Exception as e:
- return error_response(400, str(e))
|