# -*- coding: utf-8 -*- """M01 · M02 셈 끝수(PLAN 751-3) — 품셈 1-2-2 금액의 단위표준 (일위대가 금액란 0.1 원 · 계금 1 원 · 미만버림). 규칙 = B09 일위대가 · 산근과 같은 함수 `rounddown`(`B09_Estimation_Calc_Base` · 엑셀 ROUNDDOWN): 줄 금액 = 비목마다 ROUNDDOWN(·, 1) · 금액 = 그 합 하위 로직 줄(값묶음 없음) = 하위 로직도 같은 끝수로 다시 셈한 노 · 재 · 경 × 수량 덧줄 = 버린 줄 합(노무비 · 재료비 · 경비 · `줄.'이름'`)으로 식을 다시 셈 → ROUNDDOWN 1 비목 합 = ROUNDDOWN(Σ 줄, 0)(+ 로직 `끝수` 묶음) · 계 = 셋 합 엔진 줄 덧칸(단위 · 수량 · 단가 · 출처 · 까닭 · 묶음 · 옵션꺼짐 …) · 수량 · 품 자리는 그대로. 쓰는 곳 = M01 시험 계산(`check_master.calc`) · 읽는 식(`master_text.lines`) · M02 로직 줄 (`M02_Labor_Doc.run_line` — 단가산출 · 일위대가 · 펼침이 모두 거침) · 일위대가 반영(`row` · `sums` · TS 짝 `M02_MasterTemplete_Labor_Calc.ts` · 거울 시험 `test_m02_labor_calc_mirror.py`). B09 는 제 단가로 다시 버리므로 엔진 날 값(`run_line(raw=True)`)을 받음. ponytail: 흐름 로직 · 돈 결과 로직(결과 하나)은 끝수 없음 — 그 로직이 돈을 내면 여기에 더함. """ from __future__ import annotations import sys from decimal import Decimal from pathlib import Path import master_formula as mf import master_formula_sum as ms _ROOT = str(Path(__file__).resolve().parents[3]) if _ROOT not in sys.path: # 스크립트 · 시험 길 — 서버는 이미 뿌리에서 돎 sys.path.insert(0, _ROOT) from B09_Estimation.B09_Estimation_Calc_Base import rounddown # noqa: E402 COSTS = mf.COST_ITEMS _ERRORS = (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError) def down(value, places: int): """ROUNDDOWN(value, places) — Decimal 로 · 빈 값 = None.""" if value is None: return None return Decimal(str(rounddown(float(value), places))) def _line(value, rule: dict | None): """줄 값 끝수 — 줄 `자리` {방법, 자리}(791-1) · 없으면 0.1 원 버림.""" if value is None or not rule: return down(value, 1) way, places = rule.get("방법"), int(rule.get("자리") or 0) if way == "버림": return down(value, places) return mf._round(Decimal(str(value)), places, mf.ROUND_WAYS[way]) def _parts(split: dict, rule: dict | None = None) -> tuple[dict, Decimal | None]: """줄 비목 → (비목마다 0.1 원 버림 · 줄 `자리` 면 그것, 금액 = 그 합 · 모두 빈 = None).""" cut = {k: _line(v, rule) for k, v in split.items()} kept = [v for v in cut.values() if v is not None] return cut, (sum(kept, Decimal(0)) if kept else None) def row(money: dict | None) -> dict | None: """반영 한 줄 `{노무비, 재료비, 경비, 계}` — 비목마다 0.1 원 버림 · 계 = 셋 합.""" if money is None: return None cut = {k: down(money.get(k) or 0, 1) for k in COSTS} return {**cut, "계": sum(cut.values(), Decimal(0))} def sums(rows: list) -> dict: """비목 합 — 비목마다 ROUNDDOWN(Σ 줄, 0) · 계 = 셋 합(빈 줄 = 뺌).""" cut = {k: down(sum((Decimal(str(r[k])) for r in rows if r), Decimal(0)), 0) for k in COSTS} return {**cut, "계": sum(cut.values(), Decimal(0))} def _sub(master, item: dict, env: dict, depth: int) -> dict | None: """하위 로직 줄 노 · 재 · 경 — 같은 끝수로 다시 셈 · 로직 부르기가 아니거나 못 셈 = None.""" if item.get("값묶음") is not None or item.get("종류") != "로직": return None try: node = mf.parse(item["요소"]) if node[0] != "logic": return None given = {k: mf.evaluate(v, env, master, depth) for k, v in node[2]} ref = mf.fill(node[1], env) got = engine(master, ref, mf.run(master, ref, given, depth + 1), depth + 1) except _ERRORS: return None return None if "결과" in got else got def engine(master, ref: str, got: dict, depth: int = 0) -> dict: """엔진 답(`mf.run` 꼴 `{줄, 노무비, 재료비, 경비, 계, 중간}`) → 끝수 입힌 답(새 묶음).""" if "결과" in got or "줄" not in got: return got logic = master.logic(ref) if isinstance(logic.get("흐름"), dict): return got items, adds = logic.get("호표") or [], logic.get("덧줄") or [] rules = [x.get("자리") for x in (*items, *adds)] # 줄 자리(791-1) new = ms.uses(logic) skip = ms.skipped(logic) if new else set() env = dict(got.get("중간") or {}) total = dict.fromkeys(COSTS, Decimal(0)) lines = [] for n, line in enumerate(got["줄"]): split, more = dict(line.get("비목") or {}), {} if n == len(items): # 덧줄이 부르는 `줄.'이름'` = 버린 호표 줄 금액(엔진과 같이 한 번) env["줄"] = {x["이름"]: x["금액"] for x in lines if x["이름"] and x["금액"] is not None} if n < len(items): sub = _sub(master, items[n], env, depth) if line.get("수량") is not None else None if sub is not None: qty, one = line["수량"], items[n].get("비목") split = {one: qty * sub["계"]} if one else {k: qty * sub[k] for k in COSTS} more = {"단가": sub["계"]} # 하위 로직 계(1 원 버림) = 이 줄 단가 elif n - len(items) < len(adds) and line.get("금액") is not None: add = adds[n - len(items)] env.update(total) if new: # 덧줄이 부르는 번지 · 단계 = 버린 줄로 env.update(ms.scope(logic, lines)) try: value = mf.evaluate(mf.parse(add["식"]), env, master, depth) except _ERRORS: value = line["금액"] env[add["이름"]] = _line(value, rules[n]) # 뒤 덧줄 · 중간 칸 = 버린 값 split = {add["비목"]: value} cut, money = _parts(split, rules[n] if n < len(rules) else None) for k, v in cut.items(): total[k] += 0 if n in skip else v or 0 if n >= len(items) and money is not None: more = {"단가": money} out = {"합밖": True} if n in skip else {} # 합계에 안 드는 줄 표지(796-2d · 보이기만) lines.append({**line, "비목": cut, "금액": money, **more, **out}) if not new: costs = {k: mf.cut(logic, down(v, 0), "계") for k, v in total.items()} # 중간 = 덧줄이 본 값(버린 줄 합 · 덧줄 값 · `줄`) — 시험 계산 중간 칸 return {**got, "줄": lines, **costs, "계": sum(costs.values()), "중간": env} costs = settle(logic, total) more = {"단계": ms.steps(logic, lines), "셈법": ms.rule(logic)} return {**got, "줄": lines, **costs, "계": sum(costs.values()), "중간": env, **more} def settle(logic: dict, total: dict) -> dict: """블록 끝전(791-1) — `끝전` 비목버림(비목마다 원 버림 · 옛 규칙) · 큰나머지(계 원 버림 뒤 모자란 원을 소수 큰 비목부터 · 같으면 경 › 재 › 노) → 로직 `끝수` 묶음 → `조정` → 원 버림.""" costs = {k: down(v, 0) for k, v in total.items()} if logic.get("끝전") == "큰나머지": rest = int(down(sum(total.values()), 0) - sum(costs.values())) order = sorted(COSTS, key=lambda k: (costs[k] - total[k], COSTS[::-1].index(k))) for k in order[:rest]: costs[k] += 1 costs = ms.adjust(logic, {k: mf.cut(logic, v, "계") for k, v in costs.items()}) return {k: down(v, 0) for k, v in costs.items()} def kept(got: dict) -> dict: """`run_line` 이 엔진 답에서 그대로 넘기는 덧칸(792-3) — 단계 · 셈법.""" return {k: got[k] for k in ("단계", "셈법") if k in got} def plain(got: dict, gone=()) -> dict: """로직 줄 답(`run_line` 꼴 `{기준, 줄, 재료}`)을 줄만 다시 버림 — 켜쌓기율 곱 · 별도계상 뺀 뒤(`gone` = 뺄 줄 차례) · 기준 = 남은 줄 비목 합 ROUNDDOWN 0 · 답 `셈법`(791-1 새 의미 로직) = 줄 자리 · 합밖 · 단계 · 블록 끝전 · 조정 그대로(`settle`).""" if not got.get("기준"): return got rule = got.get("셈법") places = [x.get("자리") for x in ms.items(rule)] if rule else [] skip = set(gone) | (ms.skipped(rule) if rule else set()) def at(n): return places[n] if isinstance(n, int) and 0 <= n < len(places) else None lines, total = [], dict.fromkeys(COSTS, Decimal(0)) for n, line in enumerate(got.get("줄") or []): cut, money = _parts(dict(line.get("비목") or {}), at(n)) for k, v in cut.items(): total[k] += 0 if n in skip else Decimal(str(v or 0)) lines.append({**line, "비목": cut, "금액": money}) mats = [{**m, "금액": _line(m.get("금액"), at(m.get("차례")))} for m in got.get("재료") or []] kept = [x for n, x in enumerate(lines) if n not in gone] if not rule: base = {k: down(v, 0) for k, v in total.items()} return {**got, "줄": kept, "재료": mats, "기준": {**base, "계": sum(base.values())}} base = settle(rule, total) steps = ms.steps(rule, [{} if n in gone else x for n, x in enumerate(lines)]) return { **got, "줄": kept, "재료": mats, "기준": {**base, "계": sum(base.values())}, "단계": steps, }