knowledge(master_data): 요율 갱신절차 구멍 메움 - build_요율.py 대조기 추가
조달청제비율 XLSX·국민연금법 XML 원문을 요율_조달청제비율.json 과 셀 단위로 대조하는 --check 전용 스크립트 신설(정본 쓰기 기능 없음) · 지금 원문·정본 전부 일치 확인 · 국민연금 2033년 이후 요율은 법 부칙(2026~2032만 명시)에 없는 추정치임을 참고로 남김 · _갱신절차.md 요율 줄·구멍 목록 갱신 · 시험 3건 추가.
This commit is contained in:
@@ -113,8 +113,8 @@
|
||||
| 출처 | 조달청제비율 · `행정규칙/토목공사원가계산 제비율 적용기준/첨부/*조달청_토목공사_제비율.xlsx` + 국민연금법 `법률/국민연금법/현행_*.xml` |
|
||||
| 받는 법 | 조달청 고시 첨부 XLSX · 법령 XML 손으로 (`_pipeline/collect_law.py` 는 법령 쪽) |
|
||||
| 주기 | 고시 개정 때(연 1~2회) · 국민연금 요율은 매년 |
|
||||
| 빌더 | 빌더 없음 (`scripts/_migrate_base.py` 는 옛 파일 한 번 옮기기) |
|
||||
| 검사 | `check_master.py 틀 요율_조달청제비율.json` |
|
||||
| 빌더 | `scripts/build_요율.py --check`(대조만 · 정본 안 씀 · 다른 줄은 원문 셀 자리와 같이 나옴) — `scripts/_migrate_base.py` 는 옛 파일 한 번 옮기기 |
|
||||
| 검사 | `build_요율.py --check` · `check_master.py 틀 요율_조달청제비율.json` |
|
||||
|
||||
## 로직 (`로직_<원문>_<NN>장_….json`)
|
||||
|
||||
@@ -129,7 +129,8 @@
|
||||
|
||||
## 구멍 목록
|
||||
|
||||
- 요율 · 품셈재료 · 로직 — 빌더 없음.
|
||||
- 품셈재료 · 로직 — 빌더 없음.
|
||||
- 요율 — 대조(`--check`)만 됨 · 값을 정본에 반영하는 쓰기는 여전히 손으로.
|
||||
- 기계 산림품셈 · 소요량 건설 건축·유지관리 · 소요량 산림 전 장 · 계수 건설 전 장 · 계수 산림 1장 밖 — 빌더 없음.
|
||||
- 자재품목 — 나라장터 스냅샷 → 마스터 줄 빌더 없음 · 유료 물가지 받는 길 미정.
|
||||
- 유가전력 — 휘발유·경유 새 판 반영 없음.
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""조달청제비율 XLSX·국민연금법 XML → 요율_조달청제비율.json 대조 (읽기 전용 · 쓰기 기능 없음).
|
||||
|
||||
돌리기: ./venv/Scripts/python.exe resources/master_data/scripts/build_요율.py --check
|
||||
(정본 JSON 을 덮는 기능 없음 — 값 자체는 조달청 고시·국민연금법 조문을 사람이 그대로 옮긴 값이라
|
||||
자동 조립·저장은 하지 않고, 원문과 정본이 같은지 셀 단위로 대조만 한다.)
|
||||
|
||||
숫자만 대조 — 「왜」·「비고」 같은 설명 문구는 대상 아님.
|
||||
브래킷 표는 라벨 문자열을 다시 만들지 않고, 원문의 행 순서와 JSON 줄 순서가 같다는 전제로
|
||||
자리(위치)로 맞춰 비교한다 — 순서가 이미 눈으로 확인됨(이 파일 작성 시점 기준).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
RATE_JSON = ROOT / "resources/master_data/요율_조달청제비율.json"
|
||||
XLSX_PATH = (
|
||||
ROOT
|
||||
/ "resources/knowledge/original/행정규칙/토목공사원가계산 제비율 적용기준/첨부/2026.04.13_조달청_토목공사_제비율.xlsx"
|
||||
)
|
||||
PENSION_XML = ROOT / "resources/knowledge/original/법률/국민연금법/현행_20260617.xml"
|
||||
SHEET = "26. 4. 13."
|
||||
|
||||
|
||||
def num(text) -> float | None:
|
||||
if text is None:
|
||||
return None
|
||||
if isinstance(text, (int, float)):
|
||||
return float(text)
|
||||
m = re.findall(r"[-+]?[0-9]+(?:\.[0-9]+)?", str(text).replace(",", ""))
|
||||
return float(m[-1]) if m else None
|
||||
|
||||
|
||||
def close(a, b, tol=1e-6) -> bool:
|
||||
if a is None or b is None:
|
||||
return a == b
|
||||
return abs(float(a) - float(b)) <= tol
|
||||
|
||||
|
||||
class Diffs:
|
||||
def __init__(self):
|
||||
self.items: list[str] = []
|
||||
|
||||
def check(self, where: str, expect, got):
|
||||
e, g = num(expect) if not isinstance(expect, (list, tuple)) else expect, got
|
||||
if isinstance(expect, (list, tuple)):
|
||||
for i, (ev, gv) in enumerate(zip(expect, got)):
|
||||
if not close(num(ev), num(gv)):
|
||||
self.items.append(f"{where}[{i}]: 원문 {ev} != 정본 {gv}")
|
||||
if len(expect) != len(got):
|
||||
self.items.append(f"{where}: 개수 다름 원문 {len(expect)} != 정본 {len(got)}")
|
||||
return
|
||||
if not close(e, g):
|
||||
self.items.append(f"{where}: 원문 {expect} != 정본 {got}")
|
||||
|
||||
def note(self, text: str):
|
||||
self.items.append(f"[참고] {text}")
|
||||
|
||||
|
||||
def load_ws():
|
||||
import openpyxl
|
||||
|
||||
wb = openpyxl.load_workbook(XLSX_PATH, data_only=True)
|
||||
return wb[SHEET]
|
||||
|
||||
|
||||
def load_json() -> dict:
|
||||
return json.loads(RATE_JSON.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def row_by_key(data: dict, key: str) -> dict:
|
||||
for r in data["줄"]:
|
||||
if r["키"] == key or r.get("원문번호") == key:
|
||||
return r
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
# ---- 스칼라 요율 (법정 보험료 계열) --------------------------------------
|
||||
|
||||
|
||||
def check_scalars(ws, data: dict, d: Diffs):
|
||||
scalars = [
|
||||
("RT000001", "BZ51", "산재보험료"),
|
||||
("RT000003", "AU51", "건강보험료"),
|
||||
("RT000005", "BD51", "노인장기요양보험료"),
|
||||
("RT000011", "AU110", "퇴직공제부금비"),
|
||||
("RT000012", "CS99", "임금채권보장분담금"),
|
||||
("RT000013", "BZ99", "석면피해구제분담금"),
|
||||
]
|
||||
for key, cell, name in scalars:
|
||||
row = row_by_key(data, key)
|
||||
src = num(ws[cell].value)
|
||||
got = row["값"].get("rate_percent")
|
||||
d.check(f"{name}({key} {cell})", src, got)
|
||||
|
||||
|
||||
# ---- 고용보험료 등급별 -----------------------------------------------------
|
||||
|
||||
|
||||
def check_goyong(ws, data: dict, d: Diffs):
|
||||
row = row_by_key(data, "RT000002")
|
||||
brackets = row["값"]["brackets"]
|
||||
by_grade = {b["grade"]: b for b in brackets if isinstance(b.get("grade"), int)}
|
||||
grade_cells = [
|
||||
(1, "AH67"),
|
||||
(2, "AH69"),
|
||||
(3, "AH71"),
|
||||
(4, "AH73"),
|
||||
(5, "AH75"),
|
||||
(6, "AH77"),
|
||||
(7, "AH79"),
|
||||
]
|
||||
for grade, cell in grade_cells:
|
||||
src = num(ws[cell].value)
|
||||
got = by_grade.get(grade, {}).get("rate_percent")
|
||||
d.check(f"고용보험료 {grade}등급({cell})", src, got)
|
||||
|
||||
|
||||
# ---- 국민연금 — 국민연금법 부칙 제4조① -----------------------------------
|
||||
|
||||
|
||||
def check_pension(data: dict, d: Diffs):
|
||||
text = PENSION_XML.read_text(encoding="utf-8")
|
||||
# 제4조(연금보험료에 관한 특례) ① 항 — 연도별 "1만분의 NNN"
|
||||
m = re.search(r"연금보험료에 관한 특례.*?(?=제5조)", text, re.S)
|
||||
if not m:
|
||||
d.note("국민연금법 XML에서 부칙 제4조를 못 찾음 — 대조 건너뜀")
|
||||
return
|
||||
body = m.group(0)
|
||||
found = dict(re.findall(r"(20\d\d)년은 1만분의 (\d+)", body))
|
||||
row = row_by_key(data, "RT000004")
|
||||
annual = {r["year"]: r["rate_percent"] for r in row["값"]["annual_rates"]}
|
||||
for year_str, val_str in found.items():
|
||||
year = int(year_str)
|
||||
src_rate = int(val_str) / 100 # 1만분의 475 = 4.75%
|
||||
got = annual.get(year)
|
||||
d.check(f"국민연금보험료 {year}년(부칙 제4조①)", src_rate, got)
|
||||
max_year_in_law = max(int(y) for y in found)
|
||||
unseen = [y for y in annual if y > max_year_in_law]
|
||||
if unseen:
|
||||
d.note(
|
||||
f"국민연금보험료 {sorted(unseen)}년 값은 법 부칙(2026~{max_year_in_law}년만 명시)에 없음 — "
|
||||
f"정본의 rate_from_{unseen[0]}_percent={annual[unseen[0]]} 는 뒤 연도 추정치, 개정 시 재확인 필요"
|
||||
)
|
||||
|
||||
|
||||
# ---- 산업안전보건관리비 -----------------------------------------------------
|
||||
|
||||
|
||||
def check_safety(ws, data: dict, d: Diffs):
|
||||
row = row_by_key(data, "RT000006")
|
||||
brackets = row["값"]["brackets"]
|
||||
groups = [
|
||||
("lt_500_million", [19, 21, 23, 25], None),
|
||||
("500_million_to_5_billion", [27, 29, 31, 33], "FA"),
|
||||
("gte_5_billion_below_manager_threshold", [35, 37, 39, 41], None),
|
||||
("gte_5_billion_at_or_above_manager_threshold", [43, 45, 47, 49], None),
|
||||
]
|
||||
work_types = ["building", "civil", "heavy_construction", "special_construction"]
|
||||
idx = 0
|
||||
for amount_bracket, rows, base_col in groups:
|
||||
for i, r in enumerate(rows):
|
||||
work_type = work_types[i]
|
||||
src_rate = num(ws[f"ES{r}"].value)
|
||||
match = next(
|
||||
b
|
||||
for b in brackets
|
||||
if b["target_amount_bracket"] == amount_bracket and b["work_type"] == work_type
|
||||
)
|
||||
d.check(
|
||||
f"산업안전보건관리비 {amount_bracket}/{work_type}(ES{r})",
|
||||
src_rate,
|
||||
match["rate_percent"],
|
||||
)
|
||||
if base_col:
|
||||
src_base_cheonwon = num(ws[f"{base_col}{r}"].value)
|
||||
src_base = (
|
||||
src_base_cheonwon * 1000 if src_base_cheonwon is not None else None
|
||||
) # 원문 단위=천원
|
||||
d.check(
|
||||
f"산업안전보건관리비 기초액 {amount_bracket}/{work_type}(FA{r})",
|
||||
src_base,
|
||||
match.get("base_amount_krw"),
|
||||
)
|
||||
idx += 1
|
||||
|
||||
|
||||
# ---- 간접노무비 · 기타경비 (공통 5규모 x 4기간 x 3공종) --------------------
|
||||
|
||||
SCALE_ROWS = [
|
||||
("lt_1_billion", 15),
|
||||
("1_to_5_billion", 23),
|
||||
("5_to_30_billion", 31),
|
||||
("30_to_100_billion", 39),
|
||||
("gte_100_billion", 47),
|
||||
]
|
||||
DURATION_OFFSETS = [
|
||||
("lte_183_days", 0),
|
||||
("184_to_365_days", 2),
|
||||
("366_to_1095_days", 4),
|
||||
("gte_1096_days", 6),
|
||||
]
|
||||
WORK_TYPES_3 = ["civil", "landscape", "industrial_facilities_civil"]
|
||||
|
||||
|
||||
def check_labor_expense_table(ws, brackets, cols, label, d: Diffs):
|
||||
by_key = {
|
||||
(b["direct_cost_bracket"], b["duration_bracket"], b["work_type"]): b for b in brackets
|
||||
}
|
||||
for scale_key, start_row in SCALE_ROWS:
|
||||
for dur_key, offset in DURATION_OFFSETS:
|
||||
r = start_row + offset
|
||||
for work_type, col in zip(WORK_TYPES_3, cols):
|
||||
src = num(ws[f"{col}{r}"].value)
|
||||
got = by_key.get((scale_key, dur_key, work_type), {}).get("rate_percent")
|
||||
d.check(f"{label} {scale_key}/{dur_key}/{work_type}({col}{r})", src, got)
|
||||
|
||||
|
||||
def check_indirect_labor(ws, data: dict, d: Diffs):
|
||||
row = row_by_key(data, "RT000008")
|
||||
check_labor_expense_table(ws, row["값"]["brackets"], ["AC", "AF", "AI"], "간접노무비", d)
|
||||
|
||||
|
||||
def check_other_expense(ws, data: dict, d: Diffs):
|
||||
row = row_by_key(data, "RT000009")
|
||||
check_labor_expense_table(ws, row["값"]["brackets"], ["AL", "AO", "AR"], "기타경비", d)
|
||||
|
||||
|
||||
# ---- 환경보전비 ------------------------------------------------------------
|
||||
|
||||
|
||||
def check_environment(ws, data: dict, d: Diffs):
|
||||
row = row_by_key(data, "RT000010")
|
||||
all_types = {t["work_type"]: t["rate_percent"] for t in row["값"]["all_work_types"]}
|
||||
cells = [
|
||||
("civil_road", "BU68"),
|
||||
("civil_plant", "BU70"),
|
||||
("civil_subway", "BU72"),
|
||||
("civil_railway", "BU74"),
|
||||
("civil_water_and_sewer", "BU76"),
|
||||
("civil_port", "BU78"),
|
||||
("civil_port_with_silt_screen", "BU80"),
|
||||
("civil_dam", "BU82"),
|
||||
("civil_land_development", "BU84"),
|
||||
("civil_other", "BU86"),
|
||||
("landscape", "BU88"),
|
||||
("building_housing_redevelopment", "BU90"),
|
||||
("building_new_housing", "BU92"),
|
||||
("building_other", "BU94"),
|
||||
]
|
||||
for work_type, cell in cells:
|
||||
src = num(ws[cell].value)
|
||||
d.check(f"환경보전비 {work_type}({cell})", src, all_types.get(work_type))
|
||||
|
||||
|
||||
# ---- 하도급대금 지급보증 수수료 --------------------------------------------
|
||||
|
||||
|
||||
def check_subcontract_guarantee(ws, data: dict, d: Diffs):
|
||||
row = row_by_key(data, "RT000014")
|
||||
brackets = row["값"]["brackets"]
|
||||
cells = ["BZ68", "BZ71", "BZ74", "BZ77", "BZ80", "BZ83"]
|
||||
rate_cells = ["DC68", "DC71", "DC74", "DC77", "DC80", "DC83"]
|
||||
for i, (bc, rc) in enumerate(zip(cells, rate_cells)):
|
||||
src = num(ws[rc].value)
|
||||
got = brackets[i]["rate_percent"] if i < len(brackets) else None
|
||||
d.check(f"하도급대금 지급보증 {i}번째({rc})", src, got)
|
||||
if len(cells) != len(brackets):
|
||||
d.items.append(f"하도급대금 지급보증: 개수 다름 원문 {len(cells)} != 정본 {len(brackets)}")
|
||||
|
||||
|
||||
# ---- 건설기계대여대금 지급보증 수수료 --------------------------------------
|
||||
|
||||
|
||||
def check_equipment_guarantee(ws, data: dict, d: Diffs):
|
||||
row = row_by_key(data, "RT000015")
|
||||
val = row["값"]
|
||||
general = val["general_construction"]
|
||||
general_cells = ["CI19", "CI23", "CI27"]
|
||||
for i, cell in enumerate(general_cells):
|
||||
src = num(ws[cell].value)
|
||||
got = general[i]["rate_percent"] if i < len(general) else None
|
||||
d.check(f"건설기계대여대금(종합) {i}번째({cell})", src, got)
|
||||
|
||||
specialty = val["specialty_construction"]
|
||||
specialty_cells = ["DE19", "DE23", "DE27", "DE31", "DE37"]
|
||||
for i, cell in enumerate(specialty_cells):
|
||||
src = num(ws[cell].value)
|
||||
got = specialty[i]["rate_percent"] if i < len(specialty) else None
|
||||
d.check(f"건설기계대여대금(전문) {i}번째({cell})", src, got)
|
||||
|
||||
|
||||
# ---- 공사이행보증 수수료 (구간별 산식) --------------------------------------
|
||||
|
||||
|
||||
def parse_formula_numbers(text: str) -> tuple[float, float, float] | None:
|
||||
"""(기초액, 문턱값, 퍼센트) — 만원·억원 단위를 원 단위로 바꾼 뒤 뽑음."""
|
||||
if text is None:
|
||||
return None
|
||||
t = str(text)
|
||||
t = re.sub(r"([\d,]+)억원", lambda m: str(int(float(m.group(1).replace(",", "")) * 1e8)), t)
|
||||
t = re.sub(r"([\d,]+)만원", lambda m: str(int(float(m.group(1).replace(",", "")) * 1e4)), t)
|
||||
pct = re.search(r"([\d.]+)\s*%", t)
|
||||
if not pct:
|
||||
return None
|
||||
rate = float(pct.group(1))
|
||||
base = re.search(r"(?<![\d.])(\d{4,})\s*\+", t)
|
||||
base_amt = float(base.group(1)) if base else 0.0
|
||||
thresh = re.search(r"-\s*(\d{4,})\s*\)", t)
|
||||
thresh_amt = float(thresh.group(1)) if thresh else 0.0
|
||||
return base_amt, thresh_amt, rate
|
||||
|
||||
|
||||
def check_performance_guarantee(ws, data: dict, d: Diffs):
|
||||
row = row_by_key(data, "RT000016")
|
||||
brackets = row["값"]["brackets"]
|
||||
cells = ["P98", "P101", "P104", "P107", "P110"]
|
||||
for i, cell in enumerate(cells):
|
||||
src = parse_formula_numbers(ws[cell].value)
|
||||
got_formula = brackets[i]["formula"] if i < len(brackets) else None
|
||||
got = parse_formula_numbers(got_formula)
|
||||
where = f"공사이행보증 {i}번째({cell})"
|
||||
if src is None or got is None:
|
||||
d.items.append(f"{where}: 산식 못 읽음 원문={ws[cell].value!r} 정본={got_formula!r}")
|
||||
continue
|
||||
for label, sv, gv in zip(("기초액", "문턱값", "퍼센트"), src, got):
|
||||
if not close(sv, gv, tol=1e-4):
|
||||
d.items.append(f"{where} {label}: 원문 {sv} != 정본 {gv}")
|
||||
|
||||
|
||||
# ---- 일반관리비 · 이윤 ------------------------------------------------------
|
||||
|
||||
|
||||
def check_overhead(ws, data: dict, d: Diffs):
|
||||
row = row_by_key(data, "RT000017")
|
||||
val = row["값"]
|
||||
civil = val["civil_landscape_industrial"]
|
||||
civil_cells = ["BD15", "BD31", "BD39", "BD43"]
|
||||
for i, cell in enumerate(civil_cells):
|
||||
src = num(ws[cell].value)
|
||||
got = civil[i]["rate_percent"] if i < len(civil) else None
|
||||
d.check(f"일반관리비(종합) {i}번째({cell})", src, got)
|
||||
|
||||
specialty = val["specialty_electric_communication_fire_other"]
|
||||
specialty_cells = ["BL15", "BL21", "BL27", "BL35"] # 원문에 값 있는 앞 4구간만
|
||||
for i, cell in enumerate(specialty_cells):
|
||||
src = num(ws[cell].value)
|
||||
got = specialty[i]["rate_percent"] if i < len(specialty) else None
|
||||
d.check(f"일반관리비(전문) {i}번째({cell})", src, got)
|
||||
if len(specialty) > len(specialty_cells):
|
||||
d.note(
|
||||
f"일반관리비(전문) 뒤 {len(specialty) - len(specialty_cells)}구간(30~100억·100억이상)은 "
|
||||
"원문에 별도 셀이 없음(BL35 값을 그대로 늘려 쓴 정본 비고와 일치) — 대조 대상 아님"
|
||||
)
|
||||
|
||||
|
||||
def check_profit(ws, data: dict, d: Diffs):
|
||||
row = row_by_key(data, "RT000018")
|
||||
brackets = row["값"]["brackets"]
|
||||
cells = ["BS15", "BS31", "BS39", "BS43"]
|
||||
for i, cell in enumerate(cells):
|
||||
src = num(ws[cell].value)
|
||||
got = brackets[i]["rate_percent"] if i < len(brackets) else None
|
||||
d.check(f"이윤 {i}번째({cell})", src, got)
|
||||
|
||||
|
||||
# ---- 부가가치세 — 원문(제비율 XLSX·국민연금법) 밖의 값 ---------------------
|
||||
|
||||
|
||||
def check_vat(data: dict, d: Diffs):
|
||||
row_by_key(data, "RT000019")
|
||||
d.note(
|
||||
"부가가치세(RT000019)는 부가가치세법 세율 — 이 원문(조달청 제비율·국민연금법) 대상 아님, 건너뜀"
|
||||
)
|
||||
|
||||
|
||||
def run_check() -> Diffs:
|
||||
ws = load_ws()
|
||||
data = load_json()
|
||||
d = Diffs()
|
||||
check_scalars(ws, data, d)
|
||||
check_goyong(ws, data, d)
|
||||
check_pension(data, d)
|
||||
check_safety(ws, data, d)
|
||||
check_indirect_labor(ws, data, d)
|
||||
check_other_expense(ws, data, d)
|
||||
check_environment(ws, data, d)
|
||||
check_subcontract_guarantee(ws, data, d)
|
||||
check_equipment_guarantee(ws, data, d)
|
||||
check_performance_guarantee(ws, data, d)
|
||||
check_overhead(ws, data, d)
|
||||
check_profit(ws, data, d)
|
||||
check_vat(data, d)
|
||||
return d
|
||||
|
||||
|
||||
def main() -> None:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument(
|
||||
"--check", action="store_true", help="원문과 정본 대조만(기본값과 같음 — 쓰기 기능 없음)"
|
||||
)
|
||||
ap.parse_args()
|
||||
|
||||
d = run_check()
|
||||
errors = [x for x in d.items if not x.startswith("[참고]")]
|
||||
notes = [x for x in d.items if x.startswith("[참고]")]
|
||||
for n in notes:
|
||||
print(n)
|
||||
if errors:
|
||||
print(f"다름 — {len(errors)}건")
|
||||
for e in errors:
|
||||
print(" ", e)
|
||||
sys.exit(1)
|
||||
print("같음 — 대조 대상 전부 원문과 일치")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,44 @@
|
||||
"""build_요율.py 시험 — 2026-09-20 브레인 일감(랩탑 메인).
|
||||
|
||||
못박는 것
|
||||
- 지금 원문(XLSX·국민연금법 XML)과 정본 요율_조달청제비율.json 이 같음(회귀 기준).
|
||||
- 정본 값을 하나 비틀면 --check 가 실제로 다름을 잡아냄(대조 로직이 항상 통과만 하는 게 아님).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "resources/master_data/scripts"))
|
||||
|
||||
import build_요율 as bu # noqa: E402
|
||||
|
||||
|
||||
def test_지금_원문과_정본이_같음() -> None:
|
||||
d = bu.run_check()
|
||||
errors = [x for x in d.items if not x.startswith("[참고]")]
|
||||
assert not errors, f"원문과 정본이 다름 — {errors}"
|
||||
|
||||
|
||||
def test_값을_비틀면_다름을_잡아냄() -> None:
|
||||
ws = bu.load_ws()
|
||||
data = copy.deepcopy(bu.load_json())
|
||||
row = bu.row_by_key(data, "RT000001")
|
||||
row["값"]["rate_percent"] = row["값"]["rate_percent"] + 1 # 산재보험료를 일부러 어긋냄
|
||||
d = bu.Diffs()
|
||||
bu.check_scalars(ws, data, d)
|
||||
assert any("산재보험료" in x for x in d.items)
|
||||
|
||||
|
||||
def test_고용보험료_등급_어긋내면_잡아냄() -> None:
|
||||
ws = bu.load_ws()
|
||||
data = copy.deepcopy(bu.load_json())
|
||||
row = bu.row_by_key(data, "RT000002")
|
||||
grade1 = next(b for b in row["값"]["brackets"] if b.get("grade") == 1)
|
||||
grade1["rate_percent"] = 0.0
|
||||
d = bu.Diffs()
|
||||
bu.check_goyong(ws, data, d)
|
||||
assert any("1등급" in x for x in d.items)
|
||||
Reference in New Issue
Block a user