feat: P0-6 Excel导入导出全套(客户/合同/库存/统计, 自动表头探测)

This commit is contained in:
HHY
2026-09-25 03:30:27 -07:00
parent 436d006b5a
commit 40c16d56a1
14 changed files with 367 additions and 9 deletions
+76 -2
View File
@@ -1,5 +1,5 @@
"""仓库库存路由:材料 CRUD + 出入库。"""
from fastapi import APIRouter
"""仓库库存路由:材料 CRUD + 出入库 + Excel 导入导出。"""
from fastapi import APIRouter, UploadFile
from pydantic import BaseModel, Field
from sqlalchemy import desc, select
@@ -69,6 +69,80 @@ async def create_item(body: ItemBody, user: CurrentUser, db: DbSession):
return ok(data={"id": item.id}, message="创建成功")
# ---------------- Excel 导入导出 ----------------
@router.get("/excel-out")
async def excel_out(user: CurrentUser, db: DbSession):
"""库存全量导出(对齐目标站 warehouse_excel_out)。"""
from fastapi.responses import Response
from huamenchuang.services.excel_service import simple_table_workbook
items = list((await db.execute(
select(WarehouseItem).where(WarehouseItem.company_id == user.company_id)
.order_by(WarehouseItem.id))).scalars())
mat_label = {"profile": "型材", "glass": "玻璃", "hardware": "五金", "addon": "配件"}
data = [[i.bom_code, i.name, mat_label.get(i.material, i.material), i.spec, i.color,
float(i.price), float(i.cost), float(i.count), i.unit, i.length, float(i.warning)]
for i in items]
content = simple_table_workbook("库存列表",
["编码", "名称", "类别", "规格", "颜色", "售价", "成本",
"库存数量", "单位", "单根长度mm", "预警线"],
data, widths=[12, 20, 8, 14, 10, 10, 10, 10, 8, 12, 10])
return Response(content, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": "attachment; filename=warehouse.xlsx"})
@router.post("/excel-in")
async def excel_in(file: UploadFile, user: CurrentUser, db: DbSession):
"""材料批量导入(对齐目标站 materials_config_excel:同名材料按名称匹配更新数量,否则新增)。"""
if not file.filename or not file.filename.lower().endswith((".xlsx", ".xls")):
return fail("请上传 xlsx 文件")
from huamenchuang.services.excel_service import pick, read_rows
try:
rows = read_rows(await file.read())
except Exception:
return fail("文件解析失败")
if not rows:
return fail("文件中没有数据行")
mat_map = {"型材": "profile", "玻璃": "glass", "五金": "hardware", "配件": "addon",
"profile": "profile", "glass": "glass", "hardware": "hardware", "addon": "addon"}
existing = {i.name: i for i in (await db.execute(
select(WarehouseItem).where(WarehouseItem.company_id == user.company_id))).scalars()}
created, updated, skipped = 0, 0, 0
for i, row in enumerate(rows, 2):
name = pick(row, "名称", "材料名称", "name")
if not name:
skipped += 1
continue
name = str(name).strip()
mat = mat_map.get(str(pick(row, "类别", "材料", "material", default="型材") or "型材"), "profile")
values = dict(
bom_code=str(pick(row, "编码", "bom_code", default="") or ""),
material=mat, spec=str(pick(row, "规格", "spec", default="") or ""),
color=str(pick(row, "颜色", "color", default="") or ""),
price=float(pick(row, "售价", "price", default=0) or 0),
cost=float(pick(row, "成本", "cost", default=0) or 0),
count=float(pick(row, "库存数量", "数量", "count", default=0) or 0),
unit=str(pick(row, "单位", "unit", default="根") or "根"),
length=int(float(pick(row, "单根长度mm", "长度", "length", default=0) or 0)),
warning=float(pick(row, "预警线", "warning", default=0) or 0))
if name in existing: # 同名更新(数量累加)
item = existing[name]
for k, v in values.items():
if k == "count":
continue
setattr(item, k, v)
item.count = float(item.count) + values["count"]
updated += 1
else:
item = WarehouseItem(company_id=user.company_id, name=name, **values)
db.add(item)
existing[name] = item
created += 1
await db.flush()
return ok(data={"created": created, "updated": updated, "skipped": skipped},
message=f"导入完成:新增 {created} 条,更新 {updated} 条,跳过 {skipped} 条")
@router.put("/{item_id}")
async def update_item(item_id: int, body: ItemBody, user: CurrentUser, db: DbSession):
"""编辑材料。"""