"""경쟁사 산근 참조 평가기(PLAN 793-1 · 요구서_652) — 줄 나무 + 같은 책 단가 → 줄 · 블록 금액 · 검산. STmate 셈 규칙(`_분석_649-G2` 1 장) — 줄 금액 = 단가 × (계수 + 1e-13) → 소수 1 버림 · 변수 `X = 식 =` = 소수 2 반올림 · 율 `&P..&` = 범위 비목 금액 × n % → 소수 1 버림 · `&R..&` = × n · 특수 % L% · M% = 처음부터 그 비목 누계 × n % · T% = 바로 앞 계표시 값 × 수량 · 계표시 = 아래 단 누계 · `(-=)` = 찍되 위 합에서 뺌 · `->` = 번지엔 쌓되 합에서 뺌 · 블록 끝전 두 갈래(비목 버림 / 계 버림 + 큰 나머지) · 결과금액 조정(SBM = 계를 한 비목으로) · 계약단가 = 비목 × 낙찰률 원 버림. 단가 바뀜 전파 — 단가 dict 를 바꿔 다시 셈. 옛 EST 엑셀 셈 · 엑셀 검산(`run_est` · `est_check` · `stmate_check`)은 812-1 엑셀 읽기기로 걷음(EST = `M02_Doc_NativeEst`). """ from __future__ import annotations import collections import math from typing import Any from M02_MasterTemplete import M02_San_Expr as ex COMPS = ("L", "M", "S") SPECIAL = ("L%", "M%", "S%", "T%") def _zero() -> dict[str, float]: return dict.fromkeys(COMPS, 0.0) # ── STmate ───────────────────────────────────────────────────────────── def _rate_amount(node: dict, coef: float, A: dict, T: dict, since: dict, total: dict) -> dict: rate = node["율"] area = rate["범위"] if area == "A": base = _zero() for spot in rate["번지"]: for c in COMPS: base[c] += A[spot][c] elif area == "T": base = T.get(rate["번지"][0]) or _zero() else: base = since if area == "C" else total pct = coef if rate["곱"] else coef / 100 src, dst = rate["비목"], rate["옮김"] amt = _zero() if src: amt[dst or src] = ex.fl1(base[src] * pct) elif dst: amt[dst] = ex.fl1(sum(base.values()) * pct) else: for c in COMPS: amt[c] = ex.fl1(base[c] * pct) return amt def _ref_amount( node: dict, coef: float, prices: dict, total: dict, last_tot: Any, res: dict ) -> dict: src_ = node["자원"] code = src_["코드"] if code not in prices: raise ex.ExprError(f"단가 없음 {code}") pr = prices[code] src, dst = src_["비목"], src_["옮김"] amt = _zero() unt = (pr.get("meta") or {}).get("UNT") res["단가"] = {c: pr[c] for c in COMPS} if unt in SPECIAL: dst2 = dst or {"L": "L", "M": "M", "S": "S"}.get(code[0]) if unt == "T%": b0 = last_tot if last_tot is not None else sum(total.values()) amt[dst2] = ex.fl1(b0 * coef) else: b0 = total[unt[0]] amt[dst2] = ex.fl1(b0 * coef / 100) res["기준"] = b0 res["종류"] = "특수" return amt c_eps = coef + 1e-13 if src: amt[dst or src] = ex.fl1(pr[src] * c_eps) else: for c in COMPS: if pr[c]: amt[c] = ex.fl1(pr[c] * c_eps) return amt def run_stmate(nodes: list[dict], prices: dict, dec: int = 2) -> tuple[list[dict], dict]: """줄 나무 → (줄 결과 [{종류, 값 {L, M, S}, 계수, 오류 …}], 블록 소수 합 {L, M, S}).""" V: dict[str, float] = {} A: dict[str, dict] = collections.defaultdict(_zero) # 금액번지 T: dict[str, dict] = {} # 누계번지 acc = [_zero() for _ in range(6)] # 계 단 1~5 · 0 = 전체 since = _zero() # 마지막 계표시 뒤 last_tot = None out = [] for node in nodes: kind = node["종류"] res: dict[str, Any] = {"행": node.get("행"), "종류": kind, "값": None, "오류": None} try: if kind == "계": lvl = node["단"] val = dict(acc[lvl]) res |= {"단": lvl, "제외": node["제외"], "값": val} for k in range(1, lvl + 1): acc[k] = _zero() if node["제외"]: for k in [*range(lvl + 1, 6), 0]: for c in COMPS: acc[k][c] -= val[c] if node["누계번지"]: T[node["누계번지"]] = val last_tot = sum(val.values()) since = _zero() elif kind in ("참조", "율"): if node.get("오류"): raise ex.ExprError(node["오류"]) coef = ex.value(node["식"], {"V": V, "R": 1.0}) if kind == "율": amt = _rate_amount(node, coef, A, T, since, acc[0]) else: amt = _ref_amount(node, coef, prices, acc[0], last_tot, res) res |= {"값": amt, "계수": coef, "번지": node["번지"], "빼기": node["빼기"]} addr = node["번지"] if addr and addr.startswith("A"): for c in COMPS: A[addr][c] += amt[c] if not node["빼기"]: for k in range(6): for c in COMPS: acc[k][c] += amt[c] for c in COMPS: since[c] += amt[c] elif kind == "대입": for part in node["대입"]: if part["이름"] is None: try: ex.value(part["식"], {"V": V, "R": 1.0}) except ex.ExprError: pass continue v = ex.value(part["식"], {"V": V, "R": 1.0}) V[part["이름"]] = ex.rnd(v, dec) if part["찍기"] else v except ex.ExprError as error: res["오류"] = str(error) out.append(res) return out, acc[0] def block_total(tot: dict, rule: str) -> dict[str, float]: """블록 끝전 — 비목버림(비목 원 버림 · 계 = 합) / 합버림_큰나머지(계 원 버림 · 나머지를 소수 큰 차례 경 › 재 › 노 로 1 원씩).""" c = {k: math.floor(round(tot[k], 6)) for k in COMPS} if rule == "비목버림": c["T"] = c["L"] + c["M"] + c["S"] return c total = math.floor(round(sum(tot.values()), 6)) order = sorted(COMPS, key=lambda k: (-round(tot[k] - c[k], 6), "SML".index(k))) for k in order[: total - sum(c.values())]: c[k] += 1 c["T"] = total return c def stmate_block(block: dict, nodes: list[dict], prices: dict, rule: str) -> dict[str, Any]: """블록 → {줄, 계(끝전 뒤 · 계약 전), 금액(조정 · 계약 뒤), 낙찰}.""" lines, tot = run_stmate(nodes, prices) meta = (prices.get(block["코드"]) or {}).get("meta", {}) kye = block_total(tot, rule) sbm = (meta.get("SBM") or "").strip() if meta.get("ADJ") == "T" and sbm in COMPS: kye = {"L": 0, "M": 0, "S": 0, "T": kye["T"], sbm: kye["T"]} nag = float(meta["NAG"]) if meta.get("UC") == "T" and meta.get("NAG") else None fin = dict(kye) if nag: fin = {c: math.floor(round(kye[c] * nag / 100, 6)) for c in COMPS} fin["T"] = fin["L"] + fin["M"] + fin["S"] return {"줄": lines, "합": tot, "계": kye, "금액": fin, "낙찰": nag}