- 요율 화면 오른쪽 위 보기 단추 둘(기본 시트 · 세션 기억) — 한 번에 하나만 보임 - 우클릭: 리스트 줄 「시트에서 보기」 · 시트 칸 「리스트에서 보기」 · 대응 없으면 흐림 + 대응 요율 없음(메뉴 부품에 disabled 항목 · 시트 부품에 cellMenu 옵션) - 시트 칸 → RT 줄 찾기 순수 함수(UI_RateSheet_Map.ts) · 병합 칸 보정 · 겹치면 좁은 범위 - 책 대응 범위를 상자 표제 칸까지 넓힘(RT000004 값 칸 BR51 누락 바로잡음) — 값 · RT 정본 그대로 - 대조 스크립트 audit_요율대조.py(RT 잎 216 · 시트 칸 없음 8 · 시트에만 있는 칸 1 · B09 사용 표시) - 시험: test_805_1_rate_views 신설 · test_798_1 대응표 전수 추가 · 옛 「줄 누르면 이동」은 우클릭 메뉴로 대체 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
165 lines
5.8 KiB
Python
165 lines
5.8 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"]
|
|
|
|
|
|
def test_mapping_table_covers_every_rt_row(data):
|
|
"""805-1 대응표 전수 — 부가세 말고 모든 RT 줄 · 범위 안에 값 칸 있음(연금은 BR51 까지)."""
|
|
rates = json.loads(builder.rate_check.RATE_JSON.read_text(encoding="utf-8"))
|
|
assert set(data["대응"]) == {r["키"] for r in rates["줄"]} - {"RT000019"}
|
|
cells = _main(data)["칸"]
|
|
for key, spot in data["대응"].items():
|
|
first, _, last = spot["범위"].partition(":")
|
|
assert first == spot["칸"], key
|
|
in_range = [
|
|
a for a in cells if _in_range(a, spot["범위"]) and cells[a].get("값") is not None
|
|
]
|
|
assert in_range, key
|
|
assert data["대응"]["RT000004"]["범위"] == "BR47:BR51"
|
|
|
|
|
|
def _in_range(a1: str, rng: str) -> bool:
|
|
import re
|
|
|
|
def split(x: str) -> tuple[int, int]:
|
|
m = re.fullmatch(r"([A-Z]+)(\d+)", x)
|
|
col = 0
|
|
for ch in m.group(1):
|
|
col = col * 26 + ord(ch) - 64
|
|
return int(m.group(2)), col
|
|
|
|
(r0, c0), (r1, c1) = (split(p) for p in (rng.split(":") + rng.split(":"))[:2])
|
|
r, c = split(a1)
|
|
return r0 <= r <= r1 and c0 <= c <= c1
|