- 새 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>
220 lines
9.7 KiB
Python
220 lines
9.7 KiB
Python
"""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
|