- B09_Estimation_Stc — DBF 필드 이름으로 읽음 · 묶음 T · 소계 E · 제외 C · 호표 번 = 같은 글자 코드 정렬 차례 - 다공구(BOQ12) · 변경 차수 = 당초만 읽고 알림 · BDQTY · COSTN.SMT · 요율 = UNKNOWN(같은 공사 엑셀로) - Router_Import 이름 끝 갈림(.estx · .stc) · 미리 보기 알림 · UI_Import 파일 고르기 *.stc · 창 알림 줄 - 시험 test_707_6_stc 16 — 실무 STC 일곱 직접비 = PRJLST · 기번3 · 영월 · 봉화 엑셀과 줄마다 같음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
175 lines
8.2 KiB
Python
175 lines
8.2 KiB
Python
"""707-6 STmate 원본(.STC) 바로 읽기 — `B09_Estimation_Stc` (읽기만 · 저장 안 함).
|
|
|
|
- 실무 .STC 일곱(지식DB 실무문서) = 항목 수 · 직접비 · 제외 · 노임 · 자재 · 기계 · 호표 목록 줄 수 ·
|
|
직접비 = 파일 안 공사 금액(PRJLST TCOST1)
|
|
- 기번3 · 영월 · 봉화 = 같은 공사 STmate 엑셀(707-1 · 707-3)과 줄마다 같음
|
|
(이름 · 규격 · 수량 · 단가 · 금액 · 호표 코드 · 번 · 제외 · 묶음 나무 · 단가표)
|
|
- 다공구(BOQ12) · 변경 차수 = 당초만 읽고 알림 · 707-1 짜기 그릇에 그대로 부음
|
|
- 깨진 파일 · 표 없음 = 막힘 · 불러오기 길 = 이름 끝 .STC 갈림
|
|
"""
|
|
|
|
import io
|
|
import sys
|
|
import time
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from B09_Estimation import B09_Estimation_Bill_Import as imp # noqa: E402
|
|
from B09_Estimation import B09_Estimation_Import_Prices as ip # noqa: E402
|
|
from B09_Estimation import B09_Estimation_Stc as st # noqa: E402
|
|
from B09_Estimation import B09_Estimation_Store as store # noqa: E402
|
|
from B09_Estimation.B09_Estimation_Bill_Excel import read_bill as read_excel # noqa: E402
|
|
from B09_Estimation.B09_Estimation_Calc_Bill import compute_estimation # noqa: E402
|
|
|
|
K = ROOT / "resources" / "knowledge" / "original" / "실무문서"
|
|
G3 = "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3)"
|
|
YW = "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월"
|
|
BH = "04.산불 봉화/01. 내역서/{}2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km"
|
|
SG = "25년 산불진화임도(기번8)(울진소광)/"
|
|
NEW = "2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외)"
|
|
FILES = {
|
|
"기번3": K / f"{G3}.STC",
|
|
"영월": K / f"{YW}.STC",
|
|
"봉화": K / f"{BH.format('13-')}.STC",
|
|
"계류보전": K / "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).STC", # noqa: E501
|
|
"소광": K / f"{SG}1. 설계원본/03-내역/22-2025년 산불진화임도사업(울진 금강송 소광).STC",
|
|
"소광다공구": K / f"{SG}{NEW}.STC",
|
|
"소광변경": K / f"{SG}변경 1차분-{NEW}.STC",
|
|
} # fmt: skip
|
|
EXCEL = {"기번3": K / f"{G3}.xlsx", "영월": K / f"{YW}.xlsx", "봉화": K / f"{BH.format('')}.xlsx"}
|
|
#: (항목 수, 직접비, 제외, 노임, 자재, 기계, 대가, 산근)
|
|
WANT = {
|
|
"기번3": (136, 710359136, 69474220, 12, 39, 25, 34, 46),
|
|
"영월": (108, 767148700, 75263810, 16, 55, 29, 23, 69),
|
|
"봉화": (77, 781283415, 142783280, 19, 67, 25, 45, 65),
|
|
"계류보전": (98, 93027484, 0, 7, 25, 18, 13, 35),
|
|
"소광": (46, 197135038, 53316160, 14, 35, 24, 25, 40),
|
|
"소광다공구": (46, 197135038, 53316160, 14, 41, 24, 37, 62),
|
|
"소광변경": (70, 173311386, 53316160, 14, 40, 31, 39, 54),
|
|
}
|
|
NOTES = {
|
|
"계류보전": ["변경 차수 — 96 줄 있음(당초만 읽음)"],
|
|
"소광다공구": ["다공구 — BOQ12 89 줄 있음(당초 첫 공구 BOQ11 만 읽음)"],
|
|
"소광변경": ["변경 차수 — 93 줄 있음(당초만 읽음)"],
|
|
}
|
|
EMPTY = {"materials": [], "bill": {"items": []}, "hopyo": {}}
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def data():
|
|
return {k: p.read_bytes() for k, p in FILES.items()}
|
|
|
|
|
|
@pytest.mark.parametrize("name", list(FILES))
|
|
def test_실무_stc_줄_수_합계_공사금액(data, name):
|
|
t = time.perf_counter()
|
|
bill = st.read_bill(FILES[name].name, data[name])
|
|
prices = st.read_prices(FILES[name].name, data[name])
|
|
hopyo = st.read_hopyo(FILES[name].name, data[name])
|
|
print(f"{name} 읽기 {time.perf_counter() - t:.3f} s")
|
|
got = (
|
|
sum(1 for x in bill["줄"] if not x["묶음"]),
|
|
bill["합계"]["직접"],
|
|
bill["합계"]["제외"],
|
|
*(len(prices[k]) for k in ("labor", "material", "machine")),
|
|
len(hopyo["대가"]),
|
|
len(hopyo["산근"]),
|
|
)
|
|
assert got == WANT[name]
|
|
assert bill["알림"] == NOTES.get(name, [])
|
|
# 직접비 = 파일 안 공사 금액(첫 공구)
|
|
prj = st.table(st._zip(data[name]), "PRJLST")
|
|
assert bill["합계"]["직접"] == prj[0]["TCOST1"]
|
|
assert all(r["price"] > 0 for r in prices["labor"])
|
|
assert all(h["합계"] == sum(h["단가"].values()) for h in hopyo["대가"] + hopyo["산근"])
|
|
|
|
|
|
@pytest.mark.parametrize("name", list(EXCEL))
|
|
def test_엑셀과_줄마다_같음(data, name):
|
|
mine = st.read_bill("a.STC", data[name])
|
|
excel = read_excel(EXCEL[name].name, EXCEL[name].read_bytes())
|
|
|
|
def key(line, numbered):
|
|
h = line["호표"] or {}
|
|
number = h.get("번") if numbered.get(line["행"]) else None
|
|
return (line["이름"], line["규격"], line["단위"], line["수량"], line["단가"],
|
|
line["금액"], h.get("코드"), number, line["제외"]) # fmt: skip
|
|
|
|
# 번 = 엑셀 비고에 있을 때만 견줌(영월 · 봉화 자재 = 비고에 번 없음)
|
|
has = [bool((x["호표"] or {}).get("번")) for x in excel["줄"]]
|
|
assert len(mine["줄"]) == len(has)
|
|
numbered = [{x["행"]: n for x, n in zip(got["줄"], has)} for got in (mine, excel)]
|
|
items = [[key(x, n) for x in got["줄"]] for got, n in zip((mine, excel), numbered)]
|
|
assert items[0] == items[1]
|
|
|
|
def tree(got):
|
|
names = {x["행"]: x["이름"] for x in got["줄"]}
|
|
return [(x["이름"], names.get(x["위"])) for x in got["줄"]]
|
|
|
|
assert tree(mine) == tree(excel)
|
|
assert mine["합계"] == excel["합계"]
|
|
assert mine["조달수수료율"] == excel["조달수수료율"] == 0.54
|
|
|
|
|
|
@pytest.mark.parametrize("name", ["기번3", "봉화"])
|
|
def test_단가표_엑셀과_같음(data, name):
|
|
mine = st.read_prices("a.STC", data[name])
|
|
excel = ip.read_prices(EXCEL[name].name, EXCEL[name].read_bytes())
|
|
for kind in ("labor", "material", "machine"):
|
|
keys = ("name", "spec", "unit", "price", "labor", "material", "expense")
|
|
rows = [sorted(tuple(r.get(k) for k in keys) for r in got[kind]) for got in (mine, excel)]
|
|
assert rows[0] == rows[1], kind
|
|
|
|
|
|
def test_자재_출처_칸(data):
|
|
stuff = st.read_prices("a.STC", data["봉화"])["material"]
|
|
pipe = next(x for x in stuff if x["name"] == "PVC파이프")
|
|
assert pipe["price"] == 3475
|
|
assert {"출처": "거래가격", "단가": 3475, "쪽": "793"} in pipe["sources"] # 엑셀 자재단가대비표
|
|
assert [s["출처"] for s in pipe["sources"]] == ["물가자료", "물가정보", "거래가격"]
|
|
|
|
|
|
def test_707_1_그릇에_부음(data):
|
|
got = st.read_bill("a.STC", data["봉화"])
|
|
rows = compute_estimation(EMPTY, {}, None, {})["bill"]["rows"]
|
|
made, report = imp.apply(got, {}, rows, "abcd1234")
|
|
made = store.clean_user(made)
|
|
bill = compute_estimation(EMPTY, made, None, {})["bill"]
|
|
assert report["실음"] == len(got["줄"])
|
|
assert bill["totals"]["direct"]["sum"] == 781283415
|
|
assert bill["totals"]["direct"]["blank"] == 0
|
|
assert imp.differences(got, report, bill["rows"]) == []
|
|
assert made["estimate"]["procurement_fee_pct"] == 0.54
|
|
|
|
|
|
def test_깨진_파일_표_없음_막힘():
|
|
with pytest.raises(st.StcError):
|
|
st.read_bill("a.STC", b"not a zip")
|
|
with pytest.raises(st.StcError):
|
|
st.read_bill("a.STC", b"PK\x03\x04 broken")
|
|
buf = io.BytesIO()
|
|
with zipfile.ZipFile(buf, "w") as z:
|
|
z.writestr("COSTN.DBF", b"")
|
|
with pytest.raises(st.StcError, match="BOQ11"):
|
|
st.read_bill("a.STC", buf.getvalue())
|
|
|
|
|
|
def test_불러오기_길_stc_갈림(data):
|
|
"""이름 끝 .STC = 원본 읽기 · 호표 = 목록 수 + 코드로 가리킨 줄(이음 후보)."""
|
|
from B09_Estimation import B09_Estimation_Router_Import as route
|
|
|
|
name = "봉화.stc"
|
|
parsed = route._parse(name, data["봉화"])
|
|
assert parsed["꼴"] == "stc" and parsed["합계"]["직접"] == 781283415
|
|
assert route._hopyo(name, data["봉화"], parsed) == {"대가": 45, "산근": 65, "가리킴": 61}
|
|
assert route._hopyo("a.xlsx", data["봉화"], parsed) is None
|
|
with pytest.raises(route.HTTPException) as got:
|
|
route._parse("a.STC", b"not a zip")
|
|
assert got.value.status_code == 400
|