Files
Aislo/M02_MasterTemplete/M02_Hopyo_UnitLine.py
T
eomsangdonandClaude Opus 5.5 660e3318a9 feat(778-3): 단위 줄 정규화 — 단가산출 블록 · 일위대가 줄 = 노무 · 기계 · 자재 · 율 · 하위 · 값 줄(D-1 · 요구 622 · 623 · 624)
- 새 M02_Hopyo_UnitLine — 단위 줄 꼴 · 자체 검산(줄 재현 0.5 % · 줄합 = 블록 금액 덮음) · 값 줄 → 하위 가름 · 율 줄 기준꼴 · 기준줄 · 할증 풀이 · 일위대가 줄 투영(묶음 · 재현 · % 기준) · 두 어댑터 끝에 settle
- ReadEst.items — 줄 금액 · 율 · 값 줄 · 합계 · 소계 절 = 밖 · 끝 점 「28,500. ×」 · 이름 속 = · / 자재 · 「(지주)」 · 「\/㎡」 금액 꼬리 · 「직종 : P」 선언 줄 뺌 · 자재 손료율 표지 · 기계 비목마다 매개값 계수 · 절 = 제목
- Stmate — 같은 기계 코드 두 묶음 따로(울진대흥 1129 · 2119 · 울진소광 1448 단가 두 배 흠) · 비목마다 H 다르면 계수(봉화 덤프 (Cm-t1)/Cm · 영월 물탱크 ×0.722) · PR · RR 율 행(기준 표지 PA01M 꼴) · 이름 없는 율 행 = 위 라벨 줄 이름 · 기준글 · G 열 「* N'%'」 자원 = 율 줄 · 자재 손료율 · D 행 = 하위 · N 열 C = 밖
- Store.BOOK 3 — 읽기 꼴 바뀜(옛 판 HP = 다시 올림)
- 여덟 파일 불리는 블록: 줄 재현 811/836 → 836/842 · 덮음 201 → 204/206(남은 둘 = 엑셀 합이 진동로울러 노무 뺌) · 일위대가 줄 1,344/1,350
- 새 시험 test_778_3_hopyo_unit_lines

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 13:20:03 +09:00

