diff --git a/M02_MasterTemplete/M02_Hopyo_Material.py b/M02_MasterTemplete/M02_Hopyo_Material.py index 5b1766fcd..03207ea4f 100644 --- a/M02_MasterTemplete/M02_Hopyo_Material.py +++ b/M02_MasterTemplete/M02_Hopyo_Material.py @@ -181,14 +181,16 @@ def spec_fit(line: dict[str, Any], row: dict[str, Any]) -> float: return 1.0 if inside else 0.0 if not mine or not row["nums"]: return 0.0 - mine, rest, hit = list(dict.fromkeys(mine)), list(row["nums"]), 0 - for x in ( - mine - ): # 827-2 자재 수 하나는 한 번만(「d=320~400 t=3.2」 의 320 · 3.2 가 둘 다 3.2 로 맞던 것) - if (j := next((i for i, y in enumerate(rest) if _near(x, y)), None)) is not None: - hit += 1 - rest.pop(j) - return hit / len(mine) + # 827-2 · 827-6 줄 수와 그대로 같은 자재 수는 그 줄 수 몫(「d=320~400 t=3.2」 의 320 이 + # 3.2 를 다시 못 씀) · 배율로만 맞는 수는 여럿이 같이 씀(글 붙은 「#200.9mm200.4m/kg」 + # = #20 0.9mm 200.4m/kg) + mine, rest = list(dict.fromkeys(mine)), list(row["nums"]) + same = [] + for x in mine: + if x in rest: + rest.remove(x) + same.append(x) + return sum(x in same or any(_near(x, y) for y in rest) for x in mine) / len(mine) def _wants(line: dict[str, Any]) -> tuple[str | None, list[str]]: diff --git a/M02_MasterTemplete/M02_Sangeun_Imported.py b/M02_MasterTemplete/M02_Sangeun_Imported.py index 56fce215c..899fc75dc 100644 --- a/M02_MasterTemplete/M02_Sangeun_Imported.py +++ b/M02_MasterTemplete/M02_Sangeun_Imported.py @@ -35,6 +35,7 @@ SK 문서 덧칸 `가져온` = {로직(엑셀 단가 그대로 · 고치지 않 from __future__ import annotations import json +import math import re from decimal import Decimal from typing import Any @@ -379,13 +380,29 @@ def check(doc: dict, whole: Any = None) -> dict[str, Any]: return {"같음": False, "까닭": str(e), "셈": None, "엑셀": excel, "줄": []} mine = _money(got) now = {x.get("cid"): x.get("금액") for x in got.get("줄") or []} | _inner(bare, whole) - lines = [x for x in (_compare(one, now) for one in src.get("글줄") or []) if x] - ok = bool(mine) and all(_same(mine.get(k), excel.get(k)) for k in excel) and excel != {} + group = _groups(src.get("글줄")) + lines = [x for x in (_compare(one, now, group=group) for one in src.get("글줄") or []) if x] + ok = bool(mine) and excel != {} and _all_same(mine, excel) + rate = (src.get("계약") or {}).get("낙찰률") + ok = ok or bool(mine and rate and excel and _all_same(_contract(mine, rate), excel)) why = None if ok else (got.get("까닭") or ("엑셀 금액 없음" if not excel else "금액 다름")) ok = ok and all(x["같음"] for x in lines) return _json({"같음": ok, "까닭": why, "셈": mine, "엑셀": excel, "줄": lines}) +def _all_same(mine: dict, excel: dict) -> bool: + return all(_same(mine.get(k), excel.get(k)) for k in excel) + + +def _contract(mine: dict, rate: float) -> dict: + """계약단가(827-6) — 비목 × 낙찰률 원 버림 · 계 = 비목 합(`M02_San_Eval.stmate_block` 과 같음) · + 머리 금액만 있는 STmate 블록(엑셀금액 = 계약 뒤).""" + if any(mine.get(k) is None for k in COSTS): + return {} + out = {k: math.floor(round(float(mine[k]) * rate / 100, 6)) for k in COSTS} + return out | {"계": sum(out.values())} + + def _inner(doc: dict, whole: Any) -> dict: """엔진 날 셈의 중간 `mid:이름` · 단계 `단계:이름` 계(검산 맞댈 값) · 못 셈 = 빈.""" from M02_MasterTemplete import M02_Sangeun_Calc as calc @@ -408,9 +425,20 @@ def _inner(doc: dict, whole: Any) -> dict: return out | {f"단계:{x.get('이름')}": x.get("계") for x in got.get("단계") or []} -def _compare(one: Any, now: dict, loose: bool = False) -> dict | None: - """글줄 하나 맞댐 — 자리(들) 셈 합 ≈ 엑셀 `값` · 맞댈 것 없음 = None · `loose` = 셈에 없는 - 자리면 None(화면 보기 · 단계 값 없음).""" +def _groups(texts: Any) -> dict[str, Decimal]: + """첫 자리가 같은 값 든 글줄 여럿 → 첫 자리별 값 합(814-1 기계 노 · 재 · 경 세 글줄 = 묶은 + 한 줄 · 노 줄만 딸린 기계까지 자리들).""" + out: dict[str, list] = {} + for one in texts or []: + if isinstance(one, dict) and one.get("값") is not None: + if spot := (one.get("자리들") or [one.get("자리")])[0]: + out.setdefault(spot, []).append(Decimal(str(one["값"]))) + return {k: sum(v) for k, v in out.items() if len(v) > 1} + + +def _compare(one: Any, now: dict, loose: bool = False, group: dict | None = None) -> dict | None: + """글줄 하나 맞댐 — 자리(들) 셈 합 ≈ 엑셀 `값`(또는 같은 자리 글줄들 값 합 `group`) · 맞댈 것 + 없음 = None · `loose` = 셈에 없는 자리면 None(화면 보기 · 단계 값 없음).""" if not isinstance(one, dict) or one.get("값") is None: return None spots = one.get("자리들") or ([one["자리"]] if one.get("자리") else []) @@ -419,9 +447,11 @@ def _compare(one: Any, now: dict, loose: bool = False) -> dict | None: want = one["값"] vals = [now.get(s) for s in spots] got = None if None in vals else sum(Decimal(str(v)) for v in vals) - same = _same(got, want) + total = (group or {}).get(spots[0]) + same = _same(got, want) or (total is not None and _same(got, total)) why = None if same else ("짝 줄 없음" if got is None else f"셈 {got} · 엑셀 {want}") out = {"자리": spots[0], "행": one.get("행"), "셈": got, "엑셀": want, "같음": same} + out |= {"묶음합": total} if total is not None else {} return out | ({"자리들": spots} if len(spots) > 1 else {}) | ({"까닭": why} if why else {}) @@ -433,9 +463,9 @@ def more(out: dict, clean_: dict) -> dict: src = meta.get("원본") or {} spots = (out.get("시트") or {}).get("자리") or {} now = {k: v.get("지금") for k, v in spots.items()} - texts = [] + texts, group = [], _groups(src.get("글줄")) for one in src.get("글줄") or []: - hit = _compare(one, now, loose=True) or {} + hit = _compare(one, now, loose=True, group=group) or {} text = {k: one.get(k) for k in ("행", "글", "자리")} text |= {"자리들": one["자리들"]} if one.get("자리들") else {} text |= {"엑셀": one["값"]} if one.get("값") is not None else {} diff --git a/resources/tester/test_778_5_hopyo_units.py b/resources/tester/test_778_5_hopyo_units.py index f0173e77c..99831f348 100644 --- a/resources/tester/test_778_5_hopyo_units.py +++ b/resources/tester/test_778_5_hopyo_units.py @@ -91,6 +91,20 @@ def test_손료율_자재_줄_율_항목_율_줄(us): assert ran["1단위"]["계"] == pytest.approx(got["우리금액"]) +def test_글_붙은_규격_수_배율_같이_씀_같은_수는_한_번(us): + """827-6 — 「#200.9mm200.4m/kg」(#20 0.9mm 200.4m/kg 글 붙음) = 어닐링철선 이음 · 같은 수 두 번 + 맞춤(블레이드 320 · 3.2 → 4"×3.2) · 이름 다른 드릴롯드 = 이음 안 함 · 21-17-17 의 17 둘.""" + + def got(name, spec=""): + return us.line({"묶음": "M", "이름": name, "규격": spec, "품": 1}) + + wire = got("어닐링철선(결속선)(#200.9mm200.4m/kg)") + assert wire["상태"] == units.LINKED and wire["단위"] == {"자재": "MT000209"} + assert got("블레이드", "d=320~400mm t=3.2")["상태"] != units.LINKED + assert got("유압식드릴롯드", "T38×R32x3660mm")["상태"] != units.LINKED + assert got("복합비료(21-17-1720kg/포)")["단위"] == {"자재": "MT006490"} + + @pytest.fixture(scope="module") def table(us): rows, full, blocks = [], 0, 0 @@ -122,10 +136,6 @@ def test_여덟_파일_줄_연결_표(table): _floor(got, kind) -@pytest.mark.xfail( - strict=True, - reason="827-5 ㉰ — 827-2 뒤 어닐링철선(결속선)(#200.9mm200.4m/kg) 다섯 줄 이어짐 → 고를 것(후보만)", -) def test_여덟_파일_자재_줄_연결_표(table): got, full, _ = table _floor(got, "M") diff --git a/resources/tester/test_793_2_sangeun_imported.py b/resources/tester/test_793_2_sangeun_imported.py index 08a7c4a8c..6387dc7b9 100644 --- a/resources/tester/test_793_2_sangeun_imported.py +++ b/resources/tester/test_793_2_sangeun_imported.py @@ -336,15 +336,24 @@ def test_원본_보존_사본(): # ── 실무 블록(793-2a 내보내기 → 가져온 식 SK) ───────────────────────────── -# 827-5 ㉰ — 814-1 세 줄 묶기 뒤 가져온 식 검산(`M02_Sangeun_Imported._compare`)이 기계 노 · 재 · 경 글줄을 각각 -# 묶은 한 줄 합계와 맞대 「같음 False」(금액은 엑셀과 같음) — 코드 몫이라 시험은 xfail 로 표시 -_GROUPED = pytest.mark.xfail(strict=True, reason="827-5 ㉰ 기계 세 줄 묶음 검산 줄 맞댐") -REAL = [ - ("봉화", "D00102"), - pytest.param("동부청영월", "D00003", marks=_GROUPED), - pytest.param("울진소광", "D01489", marks=_GROUPED), - ("북부청임도", 18), -] +# 동부청영월 · 울진소광 = 814-1 기계 노 · 재 · 경 세 글줄이 묶은 한 줄을 가리킴(827-6 묶음 합 맞댐) +REAL = [("봉화", "D00102"), ("동부청영월", "D00003"), ("울진소광", "D01489"), ("북부청임도", 18)] + + +def test_묶은_줄_글줄_셋_합_맞댐(): + """827-6 — 첫 자리가 같은 글줄 셋(노 · 재 · 경 · 노 줄만 딸린 기계까지) = 값 합으로 맞댐 · + 하나뿐이면 제 값 · 계약(낙찰률) 블록 = 비목 × 낙찰률 원 버림과도 맞댐.""" + gc, br = "logic:GC1#0", "logic:브레이커#0" + texts = [{"행": 1, "자리": gc, "자리들": [gc, br], "값": 956.8}] + texts += [{"행": r, "자리": gc, "값": v} for r, v in ((2, 309.4), (3, 397.3))] + group = imp._groups(texts + [{"행": 4, "자리": "logic:M#0", "값": 5}]) + assert group == {gc: Decimal("1663.5")} + now = {gc: 1663.5, br: 0, "logic:M#0": 5} + assert all(imp._compare(t, now, group=group)["같음"] for t in texts) + assert imp._compare({"행": 4, "자리": "logic:M#0", "값": 5}, now, group=group)["같음"] + assert not imp._compare(texts[1], {gc: 1600}, group=group)["같음"] + mine = {"노무비": 484.3, "재료비": 134, "경비": 168, "계": 786.3} + assert imp._contract(mine, 87.745) == {"노무비": 424, "재료비": 117, "경비": 147, "계": 688} def _emitted(tag, code):