Files
Aislo/resources/tester/test_m02_hopyo_stmate.py
T

218 lines
10 KiB
Python

"""일위대가 실무자료 STmate 꼴 읽기(PLAN 338-2b · `M02_Hopyo_Stmate`).
여섯 파일 올라감(봉화 45 · 영월 23 · 울진대흥 설계 · 착공 34 · 영덕 9 · 울진소광 25 호표) ·
봉화 줄 216(구성 209 + 근거글 7) · 단산 103 · 노임 30 · 호표 22 · 줄 코드 갈래 ·
노임 이름 어긋남 0 · 블록 자원 품 · 이음 열쇠(M~Z 열 코드 꼴 첫 것) ·
문서 보안(장수 · 진안) 400 글 · 원본 엑셀 · master_data 그대로 ·
영월 정답 자리(`y_ids` — 338 실험 C1 `labels_y` Y000~Y035 = D · X 고유 줄 차례 · 골든 시험이 씀).
"""
import hashlib
from pathlib import Path
import pytest
from M02_MasterTemplete import M02_Hopyo_Basis as hb
from M02_MasterTemplete import M02_Hopyo_Excel as hx
from M02_MasterTemplete import M02_Hopyo_Stmate as st
from M02_MasterTemplete import M02_Hopyo_Store as hs
from M02_MasterTemplete import M02_Template_Layers as layers
from resources.tester import test_m02_hopyo_excel as base
from resources.tester.test_m02_hopyo_excel import ROOT, _post, master_hash
client = base.client # 공용 고정물(임시 층)
DOCS = ROOT / "resources" / "knowledge" / "original" / "실무문서"
FILES = {
"봉화": "04.산불 봉화/01. 내역서/2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx",
"영월": "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx",
"대흥설계": "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/"
"2024년 간선임도사업(기번3).xlsx",
"대흥착공": "2024년 간선임도(기번3-울진.대흥)(공통)/"
"2024년 간선임도 신설사업(기번3 울진.울진.대흥.산65외2).xlsx",
"영덕": "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/"
"2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).xlsx",
"소광": "25년 산불진화임도(기번8)(울진소광)/"
"2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx",
}
HOPYOS = {"봉화": 45, "영월": 23, "대흥설계": 34, "대흥착공": 34, "영덕": 9, "소광": 25}
LOCKED = {
"장수": "02.산불 장수/01.내역서/"
"00.단가변경_26년_산불_무주(장수,장수,덕산,산54-1)내역+수량.xlsx",
"진안": "03.산불진안/01.내역서/00.단가변경_26년_산불_무주(진안,백운,백암,산1-2)내역+수량.xlsx",
}
def _path(tag: str) -> Path:
return DOCS / {**FILES, **LOCKED}[tag]
@pytest.fixture(scope="module")
def books() -> dict[str, dict]:
return {tag: hx.read_book(_path(tag).name, _path(tag).read_bytes()) for tag in FILES}
def _lines(book: dict) -> list[dict]:
return [x for h in book["호표"] for x in h["줄"]]
def test_여섯_파일_호표_수(books):
assert {tag: len(b["호표"]) for tag, b in books.items()} == HOPYOS
for book in books.values():
assert (book["꼴"], book["시트"]) == ("STmate", "일위대가표")
assert book["검산"] == {"어긋남": [], "총괄표": 0}
assert len(_lines(books["대흥설계"])) == len(_lines(books["대흥착공"])) == 171 # 같은 설계
def test_봉화_줄_갈래(books):
bong = books["봉화"]
lines = _lines(bong)
kinds = {k: sum(1 for x in lines if x["갈래"] == k) for k in ("단가산출", "노임", "호표", "%")}
assert kinds == {"단가산출": 103, "노임": 30, "호표": 22, "%": 7}
assert len(lines) + sum(len(h.get("근거글") or []) for h in bong["호표"]) == 216
assert all(x["코드"] for x in lines)
for book in books.values():
for x in _lines(book):
if x["갈래"] == "호표":
assert x["참조"]["번"] in {h["번"] for h in book["호표"]}
if x["갈래"] == "단가산출":
assert x["참조"] == {"종류": "단산", "번": int(x["코드"][1:]), "코드": x["코드"]}
assert "근거" in x # 블록 모두 이어짐
by = {h["번"]: h for h in bong["호표"]}
assert by[13]["근거글"] == ["건설표준품셈 7-2-2(찰붙임)"] # 코드 없는 줄 = 호표 근거글
assert hb.sources(" ".join(by[13]["근거글"])) == [["건설품셈", "7-2-2"]]
def test_영월_줄과_블록(books):
yw = books["영월"]
one = yw["호표"][0]
assert (one["번"], one["코드"], one["이름"], one["단위"]) == (
1,
"B00260",
"녹생토(종자배합)",
"M3",
)
assert one["행"] == [5, 10] and one["금액"]["계"] == 155184
assert [(x["이름"], x["코드"], x["갈래"], x["수량"]) for x in one["줄"]] == [
("산림종자", "M00024", "자재", 0.25),
("산림종자", "M00211", "자재", 0.25),
("인공토양(녹생토)", "M00832", "자재", 1.1),
("보통인부", "L00002", "노임", 0.43),
]
two = {x["이름"]: x for x in yw["호표"][1]["줄"]}
assert two["국가지점번호판 검증수수료"]["갈래"] == "별도계상" # 비고 「별산」
assert two["구조물터파기(인력)"]["근거"] == {
"출처": [["건설품셈", "3-1-3"]],
"직종": ["보통인부"],
"행": [5, 54],
}
block = next(b for b in yw["단산"] if b["코드"] == "D00002")
assert (block["번"], block["이름"], block["규격"], block["단위"]) == (
2,
"구조물터파기(인력)",
"경질토사",
"M3",
)
assert block["자원"] == [
{"갈래": "L", "코드": "L00002", "이름": "보통인부", "규격": "", "품": 0.26, "절": ""}
]
assert any("건설표준품셈3-1-3" in t for t in block["글"])
def test_기계_자원_하나로(books):
block = next(b for b in books["봉화"]["단산"] if b["코드"] == "D00002")
machines = [r for r in block["자원"] if r["갈래"] == "X"]
assert len(machines) == 1 # 노무비 · 재료비 · 경비 셋 줄
assert machines[0]["이름"] == "굴삭기(무한궤도)" and machines[0]["규격"] == "0.2㎥"
assert machines[0]["품"] == pytest.approx(0.0636537237529)
def test_노임_이름_어긋남_0(books):
names = hb.trade_names()
for tag in FILES:
sheets = hx._sheets(_path(tag).name, _path(tag).read_bytes())
table = st._lists(sheets)
res = [r for b in books[tag]["단산"] for r in b["자원"] if r["갈래"] == "L" and r["이름"]]
assert res
assert sum(1 for r in res if r["코드"] in table) >= len(res) // 2
for r in res:
if r["코드"] in table: # 목록표에 있는 직종 — X 열 이름과 같음
assert st._flat(r["이름"]) == st._flat(table[r["코드"]]["이름"])
if table.get(r["코드"], {}).get("단위") == "인": # L 코드 % 줄(기구손료) 뺌
assert st._flat(r["이름"]) in names, (tag, r)
labor = [x for x in _lines(books[tag]) if x["갈래"] == "노임"]
assert all(st._flat(x["이름"]) in names for x in labor)
def test_이음_열쇠_코드_칸():
row = [None] * 26
row[12], row[14], row[15] = "별산M00985", "M00985_1", "T7_1"
assert st._code(row) == "M00985"
row = [None] * 26
row[25] = "D01352 →" # O 열 없는 판
assert st._code(row) == "D01352"
row = [None] * 47
row[14], row[13] = "PRA01_1", "_x0007_`COD|PR_x0005_`EXI"
assert st._code(row) == "PRA01"
assert (
st._code(["계", None, None, None, None, None, None, None] + [None] * 6 + ["T7_1"]) == "T7"
)
assert st._code([None] * 12 + ["D96"]) is None # 비고 글은 코드 아님
def test_문서_보안_400(client):
for tag in LOCKED:
data = _path(tag).read_bytes()
with pytest.raises(hx.ExcelError, match="문서 보안"):
hx.read_book(_path(tag).name, data)
res = _post(client, data=data, name=_path(tag).name)
assert res.status_code == 400 and res.json()["detail"] == hx.DRM
def test_여섯_파일_올라감(client):
before = master_hash()
sources = {tag: hashlib.sha256(_path(tag).read_bytes()).hexdigest() for tag in FILES}
for at, tag in enumerate(FILES, 1):
res = _post(client, data=_path(tag).read_bytes(), name=_path(tag).name)
assert res.status_code == 200, res.text
assert res.json()["호표수"] == HOPYOS[tag]
doc = hs.read(layers.personal_dir(7, 42), f"HP-{at:06d}")
assert doc["꼴"] == "STmate"
assert sum(1 for b in doc["단산"] if not b["글"]) <= 3 # 봉화 무대처리 · 흙 쌓 기 = 값 0
assert any(b["자원"] for b in doc["단산"])
first = doc["호표"][0]["번"]
view = client.get(f"{base.URL.format('personal')}/HP-{at:06d}/{first}")
assert view.status_code == 200, view.text # 맞춤 보기가 STmate 줄을 받음
assert {tag: hashlib.sha256(_path(tag).read_bytes()).hexdigest() for tag in FILES} == sources
assert master_hash() == before
def y_ids(book: dict) -> dict[str, dict]:
"""영월 정답 자리 — D · X 줄을 (이름, 규격, 단위, 코드) 고유로 처음 나온 차례 →
`{"Y000": {호표, 차례, 열쇠}}`(338 실험 C1 `s09_stmate` 와 같은 번)."""
out: dict[str, dict] = {}
seen: set[tuple] = set()
for hopyo in book["호표"]:
for line in hopyo["줄"]:
key = (line["이름"], line["규격"], line["단위"], line["코드"])
if line["코드"][0] in "DX" and key not in seen:
seen.add(key)
out[f"Y{len(out):03d}"] = {"호표": hopyo["번"], "차례": line["차례"], "열쇠": key}
return out
def test_영월_정답_자리(books):
got = y_ids(books["영월"])
assert len(got) == 36 # 정답 34 + 우리에 없음 둘(Y004 · Y007)
assert got["Y000"] == {
"호표": 2,
"차례": 1,
"열쇠": ("무근콘크리트타설", "인력운반타설", "M3", "D02058"),
}
assert got["Y005"]["열쇠"] == ("크레인(타이어)", "10톤", "시간", "X02104")
assert got["Y010"]["열쇠"][3] == "D00060" and got["Y035"]["열쇠"][3] == "D01810"
by = {h["번"]: h for h in books["영월"]["호표"]}
for one in got.values():
line = by[one["호표"]]["줄"][one["차례"]]
assert (line["이름"], line["규격"], line["단위"], line["코드"]) == one["열쇠"]
assert line["갈래"] == "기계" or "근거" in line # 단산 줄은 블록 이음