- 828-2 엑셀 읽기기와 5 건 136 호표 706 줄 다름 0(코드 · 갈래 · 수량 · 율 · 밑줄 · 관급 · 별도계상) — 시험 test_828_1 에 대조 더함 - 영월 S11039 검증수수료 · 소광 W 깬돌(구입별산) = 별도계상 · 봉화 「잡재료비 재료비의 %」 = 판 재료 아님 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
332 lines
15 KiB
Python
332 lines
15 KiB
Python
"""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
|