"""B09 산근 풀이 셈 의미(PLAN 792-3) — 791-1 엔진 새 의미(줄 자리 · 합밖 · 번지 · 단계 · 블록 끝전 · 결과 조정 · 변수 표시)를 쓰는 로직만 엔진 규칙(`master_cut` · `master_formula_sum`) 그대로 · 새 칸 없는 로직 = 옛 B09 규칙(작업량 Q = ROUND 2 · 줄 ROUNDDOWN 1 · 총계 비목마다 ROUNDDOWN 0). 쓰는 곳 = `B09_Estimation_Sangeun.Solver` · 지킴 시험 = `test_792_3_engine_paths.py`(엔진 · M02 SK · SC · B09 산근 네 길 같은 금액). """ from __future__ import annotations import importlib import re from decimal import Decimal from typing import Any from B09_Estimation.B09_Estimation_Calc_Base import round_half, rounddown, total from M01_MasterData import M01_MasterData_Store as _m01 # noqa: F401 — scripts 길을 둠 mcut = importlib.import_module("master_cut") ms = importlib.import_module("master_formula_sum") PARTS = ("labor", "material", "expense") COST_PART = {"노무비": "labor", "재료비": "material", "경비": "expense"} _Q = re.compile(r"Q\d*") # 작업량 중간값 이름(산림품셈 09장 토공 · 07장 집재 …) def shown(row: dict, step: dict, value: Any) -> Any: """중간값 — `표시` 칸 · 새 의미 로직 = 엔진 그대로(`표시` 자리 반올림) · 옛 = Q ROUND 2.""" if step.get("표시") is not None or ms.uses(row): return ms.shown(step, value) if _Q.fullmatch(step["이름"]) and isinstance(value, (Decimal, int, float)): return Decimal(str(round_half(float(value), 2))) return value def _cut(value: float, rule: dict | None) -> float: return float(mcut._line(value, rule)) if rule else rounddown(value, 1) def places(row: dict, lines: list[dict]) -> None: """호표 줄 `자리`(줄 자리) — 그 줄 금액만 단가 × 수량을 그 규칙으로 다시 버림(제자리).""" for item, ln in zip(row.get("호표", []), lines): rule = item.get("자리") if not rule or ln.get("qty") is None: continue for part, price in (ln.get("price") or {}).items(): if part in PARTS and price is not None: ln[part] = _cut(price * ln["qty"], rule) def _engine(lines: list[dict]) -> list[dict]: """B09 줄 → 엔진 줄 꼴 `{비목}`(번지 · 단계 셈).""" return [ {"비목": {k: Decimal(str(ln.get(p) or 0)) for k, p in COST_PART.items()}} for ln in lines ] def sums(row: dict, lines: list[dict]) -> dict[str, float | None]: """덧줄 밑 비목 합 — 새 의미 로직은 합밖 · 제외 단계 줄을 뺌.""" skip = ms.skipped(row) if ms.uses(row) else set() kept = [ln for n, ln in enumerate(lines) if n not in skip] return {name: total([ln[p] for ln in kept]) for name, p in COST_PART.items()} def scope(row: dict, lines: list[dict], env: dict) -> None: """덧줄 식 이름 `번지` · `단계`(지금까지 줄) — 새 의미 로직만.""" if ms.uses(row): env.update(ms.scope(row, _engine(lines))) def extra(row: dict, item: dict, value: float, env: dict) -> float: """덧줄 금액 — 줄 `자리` 면 그 규칙 · 아니면 ROUNDDOWN 1 · 새 의미 로직은 뒤 덧줄이 그 값을 부름.""" money = _cut(value, item.get("자리")) if ms.uses(row): env[item["이름"]] = Decimal(str(money)) return money def totals(row: dict, lines: list[dict]) -> dict[str, float | None] | None: """총계 — 새 의미 로직 = 합밖 · 제외 빼고 블록 끝전 · `끝수` · 조정(`master_cut.settle`) · 옛 로직 · 빈칸 비목 있음 = None(옛 `block_totals`).""" if not ms.uses(row): return None got = sums(row, lines) if any(v is None for v in got.values()): return None costs = mcut.settle(row, {k: Decimal(str(v)) for k, v in got.items()}) out = {COST_PART[k]: float(v) for k, v in costs.items()} return {**out, "sum": sum(out.values())}