Files
Aislo/resources/tester/test_827_2_material_clues.py
T
eomsangdonandClaude Opus 5.5 109789a746 feat(828-2): STmate 엑셀 일위대가표 숨은 칸 → 구성 줄 — 읽는 곳 하나 · 율 줄 · 닫기 · 일식 · 별도계상 · 판에 일위 재료
- M02_San_Xlsx_Ilwi(새) = 일위대가표 읽기 한 곳 — N 열 메타(COD · IPR · PRI · PRO · EQC · KWN · DVD) · O 열 차수 꼬리 기준 · 줄 꼴 = 828-1 M02_San_Ilwi 꼴 + 엑셀 칸(행 · 차수 · 비고 · 전체 · 율.코드 · 메타) · 숨은 칸 없음 = 옛 코드 찾기 · 단산 길 일위 빈 목록 + 표지
- 5 건 호표 136 · 줄 706 · STC 구성 줄과 코드 · 갈래 · 수량 · 율 · 밑줄 같음(다른 5 줄 = STC 별도계상 규칙이 비고 별산 표시를 놓침)
- 호표 길(M02_Hopyo_Stmate._hopyos) · 단산 길(M02_San_Xlsx_Stmate.read 일위) · 단서(M02_San_MatFind.from_book) 모두 공용 읽기 · 단서 · 산근 · 단가 전후 같음
- M02_Hopyo_Fixed._ratio = 숨은 칸 율 밑 · 대상 비목 먼저(글 짐작 대신) — 호표 길 전후 바뀜 = 영덕 제11호표 산악할증 0% 한 줄(금액만 → 비율 노무비 · 금액 0 그대로) · 단산 항목 1564 판정 그대로
- 재료 판(census) = 일위 구성 줄 자재도 셈(M02_San_MatFind.ilwi_found · 종 칸 일위 = 호표 코드 · 화면 「일위대가 N 호표」)
- 시험: 새 test_828_2_xlsx_ilwi_meta · test_827_2 단서 시트 보기에 「제N호표」 줄 넣음(실제 시트는 늘 있음 · 공용 읽기가 호표 머리부터 셈)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 20:39:15 +09:00

