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,107 @@
|
||||
"""成本核算路由:系列成本模板(取自成本核算 Excel)+ 价格库导入 + 成本估算。"""
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, UploadFile
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
|
||||
from huamenchuang.core.deps import CurrentUser, DbSession
|
||||
from huamenchuang.core.response import fail, ok
|
||||
from huamenchuang.models.business import CostSeriesTemplate
|
||||
from huamenchuang.services import costbook_service as svc
|
||||
|
||||
router = APIRouter(tags=["成本核算"])
|
||||
|
||||
|
||||
def _brief(t: CostSeriesTemplate) -> dict:
|
||||
data = json.loads(t.data)
|
||||
return {"id": t.id, "name": t.name, "series": t.series, "wall_thick": t.wall_thick,
|
||||
"source_file": t.source_file, "enabled": t.enabled, "window": data.get("window")}
|
||||
|
||||
|
||||
def _full(t: CostSeriesTemplate) -> dict:
|
||||
return {**_brief(t), "data": json.loads(t.data)}
|
||||
|
||||
|
||||
@router.post("/costbook/import-excel")
|
||||
async def import_excel(file: UploadFile, user: CurrentUser, db: DbSession):
|
||||
"""上传成本核算 Excel(12 sheet 鲁铭格式):价格库 8 表入产品价格本,铝成本页入系列模板。"""
|
||||
blob = await file.read()
|
||||
if not blob:
|
||||
return fail("文件为空")
|
||||
import io
|
||||
|
||||
from openpyxl import load_workbook
|
||||
try:
|
||||
wb = load_workbook(io.BytesIO(blob), data_only=True)
|
||||
parsed = {"file": file.filename or "upload.xlsx",
|
||||
"series_summary": svc._parse_series_summary(wb["铝成本"]),
|
||||
"libraries": {sn: svc._parse_library(wb[sn])
|
||||
for sn in svc.LIB_SHEETS if sn in wb.sheetnames},
|
||||
"optimize_stats": svc._parse_optimize_stats(wb["铝计算"]) if "铝计算" in wb.sheetnames else {}}
|
||||
except KeyError as e:
|
||||
return fail(f"缺少必需工作表 {e},请确认上传成本核算格式工作簿")
|
||||
except Exception as e: # noqa: BLE001
|
||||
return fail(f"解析失败:{e}")
|
||||
lib_stats = await svc.import_libraries(db, user.company_id, parsed["libraries"])
|
||||
await svc.import_series(db, user.company_id, parsed)
|
||||
return ok(data={"libraries": lib_stats, "optimize_stats": parsed["optimize_stats"]},
|
||||
message=f"导入完成:价格库新增 {lib_stats['created']} / 更新 {lib_stats['updated']} 条")
|
||||
|
||||
|
||||
@router.get("/costbook/series")
|
||||
async def list_series(user: CurrentUser, db: DbSession):
|
||||
rows = list((await db.execute(select(CostSeriesTemplate).where(
|
||||
CostSeriesTemplate.company_id == user.company_id)
|
||||
.order_by(CostSeriesTemplate.series, CostSeriesTemplate.id))).scalars())
|
||||
return ok(data=[_brief(t) for t in rows])
|
||||
|
||||
|
||||
class EstimateBody(BaseModel):
|
||||
series_id: int
|
||||
width: float = Field(gt=0, description="窗宽 mm")
|
||||
height: float = Field(gt=0, description="窗高 mm")
|
||||
count: int = Field(default=1, ge=1, description="樘数")
|
||||
sash_count: int | None = Field(default=None, ge=0, description="开启扇数,缺省按基准开启率")
|
||||
|
||||
|
||||
@router.post("/costbook/estimate")
|
||||
async def estimate(body: EstimateBody, user: CurrentUser, db: DbSession):
|
||||
t = await db.get(CostSeriesTemplate, body.series_id)
|
||||
if t is None or t.company_id != user.company_id:
|
||||
return fail("系列模板不存在")
|
||||
return ok(data=svc.estimate(t, body.width, body.height, body.count, body.sash_count))
|
||||
|
||||
|
||||
@router.get("/costbook/series/{sid}")
|
||||
async def get_series(sid: int, user: CurrentUser, db: DbSession):
|
||||
t = await db.get(CostSeriesTemplate, sid)
|
||||
if t is None or t.company_id != user.company_id:
|
||||
return fail("系列模板不存在")
|
||||
return ok(data=_full(t))
|
||||
|
||||
|
||||
class SeriesBody(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=64)
|
||||
series: int = Field(ge=0)
|
||||
wall_thick: float = Field(gt=0)
|
||||
data: dict
|
||||
|
||||
|
||||
@router.post("/costbook/series")
|
||||
async def create_series(body: SeriesBody, user: CurrentUser, db: DbSession):
|
||||
t = CostSeriesTemplate(company_id=user.company_id, name=body.name, series=body.series,
|
||||
wall_thick=body.wall_thick,
|
||||
data=json.dumps(body.data, ensure_ascii=False))
|
||||
db.add(t)
|
||||
await db.flush()
|
||||
return ok(data={"id": t.id}, message="系列模板已创建")
|
||||
|
||||
|
||||
@router.delete("/costbook/series/{sid}")
|
||||
async def delete_series(sid: int, user: CurrentUser, db: DbSession):
|
||||
t = await db.get(CostSeriesTemplate, sid)
|
||||
if t is None or t.company_id != user.company_id:
|
||||
return fail("系列模板不存在")
|
||||
await db.delete(t)
|
||||
return ok(message="系列模板已删除")
|
||||
@@ -6,6 +6,7 @@ from huamenchuang.api import (
|
||||
auth,
|
||||
business,
|
||||
contracts,
|
||||
costbook,
|
||||
customers,
|
||||
finance,
|
||||
optimize,
|
||||
@@ -45,6 +46,7 @@ api_router.include_router(salesmen.router)
|
||||
api_router.include_router(purchases.router)
|
||||
api_router.include_router(warehouse.router)
|
||||
api_router.include_router(business.router)
|
||||
api_router.include_router(costbook.router)
|
||||
api_router.include_router(platform.router)
|
||||
api_router.include_router(reports.router)
|
||||
api_router.include_router(platform.router)
|
||||
|
||||
Reference in New Issue
Block a user