Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
132 lines
5.8 KiB
Python
132 lines
5.8 KiB
Python
"""124-1 B08 좌측 패널 할증 모래 · 자갈 · 시멘트 — 기본 12 · 5 · 3 · 패널 값 = 총괄 세 줄 · 저장 · 거울.
|
||
|
||
정본 = `project_settings.json › quantity › material_surcharge_pct`(받은 칸만 · 기본은 읽을 때 채움) ·
|
||
총괄자재수량집계표 그 세 줄은 패널 값만 씀(표 사용자 값 무시 · 표에서 안 고침).
|
||
견본 = 121 시험의 2공구 꼴 구조물 집계표 계.
|
||
"""
|
||
|
||
import asyncio
|
||
import json
|
||
import subprocess
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
from pydantic import ValidationError
|
||
|
||
from B08_Quantity import B08_Quantity_Router_Material as material_router
|
||
from B08_Quantity import B08_Quantity_Router_Options as options
|
||
from B08_Quantity.B08_Quantity_Material_Calc import PANEL_SURCHARGE, compute
|
||
from common_util.common_util_project_settings import settings_path
|
||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||
from test_121_b08_material_sheets import EXCEL_MIX, IMPORTS, SUMMARY, _over
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
CALC_TS = ROOT / "B08_Quantity" / "B08_Quantity_Material_Calc.ts"
|
||
|
||
|
||
def _ts(summary, overrides, panel):
|
||
body = (
|
||
f"\nconst s = {json.dumps(summary, ensure_ascii=False)};"
|
||
f"\nconst o = {json.dumps(overrides, ensure_ascii=False)};"
|
||
f"\nconst p = {json.dumps(panel, ensure_ascii=False)};"
|
||
"\nconsole.log(JSON.stringify({sheets: computeMaterialSheets(s, o, p),"
|
||
" defaults: PANEL_SURCHARGE, filled: panelSurcharge(p)}));"
|
||
)
|
||
script = IMPORTS.sub("", CALC_TS.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 _rows(sheets):
|
||
return {row["key"]: row for row in sheets["material"]["rows"]}
|
||
|
||
|
||
@pytest.fixture
|
||
def root(tmp_path, monkeypatch) -> Path:
|
||
system = tmp_path / "master"
|
||
(system / "table").mkdir(parents=True)
|
||
(system / "table/구조물집계표.json").write_text(
|
||
json.dumps({"열": [], "변수": {"파형강관_1본_m": 8}}), encoding="utf-8"
|
||
)
|
||
monkeypatch.setattr(layers, "SYSTEM_ROOT", system)
|
||
project = tmp_path / "project"
|
||
project.mkdir()
|
||
return project
|
||
|
||
|
||
def test_기본_12_5_3_파일에_안_씀(root):
|
||
assert PANEL_SURCHARGE == {"sand": 12, "gravel": 5, "cement": 3}
|
||
rows = _rows(compute(SUMMARY, _over(mix=EXCEL_MIX)))
|
||
assert [rows[k]["surcharge"] for k in ("sand", "gravel", "cement")] == [12, 5, 3]
|
||
# 올림(14.5886 × 1.12) · 올림(18.0245 × 1.05) · 올림(238.73525 × 1.03)
|
||
assert [rows[k]["amount"] for k in ("sand", "gravel", "cement")] == [17, 19, 246]
|
||
assert [k for k, row in rows.items() if row["panel"]] == ["sand", "gravel", "cement"]
|
||
assert options.read_options(root)["material_surcharge_pct"] == PANEL_SURCHARGE
|
||
assert not settings_path(root).exists()
|
||
|
||
|
||
def test_패널_값이_총괄_세_줄을_바로_바꿈_표_값은_무시():
|
||
over = _over(mix=EXCEL_MIX, surcharge={"sand": 50, "sod": 20})
|
||
panel = {"sand": 6, "gravel": 4, "cement": 2} # 실무 엑셀 값
|
||
out = _ts(SUMMARY, over, panel)
|
||
rows = _rows(out["sheets"])
|
||
assert [rows[k]["amount"] for k in ("sand", "gravel", "cement")] == [
|
||
16,
|
||
19,
|
||
244,
|
||
] # 엑셀 G4 · G5 · G18
|
||
assert rows["sand"]["surcharge"] == 6 # 표 사용자 값 50 은 무시
|
||
assert rows["sod"]["surcharge"] == 20 # 나머지 줄은 표 값 그대로
|
||
assert out["sheets"] == compute(SUMMARY, over, panel)
|
||
|
||
|
||
def test_거울_빈칸_깨진_값은_기본():
|
||
for panel in (None, {}, {"gravel": 0}, {"sand": "x", "cement": 7.5}):
|
||
out = _ts(SUMMARY, _over(), panel)
|
||
assert out["defaults"] == PANEL_SURCHARGE
|
||
assert out["sheets"] == compute(SUMMARY, _over(), panel), panel
|
||
assert _rows(compute(SUMMARY, _over(), {"gravel": 0}))["gravel"]["surcharge"] == 0
|
||
|
||
|
||
def test_저장_뒤_남고_받은_칸만_씀(root, monkeypatch):
|
||
out = options.save_options(root, options.OptionsSave(material_surcharge_pct={"sand": 8}))
|
||
assert out["material_surcharge_pct"] == {"sand": 8, "gravel": 5, "cement": 3}
|
||
options.save_options(root, options.OptionsSave(material_surcharge_pct={"cement": 2.5}))
|
||
stored = json.loads(settings_path(root).read_text(encoding="utf-8"))
|
||
assert stored["quantity"]["material_surcharge_pct"] == {"sand": 8, "cement": 2.5}
|
||
# 다른 옵션 저장은 할증을 안 건드림
|
||
options.save_options(root, options.OptionsSave(soft_rock_pct=70))
|
||
assert options.read_options(root)["material_surcharge_pct"]["sand"] == 8
|
||
|
||
async def summary(_project_id):
|
||
return {**SUMMARY, "rows": [], "notes": []}
|
||
|
||
async def project_root(_project_id):
|
||
return root
|
||
|
||
monkeypatch.setattr(material_router, "structure_summary", summary)
|
||
monkeypatch.setattr(material_router, "_project_root", project_root)
|
||
got = asyncio.run(material_router.material_sheets("p"))
|
||
assert got["surcharge_pct"] == {"sand": 8, "gravel": 5, "cement": 2.5}
|
||
assert _rows(got["sheets"])["sand"]["surcharge"] == 8
|
||
|
||
|
||
def test_범위_밖_모르는_칸은_거절():
|
||
for bad in ({"sand": 120}, {"sand": -1}, {"rebar_d13": 3}):
|
||
with pytest.raises(ValidationError):
|
||
options.OptionsSave(material_surcharge_pct=bad)
|
||
|
||
|
||
def test_화면_연결():
|
||
ui = (ROOT / "B08_Quantity/B08_Quantity_UI_Material.ts").read_text(encoding="utf-8")
|
||
assert "liveSurcharge(projectId, payload.surcharge_pct)" in ui
|
||
assert "onOptionsChange(host," in ui
|
||
assert 'key === "surcharge" && !rows[i].panel' in ui and "할증 = 좌측 패널" in ui
|
||
panel = (ROOT / "B08_Quantity/B08_Quantity_UI_Options.ts").read_text(encoding="utf-8")
|
||
assert "put({ material_surcharge_pct:" in panel and "body.material_surcharge_pct" in panel
|