- 새 `M02_Logic_Settle` — 엔진 구조 그대로 줄 ROUNDDOWN 1 · 비목 ROUNDDOWN 0 · 작업량 Q ROUND 2 · 하위 로직 같은 규칙 - 기준 목록 금액 · 계산서 줄 · 소계 · 계가 같은 값 · 목록 저장본 꼴 번호 올림 · 머리 글에 지식DB 단수처리 근거 - 시험: 거울 test_704_settle(B09 _money · block_totals) · 옛 목록 시험은 끝수 차 3 원 안으로 고침 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
116 lines
5.4 KiB
Python
116 lines
5.4 KiB
Python
"""M02 기준 목록 · 계산서 끝수(PLAN 704-2 딸림) — B09 산근 끝수 규칙으로 셈(③ 서버 단독 ·
|
|
`B09_Estimation_Sangeun` 과 규칙 짝 · 거울 시험 `test_704_settle`).
|
|
|
|
줄 금액 = ROUNDDOWN(단가 × 수량, 1) 비목마다 · 비목 총계 = ROUNDDOWN(Σ 줄, 0) · 계 = 셋 합 ·
|
|
작업량 Q(중간 이름 Q · Q1 …) = ROUND(식, 2) 뒤 쓰임 · % 덧줄 밑수 = 그때까지 줄 합(줄 끝수 뒤).
|
|
근거 = 지식DB `technical_info/01_임도/05_원가정보/단수처리_규칙.md`(행 금액 절사 · 관측 후보 규칙).
|
|
M01 엔진(`master_formula.run`)의 구조 그대로 — 끝수 자리만 다름(엔진은 줄 끝수 없이 계에서 한 번).
|
|
하위 로직(기계경비 포함)은 같은 규칙으로 다시 셈 · 흐름 · 결과 로직은 엔진 값 그대로.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from decimal import ROUND_DOWN, ROUND_HALF_UP, Decimal
|
|
from typing import Any
|
|
|
|
from M01_MasterData import M01_MasterData_Store as m01
|
|
|
|
mf = m01.cm.mf
|
|
_Q = re.compile(r"Q\d*")
|
|
|
|
|
|
def _down(value: Any, places: int) -> Any:
|
|
return None if value is None else mf._round(Decimal(value), places, ROUND_DOWN)
|
|
|
|
|
|
def _bundle(whole: Any, item: dict, env: dict, depth: int) -> dict:
|
|
"""로직 호표 줄이 부르는 하위 로직 — 같은 규칙으로 다시 셈(엔진 `logic` 마디 그대로)."""
|
|
node = mf.parse(item["요소"])
|
|
if node[0] != "logic":
|
|
return mf.evaluate(node, env, whole, depth)
|
|
given = {k: mf.evaluate(v, env, whole, depth) for k, v in node[2]}
|
|
got = settle(whole, mf.fill(node[1], env), given, depth + 1)
|
|
return got["결과"] if "결과" in got else {k: got[k] for k in mf.BUNDLE}
|
|
|
|
|
|
def settle(whole: Any, ref: str, given: dict, depth: int = 0) -> dict:
|
|
"""`master_formula.run` 과 같은 꼴 `{줄, 노무비, 재료비, 경비, 계, 중간}` — 끝수만 B09 규칙."""
|
|
row = whole.logic(ref)
|
|
if isinstance(row.get("흐름"), dict) or "결과" in row:
|
|
return mf.run(whole, ref, given, depth)
|
|
env = mf._inputs(row, given)
|
|
for step in row.get("중간", []):
|
|
try:
|
|
value = mf.evaluate(mf.parse(step["식"]), env, whole, depth)
|
|
except mf.EmptyInput as e:
|
|
env[step["이름"]] = mf.Blank(str(e))
|
|
continue
|
|
if _Q.fullmatch(step["이름"]) and isinstance(value, Decimal):
|
|
value = value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
|
env[step["이름"]] = value
|
|
lines: list[dict] = []
|
|
for n, item in enumerate(row.get("호표", [])):
|
|
try:
|
|
qty = mf.evaluate(mf.parse(item["수량"]), env, whole, depth)
|
|
if item["종류"] == "로직":
|
|
bundle = _bundle(whole, item, env, depth)
|
|
price = bundle["계"]
|
|
split = (
|
|
{k: qty * bundle[k] for k in mf.COST_ITEMS}
|
|
if "비목" not in item
|
|
else {item["비목"]: qty * price}
|
|
)
|
|
else:
|
|
ref_ = (
|
|
{
|
|
k: mf.fill(v, env) if isinstance(v, str) else v
|
|
for k, v in item["요소"].items()
|
|
}
|
|
if isinstance(item["요소"], dict)
|
|
else mf.fill(item["요소"], env)
|
|
)
|
|
if isinstance(ref_, str) and qty != 0:
|
|
whole.get(ref_)
|
|
price = mf.mp.priced(whole, item, ref_, env, n if depth == 0 else None)[0]
|
|
if price is None and qty == 0:
|
|
price = Decimal(0)
|
|
split = {item["비목"]: None if price is None else qty * price}
|
|
except mf.EmptyInput as e:
|
|
lines.append(mf._blank_line(item, str(e)))
|
|
continue
|
|
lines.append(_line(item["이름"], split))
|
|
sums = {k: sum((x["비목"].get(k) or 0 for x in lines), Decimal(0)) for k in mf.COST_ITEMS}
|
|
env["줄"] = {x["이름"]: x["금액"] for x in lines if x["이름"] and x["금액"] is not None}
|
|
for extra in row.get("덧줄", []):
|
|
env.update(sums)
|
|
try:
|
|
value = mf.evaluate(mf.parse(extra["식"]), env, whole, depth)
|
|
env[extra["이름"]] = value
|
|
except mf.FormulaError:
|
|
value = None
|
|
sums[extra["비목"]] += _down(value, 1) or 0
|
|
lines.append(_line(extra["이름"], {extra["비목"]: value}))
|
|
if value is not None:
|
|
env["줄"][extra["이름"]] = lines[-1]["금액"]
|
|
sums = {k: mf.cut(row, _down(v, 0), "계") for k, v in sums.items()}
|
|
return {"줄": lines, **sums, "계": sum(sums.values()), "중간": mf._shown(env)}
|
|
|
|
|
|
def _line(name: str, split: dict) -> dict:
|
|
cut = {k: _down(v, 1) for k, v in split.items()}
|
|
whole_sum = None if all(v is None for v in cut.values()) else sum(v or 0 for v in cut.values())
|
|
return {"이름": name, "비목": cut, "금액": whole_sum}
|
|
|
|
|
|
def apply(groups: list[dict], engine: dict, done: dict) -> None:
|
|
"""계산서 묶음의 줄 금액 · 소계를 `settle` 값으로 바꿈 — 줄 차례는 엔진과 같음."""
|
|
seq: dict[str, list] = {k: [] for k in mf.COST_ITEMS}
|
|
for eng, mine in zip(engine["줄"], done["줄"]):
|
|
for part, _ in m01.mtx._shares(eng):
|
|
seq[part].append(mine["비목"].get(part))
|
|
for group in groups:
|
|
for line, amount in zip(group["줄"], seq.get(group["비목"], [])):
|
|
line["금액"] = amount
|
|
group["소계"] = done[group["비목"]]
|