fix(827-6): 호표 자재 이음 되살림(어닐링철선 결속선) · 가져온 식 검산 묶은 줄 합 맞댐 · 계약 블록

- M02_Hopyo_Material.spec_fit: 827-2 「자재 수 한 번씩」 을 좁힘 — 줄 수와 그대로 같은 자재 수만 그 줄 수 몫 · 배율로만 맞는 수는 같이 씀(글 붙은 「#200.9mm200.4m/kg」 = #20 0.9mm 200.4m/kg → MT000209 되살림 · 블레이드 d=320~400 t=3.2 → 4"×3.2 는 그대로 풀림 · 21-17-17 의 17 둘 각각)
- 호표 길 전수(실무 엑셀 8 · 일위대가 줄 1564): 827-2 앞 대비 바뀜 = 블레이드 1 줄(틀린 이음 풀림)뿐 · 어닐링철선 5 줄 되살림 · 단산 길 전수 827-2 전후표 수치 그대로(새 이음 · 풀림 · 키 바뀜 0)
- M02_Sangeun_Imported 검산: 첫 자리가 같은 글줄 여럿(814-1 기계 노 · 재 · 경 · 노 줄만 딸린 기계까지) = 값 합으로도 맞댐(check · 화면 more 둘 다) · 계약(낙찰률) 블록 = 비목 × 낙찰률 원 버림과도 맞댐(머리 금액만 있는 STmate)
- 세 원천 3241 블록 검산 다름 287 → 4(EST main 1308 · 북부청임도 2 · 4 · 48 = 엑셀 글 소계가 줄 합과 다름 — 진짜 다름 · 금액은 같음)
- 시험: 827-5 xfail 셋 뗌(test_778_5 자재 표 · test_793_2 동부청영월 · 울진소광) · 지킴 둘 더함(글 붙은 규격 수 · 묶은 줄 합 · 계약 셈)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-13 15:48:04 +09:00
co-authored by Claude Opus 5.5
parent 5fee03ff0e
commit 1f9f8a1467
4 changed files with 80 additions and 29 deletions
+10 -8
View File
@@ -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]]:
+38 -8
View File
@@ -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 {}
+14 -4
View File
@@ -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")
@@ -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):