Files
Aislo/M02_MasterTemplete/M02_San_Ilwi.py
T
eomsangdonandClaude Opus 5.5 8709dc9ffd feat(842-4): 불러오기 내 기계 — 기계 한 대 안 운전사 · 연료 · 잡품 · 소모 재료 줄을 모두 받음
- 구성 읽기 넷(EST 원파일 · EST 엑셀 · STmate 원파일 일위 구성 · STmate 엑셀) = 인력줄 · 연료줄 · 잡품줄 · 재료줄 목록(첫 줄 = 옛 단일 칸 그대로)
- 몸 = 842-3 꼴 — 둘째 운전사부터 인력 줄(LB:원문번호) · 둘째 연료 = 잡품 키 줄(우리 유가 · 율 100) · 잡품 율 = 연료 차례 짝 ·
  소모 재료(치즐 EA) = 우리 자재 하나에 이어지면 잡품 키 줄 · 못 이음 = 줄 없이 비고 「확인 필요」
- 덤: 「잡비」 = 잡품 · 첫 연료 못 이음 때 잡품 율을 비고에 · 연료 없는 전력 기계 잡품 = 전력의 %(알림 없이 버리던 것)
- 한 줄 구성 몸은 전과 같음 · 기계 두 대 조합 나누기 그대로
- 견본 1,528 구성 · 여럿 69(다산소프트 main 만 · 실무 0) · 버린 줄 0 · 시험 test_842_4 새로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-15 18:46:40 +09:00

336 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` 와 같은 값 · 줄 여럿 = 목록(842-4 `M02_San_MyMachine`)."""
from M02_MasterTemplete.M02_San_MyMachine import _crew, _fuel, _misc
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:
_crew(make, one["이름"], q)
elif g == "M" and unit in ("L", "ℓ") and q > 0:
_fuel(make, one["이름"], q)
elif g == "M" and unit == "KWH" and q > 0:
make["전력"] = q
elif g == "율" or unit.endswith("%"):
_misc(make, q)
elif g == "M" and q > 0: # 소모 재료(842-4)
make.setdefault("재료줄", []).append([one["이름"].strip(), q, one["단위"].strip()])
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