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:
@@ -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]]:
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user