Files
Aislo/B09_Estimation/B09_Estimation_Labor_Rule.py
eomsangdonandClaude Opus 5.5 d8834c1d63 feat(792-4): B09 일위대가 대가 블록이 엔진 새 의미를 따름 — 다섯 길 같은 금액 · 가져온 식 SK 내역 = 엑셀
- 새 B09_Estimation_Labor_Rule: 새 의미 로직만 대가 블록 줄 `place`(줄 자리) · `out`(합밖 · 제외 단계) · 덧줄 = 수 하나 × 밑 합 꼴이면 `rows:` % 줄(줄.'이름' · 앞 덧줄 · 번지 · 단계 · 비목 합 · 793-2a 괄호 꼴) · 이름 없는 수 식 덧줄 = 고정 줄 · 블록 `rule` {끝전, 조정, 끝수} · 다른 꼴 = 빈 줄 그대로(틀린 값 대신 빈칸)
- Labor_Source: 품셈 블록에 표지 · 단가산출 블록이 줄을 바꿔도 표지 이어 받음(657 줄)
- Calc_Labor .py · .ts 짝: 줄 금액 `cut`(place) · `out` 줄은 비목 합 · part: 밑에서 뺌 · 블록 비목 합 `settle`(큰나머지 → 끝수 → 조정 → 원 버림 · master_cut.settle 차례) · 옛 블록 = 표지 없음 = 전과 같음
- 산근 글 로직 부르기 줄(중기 세 줄 · 하위 산근) 금액도 줄 자리 규칙(792-3 남은 것)
- test_792_3: 다섯째 길 B09 대가(SC → 굳힌 SK 수량 1 → 내역 단가 · 파이썬 = TS 거울) · 실무 봉화 D00102 · 북부청임도 18 을 793-2 길(임시 층)로 가져온 식 SK → 내역 단가 = 엑셀
- test_712_3 낡은 기대: 표 비율 0.78 → 단위 수 1,601 이상 · 0.76 — 782-1 · 782-2 · 782-3 계수 표 94 가 로직에 붙음(그중 단위 기준 25 · 시공능력 · 적용기준 계수 = 단위 기준 없음) · 712-3 때 2,023 중 1,601 그대로 · 마스터 자료 안 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:26:03 +09:00

