- 새 `M02_Sangeun_Views` — SK 고친 줄(`_Over`) · 같은 입력 · 같은 고름 위에서 `master_text.lines` + `annotate` = `글` · 줄마다 cid · 금액 · 소계 · 계 = SK 줄 · 기준(별도계상 뺌 · 단위 환산 ÷배 · 못 셈 까닭) · `중간` = 시험 계산 중간값 - `calc_doc` — 문서 `줄모드` true = 일위대가 그 줄 셈(`line` · 별도계상 기본) · 몸 `보기` false = 덧칸 안 지음(바탕 원값 셈) - `master_text.lines` · `master_sheet_line.annotate` — `files` 자리에 Master 도 받음(파일 묶음 길 그대로 · M01 `/text` 답 같음) - `lineDoc` — `줄모드: true`(일위대가 줄 모달 89,299.60 = 그 줄 금액) - 시험 `test_717_3` — lineDoc = as_doc 대조에서 `줄모드` 칸 따로 확인(서버 as_doc 은 `line` 인자로 줄 셈 · 문서 칸 없음) - 새 지킴 `test_750_1_sk_views` — 변형 일곱 · 참조 바탕 · 줄 모달 · 보기 false Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
179 lines
7.8 KiB
Python
179 lines
7.8 KiB
Python
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
|
|
"""일위대가 줄 [단가산출 보기] 모달(PLAN 717-3 · 724-1) — 임시 storage · 순수 함수는 Node 로.
|
|
|
|
화면 문서(`logic/M02_Sangeun_Cover_Model.lineDoc`) = 서버 `M02_Labor_Patch.as_doc` 꼴
|
|
(같은 고름 · 자재표시 cid) ·
|
|
모달 셈 길(`/sangeun/calc` = `calc_doc`) 값 = 일위대가 셈의 그 줄 값 · 고친 칸 = 덮개 칸 이름 ·
|
|
패치 정리 순수 함수 · `normalizeLabor` 가 줄 `패치` 를 지킴 ·
|
|
화면 이음(모달 = 줄 모드 SK 상세 · 공용 상세).
|
|
"""
|
|
|
|
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", "logic", "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"].pop("줄모드") is True and got["ref"].pop("줄모드") is True # 750-1
|
|
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_패치_정리_순수_함수():
|
|
"""원값 뺌 · 빈 칸 뺌 · 다 비면 null(줄 칸 안 둠) — 덮개 고침 자체는 717-2b `setLineCover`."""
|
|
one = {"덮개": {"labor:LB1#0": {"qty": 0.5, "원값": {"qty": 0.2}}, "x:a#0": {"원값": {}}}}
|
|
got = _node(
|
|
{
|
|
"clean": ["cleanPatch", one],
|
|
"empty": ["cleanPatch", {"덮개": {}, "뺀줄": [], "더한줄": []}],
|
|
"none": ["cleanPatch", None],
|
|
"keep": ["cleanPatch", {"뺀줄": ["a:b#0"]}],
|
|
}
|
|
)
|
|
assert got["clean"] == {"덮개": {"labor:LB1#0": {"qty": 0.5}}} # 원값 = 서버 몫
|
|
assert got["empty"] is None and got["none"] is None
|
|
assert got["keep"] == {"뺀줄": ["a:b#0"]}
|
|
|
|
|
|
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 "mountRowSk(host," in modal # 724-1 — 줄 모드 SK 상세(공용 상세)
|
|
assert "!done && sk.take() && !(await askConfirm(" in modal # 적용 안 한 고침 물음
|
|
assert "saveAsSk(o.row, now(), name, o.unit)" in modal # 새 SK = 내 개인 층(saveAsSk)
|
|
sk = (M02 / "M02_MasterTemplete_Labor_RowSk.ts").read_text("utf-8")
|
|
assert '{ layer: "personal", owner: null }' in sk
|
|
cover = (M02 / "M02_MasterTemplete_Sangeun_Cover.ts").read_text("utf-8")
|
|
assert "/sangeun/calc" in cover and "/sangeun/from-line" in cover
|
|
assert "coverTable" not in cover and not (M02 / "M02_Sangeun_Cover_Model.ts").exists()
|
|
for name in (
|
|
"M02_MasterTemplete_Labor_Rows.ts",
|
|
"M02_MasterTemplete_Labor_Detail.ts",
|
|
"M02_MasterTemplete_Labor_SangeunModal.ts",
|
|
"M02_MasterTemplete_Sangeun_Cover.ts",
|
|
"logic/M02_Sangeun_Cover_Model.ts",
|
|
):
|
|
assert len((M02 / name).read_text("utf-8").splitlines()) <= 700, name
|