Files
Aislo/M02_MasterTemplete/M02_San_Ilwi.py
T
eomsangdonandClaude Opus 5.5 9212219960 fix(828-1): 원파일 일위 별도계상 = 경비 · 일식 「별산」 줄도 · 판 재료에서 율 자원(단위 %) 뺌 · HP 율 줄 밑 · 대상
- 828-2 엑셀 읽기기와 5 건 136 호표 706 줄 다름 0(코드 · 갈래 · 수량 · 율 · 밑줄 · 관급 · 별도계상) — 시험 test_828_1 에 대조 더함
- 영월 S11039 검증수수료 · 소광 W 깬돌(구입별산) = 별도계상 · 봉화 「잡재료비 재료비의 %」 = 판 재료 아님

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 21:06:22 +09:00

332 lines
15 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.
"""STmate 구성표 → 일위대가 구성 줄 · 다시 셈 · 중기 구성(PLAN 828-1 · 요구 814 ㉯ ㉰ · 서버 단독).
셈 짝 = `tools/stmate-test/smt_eval.js` `evaluateComposition`(+ 시험 `resources/tester/helper_stmate_ilwi.cjs`
설정) — 닫기(C*) 뒤 PR 줄 밑수 · 대상 「전체」(4 · 4) = 앞 금액 전체 × % 만 더함(분석 813 1-3 B00038).
일위 구성 줄 꼴(원파일 · 엑셀 같은 꼴 — 읽기기 답 `일위` 블록 `줄`):
{차례, 코드(자원 · PR · RR · C1 · T1 · 빈), 갈래(M L S X B D W · 율 · 닫기 · 구분 · 글), 이름, 규격, 단위,
수량(1 차), 단가 {T L M S}?(셈에 쓴 자원 단가 · 중기 조종원 = 시간 환산), 금액 {T L M S}?(줄 금액),
율? {밑, 대상(M L S T), 곱(RR)}, 밑줄?(율 밑수에 듦), 관급?, 별도계상?(자재 · 경비 · 일식 이름 · 규격 「별도계상」 · 「별산」)}
블록 = {코드, 이름, 규격, 단위, 저장, 줄, 줄합 {T L M S}(다시 셈 · 끝전 뒤), 표지}
율 줄 이름 = 지은 글(「노무비의 3%」 · 원문 이름 칸은 못 읽음).
HP 호표(`hopyo`) = 엑셀 일위대가표 읽기(`M02_Hopyo_Stmate.read`)와 같은 꼴 — 글 · 구분 · 닫기 줄 뺌.
중기 구성(`makes`) = 엑셀 「중기사용료」(`M02_San_MyMachine._stm_sheet`)와 같은 꼴.
"""
from __future__ import annotations
import math
import re
from typing import Any
COMP = {"1": "M", "2": "L", "3": "S", "4": "T"}
WORD = {"M": "재료비", "L": "노무비", "S": "경비", "T": "전체"}
KIND = {"M", "L", "S", "X", "B", "D", "W"}
MISMATCH = "구성 다시 셈 ≠ 저장 단가"
_SBMOK = {"1": "M", "2": "L", "3": "S", "M": "M", "L": "L", "S": "S"}
_HP = {"T": "계", "L": "노무비", "M": "재료비", "S": "경비"}
def _f(v: Any) -> float:
try:
return float(v or 0)
except (TypeError, ValueError):
return 0.0
def _tr(v: float, d: int = 0) -> float:
f = 10**d
s = v * f
return math.trunc(s + (1e-9 if s >= 0 else -1e-9)) / f
def _half(v: float, d: int = 0) -> float:
f = 10**d
r = math.floor(abs(v) * f + 0.5 + 1e-9) / f
return -r if v < 0 else r
def _wage(daily: float, expr: str, digits: int = 0) -> float:
acc = daily
for op, n in re.findall(r"([*/])?(\d+(?:\.\d+)?)", re.sub(r"\s+", "", expr or "")):
acc = acc / float(n) if op == "/" else acc * float(n)
return _tr(acc, digits)
def options(parent: str, ini: dict, sbmok: str | None) -> dict[str, Any]:
"""`helper_stmate_ilwi.cjs` 설정 — 총액우선(일위 · CHBNM 0) · 차례 OPTNM · 중기 조종원 시간 환산(CONLD · RXXNM)."""
return {
"총액우선": parent[:1] == "B" and str(ini.get("CHBNM", "1")) == "0",
"차례": str(ini.get("OPTNM") or "SML").upper(),
"비목": _SBMOK.get(str(sbmok or "").strip()),
"환산": set(re.findall(r".{1,6}", str(ini.get("CONLD") or "")))
if parent[:1] == "X"
else set(),
"환산식": str(ini.get("RXXNM") or ""),
"달러": _f(ini.get("EXLNM1")),
}
def _finish(raw: dict[str, float], opt: dict) -> dict[str, float]:
out = {k: _tr(raw[k]) for k in "MLS"}
if opt["총액우선"]:
order = list(opt["차례"])
frac = {k: _half(raw[k] - out[k], 6) for k in order}
rest = _tr(raw["T"]) - sum(out.values())
while rest > 0:
pick = None
for k in order:
if frac[k] < 1 and (pick is None or frac[k] > frac[pick]):
pick = k
if pick is None:
break
out[pick] += 1
frac[pick] += 1
rest -= 1
if to := opt["비목"]:
out = {**dict.fromkeys("MLS", 0.0), to: sum(out.values())}
return {**out, "T": out["M"] + out["L"] + out["S"]}
def _rate_line(row: dict, q: float) -> dict[str, Any]:
base, to = COMP.get(row.get("PRIN") or ""), COMP.get(row.get("PROUT") or "")
out: dict[str, Any] = {"이름": "", "규격": "", "단위": "%"}
if base and to:
mul = row.get("CODE") == "RR"
out["이름"] = f"{WORD[base]}의 {q:g}{'배' if mul else '%'}"
out["규격"] = f"{WORD[base]}의 {'배' if mul else '%'}"
out["단위"] = "" if mul else "%"
out["율"] = {"밑": base, "대상": to, "곱": mul}
return out
def lines(rows: list[dict], res: dict[str, dict], opt: dict) -> tuple[list[dict], dict[str, float]]:
"""구성 행(부모 하나) → (구성 줄 꼴, 다시 셈 {T L M S})."""
s = {"M": 0.0, "L": 0.0, "S": 0.0, "T": 0.0}
base = {"M": 0.0, "L": 0.0, "S": 0.0}
closed, out = False, []
for i, row in enumerate(rows):
code, q = row.get("CODE") or "", _f(row.get("QTY1"))
head = res.get(code) or {}
if code in ("PR", "RR"):
kind = "율"
elif re.fullmatch(r"C\d", code):
kind, closed = "닫기", True
elif re.fullmatch(r"T\d", code):
kind = "구분"
else:
kind = code[:1] if code[:1] in KIND and head else "글"
one: dict[str, Any] = {"차례": i, "코드": code, "갈래": kind, "수량": q}
one |= {k: str(head.get(k) or "") for k in ("이름", "규격", "단위")}
if kind == "율":
one |= _rate_line(row, q)
if str(row.get("PRPT") or "0") not in ("0", ""):
one["밑줄"] = True
if row.get("KWNGP") == "1":
one["관급"] = True
apart = re.search(r"별도계상|별산", one["이름"] + one["규격"])
if kind in ("M", "S", "W") and apart: # 엑셀 비고 「별산…」 짝
one["별도계상"] = True
out.append(one)
before = dict(s)
if kind == "율":
rate = one.get("율")
if not rate:
continue
if closed and rate["밑"] == rate["대상"] == "T": # 닫기 뒤 전체 % (B00038)
for k in "MLST":
s[k] = _tr(s[k] * q / 100, 1)
else:
bv = sum(base.values()) if rate["밑"] == "T" else base[rate["밑"]]
v = _tr(bv * q, 1) if rate["곱"] else _tr(bv * q / 100, 1)
s["M" if rate["대상"] == "T" else rate["대상"]] += v
s["T"] += v
one["단가"] = {"T": bv}
one["금액"] = {k: s[k] - before[k] for k in "TLMS"}
continue
if kind not in KIND:
continue
saved = (
head.get("원단가") or head.get("저장") or {}
) # 낙찰률 줄 = 2 칸(smt_engine costSlot)
unit = one["단위"].strip()
own = "L" if code[0] == "L" else "S" if code[0] == "S" else "M"
pct = re.fullmatch(r"([MLST])%", unit)
if pct and closed:
for k in "MLST":
s[k] = _tr(s[k] * q / 100, 1)
elif pct:
bv = s["M"] + s["L"] + s["S"] if pct.group(1) == "T" else s[pct.group(1)]
v = _tr(bv * q / 100, 1)
s[own] += v
s["T"] += v
one["단가"] = {"T": bv}
else:
p = {k: _f(saved.get(k)) for k in "MLS"}
if code in opt["환산"]:
p = {"M": 0.0, "L": _wage(p["L"], opt["환산식"]), "S": 0.0}
if unit == "$": # 달러 → 천원(엑셀 「중기사용료」 값 · 원 미만 버림)
p = {k: _tr(v * opt["달러"] / 1000) for k, v in p.items()}
elif unit in ("€", "¥", "¥"):
p = dict.fromkeys("MLS", 0.0)
one["단가"] = {"T": p["M"] + p["L"] + p["S"], **p}
s["T"] += _tr(q * one["단가"]["T"], 1)
for k in "MLS":
a = _tr(q * p[k], 1)
s[k] += a
if one.get("밑줄"):
base[k] += a
one["금액"] = {k: round(s[k] - before[k], 6) for k in "TLMS"}
return out, _finish(s, opt)
def same(got: dict[str, float], saved: dict | None) -> bool:
return all(abs(got[k] - _f((saved or {}).get(k))) < 0.5 for k in "MLS")
# ── 중기 구성(㉰) ─────────────────────────────────────────────────────
def make_of(name: str, spec: str, rows: list[dict]) -> dict[str, Any]:
"""중기 구성 줄 → `M02_San_MyMachine` 구성 꼴(손료 계수 × 10⁴ · 가격 천원 × 1000 · 조종원 인 · 주연료 ℓ ·
잡재료율 % · 전력 kWh) — 엑셀 「중기사용료」 `_stm_sheet` 와 같은 값."""
make: dict[str, Any] = {"이름": name, "규격": spec, "손료": []}
for one in rows:
g, q, unit = one["갈래"], one["수량"], one["단위"].strip().upper()
if g == "S" and unit in ("천원", "$") and q and (one.get("단가") or {}).get("S"):
make["손료"].append([q * 10_000, one["단가"]["S"] * 1000, False])
elif g == "L" and q > 0 and not make.get("조종원"):
make["조종원"], make["조종원직종"] = q, one["이름"].strip()
elif g == "M" and unit in ("L", "ℓ") and q > 0 and make.get("주연료") is None:
make["주연료종류"], make["주연료"] = one["이름"].strip(), q
elif g == "M" and unit == "KWH" and q > 0:
make["전력"] = q
elif (g == "율" or unit.endswith("%")) and make.get("잡재료율") is None:
make["잡재료율"] = q
return make
def peer_of(make: dict) -> dict[str, Any]:
"""같은 파일 구성 대조 꼴(`M02_Machine_Find.peers` — 손료 [[계수, 가격]] · 주연료 · 조종원)."""
loss = [x[:2] for x in make["손료"]]
return {"손료": loss, "주연료": make.get("주연료"), "조종원": bool(make.get("조종원"))}
def compose(
rows: list[dict], res: dict[str, dict], ini: dict, sbm: dict
) -> tuple[dict[str, dict], dict[str, dict], list[str]]:
"""구성 행 → ({일위 코드: {줄, 줄합, 표지}}, {중기 코드 · 「이름|규격」: 구성}, 파일 표지) ·
다시 셈 ≠ 저장(값 있는 호표) = 호표 표지(일위) · 파일 표지(중기) — 가져오기는 됨."""
from M02_MasterTemplete.M02_San_MyMachine import key_of
groups: dict[str, list[dict]] = {}
for r in rows:
groups.setdefault(r.get("BDCODE") or "", []).append(r)
blocks, makes, marks = {}, {}, []
for parent, group in groups.items():
head = res.get(parent)
if parent[:1] not in ("B", "X") or not head:
continue
got, tot = lines(group, res, options(parent, ini, sbm.get(parent)))
saved = head.get("원단가") or head.get("저장") or {}
ok = not _f(saved.get("T")) or same(tot, saved)
if parent[0] == "B":
blocks[parent] = {"줄": got, "줄합": tot, "표지": [] if ok else [MISMATCH]}
continue
if not ok:
marks.append(f"중기 {parent} {MISMATCH}")
make = make_of(str(head.get("이름") or ""), str(head.get("규격") or ""), got)
if make["손료"]:
makes[parent] = makes[key_of(make["이름"], make["규격"])] = make
return blocks, makes, marks
# ── HP 호표(일위대가 불러오기 길) ───────────────────────────────────────
def _hp(m: dict | None) -> dict[str, Any]:
return {_HP[k]: (round(v, 6) if isinstance(v, float) else v) for k, v in (m or {}).items()}
def hopyo(blocks: list[dict]) -> list[dict[str, Any]]:
"""일위 블록(줄 있음) → HP 호표 `{번, 코드, 이름, 규격, 단위, 금액, 줄}`(번 = 코드 숫자 · 줄 = 자원 · 율 줄)."""
from M02_MasterTemplete import M02_Hopyo_Excel as he
out = []
for b in blocks:
if not b.get("줄"):
continue
rows = []
for one in b["줄"]:
if one["갈래"] in ("글", "구분", "닫기"):
continue
code = one["코드"]
line = {"차례": len(rows), **{k: one[k] for k in ("이름", "규격", "수량", "단위")}}
line |= {"단가": _hp(one.get("단가")), "금액": _hp(one.get("금액")), "코드": code}
line["비고"] = "별산" if one.get("별도계상") else ""
if rate := one.get(
"율"
): # 숨은 칸 율 밑 · 대상(828-2 `M02_Hopyo_Fixed._ratio` 와 같은 글)
line["율"] = {k: WORD[rate[k]] for k in ("밑", "대상")}
for k in ("계", "노무비", "재료비", "경비"):
line["단가"].setdefault(k, 0)
line["금액"].setdefault(k, 0)
line["갈래"] = he.kind_of(line)
line["참조"] = (
{"종류": "단산", "번": int(code[1:]), "코드": code}
if code[:1] == "D" and code[1:].isdigit()
else {"종류": "일위", "번": int(code[1:]), "코드": code}
if code[:1] == "B" and code[1:].isdigit()
else None
)
rows.append(line)
head = {"번": int(b["코드"][1:]), "코드": b["코드"]}
head |= {k: str(b.get(k) or "") for k in ("이름", "규격", "단위")}
out.append({**head, "금액": _hp(b.get("저장")), "줄": rows})
return out
SHEET = "일위대가표"
_COLS = "ABCDEFGHI"
_HEAD = ("품명", "규격", "수량", "단위", "재료비", "노무비", "경비", "합계", "코드")
def sheet(hps: list[dict], sid: str) -> dict[str, Any]:
"""HP 호표(행 있음) → 격자 「일위대가표」 시트(머리 줄 = 「제N호표」 · 금액 = 저장 · 줄 금액 = 다시 셈)."""
cells: dict[str, dict] = {f"{c}1": {"값": h} for c, h in zip(_COLS, _HEAD)}
def put(at: int, vals: list) -> None:
for c, v in zip(_COLS, vals):
if v not in (None, ""):
cells[f"{c}{at}"] = {"값": v}
for h in hps:
money = [h["금액"].get(k) for k in ("재료비", "노무비", "경비", "계")]
put(
h["행"][0],
[f"제{h['번']}호표 {h['이름']}", h["규격"], None, h["단위"], *money, h["코드"]],
)
for ln in h["줄"]:
money = [ln["금액"].get(k) for k in ("재료비", "노무비", "경비", "계")]
put(ln["행"], [ln["이름"], ln["규격"], ln["수량"], ln["단위"], *money, ln["코드"]])
widths = (36, 22, 10, 6, 12, 12, 12, 12, 10)
return {
"id": sid,
"이름": SHEET,
"칸": cells,
"틀고정": {"행": 1},
"열": {c: {"폭": w} for c, w in zip(_COLS, widths)},
}
def materials(saved: dict):
"""일위 구성 자재 줄(M) → (블록, 줄, 자원 {이름 · 규격 · 단위 · 코드 · 재료찾기?}) — 재료 찾기 = 산근 줄과 같은
길(827-2 `M02_San_Link.m01` · 파일 단서 · 머리 = 부모 일위 이름 규격) · 유가 · 환율 줄 = 재료찾기 없음."""
from M02_MasterTemplete import M02_San_Link as link
from M02_MasterTemplete import M02_San_MatFind as mf
for b in saved.get("일위") or []:
head = f"{b.get('이름') or ''} {b.get('규격') or ''}"
for ln in b.get("줄") or []:
if ln.get("갈래") != "M" or str(ln.get("단위") or "").strip().endswith("%"):
continue # 율 자원(「잡재료비 재료비의 %」) = 재료 아님
res = {k: ln.get(k) or "" for k in ("이름", "규격", "단위", "코드")} | {"종류": "자재"}
with mf.clues_on(saved.get("단서"), head):
link.m01("재료", res, "")
yield b, ln, res