Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4
133 lines
5.5 KiB
Python
133 lines
5.5 KiB
Python
"""켜쌓기율(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"]
|