"""켜쌓기율(85-2) — 표 주문 「골쌓기 기준」 로직만 · 세부항목 줄 전부 수량 × 율 · 문서 · 사본 같은 함수. 못박는 것 - 대상 = 표 주문 자동 판정(지금 GC000260 · GC000261) · 입력 목록에 「켜쌓기」 표시. - GC000260 90% = 100% × 0.9(비목 · 계 · 줄 수량 · 금액) · 단가는 그대로. - clean = 0 초과 200 이하 · 소수 한 자리 · 100 · 대상 밖은 안 둠. - 프로젝트 사본 계산(`M02_Item_Prices.calc`)도 같은 율 · 같은 값. - 서고(단가승격)도 율을 담고 같은 값. """ from __future__ import annotations from decimal import Decimal import pytest from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Item_Prices as prices from M02_MasterTemplete import M02_Labor_Doc as labor from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_MasterTemplete_Store as store CODE = "GC000260" def _d(value) -> Decimal: return Decimal(str(value)) def test_대상은_골쌓기_기준_주문_로직() -> None: assert labor_inputs.layered() == {"GC000260", "GC000261"} got = labor_inputs.logic_inputs([CODE, "GF000219"])["로직"] assert [x["켜쌓기"] for x in got] == [True, False] def test_90퍼센트는_100퍼센트_곱하기_0점9() -> None: whole = m01.loaded()[1] full = labor.run_line(whole, CODE, {}) part = labor.run_line(whole, CODE, {}, 90) for k in (*labor.COSTS, "계"): assert part["기준"][k] == pytest.approx(full["기준"][k] * Decimal("0.9")) for a, b in zip(full["줄"], part["줄"]): assert b["단가"] == a["단가"] assert b["수량"] == pytest.approx(a["수량"] * Decimal("0.9")) assert b["금액"] == pytest.approx(a["금액"] * Decimal("0.9")) for k, v in a["비목"].items(): assert b["비목"][k] == pytest.approx(v * Decimal("0.9")) assert labor.run_line(whole, CODE, {}, 100) == full @pytest.mark.parametrize("bad", [0, -5, 200.1, 90.55, "abc"]) def test_율_범위_밖은_400(bad) -> None: with pytest.raises(store.StoreError): labor.clean({"담은로직": [{"로직": CODE, "켜쌓기율": bad}]}) def test_율_정리() -> None: def rate(code: str, value): return labor.clean({"담은로직": [{"로직": code, "켜쌓기율": value}]})["담은로직"][0].get( "켜쌓기율" ) assert rate(CODE, "90") == 90 assert rate(CODE, 87.5) == 87.5 assert rate(CODE, 200) == 200 assert rate(CODE, 100) is None # 100 = 없음 assert rate(CODE, None) is None assert rate("GF000219", 90) is None # 대상 밖은 안 둠 def test_문서_계산과_프로젝트_사본_같음() -> None: doc = labor.clean({"단위": "㎡", "담은로직": [{"로직": CODE, "켜쌓기율": 90}]}) whole = m01.loaded()[1] auto = m01.mau.values(whole, whole.logic(CODE)) values = {CODE: {k: {"값": labor._json(v), "확인": True} for k, v in auto.items()}} copy = {"줄": doc, "알약": labor.pills(doc)} got = prices.calc(copy, prices.to_json(prices.collect([CODE])), values, {}) origin = labor.calc(doc, {}) (line,) = got["줄"] assert line["계"] == pytest.approx(origin["줄"][0]["기준"]["계"]) full = labor.calc({**doc, "담은로직": [{**doc["담은로직"][0], "켜쌓기율": None}]}, {}) assert _d(line["계"]) == pytest.approx(_d(full["줄"][0]["기준"]["계"]) * Decimal("0.9")) def test_서고_단가승격도_율() -> None: doc = refs.clean_doc( "promote", {"키": "SP-900001", "담은로직": [{"로직": CODE, "켜쌓기율": 90}]} ) assert doc["담은로직"][0]["켜쌓기율"] == 90 whole = m01.loaded()[1] expand = refs.Expand(whole, lambda ref: doc) got = expand({"층": "system", "주인": None, "키": "SP-900001"}) full = labor.run_line(whole, CODE, {}) times = _d(doc["배"]) assert got["기준"]["계"] == pytest.approx(full["기준"]["계"] * Decimal("0.9") / times) RATE_SCRIPT = r""" const fs = require("fs"); const path = require("path"); const root = process.argv[1]; const ts = require(path.join(root, "config", "node_modules", "typescript")); const stub = new Proxy({}, { get: (_t, k) => (k === "currentLanguageIndex" ? 0 : () => undefined) }); const f = path.join(root, "M02_MasterTemplete", "M02_MasterTemplete_Labor_Input.ts"); const js = ts.transpileModule(fs.readFileSync(f, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; new Function("exports", "module", "require", js)(box.exports, box, () => stub); const m = box.exports; console.log(JSON.stringify(["", "90", "87.5", "100", "200", "0", "200.1", "90.55", "x"].map( (v) => { const r = m.layerRate(v); return Number.isNaN(r) ? "bad" : r; }))); """ def test_화면_율_칸_검사() -> None: """옵션 모달 켜쌓기율 칸 — 서버 `_rate` 와 같은 범위(빈 칸 · 100 = 없음).""" import json import shutil import subprocess from pathlib import Path node = shutil.which("node") if not node: pytest.skip("node 없음") root = Path(__file__).resolve().parents[2] out = subprocess.run( [node, "-e", RATE_SCRIPT, str(root)], capture_output=True, text=True, encoding="utf-8" ) assert out.returncode == 0, out.stderr assert json.loads(out.stdout) == [None, 90, 87.5, None, 200, "bad", "bad", "bad", "bad"]