- 셈 짝(Calc_Total py ↔ ts) — STmate 일반형식 식 · 자리(지식DB 12 · 35 분석) · 경비 묶음 안 · 나.소계에 듦 퇴직공제부금비 = 직접노무비 × 율 · 임금채권 · 석면 = (직노 + 간노) × 율 · 하도급 = 순공사비 × 율 · 공사이행보증 = 금액 직접 입력(performance_fee) · 자동 요율 없음 · 요율 빈칸 = 해당 없음(0) - 요율 출처 「불러옴」 = rates_imported(707-2 불러오기가 실음 · 요율 고치면 사용자) - 707-2 불러오기 — 「셈 없음」 알림 뺌 · 공사이행보증 줄 금액 = 손입력 칸 - 총괄 · 원가계산서 탭 · 인쇄 미리보기 = 같은 줄 짜기 - 낡은 시험 고침 — 현행 비목 기대값 빈칸 → 0 · 퇴직공제 요율 칸 고침 가능 · 거창 총공사비 = 엑셀 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
175 lines
7.9 KiB
Python
175 lines
7.9 KiB
Python
"""707-2 원가계산서 요율 · 707-3 기초 단가표 불러오기 — 실무 xlsx 넷(EST 거창 · STmate 영월 · 울진 대흥 ·
|
||
울진 소광) · 이음 규칙 · 덮개 지킴(새 판 [업데이트] 뒤).
|
||
|
||
총액 대조 = 엑셀 비목 합 + 실은 요율 · 손입력으로 우리 식(`compute_total`) → 엑셀 총공사비:
|
||
대흥 = 같음 · 영월 = 「국가지점번호판검증수수료」 762,000 원만 다름(우리 표에 줄 없음) ·
|
||
거창 = 같음(708-2 퇴직공제부금비 셈 뒤) · 소광 = 산출근거 빈칸 율 넷 · 폐기물처리 줄.
|
||
"""
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
if str(ROOT) not in sys.path:
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
from B09_Estimation import B09_Estimation_Import_Prices as ip # noqa: E402
|
||
from B09_Estimation import B09_Estimation_Import_Rates as ir # noqa: E402
|
||
from B09_Estimation import B09_Estimation_Store as store # noqa: E402
|
||
from B09_Estimation.B09_Estimation_Calc_Base import compute_base # noqa: E402
|
||
from B09_Estimation.B09_Estimation_Rates import rate_table # noqa: E402
|
||
|
||
K = ROOT / "resources" / "knowledge" / "original" / "실무문서"
|
||
FILES = {
|
||
"거창": "01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx",
|
||
"영월": "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx",
|
||
"대흥": "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).xlsx",
|
||
"소광": "25년 산불진화임도(기번8)(울진소광)/"
|
||
"2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx",
|
||
}
|
||
|
||
|
||
def _file(tag):
|
||
path = K / FILES[tag]
|
||
if not path.is_file():
|
||
pytest.skip(f"실무 xlsx 없음 — {tag}")
|
||
return path.name, path.read_bytes()
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def rates():
|
||
return {tag: ir.read_rates(*_file(tag)) for tag in FILES}
|
||
|
||
|
||
def test_707_2_요율_짝(rates):
|
||
dh = rates["대흥"]
|
||
assert dh["kind"] == "STmate" and rates["거창"]["kind"] == "EST"
|
||
assert len(dh["rates"]) == 13 # 율 줄 10 + 일반관리비 · 이윤 · 부가세
|
||
assert (dh["rates"]["indirect_labor"], dh["rates"]["safety"], dh["rates"]["profit"]) == (
|
||
14.5,
|
||
1.86,
|
||
15.0,
|
||
)
|
||
assert dh["estimate"]["safety_base"] == 5349000 # 「x1.86% + 5,349,000」
|
||
assert dh["estimate"]["procurement_fee_pct"] == 0.54 # 375,160 ÷ 69,474,220
|
||
gc = rates["거창"] # EST 요율 칸 · 건강보험 글 「13.595%」 대신 칸 3.595
|
||
assert (gc["rates"]["health"], gc["rates"]["care"], gc["rates"]["retirement"]) == (
|
||
3.595,
|
||
13.14,
|
||
2.3,
|
||
)
|
||
assert gc["estimate"]["safety_base"] == 0 # 글에 + 기초액 없음
|
||
assert gc["notes"] == [] # 708-2 퇴직공제부금비 = 셈(「셈 없음」 없음)
|
||
sk = rates["소광"]
|
||
assert sum("율 없음" in n for n in sk["notes"]) == 4 # 산출근거 빈칸 — 지금 율 그대로
|
||
assert any("페기물처리" in n for n in sk["notes"])
|
||
|
||
|
||
def test_707_2_총액_대조(rates):
|
||
got = {tag: ir.check(r, {}, rate_table()) for tag, r in rates.items()}
|
||
assert got["대흥"]["grand"] == got["대흥"]["excel_grand"] == 1201879000
|
||
assert got["대흥"]["differ"] == []
|
||
assert got["영월"]["excel_grand"] - got["영월"]["grand"] == 762000 # 국가지점번호판 줄 몫
|
||
assert set(got["영월"]["differ"]) == {"도급공사비", "총공사비"}
|
||
# 708-2 — 퇴직공제부금비 5,603,907(직노 × 2.3 %)가 순공사원가에 듦 → 총공사비 = 엑셀
|
||
assert got["거창"]["grand"] == got["거창"]["excel_grand"] == 744653000
|
||
assert got["거창"]["differ"] == []
|
||
lines = {ln["key"]: ln for ln in got["거창"]["lines"]}
|
||
assert lines["retirement"]["ours"] == lines["retirement"]["excel"] == 5603907
|
||
|
||
|
||
def test_707_3_단가표_읽기():
|
||
counts = {}
|
||
for tag in FILES:
|
||
got = ip.read_prices(*_file(tag))
|
||
counts[tag] = tuple(len(got[k]) for k in ip.KINDS)
|
||
assert counts == {"거창": (17, 52, 22), "영월": (16, 55, 29), "대흥": (12, 39, 25),
|
||
"소광": (14, 35, 24)} # fmt: skip
|
||
|
||
|
||
BASE = {
|
||
"labor": [
|
||
{"key": "LB1", "name": "석공", "spec": "", "unit": "인", "price": 268908},
|
||
{"key": "LB2", "name": "보통인부", "spec": "", "unit": "인", "price": 172068},
|
||
],
|
||
"machine": [
|
||
{
|
||
"key": "EQ1",
|
||
"name": "굴착기(무한궤도)",
|
||
"spec": "0.7㎥",
|
||
"labor": 1,
|
||
"material": 2,
|
||
"expense": 3,
|
||
},
|
||
{
|
||
"key": "EQ2",
|
||
"name": "굴착기(무한궤도)",
|
||
"spec": "0.2㎥",
|
||
"labor": 1,
|
||
"material": 2,
|
||
"expense": 3,
|
||
},
|
||
], # fmt: skip
|
||
"compare": [
|
||
{"key": "MO1", "name": "휘발유", "spec": "전국평균", "unit": "L", "applied": 1863.25},
|
||
{"key": "MT1", "name": "레미콘", "spec": "25-18-80", "unit": "㎥", "applied": 92330},
|
||
],
|
||
}
|
||
|
||
|
||
def _m(name, spec, n):
|
||
return {"name": name, "spec": spec, "unit": "hr", "labor": n, "material": n, "expense": n}
|
||
|
||
|
||
READ = {
|
||
"labor": [
|
||
{"name": "석 공", "spec": "", "unit": "인", "price": 258935},
|
||
{"name": "보통인부", "spec": "", "unit": "인", "price": 172068}, # 책과 같음
|
||
{"name": "벌 목 부", "spec": "", "unit": "인", "price": 253243},
|
||
],
|
||
"machine": [
|
||
_m("굴삭기(무한궤도)", "0.7㎥", 10),
|
||
_m("굴삭기(타이어형)", "0.7㎥", 11), # 타이어 = 다른 기계
|
||
_m("굴삭기+브레이카", "0.7㎥", 12),
|
||
_m("굴삭기(무한궤도)", "0.2㎥", 13),
|
||
_m("굴삭기(무한궤도)", "0.2㎥", 14), # 같은 책 줄에 둘 = 못 고름
|
||
],
|
||
"material": [
|
||
{"name": "휘 발 유", "spec": "", "unit": "ℓ", "price": 1473}, # 이름 · 단위(ℓ = L)
|
||
{"name": "레미콘(관급)", "spec": "25-18-80", "unit": "m3", "price": 92330},
|
||
],
|
||
}
|
||
|
||
|
||
def test_707_3_이음_규칙():
|
||
user = {"labor_cover": {"LB2": 1}, "materials": {"MO1": {"pick": "유가", "supply": "관급"}}}
|
||
out = ip.propose(READ, BASE, user)
|
||
patch = out["patch"]
|
||
assert patch["labor_cover"] == {"LB1": 258935} # 같은 값 = 옛 덮개 걷음
|
||
assert patch["machine_cover"] == {"EQ1": {"labor": 10, "material": 10, "expense": 10}}
|
||
assert patch["materials"]["MO1"] == {"supply": "관급", "applied": 1473} # pick 뺌
|
||
assert "MT1" not in patch["materials"] # 「(관급)」 이름 다름 = 짝 없음
|
||
assert "굴삭기(무한궤도) 0.2㎥(짝 여럿)" in out["missing"]["machine"]
|
||
assert out["counts"]["machine"] == {"read": 5, "linked": 1, "covered": 1, "missing": 4,
|
||
"book": 2} # fmt: skip
|
||
assert out["counts"]["labor"]["linked"] == 2 and out["missing"]["labor"] == ["벌 목 부"]
|
||
|
||
|
||
def test_707_3_덮개_새_판_뒤_남음(tmp_path):
|
||
"""[업데이트] = 단가 책만 새로 굳힘 — 사용자 값의 노임 · 기계 덮개는 그대로 · 셈은 덮개."""
|
||
cover = {"labor_cover": {"LB1": 258935, "": None},
|
||
"machine_cover": {"EQ1": {"labor": 10, "material": 10, "expense": 10},
|
||
"EQ2": {"labor": 1}}} # fmt: skip
|
||
saved = store.write_user(tmp_path, cover)
|
||
assert saved["labor_cover"] == {"LB1": 258935} # 빈 값 버림
|
||
assert saved["machine_cover"] == {"EQ1": {"labor": 10, "material": 10, "expense": 10}}
|
||
for price in (268908, 280000): # 옛 판 → 새 판 책
|
||
store.write_book(tmp_path, {"rows": {"LB1": {"key": "LB1", "price": price}}})
|
||
user = store.read_user(tmp_path)
|
||
assert user["labor_cover"] == saved["labor_cover"]
|
||
inputs = {"labor": [{"key": "LB1", "name": "석공", "price": price}]}
|
||
row = compute_base(inputs, user)["labor"][0]
|
||
assert (row["price"], row["book"], row["covered"]) == (258935, price, True)
|