936be972 실측: 봉화 제6호표 가져오기 → 내역 「기슭막이 H=2.0m 10m」 2줄이 「막힌 줄 9: 공종 코드 미정」으로 섬(전엔 줄이 사라짐) → 코드 3줄 고르고 6줄 수동 단가(검증용) → 일위대가 482,766원/m · 내역 4,827,660원 × 2줄 「수동 단가 6건 미확정」 · 되돌려 본체·인계·내역·원단위·장 전부 같음 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
134 lines
5.4 KiB
Python
134 lines
5.4 KiB
Python
"""STmate 출력 엑셀 「일위대가표」 읽개 — 고정형 라이브러리 항목의 둘째 길(PLAN 4장 · 2026-09-14 브레인 판정 ①).
|
|
|
|
왜 엑셀인가 — STC 는 호표(B) 구성이 난독화된 BDQTY 에만 있어 못 읽음(7파일 전수 확인) ·
|
|
경쟁사 보호 장치는 풀지 않음. 출력 엑셀은 평문이고 코덱스 `recipe_extract.py` 가 실무 6건 누락 0 으로 뽑은 모양.
|
|
⚠ **모양이 다르면 억지로 읽지 않음** — 「어느 칸이 안 맞는지」 사유로 돌려보냄(추측해서 맞추면
|
|
수량이 조용히 틀린 채 라이브러리에 들어가 계속 쓰임).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import openpyxl
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from B08_Quantity.B08_Quantity_Engine_StmateRecipe import read_recipes # noqa: E402
|
|
|
|
PRACTICE = ROOT / "resources" / "knowledge" / "original" / "실무문서"
|
|
BONGHWA = next(
|
|
(p for p in PRACTICE.rglob("*.xlsx") if "기번41" in p.name and not p.name.startswith("~$")),
|
|
None,
|
|
)
|
|
|
|
|
|
def _book(tmp_path: Path, rows: list[list[object]], sheet: str = "일위대가표") -> Path:
|
|
book = openpyxl.Workbook()
|
|
ws = book.active
|
|
ws.title = sheet
|
|
for row in rows:
|
|
ws.append(row)
|
|
path = tmp_path / "book.xlsx"
|
|
book.save(path)
|
|
return path
|
|
|
|
|
|
HEAD = [
|
|
["일 위 대 가 표"],
|
|
["공사명 : 시험"],
|
|
["명 칭", "규 격", "수 량", "단위", "합 계"],
|
|
[None],
|
|
]
|
|
|
|
|
|
@pytest.mark.skipif(BONGHWA is None, reason="실무 엑셀(지식DB 원문) 없음")
|
|
def test_봉화_호표_45_기슭막이_H2_구성_9줄() -> None:
|
|
read = read_recipes(BONGHWA)
|
|
assert read["problems"] == []
|
|
assert len(read["hopyo"]) == 45
|
|
target = next(
|
|
h
|
|
for h in read["hopyo"]
|
|
if h["name"] == "기슭막이(깬잡석,찰쌓기 L3=45m)" and h["spec"] == "H=2.0m, 채집"
|
|
)
|
|
assert target["unit"] == "M" and target["source_code"] == "B00010"
|
|
rows = target["rows"]
|
|
assert len(rows) == 9
|
|
assert (rows[0]["name"], rows[0]["spec"], rows[0]["amount"], rows[0]["unit"]) == (
|
|
"깬잡석채집",
|
|
"L=45cm 내외",
|
|
2.09,
|
|
"M2",
|
|
)
|
|
assert rows[1]["source_code"] == "B00004" # 하위 호표 참조(한 단만 — 판정 Ⓕ)
|
|
assert rows[-1]["name"] == "콘크리트믹서사용" and rows[-1]["amount"] == 0.42
|
|
|
|
|
|
def test_머리_칸이_다르면_안_읽고_칸을_짚는다(tmp_path: Path) -> None:
|
|
head = [row[:] for row in HEAD]
|
|
head[2][2] = "수량합"
|
|
read = read_recipes(_book(tmp_path, [*head, [" 제 1 호표"], ["벽", "H=1", None, "m"]]))
|
|
assert read["hopyo"] == []
|
|
assert any("C3" in p and "수량" in p for p in read["problems"])
|
|
|
|
|
|
def test_수량이_수가_아니면_그_호표를_안_읽는다(tmp_path: Path) -> None:
|
|
rows = [
|
|
*HEAD,
|
|
[" 제 1 호표"],
|
|
["벽", "H=1", None, "m"],
|
|
["돌", "", "약 2", "㎡"],
|
|
["합 계"],
|
|
[" 제 2 호표"],
|
|
["담", "H=2", None, "m"],
|
|
["돌", "", 3, "㎡"],
|
|
["합 계"],
|
|
]
|
|
read = read_recipes(_book(tmp_path, rows))
|
|
assert [h["name"] for h in read["hopyo"]] == ["담"] # 틀린 호표는 통째로 뺌
|
|
assert any("C7" in p for p in read["problems"])
|
|
|
|
|
|
def test_착공판_계약단가_줄은_안_뽑고_항목에_적는다(tmp_path: Path) -> None:
|
|
"""판정 ㉮㉯㉰ — 별산은 자재총괄 · % 가산 행은 참고 + 항목 사유 · 계약단가(낙찰률)는 안 뽑음 · 근거 줄 보존."""
|
|
rows = [
|
|
*HEAD,
|
|
[" 제 1 호표"],
|
|
["기슭막이", "H=2.0", None, "m"],
|
|
["건설표준품셈", "7-1-1(메쌓기)", 0, None],
|
|
["파쇄암", "별도계상", 2.09, "㎡", *[0] * 8, "별산자재 25 "],
|
|
["메쌓기", "L3=55cm이하", 2.09, "m2", *[0] * 8, "대가 11호표"],
|
|
["공구손료", "노무비의 %", 2, "%"],
|
|
["계"],
|
|
["계약단가", None, 88.5],
|
|
]
|
|
from B08_Quantity.B08_Quantity_Engine_StmateRecipe import recipe_item
|
|
|
|
read = read_recipes(_book(tmp_path, rows))
|
|
assert read["problems"] == [] and read["project"] == "시험"
|
|
item = recipe_item(read["hopyo"][0], type_id="masonry_dry", file_name="a.xlsx", project="시험")
|
|
assert [(r["name"], r["destination"]) for r in item["rows"]] == [
|
|
("파쇄암", "material"),
|
|
("메쌓기", "unit_price"),
|
|
("공구손료", "reference"),
|
|
]
|
|
assert all(r["formula"] == "" and "7-1-1" in r["formula_text"] for r in item["rows"])
|
|
assert item["rows"][1]["amount"] == "2.09"
|
|
assert "가산 행 1줄" in item["note"] and "공구손료" in item["note"]
|
|
assert "계약단가·낙찰율 줄 1줄" in item["note"] and "현행 품셈 대조 전" in item["note"]
|
|
assert item["item_kind"] == "fixed" and item["origin"]["project"] == "시험"
|
|
# ㉮ 일위대가 구성 — 일위대가로 가는 줄만(별산·가산 행 뺌) · 공종 코드는 비워 둠(사용자가 고름)
|
|
unit_rows = item["unit_price"]["rows"]
|
|
assert [(r["name"], r["from_row"]) for r in unit_rows] == [("메쌓기", 2)]
|
|
assert not any(r.get("work_item_code") or r.get("ref_code") for r in unit_rows)
|
|
|
|
|
|
def test_시트가_없으면_사유(tmp_path: Path) -> None:
|
|
read = read_recipes(_book(tmp_path, HEAD, sheet="다른표"))
|
|
assert read["hopyo"] == [] and any("일위대가표" in p for p in read["problems"])
|