136 lines
6.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""B09 대가 블록 셈법(PLAN 792-4) — 791-1 엔진 새 의미(줄 자리 · 합밖 · 번지 · 단계 · 블록 끝전 ·
결과 조정)를 쓰는 로직만 블록 줄 · 블록에 표지를 붙임(`B09_Estimation_Labor_Source._poomsem`):
줄 `place` {방법, 자리} 줄 금액 끝수(없으면 ROUNDDOWN 1)
줄 `out` true 비목 합에서 뺌(합밖 줄 · 제외 단계가 담은 줄 · 번지 · 단계엔 쌓임)
덧줄 = 수 하나 × 밑 합 꼴이면 % 줄 밑 `rows:`(그 줄 차례 · `@비목` · % = 그 수 × 100) —
밑 = `줄.'이름'` · 앞 덧줄 이름 · `번지.X.비목` · `단계.'이름'.비목` · `노무비` 등(앞 줄 합)
덧줄 = 이름 없는 수 식 = 고정 줄(수량 1 · 엔진 값)
블록 `rule` {끝전?, 조정?, 끝수?} 블록 비목 합 = `B09_Estimation_Calc_Labor.settle`
그 밖 꼴(밑마다 다른 수 · 곱 · 중간 이름) = 빈 줄 그대로(대가 단가로 못 셈 · 틀린 값 대신 빈칸).
셈 = 셈 짝(`B09_Estimation_Calc_Labor` .py · .ts)이 B09 단가로 · 옛 로직(새 칸 없음) = 표지 없음 ·
지킴 시험 = `test_792_3_engine_paths.py`(엔진 · SK · SC · B09 산근 · B09 대가 다섯 길 같은 금액).
"""
from __future__ import annotations
import importlib
from decimal import Decimal
from typing import Any
from M01_MasterData import M01_MasterData_Store as _m01 # noqa: F401 — scripts 길을 둠
mf = importlib.import_module("master_formula")
ms = importlib.import_module("master_formula_sum")
PART = {"노무비": "labor", "재료비": "material", "경비": "expense"}
FLAGS = ("place", "out") # 블록 줄 덧칸 — 단가산출 블록이 줄을 바꿔도 이어 받음
Refs = list[tuple[int, str | None]] # (줄 차례, 비목 | None = 줄 전체)
def _f(value: Any) -> float | None:
try:
return None if value is None else float(value)
except (TypeError, ValueError):
return None
def _cost(rows: list[int], cost: str) -> Refs | None:
if cost == "계":
return [(i, None) for i in rows]
return [(i, PART[cost]) for i in rows] if cost in PART else None
def _ref(row: dict, node: tuple, n: int) -> Refs | None:
"""밑 이름 하나 → 줄 차례(n 앞만) · 모름 = None."""
lines = ms.items(row)
names = {x.get("이름"): i for i, x in reversed(list(enumerate(lines[:n]))) if x.get("이름")}
if node[0] == "name":
if node[1] in PART: # 비목 합 = 앞 줄 중 합에 드는 줄
skip = ms.skipped(row)
return [(i, PART[node[1]]) for i in range(n) if i not in skip]
return [(names[node[1]], None)] if node[1] in names else None
if node[0] != "field":
return None
head, name = node[1], node[2]
if head == ("name", "줄"):
return [(names[name], None)] if name in names else None
if head[0] != "field" or head[1][0] != "name":
return None
group, key = head[1][1], head[2]
if group == ms.ADDR:
return _cost([i for i, x in enumerate(lines[:n]) if x.get(ms.ADDR) == key], name)
if group == ms.STEP:
got = next((mine for st, mine in ms._cover(row) if st.get("이름") == key), None)
return None if got is None else _cost(sorted(i for i in got if i < n), name)
return None
def _linear(row: dict, node: tuple, n: int) -> tuple[Refs, Decimal] | None:
"""식 = 수 하나 × 밑 합 → (밑, 수) · 다른 꼴 = None."""
if node[0] in ("name", "field"):
got = _ref(row, node, n)
return None if got is None else (got, Decimal(1))
if node[0] != "bin":
return None
op, a, b = node[1], node[2], node[3]
if op in ("*", "/") and b[0] == "num" and (left := _linear(row, a, n)):
return left[0], left[1] * b[1] if op == "*" else left[1] / b[1]
if op == "*" and a[0] == "num" and (right := _linear(row, b, n)):
return right[0], a[1] * right[1]
if op == "+" and (left := _linear(row, a, n)) and (right := _linear(row, b, n)):
return (left[0] + right[0], left[1]) if left[1] == right[1] else None
return None
def _pct(row: dict, formula: str, n: int) -> tuple[str, float] | None:
"""덧줄 식 → (`rows:` 밑, %) · 못 바꿈 = None(밑 없음 · 겹친 밑 포함)."""
try:
got = _linear(row, mf.parse(formula), n)
except mf.FormulaError:
return None
if not got or not got[0] or len(set(got[0])) != len(got[0]):
return None
spec = ",".join(str(i) if part is None else f"{i}@{part}" for i, part in got[0])
return f"rows:{spec}", float(got[1] * 100)
def mark(row: dict, lines: list[dict], engine: list[dict]) -> dict | None:
"""새 의미 로직 = 블록 줄에 `place` · `out` · 덧줄 % 밑 · 상수 덧줄(제자리) → 블록 `rule` ·
옛 로직 = None."""
if not ms.uses(row):
return None
items, skip, heads = ms.items(row), ms.skipped(row), len(row.get("호표") or [])
for n, (item, line) in enumerate(zip(items, lines)):
if item.get("자리"):
line["place"] = dict(item["자리"])
if n in skip:
line["out"] = True
if n < heads:
continue
formula = str(item.get("식") or "")
part = PART.get(str(item.get("비목")), "expense")
try:
plain = _plain(mf.parse(formula))
except mf.FormulaError:
continue
value = _f(engine[n].get("금액")) if n < len(engine) else None
if plain and value is not None: # 이름 없는 수 식 = 고정 줄(수량 1)
line.update(kind="fixed", qty=1.0, own={part: value}, link={}, unit="식")
elif got := _pct(row, formula, n):
line.update(kind="pct", part=part, base=got[0], spec=formula, unit="%", qty=got[1])
else:
continue
line.pop("why", None)
return {k: row[k] for k in ("끝전", "조정", "끝수") if k in row}
def _plain(node: tuple) -> bool:
"""수 · 사칙 · 부호만 있는 식(이름 없음)."""
if node[0] == "num":
return True
if node[0] == "bin":
return _plain(node[2]) and _plain(node[3])
return node[0] == "neg" and _plain(node[1])