Merge branch 'sub_laptop_2' of C:/Aislo_wt/sub_laptop_2 into main_laptop_1

This commit is contained in:
2026-11-15 19:04:20 +09:00
5 changed files with 308 additions and 42 deletions
+11 -7
View File
@@ -186,20 +186,24 @@ def same(got: dict[str, float], saved: dict | None) -> bool:
# ── 중기 구성(㉰) ─────────────────────────────────────────────────────
def make_of(name: str, spec: str, rows: list[dict]) -> dict[str, Any]:
"""중기 구성 줄 → `M02_San_MyMachine` 구성 꼴(손료 계수 × 10⁴ · 가격 천원 × 1000 · 조종원 인 · 주연료 ℓ ·
잡재료율 % · 전력 kWh) — 엑셀 「중기사용료」 `_stm_sheet` 와 같은 값."""
잡재료율 % · 전력 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 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 == "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("%")) and make.get("잡재료율") is None:
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
+135 -33
View File
@@ -16,6 +16,10 @@
`내기계` 키(셈 = 그 파일 값 · [우리 단가로] = 내 기계 시간당 · `M02_Sangeun_Imported.row_of`) ·
원본 `내기계` [{키, 이름, 규격, 새로}] · 개인 층 아님 = 안 만듦(원본 `내기계예정` [{이름, 규격}] 만).
금액(엑셀 단가 그대로)은 줄 값을 안 건드려 그대로.
기계 한 대 안 여러 줄(842-4) = 구성에 `인력줄` [[직종 글, 인]] · `연료줄` [[종류, ℓ]] · `잡품줄` [율 %] · `재료줄` [[이름, 수량, 단위]]
(읽은 차례 · 첫 줄 = 옛 단일 칸 그대로) → 몸 값 = `master_machine_crew` 꼴 `인력` · `잡품` 목록(옛 조종원 · 잡재료율 칸 없음) ·
둘째 연료부터 · 소모 재료 = 잡품 키 줄(우리 유가 · 자재 하나에 이어질 때 · 율 100) · 잡품 율 = 연료 줄 차례로 짝 ·
못 이은 직종 · 연료 · 재료 · 짝 없는 율 = 줄 안 만들고 비고 「확인 필요」(지어내지 않음 · 버린 줄 0).
"""
from __future__ import annotations
@@ -28,6 +32,7 @@ from M02_MasterTemplete.M02_Machine_Find import NONE, norm
NOTE = "불러오기 자동 생성 — {file} · 값 확인 필요"
CHECK = "확인 필요"
COST = {"L": "노무비", "M": "재료비", "S": "경비"}
FUELS = ("경유", "휘발유", "실내등유")
def key_of(name: Any, spec: Any) -> str:
@@ -48,11 +53,31 @@ def _blank(name: str = "", spec: str = "") -> dict[str, Any]:
def _fuel(make: dict, kind: Any, litre: Any) -> None:
"""주연료 종류 · ℓ(처음 것) — 종류 「전력」 = 전력 kWh 칸."""
"""주연료 종류 · ℓ(처음 것 · 모두 `연료줄`) — 종류 「전력」 = 전력 kWh 칸."""
if re.sub(r"\s", "", str(kind or "")) == "전력" and (_num(litre) or 0) > 0:
make["전력"] = _num(litre)
elif make.get("주연료") is None and (_num(litre) or 0) > 0:
make["주연료종류"], make["주연료"] = str(kind or "").strip(), _num(litre)
elif (_num(litre) or 0) > 0:
make.setdefault("연료줄", []).append([str(kind or "").strip(), _num(litre)])
if make.get("주연료") is None:
make["주연료종류"], make["주연료"] = str(kind or "").strip(), _num(litre)
def _crew(make: dict, job: Any, n: Any) -> None:
"""운전사 줄 — 모두 `인력줄` · 첫 줄 = 조종원 · 조종원직종(842-4)."""
make.setdefault("인력줄", []).append([str(job).strip(), _num(n)])
if not make.get("조종원"):
make["조종원"], make["조종원직종"] = _num(n), str(job).strip()
def _misc(make: dict, rate: Any) -> None:
"""잡품 율 — 모두 `잡품줄` · 첫 율 = 잡재료율(842-4)."""
if _num(rate) is not None:
make.setdefault("잡품줄", []).append(_num(rate))
if make.get("잡재료율") is None:
make["잡재료율"] = _num(rate)
MISC_WORDS = ("잡품", "잡유", "잡비", "잡재료", "잡재료비")
# ── 구성 읽기 ─────────────────────────────────────────────────────────
@@ -78,14 +103,16 @@ def est_makes(t: dict[str, list[dict]]) -> dict[str, dict]:
cur["손료"].append([gn, r.get("TU"), unit == "$"])
elif i == -2 and word == "주연료":
_fuel(cur, r.get("Size"), gn)
elif i == -2 and word in ("잡품", "잡유") and cur.get("잡재료율") is None:
cur["잡재료율"] = gn
elif i == -2 and word in MISC_WORDS:
_misc(cur, gn)
elif i == -2 and word == "전력":
cur["전력"] = gn
elif i == -2 and unit == "ℓ":
_fuel(cur, r["Name"], gn)
elif i == -1 and (gn or 0) > 0 and not cur.get("조종원"):
cur["조종원"], cur["조종원직종"] = gn, str(r["Name"]).strip()
elif i == -2 and (gn or 0) > 0: # 소모 재료(치즐 EA 등 · 842-4)
cur.setdefault("재료줄", []).append([str(r["Name"]).strip(), gn, unit])
elif i == -1 and (gn or 0) > 0:
_crew(cur, r["Name"], gn)
return {k: v for k, v in out.items() if v["손료"]}
@@ -109,21 +136,22 @@ def _est_sheet(rows: list[list]) -> dict[str, dict]:
cur["손료"].append([_num(v[at]), _num(v[1]), mark == "$×"])
elif word == "주연료" and len(v) > 2:
_fuel(cur, v[1], v[2])
elif word in ("잡품", "잡유") and len(v) > 2 and cur.get("잡재료율") is None:
cur["잡재료율"] = _num(v[2])
elif word in MISC_WORDS and len(v) > 2:
_misc(cur, v[2])
elif word == "전력" and len(v) > 2:
cur["전력"] = _num(v[1]) if _num(v[1]) is not None else _num(v[2])
elif len(v) > 2 and v[2] == "인" and (_num(v[1]) or 0) > 0 and not cur.get("조종원"):
cur["조종원"], cur["조종원직종"] = _num(v[1]), str(v[0]).strip()
elif len(v) > 2 and v[2] == "인" and (_num(v[1]) or 0) > 0:
_crew(cur, v[0], v[1])
elif len(v) > 3 and str(v[3]).upper() in ("ℓ", "L") and (_num(v[2]) or 0) > 0:
_fuel(cur, v[0], v[2])
return {k: m for k, m in out.items() if m["손료"] and None not in m["손료"][0][:2]}
def _stm_sheet(rows: list[list]) -> dict[str, dict]:
"""STmate 엑셀 「중기사용료」 — 「제 N 호표」 뒤 머리 [이름, 규격, 시간, 코드] · 손료 [이름, 규격, 계수(천원당),
「천원」, 가격 천원 …] · 조종원 [직종, 인, 「인」 …] · 연료 [이름, 등급, ℓ, 「L」 …] · 잡품 [잡품, 주연료의%, % …] ·
「합 계」 끝 → {코드 · 이름|규격: 구성}(손료계수 = 계수 × 10⁴ · 가격 = 천원 × 1000 원)."""
"""STmate 엑셀 「중기사용료」 — 칸 차례 [명칭, 규격, 수량, 단위, 단가 …](첫 글 칸부터 · 빈 규격도 칸 자리 지킴 ·
843-2 — 규격 빈 머리 · 손료 줄을 못 읽던 것) · 「제 N 호표」 뒤 머리(단위 「시간」 · 끝 칸 코드 X…) ·
손료(단위 「천원」 · 수량 = 계수(천원당) · 단가 = 가격 천원) · 조종원(「인」) · 연료(「L」) · 전력(「kWh」) ·
잡품(명칭 잡품 · 수량 = %) · 「합 계」 끝 → {코드 · 이름|규격: 구성}(손료계수 = 계수 × 10⁴ · 가격 = 천원 × 1000 원)."""
out: dict[str, dict] = {}
cur = None
for row in rows:
@@ -137,26 +165,24 @@ def _stm_sheet(rows: list[list]) -> dict[str, dict]:
if first == "합계":
cur = None
continue
if (
not cur
and len(v) >= 4
and str(v[2]) in ("시간", "hr")
and re.fullmatch(r"X\d+", str(v[-1]))
):
cur.update(_blank(str(v[0]), str(v[1])))
out[str(v[-1])] = out[key_of(v[0], v[1])] = cur
at = next(i for i, c in enumerate(row) if c not in (None, ""))
name, spec, qty, unit, price = (*row[at : at + 5], *[None] * 5)[:5]
spec, qty, price, unit = spec or "", _num(qty), _num(price), str(unit or "").strip()
if not cur and unit in ("시간", "hr") and re.fullmatch(r"X\d+", str(v[-1])):
cur.update(_blank(str(name), str(spec)))
out[str(v[-1])] = out[key_of(name, spec)] = cur
elif not cur:
continue
elif len(v) > 4 and str(v[3]) == "천원" and _num(v[2]) and _num(v[4]):
cur["손료"].append([_num(v[2]) * 10_000, _num(v[4]) * 1000, False])
elif len(v) > 2 and str(v[2]) == "인" and (_num(v[1]) or 0) > 0 and not cur.get("조종원"):
cur["조종원"], cur["조종원직종"] = _num(v[1]), str(v[0]).strip()
elif first in ("잡품", "잡재료", "잡재료비") and len(v) > 2 and cur.get("잡재료율") is None:
cur["잡재료율"] = _num(v[2])
elif len(v) > 3 and str(v[3]).upper() in ("L", "ℓ"):
_fuel(cur, v[0], v[2])
elif len(v) > 3 and str(v[3]).upper() == "KWH":
cur["전력"] = _num(v[2])
elif unit == "천원" and qty and price:
cur["손료"].append([qty * 10_000, price * 1000, False])
elif unit == "인" and (qty or 0) > 0:
_crew(cur, name, qty)
elif first in MISC_WORDS and qty is not None:
_misc(cur, qty)
elif unit.upper() in ("L", "ℓ"):
_fuel(cur, name, qty)
elif unit.upper() == "KWH":
cur["전력"] = qty
return {k: m for k, m in out.items() if m.get("손료")}
@@ -206,7 +232,10 @@ def _kinds(make: dict, notes: list[str]) -> dict[str, Any]:
out |= {"주연료": litre, "주연료종류": ours}
out |= {"잡재료율": make["잡재료율"]} if make.get("잡재료율") else {}
else:
notes.append(f"주연료 「{fuel}」 {litre:g}ℓ — 우리 유가에 못 이음({CHECK})")
rate = (
f"(잡품 {make['잡재료율']:g}%)" if make.get("잡재료율") else ""
) # 842-4 율도 적음
notes.append(f"주연료 「{fuel}」 {litre:g}ℓ{rate} — 우리 유가에 못 이음({CHECK})")
if make.get("조종원"):
job = norm(make.get("조종원직종"))
hit = [k for k, r in (index.numbers.get("LB") or {}).items() if norm(r.get("이름")) == job]
@@ -218,9 +247,82 @@ def _kinds(make: dict, notes: list[str]) -> dict[str, Any]:
)
if make.get("전력"):
out["전력"] = make["전력"]
if not litre and make.get("잡재료율"): # 연료 없는 전력 기계 잡품 = 전력의 %(842-4)
out["잡재료율"] = make["잡재료율"]
crew, misc = _extra(make, out, index, notes)
if crew or misc: # 둘째 줄부터(842-4) — 첫 줄 몸은 옛 칸 그대로 바꿈 + 덧줄
import master_machine_crew as mcrew
out = mcrew.from_old(out)
out[mcrew.CREW] = [*out.get(mcrew.CREW, []), *crew]
out[mcrew.MISC] = [*out.get(mcrew.MISC, []), *misc]
return out
def _job(index: Any, name: Any) -> str | None:
hit = [
k for k, r in (index.numbers.get("LB") or {}).items() if norm(r.get("이름")) == norm(name)
]
return hit[0] if len(hit) == 1 else None
def _oil(index: Any, fuel: Any) -> dict | None:
t = re.sub(r"\s", "", str(fuel or ""))
ours = next((n for n in FUELS if n == t), None) or next(
(n for n in FUELS if t.startswith(n)), None
)
return (index.numbers.get("MO") or {}).get(f"{ours}:전국평균") if ours else None
def _goods(name: str, unit: str) -> dict | None:
"""소모 재료 → 우리 자재 하나(이름 표기 · 단위 같음) · 여럿 · 없음 = None."""
from M01_MasterData import M01_MasterData_Store as m01
q = re.sub(r"\(.*", "", re.sub(r"\s", "", name))
hit = [
x
for x in m01.pick("price", q, 200)["items"]
if norm(f"{x['이름']}{x.get('규격') or ''}") == norm(name)
and norm(x.get("단위")) == norm(unit)
]
return hit[0] if len(hit) == 1 else None
def _extra(make: dict, out: dict, index: Any, notes: list[str]) -> tuple[list, list]:
"""기계 한 대 안 둘째 줄부터(842-4) → (인력 줄, 잡품 줄) — 못 이음 = 줄 없이 비고 「확인 필요」."""
crew, misc = [], []
for job, n in (make.get("인력줄") or [])[1:]:
code = _job(index, job)
if code:
crew.append({"키": f"LB:{code}", "인": n, "이름": job})
else:
notes.append(f"인력 「{job}」 {n:g}인 — 우리 직종에 못 이음({CHECK})")
fuels = make.get("연료줄") or []
for fuel, litre in fuels[1:]:
row = _oil(index, fuel)
if row:
misc.append({"키": row["키"], "이름": str(fuel), "수량": litre, "율": 100})
else:
notes.append(f"연료 「{fuel}」 {litre:g}ℓ — 우리 유가에 못 이음({CHECK})")
for i, rate in enumerate((make.get("잡품줄") or [])[1:], 1):
row = _oil(index, fuels[i][0]) if i < len(fuels) else None
if row:
misc.append(
{"키": row["키"], "이름": f"{fuels[i][0]} 잡품", "수량": fuels[i][1], "율": rate}
)
elif out.get("주연료"): # 짝 연료 없음 = 주연료의 %
misc.append({"줄": "주연료", "율": rate})
else:
notes.append(f"잡품 {rate:g}% — 이을 연료 없음({CHECK})")
for name, qty, unit in make.get("재료줄") or []:
got = _goods(name, unit)
if got:
misc.append({"키": got["ref"], "이름": name, "수량": qty, "율": 100})
else:
notes.append(f"재료 「{name}」 {qty:g}{unit} — 우리 자재에 못 이음({CHECK})")
return crew, misc
def body(make: dict | None, n: int, line: dict, file: str) -> dict | None:
"""내 기계 몸(`M02_MyMachine.create`) — 구성 있음 = 바로 · 없음 = 직접(조합 조각 = 안 만듦)."""
from M01_MasterData import M01_MasterData_Store as m01
+4 -1
View File
@@ -15,6 +15,7 @@ from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from common_util.common_util_auth import verify_session
from M01_MasterData import M01_MasterData_Router as m01_router
from M01_MasterData import M01_MasterData_Store as store
from M02_MasterTemplete import M02_Logic_Lib_Read as lib
@@ -78,7 +79,9 @@ def test_요소_표_밖_파일은_막음(user: TestClient) -> None:
def test_저장_안_한_줄_셈_쓰기_없음(user: TestClient) -> None:
before = _prints()
saved = user.post(f"{BASE}/calc", json={"key": KEY, "inputs": {}}).json()
row = store.logic(KEY)["logic"]
row = m01_router._num(
store.logic(KEY)["logic"]
) # 화면이 받는 꼴(M01 /logic = _num · 794-2b 「개정」 수 칸)
same = user.post(f"{BASE}/calc", json={"key": KEY, "inputs": {}, "row": row}).json()
line = row["호표"][0]
row2 = {**row, "호표": [{**line, "수량": f"({line['수량']}) * 2"}, *row["호표"][1:]]}
@@ -262,7 +262,7 @@ def test_machines_equal_excel() -> None:
heads = [rows[i + 1] for i, r in enumerate(rows) if re.fullmatch(r"제\d+호표", _flat(r[0]))]
assert all(mm.key_of(h[0], h[1]) in mine for h in heads), xt # 엑셀 읽기기가 못 읽는 머리도
m += len(heads)
assert (n, m) == (100, 118) # 엑셀 읽기기 100(울진대흥 머리 코드 없음 9 만) · 엑셀 호표 118
assert (n, m) == (118, 118) # 843-2 규격 빈 머리 · 손료 줄도 칸 자리로 읽음 = 엑셀 호표 118 모두
def test_my_machine_seed_and_peers() -> None:
@@ -0,0 +1,157 @@
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""842-4 지킴 — 불러오기 내 기계 = 기계 한 대 안 여러 줄(운전사 · 연료 · 잡품 · 소모 재료)을 모두 받음(임시 storage).
① 구성 읽기 = 줄 목록(EST main — 크러셔 운전사 둘 · 펌프 준설선 넷 · 표층재생기 연료 둘 · 잡품 둘 · 대형브레이카 치즐 EA)
② 몸 = `master_machine_crew` 꼴 인력 · 잡품 줄 · 못 이음 = 비고 「확인 필요」 — main 구성 전부 버린 줄 0
③ 층에 만든 내 기계 시간당 노무비 = 줄마다 우리 노임 × 인 × 5/24 의 합(둘째 줄부터도 셈에 들어감)
④ 한 줄 구성은 전과 같은 몸(옛 칸 그대로 · 저장 때 목록으로 바뀜)
⑤ STmate 엑셀 「중기사용료」 규격 빈 칸(머리 · 손료 줄) = 칸 자리로 읽음(843-2) — 울진대흥 호표 25 모두 · .STC 구성과 같음
"""
from __future__ import annotations
from functools import lru_cache
import pytest
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Machine_Hourly as mh
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_MyMachine as mym
from M02_MasterTemplete import M02_San_MyMachine as mm
from resources.tester.test_821_4_import_my_machine import GEN, _line, _main
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
CRUSHER = ("크러셔(이동식,발전기)", "150Ton")
DREDGE = ("펌프 준설선", "224kW")
RECYCLER = ("현장가열 표층재생기", "479kW")
BREAKER = ("대형브레이카", "0.7㎥경암")
@lru_cache(maxsize=1)
def _makes() -> dict:
return {mm.key_of(m["이름"], m["규격"]): m for m in _main()["기계구성"].values()}
def _make(want: tuple[str, str]) -> dict:
return _makes()[mm.key_of(*want)]
def _body(want: tuple[str, str]) -> dict:
return mm.body(_make(want), 0, _line(*want), "main.estx")
def _lb(code: str) -> float:
return float(m01.loaded()[1].numbers["LB"][code]["값"])
# ① 구성 읽기 ──────────────────────────────────────────────────────
def test_lines_read() -> None:
assert [j for j, _ in _make(CRUSHER)["인력줄"]] == ["건설기계운전사", "일반기계운전사"]
assert [n for _, n in _make(DREDGE)["인력줄"]] == [1.0, 2.0, 2.0, 3.0]
assert _make(RECYCLER)["연료줄"] == [["경유", 73.7], ["휘발유", 54.5]]
assert _make(RECYCLER)["잡품줄"] == [20.0, 20.0]
assert _make(BREAKER)["재료줄"] == [["치즐(0.7㎥)", 0.03, "EA"]]
assert _make(CRUSHER)["조종원직종"] == "건설기계운전사" # 첫 줄 = 옛 단일 칸 그대로
# ② 몸 ────────────────────────────────────────────────────────────
def test_body_lists() -> None:
crew = _body(CRUSHER)["값"]["인력"]
assert [r["키"] for r in crew] == ["LB:1048", "LB:1050"] and [r["인"] for r in crew] == [1, 1]
dredge = _body(DREDGE)
assert [r["키"] for r in dredge["값"]["인력"]] == ["LB:1052", "LB:1053", "LB:1054", "LB:1055"]
assert (
"선박용경유" in dredge["비고"]
and "잡품 36%" in dredge["비고"]
and mm.CHECK in dredge["비고"]
)
misc = _body(RECYCLER)["값"]["잡품"]
oil = m01.loaded()[1].numbers["MO"]["휘발유:전국평균"]["키"]
assert misc == [
{"줄": "주연료", "율": 20.0},
{"키": oil, "이름": "휘발유", "수량": 54.5, "율": 100},
{"키": oil, "이름": "휘발유 잡품", "수량": 54.5, "율": 20.0},
]
chisel = _body(BREAKER)["값"]["잡품"]
assert len(chisel) == 1 and chisel[0]["수량"] == 0.03 and chisel[0]["율"] == 100
assert m01.loaded()[1].get(chisel[0]["키"])["이름"].replace(" ", "") == "치즐(0.7㎥)"
def test_no_line_dropped() -> None:
"""main 구성 전부 — 원천 줄 수 = 받은 줄 + 비고 「확인 필요」(버린 줄 0)."""
for m in _makes().values():
b = mm.body(m, 0, _line(m["이름"], m["규격"]), "main.estx")
if b is None or b["방식"] != "바로":
continue
v, notes = (
b["값"],
[x for x in b["비고"].split(" · ") if mm.CHECK in x and x != "값 확인 필요"],
)
src = sum(len(m.get(k) or []) for k in ("인력줄", "연료줄", "재료줄"))
src += len([r for r in m.get("잡품줄") or [] if r]) # 0 % = 셈할 것 없음
crew = len(v["인력"]) if "인력" in v else int(bool(v.get("조종원")))
kept = crew + int(bool(v.get("주연료"))) + int(bool(v.get("잡재료율")))
kept += len(v.get("잡품") or [])
lost = sum(1 + ("(잡품" in x) for x in notes)
assert kept + lost == src, (m["이름"], m["규격"], kept, lost, src)
# ③ 셈 ────────────────────────────────────────────────────────────
def test_hourly_counts_every_crew(world) -> None:
me = world["personal"]
me.mkdir(parents=True, exist_ok=True)
with store.at(me):
for want, codes in (
(CRUSHER, ("1048", "1050")),
(DREDGE, ("1052", "1053", "1054", "1055")),
):
body = _body(want)
if want == DREDGE: # 선박용경유 못 이음 = 연료 없음 — 손료 · 인력만 셈
assert "주연료" not in body["값"]
key = mym.create(body)["줄"]["키"]
row = next(r for r in mym.rows() if r["키"] == key)
assert [r["키"] for r in row["값"]["인력"]] == [f"LB:{c}" for c in codes]
hour = mh.hourly(key)
assert hour["ok"], hour
want_pay = sum(_lb(c) * r["인"] for c, r in zip(codes, row["값"]["인력"])) * 5 / 24
assert abs(hour["노무비"] - want_pay) < 2, (want, hour["노무비"], want_pay)
# ④ 한 줄 구성 ─────────────────────────────────────────────────────
def test_single_line_same_as_before() -> None:
v = mm.body(_main()["기계구성"][GEN], 0, _line("발전기", "5.5kW"), "main.estx")["값"]
assert "인력" not in v and v["조종원"] == 1.0 and v["잡재료율"] == 11.0
# ⑤ STmate 엑셀 빈 규격 ─────────────────────────────────────────────
def test_stmate_excel_blank_spec() -> None:
import test_797_1a_import_flags as practice
from M02_MasterTemplete import M02_Doc_FromNative as fn
from M02_MasterTemplete import M02_San_Book as book
p = practice.FILES.get("울진대흥")
stc = next(iter(practice.golden.DOCS.rglob("2024년 간선임도사업(기번3).STC")), None)
if not p or not p.exists() or stc is None:
pytest.skip("실무 견본 없음")
got = mm.xlsx_makes(book.sheets(p.name, p.read_bytes()))
codes = sorted(k for k in got if k.startswith("X"))
assert len(codes) == 25 # 호표 25 모두(전 = 9 · 규격 빈 머리 16 못 읽음)
assert got["X00003"]["손료"] == [[2085.0, 62550000.0, False]] # 「굴삭기(0.2m3)」 규격 빈 머리
assert [x[1] for x in got["X00014"]["손료"]] == [
110926000.0,
16315000.0,
] # 둘째 손료 규격 빈 줄
native = fn.read(stc.name, stc.read_bytes())["기계구성"]
keep = ("손료", "조종원", "조종원직종", "주연료종류", "주연료", "잡재료율")
for k in codes:
a, b = got[k], native[k]
assert [[round(x, 3) for x in r[:2]] for r in a["손료"]] == [
[round(x, 3) for x in r[:2]] for r in b["손료"]
], k
assert all(a.get(f) == b.get(f) for f in keep[1:]), k
if __name__ == "__main__":
pytest.main([__file__, "-q"])