Files
Aislo/resources/tester/test_stmate_report_coll.py
T

175 lines
7.6 KiB
Python

"""STMATE 재현 앱 보고서 2차(G5) — 집계표 · 단가산출근거 펼침 · INITIAL 시트를 원본 STMATE 엑셀과 셀 대조.
정답 = 실무 STC 옆 원본 출력 엑셀(계산된 값). 코드에 정답값을 박지 않음.
앱 쪽 = STC 불러오기 → 연쇄 재계산 → 보고서 묶음(report_book · report_coll · report_san · report_init).
울진 = 「변경 1차분」 STC 의 당초 차수 ↔ 착공 때 내보낸 엑셀 — 코드 번호가 뒤에 바뀌어 코드 비고 칸은 대조 밖.
빼는 칸: 원본 부속 칸(STmate 표지·바로가기·숨은 메타) · 파일 자리·작성자·날짜(INITIAL) · 앞뒤 빈칸.
"""
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent))
from test_stmate_report_sheets import FULL, book, cell, norm, original # noqa: E402
COLL = {
"재료비수량금액집계표": "ABCDEFGH",
"노무비수량금액집계표": "ABCDEFGH",
"경비수량금액집계표": "ABCDEFGH",
"중기시간금액집계표": "ABCDEFGHIJ",
}
# 울진 착공 엑셀에만 있는 집계 — 코드 비고 칸(H·I·J) 빼고
ULJIN = {
"일식견적목록표": "ABCDEFGH",
"재료비수량금액집계표": "ABCDEFG",
"노무비수량금액집계표": "ABCDEFG",
"경비수량금액집계표": "ABCDEFG",
"일식견적수량금액집계표": "ABCDEFG",
"중기시간금액집계표": "ABCDEFGHI",
"일위대가수량금액집계표": "ABCDEFGHI",
"단가산출근거수량금액집계표": "ABCDEFGHI",
}
def diffs_of(name, sid, cols, start=0):
mine = book(name)[sid]
orig = original(name, mine["name"])
out = []
for i in range(start, max(len(orig), len(mine["rows"]))):
o = orig[i] if i < len(orig) else []
m = mine["rows"][i] if i < len(mine["rows"]) else {}
for c in cols:
ov, mv = norm(cell(o, c)), norm(m.get(c))
if isinstance(ov, str) and ov.endswith("→"):
continue
if ov != mv:
out.append((f"{c}{i + 1}", ov, mv))
return out, len(orig)
@pytest.mark.parametrize("name", FULL)
@pytest.mark.parametrize("sid", list(COLL))
def test_collection_sheet_matches_original(name, sid):
"""수량금액집계표(0501~0503 · 0505) — 투입 수량 전개(대가 구성 × 산근 식 계수 · 제외 구간 뺌) · 금액 사사오입."""
diffs, n = diffs_of(name, sid, COLL[sid], start=2)
assert n > 4, f"{name} {sid} 원본 시트 비어 있음"
assert not diffs, f"{name} {sid} 불일치 {len(diffs)}건 {diffs[:5]}"
@pytest.mark.parametrize("sid", list(ULJIN))
def test_uljin_collection_and_w_list_match_original(sid):
"""울진 착공 엑셀 — 일식·견적(0104 · 0504) · 일위대가(0506) · 산근(0507) 집계 · 단가는 낙찰률 적용 1칸."""
diffs, n = diffs_of("울진착공", sid, ULJIN[sid], start=2)
assert n > 3, f"울진 {sid} 원본 시트 비어 있음"
assert not diffs, f"울진 {sid} 불일치 {len(diffs)}건 {diffs[:5]}"
@pytest.mark.parametrize("name", FULL)
def test_san_expansion_matches_original(name):
"""단가산출근거(0402) — 식 줄 글(계산값·참조 단가 넣음) · 줄 금액 · 계표시 합 · 쪽 끝 총계 행 자리까지."""
diffs, n = diffs_of(name, "단가산출근거", "ABCDEF", start=2)
assert n > 1000
assert not diffs, f"{name} 단가산출근거 불일치 {len(diffs)}건 {diffs[:5]}"
@pytest.mark.parametrize("name", FULL)
def test_initial_sheet_matches_original(name):
"""〓 INITIAL 〓 — 시트 정보 18칸 열 자리 · 공사정보 · 초기값 · 인쇄정보 · 절 머리 행 번호."""
mine = book(name)["〓 INITIAL 〓"]["rows"]
orig = original(name, "〓 INITIAL 〓")
# 시트 마지막 행(K) — 설계·총괄내역서는 쪽 넘김·출력 단계 선택에 달려 대조 밖
skip = {"D", "C25", "C77", "C78", "K16", "K17"}
bad = []
for i, o in enumerate(orig):
m = mine[i] if i < len(mine) else {}
for c in "BCDEFGHIJKLMNOPQRSTUVWX":
if c in skip and 6 <= i + 1 <= 23 or f"{c}{i + 1}" in skip:
continue
ov, mv = cell(o, c), m.get(c)
if ov in ("", None) and mv in ("", None):
continue
if ov != mv:
bad.append((f"{c}{i + 1}", ov, mv))
assert len(orig) > 70 and not bad, f"{name} INITIAL 불일치 {bad[:5]}"
def test_amount_to_hangul_follows_doc_example():
"""AMTTOHAN — 지식DB 24 §2.4 예 「금 일억삼천사백이십만원정」."""
import json
import subprocess
js = (
"const I=require('./tools/stmate-test/report_init.js');"
"process.stdout.write(JSON.stringify([I.amtToHan(134200000),I.amtToHan(0),I.amtToHan(1010001)]))"
)
root = Path(__file__).resolve().parents[2]
got = json.loads(subprocess.check_output(["node", "-e", js], cwd=root, encoding="utf-8"))
assert got == ["금 일억삼천사백이십만원정", "금 영원정", "금 일백일만일원정"]
def _forms(name, opts):
import json
import subprocess
import server
from test_stmate_report_sheets import DOCS, HELPER, PAIRS
stc, _, stage = PAIRS[name]
load = server.api_load(server.LoadReq(source_path=str(DOCS / stc)))
out = subprocess.check_output(
["node", str(HELPER)],
input=json.dumps(
{"load": load, "stage": stage, "mode": "forms", "opts": opts}, ensure_ascii=False
),
text=True,
encoding="utf-8",
)
return json.loads(out)
def test_forms_without_original_sample_agree_with_engine_totals():
"""견본 없는 양식(부분) — 분할 100% = 설계 합계 · 총괄집계표·간이명세서 끝 = 총공사비 · 구.시도 시기금+구기금 = 합계."""
f = _forms("봉화2024", {"splitRate": 100, "fundRate": 60})
sp = [r for r in f["분할내역서"]["rows"] if r.get("_k") == "final"][0]
assert sp["G"] == sp["I"] and sp["J"] == 100
total = [r for r in f["총괄집계표"]["rows"] if r.get("_k") == "cost"]
assert total[-1]["C"] == f["_grand"] and abs(total[-1]["D"] - 100) < 1e-9
simple = [r for r in f["간이명세서"]["rows"] if r.get("_k") == "cost"]
assert simple[-1]["B"] == f["_grand"]
fund = [r for r in f["설계내역서구시도"]["rows"] if r.get("_k") == "item"]
assert fund and all(r["E"] + r["F"] == r["G"] for r in fund)
# 공종별 재료비 집계(0675) 금액 합 = 재료비 수량금액집계표(0501) 수량 기반 — 같은 전개에서 나옴
mat = [r for r in f["재료비집계표"]["rows"] if r.get("_k") == "item"]
assert mat and all(r["C"] for r in mat)
def test_xlsx_formulas_and_print_options():
"""엑셀 수식 넣기(Prn_FMLA_INFO) — 집계 ROUND · 내역 ROUNDDOWN 수식 · 인쇄 옵션(글꼴·방향)."""
from io import BytesIO
import openpyxl
import server
from fastapi.testclient import TestClient
client = TestClient(server.app)
bk = book("봉화2024")
sheets = [bk["재료비수량금액집계표"], bk["설계내역서"]]
body = {"sheets": sheets, "filename": "f.xlsx", "formulas": True}
body["print"] = {"font": "돋움체", "orient": "portrait"}
res = client.post("/api/report/xlsx", json=body)
assert res.status_code == 200
wb = openpyxl.load_workbook(BytesIO(res.content))
ws = wb[sheets[0]["name"]]
assert ws["G4"].value == "=ROUND(D4*F4,0)" and ws["B4"].font.name == "돋움체"
assert ws.page_setup.orientation == "portrait"
d = wb[sheets[1]["name"]]
fml = [
c.value
for row in d.iter_rows()
for c in row
if isinstance(c.value, str) and "ROUNDDOWN" in c.value
]
assert fml and fml[0].startswith("=ROUNDDOWN(")