Files
Aislo/resources/tester/test_masterdata_check.py
T
eomsangdon 1c58974935 Merge remote-tracking branch 'origin/sub_laptop_1' into sub_laptop_2
# Conflicts:
#	resources/master_data/로직_건설품셈_공통8장.json
#	resources/master_data/재료_자체.json
2026-09-19 19:16:22 +09:00

460 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""마스터 데이터 검사 도구 — 견본 셋 손 계산 · 견본 파일 검사 통과 (`resources/master_data/scripts/check_master.py`)."""
import sys
from decimal import ROUND_HALF_UP, Decimal
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "master_data/scripts"))
import check_master as cm # noqa: E402
import master_formula as mf # noqa: E402
SAMPLES = [
"계수_건설품셈_공통8장.json",
"계수_산림품셈_9장.json",
"기계_건설품셈.json",
"로직_건설품셈_공통3장.json",
"로직_건설품셈_공통4장.json",
"로직_건설품셈_공통7장.json",
"로직_건설품셈_공통8장.json",
"로직_건설품셈_토목1장.json",
"로직_건설품셈_토목2장.json",
"로직_건설품셈_토목3장.json",
"로직_건설품셈_토목4장.json",
"로직_건설품셈_토목5장.json",
"로직_건설품셈_토목6장.json",
"로직_산림품셈_03장.json",
"로직_산림품셈_04장.json",
"로직_산림품셈_05장.json",
"로직_산림품셈_06장.json",
"로직_산림품셈_07장.json",
"로직_산림품셈_08장.json",
"로직_산림품셈_10장.json",
"로직_산림품셈_11장.json",
"로직_산림품셈_13장.json",
"로직_산림품셈_14장.json",
"로직_산림품셈_9장.json",
"소요량_산림품셈_13장.json",
]
EXCAVATOR = "건설품셈:8 굴착기(무한궤도) 시간당 경비"
@pytest.fixture(scope="module")
def whole():
return cm.master()
def cents(x) -> str:
return str(Decimal(x).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
def test_메쌓기(whole):
got = mf.run(
whole,
"산림품셈:13-4-1 메쌓기",
{"뒷길이": 35, "돌": "견치돌", "쌓기": "골쌓기", "높이": 2, "초과증가율": 80},
)
assert cents(got["계"]) == "203281.20"
def test_메쌓기_높이_증가율(whole):
got = mf.run(
whole,
"산림품셈:13-4-1 메쌓기",
{"뒷길이": 35, "돌": "견치돌", "쌓기": "골쌓기", "높이": 5, "초과증가율": 80},
)
assert [line["수량"] for line in got["줄"]] == [Decimal("0.70"), Decimal("0.56")]
def test_굴착기(whole):
got = mf.run(
whole, EXCAVATOR, {"기계": "0201-0070", "지역": "전국평균", "보정작업": "해당 없음"}
)
assert [cents(got[k]) for k in ("노무비", "재료비", "경비", "계")] == [
"35412.13",
"26130.11",
"24554.00",
"86096.24",
]
def test_굴착기_암석_보정(whole):
got = mf.run(
whole,
EXCAVATOR,
{"기계": "0201-0070", "지역": "전국평균", "보정작업": "암석작업(연암・보통암・경암)"},
)
assert got["중간"]["손료"] == 28322
assert cents(got["계"]) == "89864.24"
def test_노체포설(whole):
assert mf.run(whole, "산림품셈:9-16-1 노체포설 작업량", {})["결과"] == Decimal("24.255")
got = mf.run(whole, "산림품셈:9-16-1 노체포설", {"지역": "전국평균", "보정작업": "해당 없음"})
assert [cents(got[k]) for k in ("노무비", "재료비", "경비", "계")] == [
"1459.99",
"1077.31",
"1012.33",
"3549.63",
]
def test_범위규칙_이상_미만(whole):
"""9-2 횡단경사 「60% 미만 · 60% 이상」 — 60 은 윗줄."""
qty = [
mf.run(whole, "산림품셈:9-2 노선 굴진 보조원", {"횡단경사": x})["줄"][0]["수량"]
for x in (59.9, 60)
]
assert qty == [5, 10]
def test_분수_글자_값(whole):
"""9-4-2 f 원문 「1/1.40」 — 3600×0.7×0.55×(1/1.40)×0.35/20."""
assert mf.run(whole, "산림품셈:9-4-2 암절취 집토 작업량", {})["결과"] == Decimal("17.325")
def test_범위규칙_이하_위끝만(whole):
"""13-4-2 뒷길이 「35cm 이하 · 55cm 이하」 — 36 은 55 칸."""
region = {"지역": "전국평균", "보정작업": "해당 없음", "잡석량": 0}
qty = [
mf.run(whole, "산림품셈:13-4-2 메쌓기(장비)", {"뒷길이": x, **region})["줄"][0]["수량"]
for x in (35, 36, 75)
]
assert qty == [Decimal("0.1"), Decimal("0.09"), Decimal("0.08")]
def test_범위규칙_마지막칸_이하_포함(whole):
"""13-6-1 직경 「80㎝이상∼100㎝이하」 — 100 은 맞고 101 은 막힘."""
given = {"직경": 100, "지역": "전국평균", "보정작업": "해당 없음"}
assert mf.run(whole, "산림품셈:13-6-1 큰돌 메쌓기", given)["줄"][0]["수량"] == Decimal("0.68")
with pytest.raises(mf.FormulaError):
mf.run(whole, "산림품셈:13-6-1 큰돌 메쌓기", {**given, "직경": 101})
def test_굴착기_부착용_집게(whole):
"""잡재료 16%(8-4-8 주⑤) · 집게 손료 7,783천원×7435×10⁻⁷ 버림."""
got = mf.run(
whole,
"건설품셈:8 굴착기+부착용 집게 시간당 경비",
{"기계": "0201-0060", "집게": "7206-0070", "지역": "전국평균", "보정작업": "해당 없음"},
)
assert got["중간"]["집게손료"] == 5786
assert cents(got["계"]) == "86467.61"
def test_불도저_운반_작업량(whole):
"""10-11 — q=3.2×0.96 · ㎝=20/55+20/70+0.25 · Q=60×q×(1/1.30)×0.55/㎝."""
got = mf.run(whole, "산림품셈:10-11 불도저 운반 작업량", {"재료": "토사"})
assert got["결과"] == Decimal("86.709")
def test_중기운반_트레일러_횟수(whole):
"""10-4 — ㎝=20+30+20+0.42 · N=60×0.9/㎝ · 1회 = 1/N 시간."""
got = mf.run(whole, "산림품셈:10-4 중기운반 트레일러", {"t2": 30, "지역": "전국평균"})
assert got["줄"][0]["수량"].quantize(Decimal("0.0001")) == Decimal("1.3041")
def test_예정지정리_할증_합산(whole):
"""3-5 — 바이오매스 10 + 침엽수림 10 + 개벌지 10 + 산불 5×2/10 = 31% · 2.4×50%×1.31."""
given = {
"작업내용": "보완사업지 정리",
"정리량": 120,
"임상": "침엽수림",
"개벌지": "700㎡ 이하(또는 벌채폭 30m)",
"산불피해면적": 2,
"조림면적": 10,
}
got = mf.run(whole, "산림품셈:3-5 예정지정리", given)
assert [line["수량"] for line in got["줄"]] == [Decimal("1.572"), Decimal("1.572")]
def test_작업안전_보조_올림(whole):
"""4-6 — 벌목부 4인당 보통인부 1인 · 5인이면 2인."""
qty = [
mf.run(whole, "산림품셈:4-6 벌목부 작업안전 보조", {"벌목부": n})["줄"][0]["수량"]
for n in (3, 4, 5, 8)
]
assert qty == [1, 1, 2, 2]
def test_동력상하차기_잔존목_할증(whole):
"""7-11 — 40m · 4160㎥ · 1.2m = 22.03㎥ · 잔존목 10% · 소모성경비 0.02105×380만/50."""
given = {
"최대집재거리": 40,
"집재재적": 50,
"원목길이": 1.2,
"잔존목": "잔존목 100본 이상 130본 이하",
"지역": "전국평균",
}
got = mf.run(whole, "산림품셈:7-11 동력상하차기 집재", given)
assert got["줄"][0]["수량"] == Decimal("1.1") / Decimal("22.03")
assert got["중간"]["소모성경비"] == Decimal("1599.8")
def test_방제_벌목조재_감_할증(whole):
"""8-3 — 경급 6 = 1.13㎥/인 · 소군락 −30% · 잣나무 +20% · 벌목부 50%."""
given = {
"경급": 6,
"소군락": "소군락 모두베기",
"방제수종": "잣나무",
"체인톱가격": 1200000,
"체인오일단가": 5000,
"지역": "전국평균",
}
got = mf.run(whole, "산림품셈:8-3 벌목조재", given)
want = Decimal("0.7") * Decimal("1.2") * Decimal("0.5") / Decimal("1.13")
assert got["줄"][0]["수량"].quantize(Decimal("0.000001")) == want.quantize(Decimal("0.000001"))
def test_건설_석재판_습식_공구손료(whole):
"""공통 7-4-1 — 바닥부 테라조판 석공 3 · 줄눈공 1 · 보통인부 2 ÷ 15㎡ · 공구손료 인력품 2%."""
got = mf.run(whole, "건설품셈:7-4-1 습식공법", {"시공부위": "바닥부", "자재": "테라조판"})
assert [line["수량"] for line in got["줄"][:3]] == [
Decimal(3) / 15,
Decimal(1) / 15,
Decimal(2) / 15,
]
assert got["경비"] == got["노무비"] * Decimal("0.02")
def test_건설_정원석_난이도_가산(whole):
"""공통 4-4-1 — 쌓기 20ton 미만 조경공 1.212 × 1.1 · 굴착기 0.7㎥ 0.657hr 그대로."""
given = {
"작업": "쌓기",
"중량": "20ton 미만",
"난이도가산": 10,
"지역": "전국평균",
"보정작업": "해당 없음",
}
got = mf.run(whole, "건설품셈:4-4-1 정원석 쌓기 및 놓기", given)
assert [line["수량"] for line in got["줄"][:2]] == [Decimal("1.3332"), Decimal("0.657")]
def test_건설_벌목_집재거리_가산(whole):
"""공통 3-9-1 — 나무높이 6m 벌목부 2.8 · 집재 250m = 100m 초과 두 번 × 30%."""
given = {"나무높이": 6, "집재거리": 250, "지역": "전국평균", "보정작업": "해당 없음"}
got = mf.run(whole, "건설품셈:3-9-1 벌목", given)
assert got["줄"][0]["수량"] == Decimal("4.48")
def test_건설_흙깎기_암_치즐(whole):
"""공통 3-2-3 — 풍화암 65㎥/일 · 굴착기+대형브레이커 8/65 시간 · 치즐 1%."""
given = {"암분류": "풍화암", "집토": "안 함", "지역": "전국평균", "보정작업": "해당 없음"}
got = mf.run(whole, "건설품셈:3-2-3 흙깎기(기계) 암", given)
brk = got["줄"][1]
assert brk["수량"] == Decimal(8) / 65
assert got["줄"][-1]["금액"] == brk["금액"] / 100
def test_토목_동상방지층_인력_살수(whole):
"""토목 1-2-1 — 165㎥/일 · 인력 살수면 살수차 0 · 보통인부 100㎡당 1인 = 두께 20㎝ 에서 1/20 인/㎥."""
given = {"살수": "인력 살수", "두께": 20, "지역": "전국평균", "보정작업": "해당 없음"}
got = mf.run(whole, "건설품셈:1-2-1 인력식 소규모장비 포설", given)
qty = {line["이름"]: line["수량"] for line in got["줄"]}
assert qty["굴착기 0.6㎥"] == Decimal(8) / 165
assert qty["살수차 5,500"] == 0
assert got["줄"][-1]["수량"] == Decimal(1) / 20
def test_토목_보도블록_감(whole):
"""토목 1-7-1 — B-Type 190㎡/일 × 0.9(유도블록) · 정밀절단 아니면 특별인부 2 - 1."""
given = {
"형식": "B-Type",
"유도블록": "유도・점자블록",
"정밀절단": "아님",
"지역": "전국평균",
"보정작업": "해당 없음",
}
got = mf.run(whole, "건설품셈:1-7-1 보도용 블록 설치(소형)", given)
assert got["줄"][1]["수량"] == Decimal(1) / Decimal(171)
def test_토목_블록붙이기_굴착기_대체(whole):
"""토목 2-3-3 주④ — 크레인 대신 굴착기 0.2㎥ 0.063hr · 크레인 0."""
given = {"장비": "굴착기", "크레인": "2104-0010", "지역": "전국평균", "보정작업": "해당 없음"}
got = mf.run(whole, "건설품셈:2-3-3 블록 붙이기(기계)", given)
assert [line["수량"] for line in got["줄"][2:]] == [0, Decimal("0.063")]
def test_칸_이름_정리_뒤_기계_줄(whole):
"""공통 3-4-8 — 괄호·빈칸 칸 이름을 고친 뒤 모래지정은 플레이트 콤팩터만(8/110)."""
given = {"지정": "모래지정", "지역": "전국평균", "보정작업": "해당 없음"}
got = mf.run(whole, "건설품셈:3-4-8 기초지정", given)
qty = {line["이름"]: line["수량"] for line in got["줄"]}
assert round(qty["플레이트 콤팩터 1.5ton"], 4) == Decimal("0.0727")
assert qty["진동롤러(핸드가이드식) 0.7ton"] == 0
def test_토목_관부설_시공조건_요율(whole):
"""토목 6-2-2 — 열쇠 「K.P」 점을 식이 읽음 · 시공조건 B 는 시공량 133%."""
given = {"관경": 80, "시공조건": "시공조건 B", "압륜가산": 0, "요율구간": "300mm이하"}
got = mf.run(whole, "건설품셈:6-2-2 K.P 메커니컬 접합 및 부설 인력", given)
assert round(got["줄"][0]["수량"], 4) == round(Decimal(3) / (Decimal(13) * Decimal("1.33")), 4)
def test_토목_터널굴착_장대할증(whole):
"""토목 3-2-5 — A군 싸이클 480분 · 갱구 1,200m(α1 20%) = 1인 × 480/60/8 × 1.2."""
given = {
"공법군": "발파굴착A군",
"싸이클": 480,
"착암공인원": 3,
"보통인부인원": 3,
"갱구거리": 1200,
"병렬터널": "아님",
}
got = mf.run(whole, "건설품셈:3-2-5 작업인원", given)
assert got["줄"][0]["수량"] == Decimal("1.2")
def test_토목_궤광조립_직결형_단선감(whole):
"""토목 4-3-1 — 직결형 시공량은 분리형 250m(병합 칸) · 단선 5% 감 = 궤도공 16 ÷ 237.5."""
given = {"단선감": 5, "구분": "침목매립식(직결형)", "지역": "전국평균", "보정작업": "해당 없음"}
got = mf.run(whole, "건설품셈:4-3-1 궤광조립(콘크리트)", given)
assert got["줄"][0]["수량"] == Decimal(16) / Decimal("237.5")
def test_토목_강교_제작공수_보정(whole):
"""토목 5-1-2 — 연수 2 는 「이상·이하」 [2,2] · 총중량 표 범위규칙 뒤 괄호 설명은 뺌."""
given = {
"형식": "박스거더",
"고강도강비율": Decimal("0.5"),
"대형단품평균중량": 12,
"부품평균중량": 120,
"연수": 2,
"총중량": 600,
"사각": 60,
"곡률반경": 200,
"높이곡선변화": "있음",
}
got = mf.run(whole, "건설품셈:5-1-1 기본제작공수", given)
# 0.95 × (1 + 0.25×0.5) × 0.97 × 0.95 × (1 0.03 0.02 + 0.25 + 0.11)
want = Decimal("0.95") * Decimal("1.125") * Decimal("0.97") * Decimal("0.95") * Decimal("1.31")
assert got["줄"][0]["수량"] == want
def test_범위_입력은_막음(whole):
with pytest.raises(mf.FormulaError):
mf.run(
whole,
"산림품셈:13-4-1 메쌓기",
{"뒷길이": 35, "돌": "견치돌", "쌓기": "골쌓기", "높이": 8, "초과증가율": 120},
)
def test_견본_파일_검사_통과(whole):
files = cm.load(SAMPLES)
assert len(files) == len(SAMPLES)
assert [x for name, data in files.items() for x in cm.check_form(name, data)] == []
# 9장 계수 표는 같은 절의 소요량 표와 함께 대조 — 소요량 파일 자체의 알림은 그 창 몫
with_09 = files | cm.load(["소요량_산림품셈_09장.json"])
# 절 통째를 출처로 둔 다른 창 표(9-13 · 9-20)의 결손도 그 창 몫
# 8-4-8 주⑤ 표는 절의 운전경비 수가 기계 요소(표 그룹 밖)에 있어 결손으로 뜸
others = ("9-13 관보호공 인력 가산", "9-20 대상 근주", "8-4-8 주⑤ 조합 잡재료율")
body = cm.check_body(with_09)
assert [
x for x in body if not x.startswith("소요량_산림품셈_09장") and not x.endswith(others)
] == []
assert cm.check_logics(files, whole) == []
def test_열쇠_겹침을_잡음():
"""다른 부문 표와 같은 열쇠면 먼저 읽힌 표만 찾기에 걸림."""
files = {
"계수_건설품셈_건축1장.json": {
"그룹": "계수",
"원문": "건설품셈",
"표": [{"열쇠": "1-1-1 계수"}],
},
"계수_건설품셈_기계설비1장.json": {
"그룹": "계수",
"원문": "건설품셈",
"표": [{"열쇠": "1-1-1 계수"}],
},
}
assert len(cm.duplicate_keys(files)) == 1
assert cm.duplicate_keys(cm.load()) == []
def test_기계설비_강관_용접배관(whole):
"""1-1-2 관규격 15 · (배관공 3인 × 247,897 + 보통인부 1인 × 172,068) ÷ 83m · 기계경비 2%."""
got = mf.run(
whole,
"건설품셈:1-1-2 강관 용접배관",
{"관규격": 15, "시공방법": "인력시공", "시공위치": "옥내일반배관"},
)
labor = (3 * Decimal(247897) + Decimal(172068)) / 83
assert cents(got["노무비"]) == cents(labor)
assert cents(got["경비"]) == cents(labor * Decimal("0.02"))
def test_기계설비_사각댐퍼_면적_가산(whole):
"""2-2-4 방화댐퍼 0.35㎡ · 0.1㎡ 초과 0.25㎡ = 증 3번 · 0.415 + 0.125×3 = 0.79인."""
got = mf.run(
whole, "건설품셈:2-2-4 일반댐퍼(사각) 설치", {"댐퍼": "방화댐퍼", "댐퍼면적": 0.35}
)
assert got["줄"][0]["수량"] == Decimal("0.790")
def test_로직_검사가_틀린_열쇠를_잡음(whole):
row = {
"열쇠": "시험",
"입력": [{"이름": "지역", "고르기": ["전국평균", "없는곳"]}],
"중간": [{"이름": "x", "식": "찾기(계수:산림품셈:9-16-1 노체포설, 없는조건='K').적용"}],
"호표": [
{"종류": "재료", "요소": "재료:오피넷유가:경유:{지역}", "수량": "y", "비목": "재료비"}
],
"덧줄": [],
}
found, _ = mf.check_logic(whole, "산림품셈", row)
assert any("없는조건" in x for x in found)
assert any("경유:없는곳" in x for x in found)
assert any("「y」" in x for x in found)
def test_돌고_도는_참조():
assert mf.find_loops({"가": {"나"}, "나": {"가"}}) == [["가", "나", "가"]]
def test_찾기_값칸_있는_줄만():
table = {
"열쇠": "시험",
"조건": {"구분": "고르기", "높이": "고르기"},
"줄": [
{"구분": "비계공", "수량": 3},
{"높이": "4m이하", "시공량(m)": 100},
{"높이": "7m이하", "시공량(m)": 70},
],
}
assert mf.find(table, {"구분": "비계공"}, "수량")["수량"] == 3
assert mf.find(table, {"높이": "7m이하"}, "시공량(m)")["시공량(m)"] == 70
with pytest.raises(mf.FormulaError):
mf.find(table, {"높이": "7m이하"})
assert mf.parse("찾기(소요량:건설품셈:시험, 높이='7m이하').'시공량(m)'")[2] == "시공량(m)"
assert (
mf.parse("찾기(소요량:건설품셈:시험, '규준틀 높이'=높이).수량")[1][2][0][0] == "규준틀 높이"
)
assert (
mf.parse("찾기(소요량:건설품셈:5-1-1 P.P마대 쌓기, 구분=구분).수량")[1][1]
== "소요량:건설품셈:5-1-1 P.P마대 쌓기"
)
def test_범위규칙_이상이하():
table = {
"열쇠": "시험",
"조건": {"길이": "범위"},
"범위규칙": "이상·이하 · 겹친 누적 구간",
"줄": [
{"길이": [0, 10], "값": 1},
{"길이": [0, 20], "값": 2},
{"길이": [30, 40], "값": 3},
],
}
assert mf.find(table, {"길이": Decimal(10)})["값"] == 1
assert mf.find(table, {"길이": Decimal(15)})["값"] == 2
assert mf.find(table, {"길이": Decimal(30)})["값"] == 3
with pytest.raises(mf.FormulaError):
mf.find(table, {"길이": Decimal(25)})