Files
Aislo/M02_MasterTemplete/M02_San_Ilwi.py
T
eomsangdonandClaude Opus 5.5 43e9455688 feat(828-1): 원파일 일위대가 구성 줄 → 일위대가 길 · 중기 구성 → 내 기계 씨앗 · 낙찰률 줄 2 칸 좁힘
- ㉯ STC 일위 = HP 호표(엑셀 일위대가표 길과 같은 꼴 · 「일위대가표」 시트) → 목록 · 만들기 · 고를 재료 판(구성 줄 · 「일위대가 N」)
- ㉰ 구성표 중기 → 기계구성(엑셀 중기사용료와 같은 값 · 달러 손료 = 천원) · 같은 파일 구성 대조 같은구성 → STmate 기계 찾기
- ㉱ 낙찰률 줄 2 칸(원단가) = 구성 셈 · 일위 머리만(엑셀 일위대가표와 같음) · 산근 · 자원 = 1 칸 그대로(엑셀 단가산출근거와 같음 · 812-1 짝 0)
- 별도계상 = 자재 이름 · 규격 「별도계상」 · 「별산」(엑셀 비고 「별산…」 짝 · 828-2 대조)
- MyMachine 엑셀 중기사용료 잡품 줄 = 잡재료 · 잡재료비 이름도(원파일과 같은 값)
- 시험 test_828_1_stc_composition 새로 · test_814_1 낡은 칸 고침(원파일 호표 0 → 45 · 격자 일위대가표 시트) · test_810_1 거울 대조 글 칸 앞 빈칸 걷음

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

329 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
if kind == "M" and re.search(
r"별도계상|별산", one["이름"] + one["규격"]
): # 엑셀 비고 「별산…」 짝
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 ""
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":
continue
res = {k: ln.get(k) or "" for k in ("이름", "규격", "단위", "코드")} | {"종류": "자재"}
with mf.clues_on(saved.get("단서"), head):
link.m01("재료", res, "")
yield b, ln, res