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:
@@ -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))
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user