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>
This commit is contained in:
2026-11-15 18:46:40 +09:00
co-authored by Claude Opus 5.5
parent bbefb57e69
commit 8709dc9ffd
3 changed files with 254 additions and 23 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
+119 -16
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,12 +136,12 @@ 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]}
@@ -149,10 +176,10 @@ def _stm_sheet(rows: list[list]) -> dict[str, dict]:
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) > 2 and str(v[2]) == "인" and (_num(v[1]) or 0) > 0:
_crew(cur, v[0], v[1])
elif first in MISC_WORDS and len(v) > 2:
_misc(cur, 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":
@@ -206,7 +233,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 +248,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
@@ -0,0 +1,124 @@
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""842-4 지킴 — 불러오기 내 기계 = 기계 한 대 안 여러 줄(운전사 · 연료 · 잡품 · 소모 재료)을 모두 받음(임시 storage).
① 구성 읽기 = 줄 목록(EST main — 크러셔 운전사 둘 · 펌프 준설선 넷 · 표층재생기 연료 둘 · 잡품 둘 · 대형브레이카 치즐 EA)
② 몸 = `master_machine_crew` 꼴 인력 · 잡품 줄 · 못 이음 = 비고 「확인 필요」 — main 구성 전부 버린 줄 0
③ 층에 만든 내 기계 시간당 노무비 = 줄마다 우리 노임 × 인 × 5/24 의 합(둘째 줄부터도 셈에 들어감)
④ 한 줄 구성은 전과 같은 몸(옛 칸 그대로 · 저장 때 목록으로 바뀜)
"""
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
if __name__ == "__main__":
pytest.main([__file__, "-q"])