Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
171 lines
7.4 KiB
Python
171 lines
7.4 KiB
Python
"""144-2~4 B09 바탕 단가 셈 — 엑셀 검산 대조 · 거울(`B09_Estimation_Calc_Base.py` ↔ `.ts`).
|
||
|
||
견본 = 기번3 공통 엑셀(`2024년 간선임도사업(기번3).xlsx`) 같은 입력:
|
||
노무비목록표 E10~E12 · 환율및기초자료 G6~G8 · 경비목록표 E4 · E5 · E7 · E13 · E16 · E21 ·
|
||
자재단가대비표 5~12줄 · 중기사용료 1 · 2 · 4 · 11 · 14 · 16 호표(F11 126,000 …)
|
||
"""
|
||
|
||
import json
|
||
import re
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
if str(ROOT) not in sys.path:
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
from B09_Estimation.B09_Estimation_Calc_Base import ( # noqa: E402
|
||
compute_base,
|
||
round_half,
|
||
rounddown,
|
||
roundup,
|
||
)
|
||
|
||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||
FACTOR = {"num": 400, "den": 1920, "text": "1/8 × 16/12 × 25/20"}
|
||
|
||
LABOR = [
|
||
{"key": "LB000048", "code": "1048", "name": "건설기계운전사", "price": 267360},
|
||
{"key": "LB000049", "code": "1049", "name": "화물차운전사", "price": 226709},
|
||
{"key": "LB000050", "code": "1050", "name": "일반기계운전사", "price": 161142},
|
||
{"key": "LB000002", "code": "1002", "name": "보통인부", "price": None},
|
||
]
|
||
|
||
|
||
def _m(key, name, price, loss, fuel, lph, misc, crew_code):
|
||
return {"key": key, "name": name, "spec": "", "price": price, "loss": loss, "fuel": fuel,
|
||
"fuel_lph": lph, "misc_pct": misc, "crew": 1 if crew_code else None,
|
||
"crew_code": crew_code} # fmt: skip
|
||
|
||
|
||
MACHINES = [
|
||
_m("X00001", "불도우저19ton", 184499, 1811, "경유", 25, 16, "1048"),
|
||
_m("X00003", "굴삭기(0.2m3)", 62550, 2085, "경유", 5, 21, "1048"),
|
||
_m("X00005", "굴삭기(0.7m3)", 110926, 2085, "경유", 11.6, 22, "1048"),
|
||
_m("X00032", "덤프자동덮개", 1546, 2684, None, None, None, None),
|
||
_m("X00048", "콘크리트믹서0.45m3", 5409, 3039, "휘발유", 3.9, 2, "1050"),
|
||
_m("X00074", "덤프트럭2.5ton(토사)", 20793, 2967, "경유", 2.9, None, "1049"),
|
||
_m("X09999", "기준가 없는 기계", None, 2000, "중유", 3, 10, "1048"),
|
||
]
|
||
|
||
|
||
def _mt(key, name, **prices):
|
||
return {"key": key, "name": name, "spec": "", "unit": "", "origin": "M01", "prices": prices}
|
||
|
||
|
||
MATERIALS = [
|
||
_mt("MO1", "휘발유", 관급=1369, 물가정보=0, 물가자료=0),
|
||
_mt("MO2", "경유", 관급=1272),
|
||
_mt("M3", "각재", 관급=500661, 물가정보=829341, 물가자료=748503),
|
||
_mt("M4", "못", 관급=1503, 물가정보=1935, 물가자료=1665),
|
||
_mt("M8", "화강석", 관급=0, 물가정보=288000, 물가자료=288000),
|
||
_mt("M9", "사용자 값 줄", 물가자료=1000),
|
||
_mt("b08:sand", "골재"),
|
||
]
|
||
|
||
INPUTS = {
|
||
"labor": LABOR,
|
||
"machines": MACHINES,
|
||
"materials": MATERIALS,
|
||
"fuel_keys": {"경유": "MO2", "휘발유": "MO1"},
|
||
"driver_factor": FACTOR,
|
||
"fx": [
|
||
{"key": "FX000001", "name": "USD", "unit": "원/USD", "value": 1414.9, "date": "2026-08-14"}
|
||
],
|
||
}
|
||
USER = {"materials": {"M9": {"applied": 950, "supply": "관급"}, "M4": {"supply": "별도"},
|
||
"b08:sand": {"applied": None}}} # fmt: skip
|
||
|
||
|
||
def _run_ts(inputs, user):
|
||
calc = ROOT / "B09_Estimation" / "B09_Estimation_Calc_Base.ts"
|
||
body = (
|
||
f"\nconst i = {json.dumps(inputs, ensure_ascii=False)};"
|
||
f"\nconst u = {json.dumps(user, ensure_ascii=False)};"
|
||
"\nconsole.log(JSON.stringify(computeBase(i, u)));"
|
||
)
|
||
script = IMPORTS.sub("", calc.read_text(encoding="utf-8")) + body
|
||
result = subprocess.run(
|
||
["node", "--experimental-strip-types", "--no-warnings",
|
||
"--input-type=module-typescript", "-e", script],
|
||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||
) # fmt: skip
|
||
assert result.returncode == 0, result.stderr
|
||
return json.loads(result.stdout)
|
||
|
||
|
||
def test_끝수_엑셀_자리():
|
||
assert rounddown(33412.7689, 1) == 33412.7 and rounddown(126000.9) == 126000
|
||
assert rounddown(-1.25, 1) == -1.2 # 0 쪽으로
|
||
assert roundup(69849380, -3) == 69850000 and roundup(69850000, -3) == 69850000
|
||
assert round_half(2.5) == 3 and round_half(0.125, 2) == 0.13 and round_half(-2.5) == -3
|
||
assert rounddown(0.1 + 0.2, 1) == 0.3 # 떠도는 끝수는 버리지 않음
|
||
|
||
|
||
def test_144_2_노무_경비_환율_기초자료():
|
||
s = compute_base(INPUTS, {})
|
||
assert [r["price"] for r in s["labor"]] == [267360, 226709, 161142, None]
|
||
assert ("labor", "LB000002") in {(g["tab"], g["key"]) for g in s["gaps"]}
|
||
hours = {d["key"]: d["hour"] for d in s["basis"]["drivers"]}
|
||
assert hours == {"1048": 55700, "1050": 33571, "1049": 47231} # 기초자료 G6 · G8 · G7
|
||
assert s["basis"]["drivers"][0]["factor"] == "1/8 × 16/12 × 25/20"
|
||
assert [r["price"] for r in s["expense"]][:3] == [
|
||
184499,
|
||
62550,
|
||
110926,
|
||
] # 경비목록표 E4 · E5 · E7
|
||
assert s["expense"][0]["unit"] == "천원"
|
||
assert s["basis"]["fx"][0]["value"] == 1414.9
|
||
|
||
|
||
def test_144_3_자재단가대비_최저_사용자_관급():
|
||
s = compute_base(INPUTS, USER)
|
||
rows = {r["key"]: r for r in s["compare"]}
|
||
# 엑셀 O 열 = 최저 · P 열 출처(0 은 뺌 · 같은 값이면 앞 칸 — 화강석 「물정」)
|
||
assert (rows["MO1"]["applied"], rows["MO1"]["applied_from"]) == (1369, "관급")
|
||
assert (rows["M3"]["applied"], rows["M3"]["lowest_from"]) == (500661, "관급")
|
||
assert rows["M4"]["applied"] == 1503 and rows["M4"]["supply"] == "별도"
|
||
assert (rows["M8"]["applied"], rows["M8"]["applied_from"]) == (288000, "물가정보")
|
||
assert (rows["M9"]["lowest"], rows["M9"]["applied"], rows["M9"]["applied_from"]) == (
|
||
1000,
|
||
950,
|
||
"사용자",
|
||
)
|
||
assert rows["M9"]["supply"] == "관급"
|
||
assert rows["b08:sand"]["applied"] is None # 출처 없음 · 사용자 빈칸 = 빈칸
|
||
material = {r["key"]: r["price"] for r in s["material"]}
|
||
assert material == {r["key"]: r["applied"] for r in s["compare"]} # 재료비목록표 = 적용 단가
|
||
gaps = [(g["tab"], g["key"]) for g in s["gaps"] if g["tab"] == "compare"]
|
||
assert gaps == [("compare", "b08:sand")]
|
||
|
||
|
||
def test_144_4_중기사용료_엑셀_검산():
|
||
s = compute_base(INPUTS, {})
|
||
got = {r["key"]: (r["sum"], r["labor"], r["material"], r["expense"]) for r in s["machine"]}
|
||
assert got["X00001"] == (126000, 55700, 36888, 33412) # 중기목록표 4줄
|
||
assert got["X00003"] == (76436, 55700, 7695, 13041)
|
||
assert got["X00005"] == (96829, 55700, 18001, 23128)
|
||
assert got["X00032"] == (414, 0, 0, 414) # 덮개 — 운전사 · 연료 없음
|
||
assert got["X00048"] == (40659, 33571, 5445, 1643) # 일반기계운전사 · 휘발유 · 잡유 2 %
|
||
assert got["X00074"][1:3] == (47231, 3688) # 화물차운전사 · 잡품 없음
|
||
assert got["X09999"] == (None, 55700, None, None) # 기준가 · 중유 단가 없음 = 그 비목 · 합 빈칸
|
||
block = s["machine_blocks"][0]
|
||
assert [ln["amount"] for ln in block["lines"]] == [
|
||
33412.7,
|
||
55700,
|
||
31800,
|
||
5088,
|
||
] # 중기사용료 F7~F10
|
||
assert block["lines"][0]["qty"] == 0.1811 and block["lines"][0]["ref"] == {
|
||
"tab": "expense",
|
||
"key": "X00001",
|
||
}
|
||
why = {g["why"] for g in s["gaps"] if g["key"] == "X09999"}
|
||
assert why == {"연료(중유) 단가 없음", "기계 기준가 없음", "손료 기준가 · 계수 없음"}
|
||
|
||
|
||
def test_거울_TS_가_같은_값():
|
||
for user in ({}, USER):
|
||
assert _run_ts(INPUTS, user) == compute_base(INPUTS, user)
|