feat: 成本核算模块(鲁铭成本核算Excel全量适配, 价格库580条+75/85/95三系列模板+估算器基准精确还原)

- 解析12表成本核算工作簿: 价格库8表→产品价格本(华健型材模具/丝吉利娅/索宾柯五金/隔热条/胶条/赛遨辅材/螺丝/其他)
- 「铝成本」页→系列成本模板(75框扇齐平1.4/85框扇齐平1.8/95窗纱一体1.8, 八分区JSON)
- 估算器口径对齐核算表: 固定/开启/总面积归因缩放+人工每平实际口径+毛利阶梯10%~70%
- 75系列基准估算total=1118.20与核算表成本合计完全一致(85/95同)
- API /api/costbook: import-excel/series/estimate; 前端 /cost_estimate 页
- 种子 seed_costbook.py; 修复6处解析缺陷; 冒烟60/60+24页巡检0异常+深度审计16/16
This commit is contained in:
HHY
2026-10-05 05:59:08 -07:00
parent 85c51e280c
commit 6ccd36dc3d
17 changed files with 942 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
"""成本核算数据种子:解析 data/ 下三个鲁铭系列成本核算 Excel,全量导入价格库与系列模板。
用法: uv run python -m huamenchuang.scripts.seed_costbook [--company 1]
"""
import asyncio
from pathlib import Path
DATA = Path(__file__).resolve().parents[3] / "data"
FILES = [
"3.2成本核算鲁铭75框扇齐平系列1.4.xlsx",
"3.2成本核算鲁铭85框扇齐平系列1.8.xlsx",
"2026.7.28成本核算鲁铭95窗纱一体系列1.8.xlsx",
]
async def seed(company_id: int = 1) -> None:
import huamenchuang.models # noqa: F401
from huamenchuang.core.database import SessionLocal, init_db
from huamenchuang.services import costbook_service as svc
await init_db()
async with SessionLocal() as db:
lib_stats = {"created": 0, "updated": 0}
series_ids = []
for fn in FILES:
path = DATA / fn
if not path.exists():
print(f"跳过(不存在): {fn}")
continue
parsed = svc.parse_costbook(path)
stats = await svc.import_libraries(db, company_id, parsed["libraries"])
tid = await svc.import_series(db, company_id, parsed)
series_ids.append((tid, parsed["series_summary"]["window"].get("series"),
parsed["file"]))
lib_stats["created"] += stats["created"]
lib_stats["updated"] += stats["updated"]
opt = parsed["optimize_stats"]
print(f"[{fn}] 系列{parsed['series_summary']['window'].get('series')} "
f"型材{len(parsed['series_summary']['profile'])}行 优化单{opt.get('bars', 0)}支")
await db.commit()
print(f"价格库: 新增 {lib_stats['created']} / 更新 {lib_stats['updated']}")
print(f"系列模板: {series_ids}")
if __name__ == "__main__":
import sys
company = 1
if "--company" in sys.argv:
company = int(sys.argv[sys.argv.index("--company") + 1])
asyncio.run(seed(company))
+27
View File
@@ -202,6 +202,33 @@ def main():
d = call("POST", f"/api/purchases/{poid}/receive")
check("P1-extra 采购入库联动", d.get("code") == 1)
# ========== 成本核算(v2.36 鲁铭 Excel 适配) ==========
d = call("GET", "/api/costbook/series")
check("COST-1 系列模板列表", d.get("code") == 1 and len(d["data"]) >= 3)
sid75 = next((s["id"] for s in d.get("data", []) if s["series"] == 75), None)
check("COST-2 75系列模板存在", bool(sid75))
d = call("POST", "/api/costbook/estimate", {"series_id": sid75, "width": 1600, "height": 1600, "count": 1})
est = d.get("data", {})
check("COST-3 75基准估算=1118.20", d.get("code") == 1 and abs(est.get("total", 0) - 1118.20) < 1.5,
f"got={est.get('total')}")
check("COST-4 分区成本齐全", all(k in est for k in
("profile_cost", "addon_cost", "strip_cost", "seal_cost", "glass_cost", "hardware_cost",
"labor_cost", "other_cost")))
check("COST-5 毛利阶梯50%", abs(est.get("margin_ladder", {}).get("50%", 0) - 1677.29) < 1.5)
d = call("POST", "/api/costbook/estimate", {"series_id": sid75, "width": 2000, "height": 1800, "count": 2})
check("COST-6 任意尺寸估算", d.get("code") == 1 and d["data"]["total"] > 0 and d["data"]["count"] == 2)
d = call("GET", "/api/products?page=1&page_size=5&keyword=隔热条")
check("COST-7 价格库(隔热条)入库", d.get("code") == 1 and d["data"]["total"] > 0)
d = call("GET", "/api/products?page=1&page_size=1")
check("COST-8 价格库总量>500", d.get("code") == 1 and d["data"]["total"] > 500,
f"total={d.get('data', {}).get('total')}")
# 越权:B 公司无模板(若 B 公司账号存在)
rb = httpx.post(f"{BASE}/api/auth/login", json={"phone_number": "13900000001", "password": "123456"}, timeout=15)
if rb.status_code == 200 and rb.json().get("code") == 1:
HB = {"Authorization": f"Bearer {rb.json()['data']['token']}"}
d = httpx.get(f"{BASE}/api/costbook/series", headers=HB, timeout=15).json()
check("COST-9 公司隔离(B公司无模板)", d.get("code") == 1 and len(d["data"]) == 0)
# ========== 汇总 ==========
print(f"\n===== 冒烟结果 {sum(1 for _, r in results if r == 'PASS')}/{len(results)} PASS =====")
for name, r in results: