- build_제비율책.py: 조달청 xlsx → 내장 엑셀 책 JSON(병합 · 채움 · 글꼴 · 테두리 · 열 폭 · 행 높이 · 시트 둘 · 수식 결과) · --check(판 바뀜 · RT 칸 = 정본 값) - 제비율엑셀/조달청_제비율_책.json · /api/m01/rate-sheet · 요율 화면 위 엑셀 + 아래 RT 표 · 확대 · 축소 - 정본 요율_조달청제비율.json · B09 요율 셈 그대로 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
135 lines
4.7 KiB
Python
135 lines
4.7 KiB
Python
"""798-1 M01 요율 제비율 내장 엑셀 — 빌더 결과 · 서식 · RT 칸 = 정본 값 · 판 바뀌면 --check 실패.
|
|
|
|
서버 · 화면 없이 도는 지킴 시험(읽기만 · 정본 JSON 을 쓰지 않음).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import openpyxl
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / "resources/master_data/scripts"))
|
|
import build_제비율책 as builder # noqa: E402
|
|
|
|
from M01_MasterData import M01_MasterData_RateSheet as rate_sheet # noqa: E402
|
|
from M01_MasterData import M01_MasterData_Store as store # noqa: E402
|
|
|
|
MAIN = "26. 4. 13."
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def data() -> dict:
|
|
return json.loads(builder.OUT.read_text(encoding="utf-8"))
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def xlsx():
|
|
wb = openpyxl.load_workbook(builder.XLSX_PATH)
|
|
wv = openpyxl.load_workbook(builder.XLSX_PATH, data_only=True)
|
|
return wb, wv
|
|
|
|
|
|
def _main(data: dict) -> dict:
|
|
return next(s for s in data["책"]["시트"] if s["이름"] == MAIN)
|
|
|
|
|
|
def test_book_matches_xlsx_and_rates():
|
|
"""--check 가 통과 = 저장된 책이 xlsx 를 다시 지은 것과 같고 RT 칸 값이 정본과 같음."""
|
|
assert builder.run_check() == []
|
|
|
|
|
|
def test_sheets_and_merges(data, xlsx):
|
|
wb, _ = xlsx
|
|
names = [s["이름"] for s in data["책"]["시트"]]
|
|
assert names == [ws.title for ws in wb.worksheets] and len(names) == 2
|
|
main = _main(data)
|
|
assert len(main["병합"]) == 433
|
|
assert len(data["책"]["시트"][1]["병합"]) == 7
|
|
assert data["책"]["활성"] == main["id"]
|
|
|
|
|
|
def test_format_survives(data, xlsx):
|
|
wb, _ = xlsx
|
|
book, ws = data["책"], wb[MAIN]
|
|
cells, styles = _main(data)["칸"], data["책"]["서식"]
|
|
fills = {s["채움"] for s in styles if "채움" in s}
|
|
assert fills == {"#FFEBEB", "#EBF7FF", "#EBFFEB", "#FFFF00"}
|
|
bordered = sum(1 for c in cells.values() if "테두리" in styles[c.get("서식", 0)])
|
|
src = sum(
|
|
1
|
|
for row in ws.iter_rows()
|
|
for c in row
|
|
if any(getattr(c.border, s).style for s in ("top", "bottom", "left", "right"))
|
|
)
|
|
assert bordered == src and bordered > 1300
|
|
assert any(s.get("굵게") for s in styles)
|
|
assert any(s.get("글자색") == "#FF0000" for s in styles)
|
|
assert any(s.get("형식") == "0.0" for s in styles)
|
|
assert len(_main(data)["열"]) == 189 # 잔 열 189
|
|
assert _main(data)["열"]["B"]["폭"] == pytest.approx(1.625)
|
|
assert _main(data)["행"]["2"]["높이"] == pytest.approx(62.25)
|
|
assert book["기본"]["글꼴"] == "맑은 고딕"
|
|
|
|
|
|
def test_formula_results_not_formulas(data, xlsx):
|
|
wb, wv = xlsx
|
|
cells = _main(data)["칸"]
|
|
formulas = [
|
|
c.coordinate
|
|
for row in wb[MAIN].iter_rows()
|
|
for c in row
|
|
if isinstance(c.value, str) and c.value.startswith("=")
|
|
]
|
|
assert len(formulas) == 40
|
|
for a1 in formulas:
|
|
assert "식" not in cells[a1]
|
|
assert cells[a1]["값"] == wv[MAIN][a1].value
|
|
|
|
|
|
def test_rt_cells_match_master(data):
|
|
rates = json.loads(builder.rate_check.RATE_JSON.read_text(encoding="utf-8"))
|
|
keys = {r["키"] for r in rates["줄"]}
|
|
assert set(data["대응"]) <= keys and "RT000019" not in data["대응"] # 부가세는 이 엑셀 밖
|
|
assert builder.check_values(data) == []
|
|
cells = _main(data)["칸"]
|
|
for spot in data["대응"].values():
|
|
assert cells[spot["칸"]].get("값") is not None
|
|
|
|
|
|
def test_check_fails_when_edition_changes(tmp_path, monkeypatch, data):
|
|
out = tmp_path / "책.json"
|
|
monkeypatch.setattr(builder, "OUT", out)
|
|
out.write_text(builder.dump(data), encoding="utf-8")
|
|
assert builder.run_check() == []
|
|
# 판이 바뀐 흉내 ① 책의 한 칸 값이 xlsx 와 달라짐
|
|
changed = json.loads(json.dumps(data))
|
|
_main(changed)["칸"]["BZ51"]["값"] = "(노) x 9.99"
|
|
out.write_text(builder.dump(changed), encoding="utf-8")
|
|
errors = builder.run_check()
|
|
assert any("판이 바뀜" in e for e in errors)
|
|
assert any("산재보험료" in e for e in errors) # RT000001 값 대조도 깨짐
|
|
# ② 책 파일이 없음
|
|
out.unlink()
|
|
assert "책 JSON 없음" in builder.run_check()[0]
|
|
|
|
|
|
def test_api_reads_book(data, tmp_path, monkeypatch):
|
|
assert rate_sheet.load()["대응"] == data["대응"]
|
|
monkeypatch.setattr(store, "FOLDER", tmp_path)
|
|
with pytest.raises(store.StoreError) as err:
|
|
rate_sheet.load()
|
|
assert err.value.status == 404
|
|
|
|
|
|
def test_not_picked_up_as_rate_file():
|
|
"""요율 파일 목록(`요율_*.json` 글롭)에 책이 섞이지 않음."""
|
|
names = [p.name for p in store.FOLDER.glob("요율_*.json")] + [
|
|
p.name for p in store.FOLDER.glob("요율.json")
|
|
]
|
|
assert names == ["요율_조달청제비율.json"]
|