Files
Aislo/resources/tester/test_m02_labor_layer.py
T
eomsangdonandClaude Opus 5.5 15f2e7c4f5 feat(M02): 단가산출 승격 서고 지움 · 옛 단가승격 참조 줄 = 읽을 때 로직 줄 + 승격(134-2)
같은 로직 줄이 이미 있으면 옛 참조 줄 그대로(브레인 승인 ③) · 마스터 · 개인 파일 안 고침 · 프로젝트 사본 셈 그대로.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SE9MxeXTu2uaDZ55BJowAY
2026-10-09 14:54:58 +09:00

130 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""켜쌓기율(85-2) — 표 주문 「골쌓기 기준」 로직만 · 세부항목 줄 전부 수량 × 율 · 문서 · 사본 같은 함수.
못박는 것
- 대상 = 표 주문 자동 판정(지금 GC000260 · GC000261) · 입력 목록에 「켜쌓기」 표시.
- GC000260 90% = 100% × 0.9(비목 · 계 · 줄 수량 · 금액) · 단가는 그대로.
- clean = 0 초과 200 이하 · 소수 한 자리 · 100 · 대상 밖은 안 둠.
- 프로젝트 사본 계산(`M02_Item_Prices.calc`)도 같은 율 · 같은 값.
- 옛 단가승격 서고 문서도 율을 따라 같은 값(134-2 · 프로젝트 사본 셈).
"""
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 = {"키": "SP-900001", "배": 1, "담은로직": [{"로직": CODE, "켜쌓기율": 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"]