feat(B09 불러오기): 원가계산서 요율 · 기초 단가표 읽기 · 이 공사 단가 책 이음 · 올린 파일 열쇠 길 (PLAN 707-2 · 707-3)
- POST estimation/import/rates · import/prices (file | file_key · 저장 안 함 · patch 만) - 요율 = 행 이름 → 줄 키 · 손입력 칸(안전 기초액 · 이윤 보정 · 부가세 조정 · 조달수수료율) · 엑셀 대조 - 단가표 = 노임 · 자재 · 기계 → labor_cover · machine_cover · materials.applied · 짝 없음 · 짝 여럿 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
"""707-2 원가계산서 요율 · 707-3 기초 단가표 불러오기 — 실무 xlsx 넷(EST 거창 · STmate 영월 · 울진 대흥 ·
|
||||
울진 소광) · 이음 규칙 · 덮개 지킴(새 판 [업데이트] 뒤).
|
||||
|
||||
총액 대조 = 엑셀 비목 합 + 실은 요율 · 손입력으로 우리 식(`compute_total`) → 엑셀 총공사비:
|
||||
대흥 = 같음 · 영월 = 「국가지점번호판검증수수료」 762,000 원만 다름(우리 표에 줄 없음) ·
|
||||
거창 = 퇴직공제부금비(셈 없는 현행 비목)만큼 · 소광 = 산출근거 빈칸 율 넷 · 폐기물처리 줄.
|
||||
"""
|
||||
|
||||
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 any("퇴직공제부금비" in n and "셈 없음" in n for n in gc["notes"])
|
||||
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"]) == {"도급공사비", "총공사비"}
|
||||
lines = {ln["key"]: ln for ln in got["거창"]["lines"]}
|
||||
rate_keys = ("indirect_labor", "accident", "employment", "health", "care", "pension")
|
||||
assert all(lines[k]["same"] for k in (*rate_keys, "safety", "other", "environment"))
|
||||
assert not lines["overhead"]["same"] # 퇴직공제부금비 5,603,907 이 순공사원가에 없음
|
||||
assert lines["overhead"]["excel"] - lines["overhead"]["ours"] == 448312 # = 5,603,907 × 8 %
|
||||
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user