Files
shanxia2.0/backend/app/api/material.py
T
2026-05-21 03:48:06 +08:00

345 lines
12 KiB
Python

"""市场物料库 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(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/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("入库时间格式无效")
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(str(e))
except Exception as e:
return error_response(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("出库时间格式无效")
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(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)
operate_time = datetime.now()
if body.stock_in_time:
rt = parse_datetime(body.stock_in_time)
if not rt:
return error_response("入库时间格式无效")
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(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)
operate_time = datetime.now()
if body.stock_out_time:
rt = parse_datetime(body.stock_out_time)
if not rt:
return error_response("出库时间格式无效")
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(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, user_info=user,
)
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))