From 8709dc9ffd38e95359a3d0ba08e676bde920af03 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 15 Nov 2026 18:46:40 +0900 Subject: [PATCH 1/3] =?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"]) From ef19323403f7447dd87a66f11b880c7a6b19c849 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 15 Nov 2026 18:49:00 +0900 Subject: [PATCH 2/3] =?UTF-8?q?test(843-1):=20717-1=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=20=EC=95=88=20=ED=95=9C=20=EC=A4=84=20=EC=85=88=20=EC=8B=9C?= =?UTF-8?q?=ED=97=98=20=EB=82=A1=EC=9D=80=20=EC=B9=B8=20=EA=B3=A0=EC=B9=A8?= =?UTF-8?q?=20=E2=80=94=20=EC=A4=84=EC=9D=84=20=ED=99=94=EB=A9=B4=EC=9D=B4?= =?UTF-8?q?=20=EB=B0=9B=EB=8A=94=20=EA=BC=B4(M01=20/logic=20=5Fnum)?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=B4=EB=83=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 깨진 때 = 794-2b(e1a1db779) 로직 줄 칸 「개정」 [2026] — 시험이 store.logic 날 줄(Decimal)을 그대로 JSON 으로 보내 TypeError - 코드 흠 아님 — 화면은 M01 /logic(_call → _num) 으로 받은 수 꼴 줄을 보냄 · 셈 · 금액 그대로 Co-Authored-By: Claude Opus 5.5 --- resources/tester/test_717_1_logic_lib.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/resources/tester/test_717_1_logic_lib.py b/resources/tester/test_717_1_logic_lib.py index 6040c645f..706f6d297 100644 --- a/resources/tester/test_717_1_logic_lib.py +++ b/resources/tester/test_717_1_logic_lib.py @@ -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:]]} From a05328cfe361a8664569e46e42aaf9d9eb959638 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 15 Nov 2026 19:00:35 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix(843-2):=20STmate=20=EC=97=91=EC=85=80?= =?UTF-8?q?=20=E3=80=8C=EC=A4=91=EA=B8=B0=EC=82=AC=EC=9A=A9=EB=A3=8C?= =?UTF-8?q?=E3=80=8D=20=EA=B7=9C=EA=B2=A9=20=EB=B9=88=20=EC=B9=B8=20=3D=20?= =?UTF-8?q?=EC=B9=B8=20=EC=9E=90=EB=A6=AC=EB=A1=9C=20=EC=9D=BD=EC=9D=8C=20?= =?UTF-8?q?=E2=80=94=20=EB=A8=B8=EB=A6=AC=20=C2=B7=20=EC=86=90=EB=A3=8C=20?= =?UTF-8?q?=EC=A4=84=EC=9D=84=20=EB=AA=BB=20=EC=9D=BD=EB=8D=98=20=ED=98=B8?= =?UTF-8?q?=ED=91=9C=2034?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 흠: 빈 칸을 거른 뒤 차례로 읽어 규격이 빈 머리(「굴삭기(0.2m3)」 시간 …) · 손료 줄(「대형브레이카(0.7m3)」 계수 천원 …)이 한 칸씩 밀림 → 호표 통째(머리) · 조합 둘째 손료를 못 읽음 · 규격 있는 운전사 · 규격 없는 연료 줄도 같은 까닭 - 고침: 첫 글 칸부터 [명칭, 규격, 수량, 단위, 단가] 칸 자리로 읽음(단위로 갈래) - 전후: STmate 엑셀 6 파일 호표 143 — 같음 109 · 새로 읽음 34(울진대흥 · 대흥공통 16 씩 · 울진소광 2) · 바뀜 · 잃음 0 · 새로 읽은 18(같은 책 둘 뺌) 모두 같은 공사 .STC 구성과 같음 · 두 공사 87 블록 불러오기 금액 검산 같음(전후 같음) · 이 견본은 기계가 모두 M01 에 이어져 내 기계 0 대(전후 같음) · 못 이었다면 몸 = 직접 → 바로 34 - 시험: test_842_4 ⑤ 더함 · test_828_1 (100, 118) → (118, 118) — 옛 단언이 이 흠(못 읽는 머리)을 박아 둔 낡은 칸 Co-Authored-By: Claude Opus 5.5 --- M02_MasterTemplete/M02_San_MyMachine.py | 41 +++++++++---------- .../tester/test_828_1_stc_composition.py | 2 +- .../tester/test_842_4_import_machine_lines.py | 35 +++++++++++++++- 3 files changed, 55 insertions(+), 23 deletions(-) diff --git a/M02_MasterTemplete/M02_San_MyMachine.py b/M02_MasterTemplete/M02_San_MyMachine.py index 8814b864d..ea626a4a8 100644 --- a/M02_MasterTemplete/M02_San_MyMachine.py +++ b/M02_MasterTemplete/M02_San_MyMachine.py @@ -148,9 +148,10 @@ def _est_sheet(rows: list[list]) -> dict[str, dict]: 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: @@ -164,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: - _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": - 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("손료")} diff --git a/resources/tester/test_828_1_stc_composition.py b/resources/tester/test_828_1_stc_composition.py index 4065fedce..8c1279cf9 100644 --- a/resources/tester/test_828_1_stc_composition.py +++ b/resources/tester/test_828_1_stc_composition.py @@ -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: diff --git a/resources/tester/test_842_4_import_machine_lines.py b/resources/tester/test_842_4_import_machine_lines.py index 24817f9e0..06e1ca677 100644 --- a/resources/tester/test_842_4_import_machine_lines.py +++ b/resources/tester/test_842_4_import_machine_lines.py @@ -5,6 +5,7 @@ ② 몸 = `master_machine_crew` 꼴 인력 · 잡품 줄 · 못 이음 = 비고 「확인 필요」 — main 구성 전부 버린 줄 0 ③ 층에 만든 내 기계 시간당 노무비 = 줄마다 우리 노임 × 인 × 5/24 의 합(둘째 줄부터도 셈에 들어감) ④ 한 줄 구성은 전과 같은 몸(옛 칸 그대로 · 저장 때 목록으로 바뀜) +⑤ STmate 엑셀 「중기사용료」 규격 빈 칸(머리 · 손료 줄) = 칸 자리로 읽음(843-2) — 울진대흥 호표 25 모두 · .STC 구성과 같음 """ from __future__ import annotations @@ -83,7 +84,10 @@ def test_no_line_dropped() -> None: 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 != "값 확인 필요"] + 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("조종원"))) @@ -120,5 +124,34 @@ def test_single_line_same_as_before() -> None: 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"])