- 줄 단추 = 로직 줄 · SK 참조 줄(Labor_Rows `sangeun`) · 프로젝트 복사본(셈 주입)엔 없음 - 모달(Labor_SangeunModal) = 일위대가 머리 「1 ㎡ 당」 · 이 줄 수량 · 1 단위 금액 · 보기 고르기(기본 인력·재료·경비 = 공용 계산서 calc_sheet · 상세 = 품 · 단가 칸 고침) - 고친 줄 = 표 부품 「바뀜」 + 원래 값 곁 + 「이 일위대가에서 고침」 꼬리표 · [적용] = 줄 칸 패치(화면 캐시 · 저장은 [저장]) · 적용 안 하고 닫으면 물음 - [단가산출로 저장] = 내 개인 층 새 SK(from-line) · 자기 개인 층 일위대가면 그 줄을 새 SK 참조 줄로 - 셈 = 서버 /sangeun/calc(저장 안 함 · 최신 응답만) · 화면 문서 = 서버 as_doc 꼴(Sangeun_Cover_Model · 고름 · 자재표시 cid) - 고침 표 · 셈 길 부름 = Sangeun_Cover(717-2b 도 씀) · normalizeLabor 가 줄 패치를 지킴(떨구면 [저장]에서 사라짐) - 시험 test_717_3 5 통과 · 고친 대상 166 통과(1 실패 = test_175 B09 Store_Combo._PER · 전부터 · 719-2 몫) · typecheck 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
199 lines
9.1 KiB
Python
199 lines
9.1 KiB
Python
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
|
|
"""일위대가 줄 [단가산출 보기] 모달(PLAN 717-3) — 임시 storage · 순수 함수는 Node 로.
|
|
|
|
화면 문서(`M02_Sangeun_Cover_Model.lineDoc`) = 서버 `M02_Labor_Patch.as_doc` 꼴(같은 고름 · 자재표시 cid) ·
|
|
모달 셈 길(`/sangeun/calc` = `calc_doc`) 값 = 일위대가 셈의 그 줄 값 · 고친 칸 = 덮개 칸 이름 ·
|
|
덮개 고침 순수 함수 · 계산서 묶음 · `normalizeLabor` 가 줄 `패치` 를 지킴 · 화면 이음.
|
|
"""
|
|
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from M01_MasterData import M01_MasterData_Store as m01
|
|
from M02_MasterTemplete import M02_Labor_Doc as labor
|
|
from M02_MasterTemplete import M02_Labor_Patch as patch
|
|
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
|
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
|
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
|
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
M02 = ROOT / "M02_MasterTemplete"
|
|
PCT = "GC001109" # 원/㎡ · 인력 둘 + 「노무비 * 3 / 100」
|
|
TWO_MATS = "GC000565" # 원/m · 재료 줄 둘
|
|
COVER = {"덮개": {"labor:LB000036#0": {"qty": 0.5}}}
|
|
|
|
MODEL = 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 f = path.join(root, "M02_MasterTemplete", "M02_Sangeun_Cover_Model.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, () => ({}));
|
|
const m = box.exports;
|
|
const job = JSON.parse(fs.readFileSync(0, "utf8"));
|
|
const out = {};
|
|
for (const [k, [fn, ...args]] of Object.entries(job)) out[k] = m[fn](...args);
|
|
process.stdout.write(JSON.stringify(out));
|
|
"""
|
|
|
|
|
|
def _node(job: dict) -> dict:
|
|
node = shutil.which("node")
|
|
if not node:
|
|
pytest.skip("node 없음")
|
|
got = subprocess.run(
|
|
[node, "-e", MODEL, str(ROOT)],
|
|
input=json.dumps(job, ensure_ascii=False),
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
assert got.returncode == 0, got.stderr
|
|
return json.loads(got.stdout)
|
|
|
|
|
|
def _whole():
|
|
return m01.loaded()[1]
|
|
|
|
|
|
def test_화면_문서_서버_as_doc_꼴():
|
|
"""고름 · 자재표시(호표 차례) → cid · 패치 · 참조 줄 = 서버 `as_doc` 와 같은 몸."""
|
|
import master_pick as mp
|
|
|
|
whole = _whole()
|
|
row = whole.logic(TWO_MATS)
|
|
item = row["호표"][2]
|
|
want = mp.search(whole, item["요소"], {}, item.get("단위"))[1]
|
|
drop = {"뺀줄": ["material:" + row["호표"][1]["이름"] + "#0"]}
|
|
one = {
|
|
"로직": TWO_MATS,
|
|
"입력값": {},
|
|
"고름": {"2": want},
|
|
"자재표시": {"2": {"별도계상": "직접계산"}},
|
|
}
|
|
ref = {"참조": {"층": "personal", "주인": None, "키": "SK-000001"}, "입력값": {}}
|
|
ids = sd.line_ids(row)
|
|
got = _node(
|
|
{
|
|
"logic": ["lineDoc", one, ids, drop],
|
|
"ref": ["lineDoc", ref, None, COVER],
|
|
"bare": ["lineDoc", one, None, None],
|
|
"needs": ["needsIds", one],
|
|
"needsRef": ["needsIds", ref],
|
|
}
|
|
)
|
|
assert got["logic"] == patch.as_doc({**one, "패치": drop}, whole)
|
|
assert got["ref"] == patch.as_doc({**ref, "패치": COVER}, whole)
|
|
assert got["bare"]["고름"] == {} and got["bare"]["덮개"] == {}
|
|
assert got["needs"] is True and got["needsRef"] is False
|
|
|
|
|
|
def test_모달_셈_길_값_일위대가_줄_값(world):
|
|
"""모달 셈(`calc_doc` · 저장 안 함) 기준 = 일위대가 셈 그 줄 기준 · 고친 줄 덮개 칸 = qty · 정체 = 바탕 줄."""
|
|
whole = _whole()
|
|
line = {"로직": PCT, "입력값": {}, "수량": {"손": 1}}
|
|
doc = _node({"d": ["lineDoc", line, None, COVER]})["d"]
|
|
bare = _node({"d": ["lineDoc", line, None, None]})["d"]
|
|
with store.at(world["personal"]):
|
|
got = calc.calc_doc(doc)
|
|
was = calc.calc_doc(bare)
|
|
mine = labor.calc(labor.clean({"담은로직": [{**line, "패치": COVER}]}), {})["줄"][0]
|
|
plain = labor.calc(labor.clean({"담은로직": [line]}), {})["줄"][0]
|
|
assert got["기준"] == pytest.approx(mine["기준"])
|
|
assert was["기준"] == pytest.approx(plain["기준"])
|
|
assert got["기준"]["계"] != was["기준"]["계"] # 품 0.5 로 바뀜
|
|
assert was["정체"] == sd.line_ids(whole.logic(PCT))
|
|
cover = [x for x in got["줄"] if x["cid"] == "labor:LB000036#0"][0]
|
|
assert cover["덮개"] == ["qty"] and cover["수량"] == pytest.approx(0.5)
|
|
|
|
|
|
def test_덮개_순수_함수_계산서_묶음():
|
|
"""칸 고침 · 풂 · 원값 뺌 · 단가 칸 비목 · 묶음 = 비목마다 서버 값 그대로."""
|
|
lines = [
|
|
{"cid": "labor:LB1#0", "이름": "보통인부", "수량": 0.2, "단가": 100, "금액": 20,
|
|
"비목": {"노무비": 20, "재료비": 0, "경비": 0}},
|
|
{"cid": "logic:GC1#0", "이름": "하위", "수량": 1, "금액": 30,
|
|
"비목": {"노무비": 10, "재료비": 20, "경비": 0}},
|
|
{"cid": "pct:공구#0", "이름": "공구손료", "수량": 0.6, "율": 3, "금액": 0.6,
|
|
"비목": {"노무비": 0, "재료비": 0, "경비": 0.6}},
|
|
] # fmt: skip
|
|
calc_ = {"기준": {"노무비": 30, "재료비": 20, "경비": 0.6, "계": 50.6}, "줄": lines}
|
|
one = {"덮개": {"labor:LB1#0": {"qty": 0.5, "원값": {"qty": 0.2}}}}
|
|
got = _node(
|
|
{
|
|
"set": ["setCover", None, "labor:LB1#0", "labor", 120],
|
|
"qty": ["setCover", {"덮개": {"x:a#0": {"배율": 2}}}, "x:a#0", "qty", 1],
|
|
"off": ["setCover", {"덮개": {"labor:LB1#0": {"qty": 1}}}, "labor:LB1#0", "qty", None],
|
|
"clean": ["cleanPatch", one],
|
|
"empty": ["cleanPatch", {"덮개": {}, "뺀줄": [], "더한줄": []}],
|
|
"keep": ["cleanPatch", {"뺀줄": ["a:b#0"]}],
|
|
"p0": ["priceKey", lines[0]],
|
|
"p1": ["priceKey", lines[1]],
|
|
"p2": ["priceKey", lines[2]],
|
|
"q2": ["qtyOf", lines[2]],
|
|
"groups": ["sheetGroups", calc_],
|
|
}
|
|
)
|
|
assert got["set"] == {"덮개": {"labor:LB1#0": {"labor": 120}}}
|
|
assert got["qty"] == {"덮개": {"x:a#0": {"qty": 1}}} # 품은 qty · 배율 중 하나
|
|
assert got["off"] is None and got["empty"] is None
|
|
assert got["clean"] == {"덮개": {"labor:LB1#0": {"qty": 0.5}}} # 원값 = 서버 몫
|
|
assert got["keep"] == {"뺀줄": ["a:b#0"]}
|
|
assert (got["p0"], got["p1"], got["p2"], got["q2"]) == ("labor", None, None, 3)
|
|
groups = got["groups"]
|
|
assert [g["비목"] for g in groups] == ["노무비", "재료비", "경비"]
|
|
assert [g["소계"] for g in groups] == [30, 20, 0.6]
|
|
assert groups[0]["줄"][0] == {"이름": "보통인부", "글": "100 * 0.2 = 20", "금액": 20}
|
|
assert [x["이름"] for x in groups[0]["줄"]] == ["보통인부", "하위"]
|
|
assert groups[2]["줄"][0]["글"] == "3 % = 0.6"
|
|
|
|
|
|
def test_normalizeLabor_줄_패치_지킴():
|
|
node = shutil.which("node")
|
|
if not node:
|
|
pytest.skip("node 없음")
|
|
doc = {"담은로직": [{"로직": PCT, "패치": COVER}, {"로직": TWO_MATS}]}
|
|
got = subprocess.run(
|
|
[node, str(ROOT / "resources/tester/helper_m02_labor_normalize.cjs")],
|
|
input=json.dumps({"doc": doc, "column": "c"}, ensure_ascii=False),
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
assert got.returncode == 0, got.stderr
|
|
rows = json.loads(got.stdout)["담은로직"]
|
|
assert rows[0]["패치"] == COVER and "패치" not in rows[1]
|
|
|
|
|
|
def test_화면_이음():
|
|
"""줄 단추 = 로직 줄 · SK 참조 줄만 · 셈 주입(프로젝트 복사본)엔 없음 · 적용 안 한 고침 물음 · 700 줄."""
|
|
rows = (M02 / "M02_MasterTemplete_Labor_Rows.ts").read_text("utf-8")
|
|
assert 'sangeun && !fx && (!row.참조 || row.참조.키.startsWith("SK-"))' in rows
|
|
detail = (M02 / "M02_MasterTemplete_Labor_Detail.ts").read_text("utf-8")
|
|
assert "const sk = opt.calc ? {} : { sangeun: openSk };" in detail
|
|
assert 'edit && opt.layer === "personal" && !library.owner' in detail
|
|
modal = (M02 / "M02_MasterTemplete_Labor_SangeunModal.ts").read_text("utf-8")
|
|
assert "same(patch, applied) || askConfirm(" in modal
|
|
assert '{ layer: "personal", owner: null }' in modal # 새 SK = 내 개인 층
|
|
assert "if (mine !== seq) return;" in modal # 최신 응답만
|
|
cover = (M02 / "M02_MasterTemplete_Sangeun_Cover.ts").read_text("utf-8")
|
|
assert "/sangeun/calc" in cover and "/sangeun/from-line" in cover
|
|
assert 'tr.classList.add("is-changed")' in cover # 표 부품 「바뀜」 상태
|
|
for name in (
|
|
"M02_MasterTemplete_Labor_Rows.ts",
|
|
"M02_MasterTemplete_Labor_Detail.ts",
|
|
"M02_MasterTemplete_Labor_SangeunModal.ts",
|
|
"M02_MasterTemplete_Sangeun_Cover.ts",
|
|
"M02_Sangeun_Cover_Model.ts",
|
|
):
|
|
assert len((M02 / name).read_text("utf-8").splitlines()) <= 700, name
|