From 8709dc9ffd38e95359a3d0ba08e676bde920af03 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 15 Nov 2026 18:46:40 +0900 Subject: [PATCH] =?UTF-8?q?feat(842-4):=20=EB=B6=88=EB=9F=AC=EC=98=A4?= =?UTF-8?q?=EA=B8=B0=20=EB=82=B4=20=EA=B8=B0=EA=B3=84=20=E2=80=94=20?= =?UTF-8?q?=EA=B8=B0=EA=B3=84=20=ED=95=9C=20=EB=8C=80=20=EC=95=88=20?= =?UTF-8?q?=EC=9A=B4=EC=A0=84=EC=82=AC=20=C2=B7=20=EC=97=B0=EB=A3=8C=20?= =?UTF-8?q?=C2=B7=20=EC=9E=A1=ED=92=88=20=C2=B7=20=EC=86=8C=EB=AA=A8=20?= =?UTF-8?q?=EC=9E=AC=EB=A3=8C=20=EC=A4=84=EC=9D=84=20=EB=AA=A8=EB=91=90=20?= =?UTF-8?q?=EB=B0=9B=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 구성 읽기 넷(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 --- M02_MasterTemplete/M02_San_Ilwi.py | 18 ++- M02_MasterTemplete/M02_San_MyMachine.py | 135 +++++++++++++++--- .../tester/test_842_4_import_machine_lines.py | 124 ++++++++++++++++ 3 files changed, 254 insertions(+), 23 deletions(-) create mode 100644 resources/tester/test_842_4_import_machine_lines.py diff --git a/M02_MasterTemplete/M02_San_Ilwi.py b/M02_MasterTemplete/M02_San_Ilwi.py index 599216b31..e8c6b16d7 100644 --- a/M02_MasterTemplete/M02_San_Ilwi.py +++ b/M02_MasterTemplete/M02_San_Ilwi.py @@ -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 diff --git a/M02_MasterTemplete/M02_San_MyMachine.py b/M02_MasterTemplete/M02_San_MyMachine.py index 0ab5625c9..8814b864d 100644 --- a/M02_MasterTemplete/M02_San_MyMachine.py +++ b/M02_MasterTemplete/M02_San_MyMachine.py @@ -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 diff --git a/resources/tester/test_842_4_import_machine_lines.py b/resources/tester/test_842_4_import_machine_lines.py new file mode 100644 index 000000000..24817f9e0 --- /dev/null +++ b/resources/tester/test_842_4_import_machine_lines.py @@ -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"])