201 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""827-2 불러오기 재료 찾기 — 별도계상 품목 · 단서 넷 · 자동 규칙 A ~ F2 · 판정 네 갈래 · 별칭 안 만듦(요구 810 ~ 812).
판정 = `M02_San_MatFind`(`M02_San_Link.m01` 이 부름 · 줄 `자원["재료찾기"]`). 실무 원파일 시험은 파일 없으면 건너뜀.
"""
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path[:0] = [str(ROOT), str(ROOT / "resources" / "master_data" / "scripts"), str(ROOT / "resources" / "tester")] # fmt: skip
from M02_MasterTemplete import M02_Elem_Alias as ea # noqa: E402
from M02_MasterTemplete import M02_Hopyo_MatSame as ms # noqa: E402
from M02_MasterTemplete import M02_San_Link as sl # noqa: E402
from M02_MasterTemplete import M02_San_MatFind as mf # noqa: E402
def _go(name, spec, unit, label="", code=""):
src = {"이름": name, "규격": spec, "단위": unit, "갈래": "재료", "코드": code}
key, mark = sl.m01("재료", src, label)
return key, mark, src["재료찾기"]
# ── 자동 규칙(걸음 2) — 맞는 보기 ──────────────────────────────────────────
@pytest.mark.parametrize(
"name,spec,unit,rule,key",
[
("유압식드릴스리브", "T38×190mm", "EA", "E", "MT005687"),
("중하중그레이팅", "395×995×52", "EA", "E", "MT007025"),
("공사안전PE드럼", "500×800", "EA", "E", "MT003311"),
("아세틸렌", "용접용", "kg", "C", "MT003163"),
("요소", "", "KG", "A", "MT006763"),
("보조강재(열연강판)", "T=9-12", "kg", "B", "MT030619"),
("미송(판재)", "", "M3", "F", "MT000366"),
("각재 (외송)", "", "M3", "F", "MT000339"),
("각재", "외송", "M3", "F2", "MT000339"),
],
)
def test_rule_links(name, spec, unit, rule, key):
got, _mark, found = _go(name, spec, unit)
assert got == key and found["판정"] == mf.LINKED and found["방법"] == f"규칙 {rule}"
# ── 틀리면 안 되는 보기 ───────────────────────────────────────────────────
def _keys_named(word):
return {k for k, r in ea.book()["줄"]["MT"].items() if word in str(r.get("이름"))}
def test_no_wrong_rule_links():
nawang = _keys_named("나왕")
assert nawang
for name, spec in (
("각재(외송)", ""),
("각재 (외송)", ""),
("각재", "외송"),
("각재(외송)", "3.6×3.0×3.0"),
):
assert _go(name, spec, "M3")[0] not in nawang
me = ms._item("치즐", "0.7m3")
assert mf._rule_b({"이름": "치즐", "규격": "0.7m3", "단위": "EA"}, me) in (None, "MT000011")
assert _go("치즐", "0.7m3", "EA")[0] == "MT000011" # 이미 이은 줄 그대로
bit = _go("BUTTON BIT", "Ø38×76㎜", "EA")
assert bit[0] is None and bit[2]["판정"] == mf.NONE
rod = _go("PVC용접봉", "Ø3.2 (국산)", "KG")
assert rod[0] is None and rod[2]["판정"] == mf.NONE
for name in ("에폭시수지",):
assert _go(name, "YD-128", "kg")[0] is None
def test_doubted_three():
"""조사 810-2 의심 셋 — 블레이드 · 드릴롯드는 풀고 · 참싸리(박피)는 사용자 수동연결 과녁(817-1 #33)대로."""
assert _go("블레이드", "d=320~400mm t=3.2", "EA")[0] != "MT005744"
assert _go("유압식드릴롯드", "T38×R32x3660mm", "EA")[0] != "MT005695"
assert _go("산림종자", "참싸리(박피)", "KG")[0] == "MT006664"
assert _go("블레이드(커터)", 'd=320 400mm 14"xt3.2', "EA")[0] == "MT005749"
# ── 판정 네 갈래 ──────────────────────────────────────────────────────────
def test_four_verdicts():
assert _go("유압식드릴스리브", "T38×190mm", "EA")[2]["판정"] == mf.LINKED
no_spec = _go("철물", "", "EA")[2]
assert no_spec["판정"] == mf.NO_SPEC and no_spec["후보"][0]["키"] == "MT000511"
many = _go("결속선(철선)", "#20 , 0.9M/M", "KG")[2]
assert many["판정"] == mf.MANY and len(many["후보"]) > 1 and many["키"] is None
for name, spec, unit in (("떼", "", "m2"), ("BUTTON BIT", "Ø38×76㎜", "EA")):
assert _go(name, spec, unit)[2]["판정"] == mf.NONE
linked = _go("결속선(철선)", "#20 , 0.9M/M", "KG")[2]
assert set(linked) == {"판정", "방법", "키", "후보", "단서"}
# ── 단서 넷(걸음 1) ───────────────────────────────────────────────────────
BOQ = [ # STC 내역서 자재 직접 줄(동부청영월 꼴)
{"번": "1", "이름": "파형강관(2.7T)-영월", "규격": "Φ800-1RS", "단위": "M", "코드": "M01092"},
{"번": "2", "이름": "이형철근", "규격": "D13~D32", "단위": "Ton", "코드": "M00654"},
{"번": "3", "이름": "파형강관부설", "규격": "Φ800", "단위": "M", "코드": "D00071"},
]
def test_clue_label():
got, _mark, found = _go("철물", "", "EA", label="5) 철물(앵커용)")
assert (
got == "MT000511"
and found["방법"] == "단서"
and found["단서"] == ["설명 글 — 철물(앵커용)"]
)
def test_clue_head_and_boq_apart():
clues = mf.from_boq(BOQ)
assert [c["코드"] for c in clues] == ["M01092", "M00654"] # 자재(M) 줄만
with mf.clues_on(clues, "파형강관부설 Φ800mm"):
key, mark, found = _go("파형강관", "별도계상", "M")
assert key is None and mark == f"{sl.NOTE}{sl.APART}" # 관급 · 일반 둘 = 사람 몫 · 이음 비움
assert found["판정"] == mf.NO_SPEC
assert {c["키"] for c in found["후보"][:2]} == {"MT015181", "MT015282"}
assert all(c.get("단서") == "단서" for c in found["후보"][:2])
assert found["단서"][0] == "산근 머리 — 파형강관부설 Φ800mm"
with mf.clues_on(clues, "철근가공조립(현장) TYPE Ⅰ"):
found = _go("이형철근", "별도계상", "Ton")[2]
assert found["판정"] == mf.NO_SPEC and found["단서"] == [] # 범위 D13~D32 = 규격 하나 아님
def test_clue_book_sheet():
meta = "_x0007_`COD|{}_x0005_`EXI|0_x0005_`"
rows = [
[" 제 17 호표", *[None] * 12, "_x0005_`BDCOD|B01217_x0007_`POSS|3_x0007_`POSE|4_x0007_`"],
["파형강관부설", "D=800mm", None, "m", *[None] * 8, "대가 17", None, "B01217"],
[
"파형강관(800mm)",
"별도계상",
1,
"M",
*[0] * 8,
"별산자재 19 ",
meta.format("M00222"),
"M00222_1",
], # fmt: skip
["보통인부", None, 0.66, "인", *[0] * 8, "노무 1 ", meta.format("L00001"), "L00001_1"],
]
got = mf.from_book(rows)
assert got == [{"곳": "일위대가표", "코드": "M00222", "이름": "파형강관(800mm)", "규격": "별도계상", "머리": "파형강관부설 D=800mm"}] # fmt: skip
with mf.clues_on(got, ""):
found = _go("파형강관", "별도계상", "M", code="M00222")[2]
assert found["판정"] == mf.NO_SPEC and all("800" in c["규격"] for c in found["후보"][:3])
assert any(x.startswith("일위대가표") for x in found["단서"])
def test_apart_linked_item_keeps_zero():
"""별도계상 줄 = 품목이 하나로 정해지면 판정 키 · 줄 이음은 비움(셈 제외 · 단가 0 그대로)."""
key, mark, found = _go("요소(별도계상)", "", "KG")
assert key is None and found["키"] == "MT006763" and mark == f"{sl.NOTE}{sl.APART_ITEM}"
why = sl.why(
{"자원": {"갈래": "재료", "이름": "요소(별도계상)", "규격": "", "재료찾기": found}}
)
assert why.startswith("별도계상 · 품목: 요") and why.endswith(
"셈은 프로젝트에서"
) # 827-3 품목 이름까지
assert sl.tally({"호표": [{"이름": "요소", "자원": {"재료찾기": found}}]})["됨"] == 1
# ── 별칭 안 만듦 ──────────────────────────────────────────────────────────
def test_no_alias_written():
before = {k: r.get("별칭") for k, r in ea.book()["줄"]["MT"].items()}
for args in (
("각재", "외송", "M3"),
("종자", "초본류", "KG"),
("철물", "", "EA"),
("떼", "", "m2"),
):
_go(*args)
assert {k: r.get("별칭") for k, r in ea.book()["줄"]["MT"].items()} == before
# ── 실무 원파일(있을 때) — 단서가 원파일 · 엑셀에서 읽힘 ─────────────────────
def test_real_files_carry_clues():
import test_797_1a_import_flags as practice
import test_810_1_stc as c
from M02_MasterTemplete import M02_Doc_FromNative as fn
from M02_MasterTemplete import M02_San_Xlsx as sx
stc, xl = c.FILES["동부청영월"], practice.FILES.get("울진대흥")
if not stc.exists() or not xl or not xl.exists():
pytest.skip("실무 원파일 없음")
saved = fn.read(stc.name, stc.read_bytes())
assert any(x["이름"].startswith("파형강관") for x in saved["단서"])
book = fn.pack(sx.read(xl.name, xl.read_bytes()), xl.name)
assert any(x["곳"] == "일위대가표" and x.get("머리") for x in book["단서"])
out = fn.emit(saved, 71) # 파형강관부설 Φ800mm
line = next(
h for h in out["로직"]["호표"] if str((h.get("자원") or {}).get("규격")) == "별도계상"
)
assert (
line["이음"] is None
and not line.get("단가")
and not any((line.get("값묶음") or {}).values())
)
assert {c["키"] for c in line["자원"]["재료찾기"]["후보"][:2]} == {"MT015181", "MT015282"}