feat: v0.3 报价算料引擎与仓库库存模块
后端: - 报价算料: 从drawing_data几何提取型材清单(外框/中梃/扇框投影长度) 与玻璃面积(鞋带公式), 套价格本计价, 支持样板/订单/自定义图纸三入口 - 仓库库存: 材料CRUD/出入库/流水/负库存拦截/预警线 - SQLite默认路径锚定项目根目录(修复cwd依赖) 前端: - 仓库库存页(筛选/出入库对话框/预警高亮) - 算料报价弹窗(样板库与订单详情入口, 支持打印) - 路由与导航更新(8页面) 回归: 9个API端点全部200, 报价/优化/SPA路由验证通过
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
"""仓库库存路由:材料 CRUD + 出入库。"""
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import desc, select
|
||||
|
||||
from huamenchuang.core.deps import CurrentUser, DbSession
|
||||
from huamenchuang.core.response import fail, ok, paginated
|
||||
from huamenchuang.models.contract import StockMove, WarehouseItem
|
||||
|
||||
router = APIRouter(prefix="/warehouse", tags=["仓库"])
|
||||
|
||||
|
||||
class ItemBody(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=128)
|
||||
bom_code: str = ""
|
||||
material: str = Field(default="profile", pattern="^(profile|glass|hardware|addon)$")
|
||||
color: str = ""
|
||||
spec: str = ""
|
||||
unit: str = "根"
|
||||
cost: float = 0
|
||||
price: float = 0
|
||||
count: float = 0
|
||||
warning: float = 0
|
||||
length: int = 0
|
||||
note: str = ""
|
||||
|
||||
|
||||
class MoveBody(BaseModel):
|
||||
item_id: int
|
||||
direction: int = Field(default=1, ge=-1, le=1) # 1入库 -1出库
|
||||
count: float = Field(gt=0)
|
||||
note: str = ""
|
||||
|
||||
|
||||
def _out(i: WarehouseItem) -> dict:
|
||||
return {
|
||||
"id": i.id, "bom_code": i.bom_code, "name": i.name, "material": i.material,
|
||||
"color": i.color, "spec": i.spec, "unit": i.unit, "cost": float(i.cost),
|
||||
"price": float(i.price), "count": float(i.count), "warning": float(i.warning),
|
||||
"length": i.length, "note": i.note, "updated_at": str(i.updated_at),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/list")
|
||||
async def list_items(user: CurrentUser, db: DbSession, page: int = 1, page_size: int = 15,
|
||||
keyword: str = "", material: str | None = None):
|
||||
"""材料库存列表(分页 + 筛选,对齐目标站 warehouse_list 语义)。"""
|
||||
from sqlalchemy import func, or_
|
||||
conds = [WarehouseItem.company_id == user.company_id]
|
||||
if material:
|
||||
conds.append(WarehouseItem.material == material)
|
||||
if kw := keyword.strip():
|
||||
like = f"%{kw}%"
|
||||
conds.append(or_(WarehouseItem.name.like(like), WarehouseItem.bom_code.like(like)))
|
||||
where = *conds,
|
||||
total = (await db.execute(select(func.count()).select_from(WarehouseItem).where(*where))).scalar_one()
|
||||
stmt = (select(WarehouseItem).where(*where).order_by(WarehouseItem.id.desc())
|
||||
.offset((page - 1) * page_size).limit(page_size))
|
||||
items = list((await db.execute(stmt)).scalars())
|
||||
return ok(data=paginated([_out(i) for i in items], page, page_size, total))
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_item(body: ItemBody, user: CurrentUser, db: DbSession):
|
||||
"""新增材料。"""
|
||||
item = WarehouseItem(company_id=user.company_id, **body.model_dump())
|
||||
db.add(item)
|
||||
await db.flush()
|
||||
return ok(data={"id": item.id}, message="创建成功")
|
||||
|
||||
|
||||
@router.put("/{item_id}")
|
||||
async def update_item(item_id: int, body: ItemBody, user: CurrentUser, db: DbSession):
|
||||
"""编辑材料。"""
|
||||
item = await db.get(WarehouseItem, item_id)
|
||||
if item is None or item.company_id != user.company_id:
|
||||
return fail("材料不存在")
|
||||
for k, v in body.model_dump().items():
|
||||
setattr(item, k, v)
|
||||
return ok(message="更新成功")
|
||||
|
||||
|
||||
@router.post("/move")
|
||||
async def stock_move(body: MoveBody, user: CurrentUser, db: DbSession):
|
||||
"""出入库:direction=1 入库 / -1 出库(出库不允许负库存)。"""
|
||||
item = await db.get(WarehouseItem, body.item_id)
|
||||
if item is None or item.company_id != user.company_id:
|
||||
return fail("材料不存在")
|
||||
delta = body.count if body.direction == 1 else -body.count
|
||||
if float(item.count) + delta < 0:
|
||||
return fail(f"库存不足: 当前 {float(item.count)}{item.unit}")
|
||||
item.count = float(item.count) + delta
|
||||
db.add(StockMove(item_id=item.id, direction=body.direction, count=body.count,
|
||||
note=body.note, user_id=user.id))
|
||||
return ok(data={"count": float(item.count)}, message="操作成功")
|
||||
|
||||
|
||||
@router.get("/moves")
|
||||
async def move_logs(user: CurrentUser, db: DbSession, page: int = 1, page_size: int = 20):
|
||||
"""出入库流水。"""
|
||||
total_q = select(func.count()).select_from(StockMove)
|
||||
total = (await db.execute(total_q)).scalar_one()
|
||||
stmt = (select(StockMove).order_by(desc(StockMove.id))
|
||||
.offset((page - 1) * page_size).limit(page_size))
|
||||
moves = list((await db.execute(stmt)).scalars())
|
||||
return ok(data=paginated([{
|
||||
"id": m.id, "item_id": m.item_id, "direction": m.direction, "count": float(m.count),
|
||||
"note": m.note, "created_at": str(m.created_at),
|
||||
} for m in moves], page, page_size, total))
|
||||
Reference in New Issue
Block a user