- 새 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>
136 lines
6.1 KiB
Python
136 lines
6.1 KiB
Python
"""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])
|