"""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