feat: v0.3 报价算料引擎与仓库库存模块

后端:
- 报价算料: 从drawing_data几何提取型材清单(外框/中梃/扇框投影长度)
  与玻璃面积(鞋带公式), 套价格本计价, 支持样板/订单/自定义图纸三入口
- 仓库库存: 材料CRUD/出入库/流水/负库存拦截/预警线
- SQLite默认路径锚定项目根目录(修复cwd依赖)
前端:
- 仓库库存页(筛选/出入库对话框/预警高亮)
- 算料报价弹窗(样板库与订单详情入口, 支持打印)
- 路由与导航更新(8页面)
回归: 9个API端点全部200, 报价/优化/SPA路由验证通过
This commit is contained in:
HHY
2026-09-20 08:58:14 -07:00
parent f7a555dd6c
commit 065d387399
16 changed files with 630 additions and 8 deletions
+109
View File
@@ -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))