feat(717-2b): M02 단가산출 상세 · 고침 — M01 상세 부품(보기 넷)을 M02 길로 · 품 · 단가 칸 덮개 · 줄 더하기 · 빼기 · 바탕 M01 로직 고름
- 새 Sangeun_Detail(mountSangeunDetail) · Sangeun_Cover(고침 표 · 셈 길 최신 응답만) · logic/M02_Sangeun_Cover_Model(순수 꼴) - 고친 칸 = 칸 색 + 바탕 값 곁 글 + 「단가산출에서 고침」 · M01 개정 배지(바탕판 · 원값) · 짝잃음 띠 · 별도계상 재료 = 계 밖 표기 - M01 buildLabDetail 덧칸 cover(단가산출 상세 · 인력·재료·경비 덩이 갈음) · 마스터 이동 단추 = 등록 없으면 숨김(hasMasterJump) - Sangeun_View 자리표 · 옛 글 열쇠(준비 중 · 덮개 · 더한 줄 · 열린 입력) 걷음 - 시험 test_717_2b_sangeun_detail 3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
"""717-2b M02 단가산출 상세 · 고침 지킴 시험 — 순수 꼴(node) · 글 · 서버 저장 왕복(임시 storage).
|
||||
|
||||
- 고친 칸 → SK 덮개(qty · 배율 · 비목 칸 · 원값 곁 글) · 더한줄 수량 · 단가 · 뺀줄 · 되살림
|
||||
- 로직 줄 단가 = 노 · 재 · 경 몫 비로 나눔 · 계산서 묶음 = 표 줄 그대로(별도계상 재료 = 금액 없음)
|
||||
- 화면 문서 꼴이 서버 저장(`clean_doc`)을 지나고 서버가 원값 · 바탕판을 다시 적음 · 셈 = 손셈
|
||||
- 상세 = M01 상세 부품(M02 길) · 부품 상자만 · 마스터 이동 단추 = 등록 없으면 숨김
|
||||
"""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
M02 = ROOT / "M02_MasterTemplete"
|
||||
MODEL = M02 / "logic" / "M02_Sangeun_Cover_Model.ts"
|
||||
BASE = "GC000278"
|
||||
|
||||
|
||||
def _text(p: Path) -> str:
|
||||
return p.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _node(body: str) -> object:
|
||||
src = _text(MODEL) + "\n" + body
|
||||
out = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", src],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
) # fmt: skip
|
||||
assert out.returncode == 0, out.stderr
|
||||
return json.loads(out.stdout)
|
||||
|
||||
|
||||
SAMPLE = """
|
||||
const line = (cid, 이름, 수량, 단가, 비목, 묶음) => ({ cid, 이름, 단위: "인", 수량, 단가,
|
||||
금액: Object.values(비목).reduce((a, b) => a + b, 0), 비목, 묶음 });
|
||||
const bare = { 기준: null, 짝잃음: [], 바탕판: "x",
|
||||
정체: ["labor:A#0", "logic:G#0", "material:M#0"],
|
||||
줄: [line("labor:A#0", "석공", 0.1, 1000, { 노무비: 100 }, "인력"),
|
||||
line("logic:G#0", "굴착기", 2, 50, { 노무비: 60, 재료비: 30, 경비: 10 }, "경비")],
|
||||
재료: [{ 차례: 2, 이름: "모르타르", 수량: 0.5, 단가: 200, 금액: 100 }] };
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
def test_고침_꼴():
|
||||
body = (
|
||||
SAMPLE
|
||||
+ """
|
||||
const doc = { 덮개: {}, 더한줄: [{ 종류: "재료", 요소: "MT1", 수량: 1 }], 뺀줄: [] };
|
||||
const ans = { ...bare, 정체: [...bare.정체, "add:#0"],
|
||||
줄: [...bare.줄, line("add:#0", "더한 재료", 1, 300, { 재료비: 300 }, "재료")] };
|
||||
const rows = rowsOf(ans, bare, doc);
|
||||
const by = (cid) => rowsOf(ans, bare, doc).find((r) => r.cid === cid);
|
||||
const out = {};
|
||||
out.rows = rows.map((r) => [r.cid, r.묶음, r.apart, r.added, r.priceEdit]);
|
||||
setQty(doc, by("labor:A#0"), 0.2, false);
|
||||
out.qty = doc.덮개["labor:A#0"];
|
||||
setQty(doc, by("labor:A#0"), 0.1, false); // 바탕 값과 같음 = 덮개 지움
|
||||
out.qtyBack = doc.덮개["labor:A#0"] ?? null;
|
||||
setPrice(doc, by("labor:A#0"), 1200);
|
||||
out.price = doc.덮개["labor:A#0"];
|
||||
setPrice(doc, by("logic:G#0"), 100); // 노 60 · 재 30 · 경 10 비
|
||||
out.split = doc.덮개["logic:G#0"];
|
||||
setQty(doc, by("add:#0"), 3, false);
|
||||
setPrice(doc, by("add:#0"), 350);
|
||||
out.added = doc.더한줄[0];
|
||||
const opened = { 덮개: {} };
|
||||
setQty(opened, by("labor:A#0"), 1.5, true);
|
||||
out.scale = opened.덮개["labor:A#0"];
|
||||
setQty(opened, by("labor:A#0"), 1, true);
|
||||
out.scaleBack = opened.덮개["labor:A#0"] ?? null;
|
||||
dropLine(doc, by("labor:A#0"));
|
||||
out.drop = [doc.뺀줄, doc.덮개["labor:A#0"] ?? null];
|
||||
restoreLine(doc, "labor:A#0");
|
||||
out.restore = doc.뺀줄;
|
||||
dropLine(doc, by("add:#0"));
|
||||
out.dropAdded = doc.더한줄.length;
|
||||
const covered = { 덮개: { "labor:A#0": { qty: 0.3, 원값: { qty: 0.09, 단가: 1000 } } } };
|
||||
out.revised = rowsOf(bare, bare, covered).find((r) => r.cid === "labor:A#0").revised;
|
||||
out.sheet = sheetGroups(rowsOf(bare, bare, {}))
|
||||
.map((g) => [g.비목, g.소계, g.줄.map((x) => x.금액)]);
|
||||
console.log(JSON.stringify(out));
|
||||
"""
|
||||
)
|
||||
got = _node(body)
|
||||
assert got["rows"] == [
|
||||
["labor:A#0", "인력", False, None, True],
|
||||
["logic:G#0", "경비", False, None, True],
|
||||
["add:#0", "재료", False, 0, True],
|
||||
["material:M#0", "재료", True, None, True],
|
||||
]
|
||||
assert got["qty"] == {"qty": 0.2, "원값": {"qty": 0.1, "단가": 1000}}
|
||||
assert got["qtyBack"] is None
|
||||
assert got["price"] == {"labor": 1200, "원값": {"qty": 0.1, "단가": 1000}}
|
||||
assert got["split"]["labor"] == 60 and got["split"]["material"] == 30
|
||||
assert got["split"]["expense"] == 10
|
||||
assert got["added"]["수량"] == 3 and got["added"]["단가"] == 350
|
||||
assert got["scale"]["배율"] == 1.5 and "qty" not in got["scale"]
|
||||
assert got["scaleBack"] is None
|
||||
assert got["drop"] == [["labor:A#0"], None]
|
||||
assert got["restore"] == [] and got["dropAdded"] == 0
|
||||
assert got["revised"] is True
|
||||
# 계산서 = 표 줄 그대로 · 별도계상 재료 금액 없음(계에 안 듦)
|
||||
assert got["sheet"] == [
|
||||
["노무비", 160, [100, 60]],
|
||||
["재료비", 30, [30, None]],
|
||||
["경비", 10, [10]],
|
||||
]
|
||||
|
||||
|
||||
def test_상세_부품_길_부품상자():
|
||||
view = _text(M02 / "M02_MasterTemplete_Sangeun_View.ts")
|
||||
detail = _text(M02 / "M02_MasterTemplete_Sangeun_Detail.ts")
|
||||
cover = _text(M02 / "M02_MasterTemplete_Sangeun_Cover.ts")
|
||||
assert "mountSangeunDetail(host, doc" in view and "createEmpty" not in view
|
||||
assert 'read: "/m02/logic-lib"' in detail and "setLogicApiPaths(LIB)" in detail
|
||||
assert "buildLabDetail(editor" in detail and "cover: { ho, est }" in detail
|
||||
assert "buildCalc(calcHost" in detail and "openSangeunLinePicker(scope" in detail
|
||||
assert "if (mine !== seq || !alive) return" in detail # 최신 응답만
|
||||
for s in (detail, cover, _text(MODEL)):
|
||||
assert "<button" not in s and "<input" not in s and "<table" not in s
|
||||
assert "document.createElement" not in s and "sessionStorage." not in s
|
||||
lab = _text(ROOT / "M01_MasterData" / "M01_MasterData_UI_LogicLab_Detail.ts")
|
||||
assert "ctx.cover?.est ??" in lab and "ctx.cover.ho" in lab
|
||||
side = _text(ROOT / "M01_MasterData" / "M01_MasterData_UI_Side.ts")
|
||||
modal = _text(ROOT / "M01_MasterData" / "M01_MasterData_UI_LogicLab_Modal.ts")
|
||||
assert "export const hasMasterJump" in side and "go.hidden = !hasMasterJump()" in modal
|
||||
assert len(_text(ROOT / "M01_MasterData" / "M01_MasterData_UI_LogicLab.ts").splitlines()) <= 627
|
||||
|
||||
|
||||
def test_화면_문서_꼴_서버_저장_셈(world): # noqa: F811 — 가져온 fixture
|
||||
"""화면 꼴(덮개 + 원값 곁 글 · 로직 줄 나눈 단가 · 더한줄 · 뺀줄) → 저장 · 셈 = 손셈."""
|
||||
mine = world["personal"]
|
||||
with store.at(mine):
|
||||
plain = calc.calc_doc(
|
||||
{"키": "SK-000000", "이름": "t", "단위": "㎥", "바탕": {"로직": BASE}}
|
||||
)
|
||||
lines = {x["cid"]: x for x in plain["줄"]}
|
||||
a, g = lines["labor:LB000018#0"], lines["logic:GC000267#0"]
|
||||
parts = {k: Decimal(str(v)) for k, v in g["비목"].items()}
|
||||
whole_amount = sum(parts.values())
|
||||
split = {
|
||||
{"노무비": "labor", "재료비": "material", "경비": "expense"}[k]: float(
|
||||
round(Decimal(90000) * v / whole_amount, 4)
|
||||
)
|
||||
for k, v in parts.items()
|
||||
}
|
||||
doc = {
|
||||
"이름": "717-2b 시험",
|
||||
"단위": "㎥",
|
||||
"바탕": {"로직": BASE},
|
||||
"덮개": {
|
||||
"labor:LB000018#0": {"qty": 0.02, "원값": {"qty": 1, "단가": 1}}, # 화면 곁 글
|
||||
"logic:GC000267#0": {**split, "원값": {"qty": 9}},
|
||||
},
|
||||
"뺀줄": ["labor:LB000002#1"],
|
||||
"더한줄": [
|
||||
{"종류": "인력", "요소": "LB000002", "이름": "보통인부", "단위": "인", "수량": 0.01}
|
||||
],
|
||||
}
|
||||
key = refs.create("sangeun", doc)["이름"]
|
||||
saved = store.read("sangeun", key)["문서"]
|
||||
got = calc.calc_doc(saved)
|
||||
cover = saved["덮개"]["labor:LB000018#0"]
|
||||
assert cover["원값"]["qty"] == pytest.approx(a["수량"]) # 서버가 다시 적음(화면 값 버림)
|
||||
assert saved["바탕판"] == plain["바탕판"] and saved["뺀줄"] == ["labor:LB000002#1"]
|
||||
by = {x["cid"]: x for x in got["줄"]}
|
||||
assert "labor:LB000002#1" not in by
|
||||
assert by["labor:LB000018#0"]["금액"] == pytest.approx(0.02 * a["단가"])
|
||||
assert by["logic:GC000267#0"]["금액"] == pytest.approx(g["수량"] * 90000, rel=1e-6)
|
||||
added = by["add:#0"] # 더한 보통인부 = 바탕 보통인부 단가 × 0.01
|
||||
assert added["수량"] == pytest.approx(0.01)
|
||||
assert added["단가"] == pytest.approx(lines["labor:LB000002#0"]["단가"])
|
||||
assert got["기준"]["계"] == pytest.approx(sum(x["금액"] for x in got["줄"]))
|
||||
Reference in New Issue
Block a user