220 lines
9.7 KiB
Python
Raw 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.
"""M02 일위대가 실무자료 단위 줄(PLAN 778-3 · D-1 · 요구 622) — 단가산출 블록 · 일위대가 줄을 같은
「세부항목 계산 단위」 꼴로 · 자체 검산. 형식(EST · STmate)을 모름 — 읽기 어댑터
(`M02_Hopyo_ReadEst.items` · `M02_Hopyo_Stmate._blocks`)가 블록 `항목` 을 단위 줄로 냄.
단위 줄 = `{묶음, 이름, 규격, 단위, 품, 밑품, 율, 인자, 계수, 단가, 금액, 밖, 기준, 기준값,
하위, 할증, 재현, 직종, 절, 절율, 코드, 어떻게, 행}`:
묶음 = L 노무 · X 기계 · M 자재 · R 율 · S 하위 · E 값
품 = 매개값(L 품 · X 시간 · M 수량 · R 율 소수 · S 수량 · E 없음) · 밑품 · 율 · 인자 = 지문 몫
계수 = X 비목마다 매개값이 다를 때 `{비목: 그 비목 매개값 ÷ 품}`(덤프 재료비 (Cm-t1)/Cm)
단가 = {노무비 · 재료비 · 경비(· 계)} · 금액 = 줄 금액(None = 엑셀에 없음)
밖 = 셈 밖(STmate N 열 C · EST 합계 · 소계 절) · 기준 · 기준값 = 율 줄 기준 표지 · 기준 금액 ·
기준글 = 라벨 「…의 N%」 앞 낱말(STmate) · 기준꼴 · 기준줄 = 기준값이 어느 줄 합인가
(`base_rows`) ·
이름없음 = 율 줄 이름 못 찾음 · 손료율 = M 「수량 × 손료율 × 단가」 꼴 표지(율 줄 아님)
하위 = S `{종류 단산|일위, 번}` · 할증 = X 이름 · 규격 「할증120%」 → 1.2
재현 = 단가 × 매개값(× 계수) ≈ 금액(0.5 % · 1 원) · 율 = 기준값 × 율 · 별도 · 값 = 참
블록 `줄합` = 밖 아닌 줄 금액 합 · `덮음` = 줄합 ≈ 블록 금액(금액 없으면 None).
일위대가 줄 → `묶음` · `재현` · % 줄 `기준`(노무비 · 재료비 · 주재료비 · 경비 = 규격 글) ·
호표 `줄합` · `덮음`.
값 줄(E · 첫 수 = 다른 블록 · 호표 1 단위 금액 ±0.51 · 100 넘음) → S 하위.
"""
from __future__ import annotations
import re
from typing import Any
COSTS = ("노무비", "재료비", "경비")
TOL = 0.005
_SURCHARGE = re.compile(r"할증\s*(\d+(?:\.\d+)?)\s*%")
_KIND = {
"노임": "L",
"기계": "X",
"자재": "M",
"별도계상": "M",
"%": "R",
"단가산출": "S",
"호표": "S",
}
def near(a: Any, b: Any, tol: float = TOL) -> bool:
if a is None or b is None:
return False
a, b = float(a), float(b)
return abs(a - b) <= max(tol * max(abs(a), abs(b)), 1.0)
def money(d: Any) -> float | None:
"""금액 · 단가 칸 → 수(계 › 비목 합 · 수면 그대로)."""
if isinstance(d, (int, float)) and not isinstance(d, bool):
return float(d)
if not isinstance(d, dict):
return None
if d.get("계") not in (None, ""):
return float(d["계"])
vals = [float(d[c]) for c in COSTS if d.get(c) not in (None, "")]
return sum(vals) if vals else None
def machine_done(one: dict[str, Any]) -> None:
"""기계 줄 마무리(두 어댑터) — 비목마다 매개값이 다르면 `계수`."""
one.pop("_순", None)
got_q, main = one.pop("_q", {}), one.get("품")
if main:
ratio = {
k: round(float(q) / main, 9)
for k, q in got_q.items()
if q is not None and abs(float(q) / main - 1) > 1e-6
}
if ratio:
one["계수"] = ratio
def _parts(price: dict[str, Any]) -> float | None:
vals = [float(price[c]) for c in COSTS if price.get(c) not in (None, "")]
return sum(vals) if vals else money(price)
def redo(one: dict[str, Any]) -> bool:
"""줄 재현 — 단가 × 매개값(× 계수) ≈ 금액."""
kind, q, got = one["묶음"], one.get("품"), one.get("금액")
if one.get("어떻게") == "별도" or (kind == "E" and q is None):
return True
if kind == "S" and q is None:
return got is not None
if kind == "R":
return bool(q is not None and near((one.get("기준값") or 0) * q, got))
price = one.get("단가") or {}
if q is None or got is None:
return False
if kind == "X":
ratio = one.get("계수") or {}
want = sum(float(price[c]) * q * ratio.get(c, 1) for c in COSTS if price.get(c))
return near(want, got)
unit = _parts(price)
return bool(unit is not None and (near(unit * q, got) or (unit == 0 and got == 0)))
def _subs(units: list[dict[str, Any]], totals: dict[tuple[str, int], float]) -> None:
"""값 줄 첫 수가 다른 블록 · 호표 1 단위 금액이면 하위."""
for one in units:
p = money(one.get("단가")) if one["묶음"] == "E" else None
if not p or p < 100:
continue
hit = next((k for k, v in totals.items() if abs(p - v) <= 0.51), None)
if hit:
got = one.get("금액")
q = round(float(got) / p, 6) if got is not None else None
one.update(묶음="S", 품=q, 하위={"종류": hit[0], "번": hit[1]}, 어떻게="QTY")
def _labor_part(one: dict[str, Any]) -> float:
"""기계 줄 노무비 몫 = 노무 단가 × 시간(× 계수)."""
price, q = (one.get("단가") or {}).get("노무비"), one.get("품")
return float(price) * q * (one.get("계수") or {}).get("노무비", 1) if price and q else 0.0
def base_rows(units: list[dict[str, Any]]) -> None:
"""율 줄 기준값 = 어느 줄 합인가 → `기준꼴` · `기준줄 [행]`(못 찾으면 안 붙임) — 차례:
바로 앞 같은 묶음 줄들(절 소계) › 앞 인력 › 앞 인력 + 기계 노무 › 앞 자재 › 앞 자재 + 율 ›
앞 줄 모두 › 블록 인력 › 블록 자재."""
live = [u for u in units if not u.get("밖") and u.get("금액")]
for i, one in enumerate(units):
if one["묶음"] != "R" or not one.get("기준값"):
continue
prev = [u for u in units[:i] if u in live]
run: list[dict[str, Any]] = []
for u in reversed(prev):
if u["묶음"] == "R" or (run and u["묶음"] != run[0]["묶음"]):
break
run.insert(0, u)
of = lambda us, k: [u for u in us if u["묶음"] in k] # noqa: E731
pools = [
("절 소계", run, None),
("인력", of(prev, "L"), None),
("노무비", of(prev, "LX"), "L+X노무"),
("자재", of(prev, "M"), None),
("자재 + 율", of(prev, "MR"), None),
("앞 줄 모두", of(prev, "LXM"), None),
("인력(블록)", of(live, "L"), None),
("자재(블록)", of(live, "M"), None),
]
for name, pool, how in pools:
total = sum(
_labor_part(u) if how and u["묶음"] == "X" else float(u["금액"]) for u in pool
)
if pool and near(total, one["기준값"]):
one["기준꼴"], one["기준줄"] = name, [u.get("행") for u in pool]
break
def check_block(block: dict[str, Any], totals: dict[tuple[str, int], float]) -> None:
units = block.get("항목") or []
own = ("단산", int(block["번"]))
_subs(units, {k: v for k, v in totals.items() if k != own})
base_rows(units)
for one in units:
if one["묶음"] == "X" and (hit := _SURCHARGE.search(f"{one['이름']} {one['규격']}")):
one["할증"] = round(float(hit.group(1)) / 100, 4)
one["재현"] = redo(one)
total = money(block.get("금액"))
lines = round(sum(float(one.get("금액") or 0) for one in units if not one.get("밖")), 4)
block["줄합"] = lines
block["덮음"] = near(lines, total) if total else None
def check_hopyo(hopyo: dict[str, Any]) -> None:
"""일위대가 줄 → 단위 줄 투영(묶음 · 재현 · % 기준) · 호표 줄합 · 덮음."""
run = dict.fromkeys(COSTS, 0.0)
main = lines = 0.0
for line in hopyo.get("줄") or []:
kind = _KIND.get(str(line.get("갈래")), "E")
q, got = line.get("수량"), money(line.get("금액"))
line["묶음"] = kind
if line.get("갈래") == "별도계상":
line["재현"] = True
elif kind == "R":
spec = re.sub(r"\s+", "", str(line.get("규격") or ""))
base = (
"주재료비"
if "주재료" in spec
else "재료비"
if "재료비" in spec
else "경비"
if spec.startswith("경비")
else "노무비"
)
line["기준"] = base
value = main if base == "주재료비" else run[base]
line["재현"] = bool(q and got is not None and near(value * float(q) / 100, got))
else:
p = money(line.get("단가"))
line["재현"] = bool(q is not None and p is not None and near(float(q) * p, got))
if kind != "R":
for c in COSTS:
run[c] += float((line.get("금액") or {}).get(c) or 0)
if line.get("갈래") == "자재":
main += float((line.get("금액") or {}).get("재료비") or 0)
lines += got or 0
total = money(hopyo.get("금액"))
hopyo["줄합"] = round(lines, 4)
hopyo["덮음"] = near(lines, total) if total else None
def settle(book: dict[str, Any]) -> dict[str, Any]:
"""읽은 책(HP 꼴) → 블록 단위 줄 하위 가름 · 재현 · 덮음 + 일위대가 줄 투영(제자리)."""
totals: dict[tuple[str, int], float] = {}
for block in book.get("단산") or []:
if (v := money(block.get("금액"))) is not None:
totals[("단산", int(block["번"]))] = v
for hopyo in book.get("호표") or []:
if (v := money(hopyo.get("금액"))) is not None:
totals[("일위", int(hopyo["번"]))] = v
for block in book.get("단산") or []:
check_block(block, totals)
for hopyo in book.get("호표") or []:
check_hopyo(hopyo)
return book