- 새 M02_Sangeun_Doc.py — clean(꼴 400 · 열린입력 = 배율만 · 원값 버림) · line_ids · patched(고름 · 자재표시 cid 열쇠 → 새 차례 · 짝잃음 남김) - 엔진 안 만짐 · 시험 test_715_sangeun_doc.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
180 lines
7.2 KiB
Python
180 lines
7.2 KiB
Python
"""M02 단가산출(SK) 문서 꼴 · 줄 정체 · 고친 줄(PLAN 715-1a · `M02_Sangeun_Doc`) — 엔진 안 부름."""
|
|
|
|
from decimal import Decimal
|
|
|
|
import pytest
|
|
|
|
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
|
|
|
ROW = {
|
|
"키": "GX000001",
|
|
"결과단위": "원/㎡",
|
|
"입력": [{"이름": "높이"}, {"이름": "돌", "고르기": ["깬잡석", "견치돌"]}],
|
|
"호표": [
|
|
{
|
|
"종류": "인력",
|
|
"요소": "LB000033",
|
|
"이름": "석공",
|
|
"수량": "높이 * 0.1",
|
|
"비목": "노무비",
|
|
},
|
|
{"종류": "재료", "요소": {"검색어": "모래"}, "이름": "모래", "수량": "1", "비목": "재료비"},
|
|
{"종류": "인력", "요소": "LB000033", "이름": "석공", "수량": "0.2", "비목": "노무비"},
|
|
{"종류": "재료", "요소": {"검색어": "자갈"}, "이름": "자갈", "수량": "2", "비목": "재료비"},
|
|
{
|
|
"종류": "로직",
|
|
"요소": "로직(GC000276, 기계='5401-0017')",
|
|
"이름": "굴착기",
|
|
"수량": "0.5",
|
|
},
|
|
],
|
|
"덧줄": [{"이름": "공구손료", "식": "(석공 * LB000033) * 3 / 100", "비목": "경비"}],
|
|
}
|
|
|
|
|
|
def _doc(**extra):
|
|
return {
|
|
"키": "SK-000001",
|
|
"이름": "돌쌓기 내 값",
|
|
"단위": "㎡",
|
|
"바탕": {"로직": "GX000001"},
|
|
**extra,
|
|
}
|
|
|
|
|
|
def test_line_ids_kind_key_n():
|
|
assert sd.line_ids(ROW) == [
|
|
"labor:LB000033#0",
|
|
"material:모래#0",
|
|
"labor:LB000033#1",
|
|
"material:자갈#0",
|
|
"logic:GC000276#0",
|
|
"pct:공구손료#0",
|
|
]
|
|
|
|
|
|
def test_clean_shape_and_refusals():
|
|
got = sd.clean(_doc(입력값={"높이": 3, "모름": 1, "돌": ""}, 원값=1), ROW)
|
|
assert got["입력값"] == {"높이": 3} and got["종류"] == "단가산출" and got["뺀줄"] == []
|
|
for bad, why in (
|
|
({"키": "SC-000001"}, "SK-"),
|
|
({"이름": " "}, "이름"),
|
|
({"단위": ""}, "단위"),
|
|
({"바탕": {"참조": {"키": "SC-000001"}}}, "SK-"),
|
|
({"덮개": {"labor:LB000033#0": {"qty": "식"}}}, "수가 아님"),
|
|
({"덮개": {"labor:LB000033#0": {"qty": 1, "배율": 2}}}, "하나"),
|
|
({"덮개": {"labor:LB#x": {"qty": 1}}}, "kind:키#n"),
|
|
({"뺀줄": ["material:모래#0", "material:모래#0"]}, "두 번"),
|
|
({"열린입력": ["높이"], "입력값": {"높이": 3}}, "열린입력"),
|
|
({"더한줄": [{"종류": "인력", "요소": "LB000002"}]}, "수량"),
|
|
({"더한줄": [{"종류": "식", "수량": 1}]}, "종류"),
|
|
):
|
|
with pytest.raises(sd.DocError, match=why):
|
|
sd.clean(_doc(**bad), ROW)
|
|
# 원값 · 바탕판 = 서버 몫 — 브라우저 값 버림
|
|
got = sd.clean(_doc(덮개={"labor:LB000033#0": {"qty": 0.5, "원값": 9}}), ROW)
|
|
assert got["덮개"] == {"labor:LB000033#0": {"qty": 0.5}}
|
|
|
|
|
|
def test_open_inputs_only_ratio():
|
|
with pytest.raises(sd.DocError, match="배율만"):
|
|
sd.clean(_doc(열린입력=["돌"], 덮개={"labor:LB000033#0": {"qty": 1}}), ROW)
|
|
got = sd.clean(_doc(열린입력=["돌"], 덮개={"labor:LB000033#0": {"배율": 1.1}}), ROW)
|
|
assert got["덮개"]["labor:LB000033#0"] == {"배율": 1.1}
|
|
|
|
|
|
def test_patched_cover_qty_price_ratio():
|
|
doc = sd.clean(
|
|
_doc(
|
|
덮개={
|
|
"labor:LB000033#1": {"qty": 0.25, "labor": 200000},
|
|
"labor:LB000033#0": {"배율": 1.5},
|
|
"logic:GC000276#0": {"expense": 1000},
|
|
"pct:공구손료#0": {"qty": 5},
|
|
}
|
|
),
|
|
ROW,
|
|
)
|
|
got = sd.patched(ROW, doc)
|
|
rows = got["줄"]["호표"]
|
|
assert rows[0]["수량"] == "(높이 * 0.1) * 1.5"
|
|
assert rows[2]["수량"] == "0.25" and rows[2]["단가"] == Decimal(200000)
|
|
assert rows[4]["값묶음"] == {"경비": Decimal(1000)}
|
|
assert got["줄"]["덧줄"][0]["식"] == "(석공 * LB000033) * 5 / 100"
|
|
assert got["짝잃음"] == [] and ROW["호표"][2]["수량"] == "0.2" # 바탕은 안 바뀜
|
|
|
|
|
|
def test_drop_keeps_pick_on_its_line():
|
|
"""뺀줄 뒤 고름 제자리 — 자갈(바탕 3째) 고름이 뺀줄 뒤 새 2째로 옮겨 감 · 모래 줄에 안 붙음."""
|
|
doc = sd.clean(_doc(뺀줄=["material:모래#0"], 고름={"material:자갈#0": "MT000010"}), ROW)
|
|
got = sd.patched(ROW, doc)
|
|
names = [x["이름"] for x in got["줄"]["호표"]]
|
|
assert names == ["석공", "석공", "자갈", "굴착기"]
|
|
assert got["고름"] == {"2": "MT000010"}
|
|
assert got["정체"][:4] == [
|
|
"labor:LB000033#0",
|
|
"labor:LB000033#1",
|
|
"material:자갈#0",
|
|
"logic:GC000276#0",
|
|
]
|
|
|
|
|
|
def test_added_lines_before_extras():
|
|
doc = sd.clean(
|
|
_doc(
|
|
더한줄=[
|
|
{"종류": "인력", "요소": "LB000002", "이름": "보통인부", "단위": "인", "수량": 0.1},
|
|
{
|
|
"종류": "로직",
|
|
"로직": "GC000001",
|
|
"입력값": {"규격": "가", "두께": 2},
|
|
"수량": 1,
|
|
},
|
|
{"참조": {"층": "system", "주인": None, "키": "SK-000002"}, "수량": 2},
|
|
]
|
|
),
|
|
ROW,
|
|
)
|
|
got = sd.patched(ROW, doc)
|
|
added = got["줄"]["호표"][5:]
|
|
assert added[0]["비목"] == "노무비" and added[0]["수량"] == "0.1"
|
|
assert added[1]["요소"] == "로직(GC000001, 규격='가', 두께=2)"
|
|
assert added[2]["참조"]["키"] == "SK-000002" and added[2]["값묶음"] is None
|
|
assert got["정체"][5:] == ["add:#0", "add:#1", "add:#2", "pct:공구손료#0"]
|
|
|
|
|
|
def test_lost_covers_kept_and_listed():
|
|
"""짝 잃은 덮개 = 문서에 남김 + 목록(지우지 않음) · 비목 다른 단가 · 재료 아닌 고름."""
|
|
doc = sd.clean(
|
|
_doc(
|
|
덮개={"labor:LB999999#0": {"qty": 1}, "labor:LB000033#0": {"material": 5}},
|
|
고름={"labor:LB000033#1": "MT000010"},
|
|
뺀줄=["material:없음#0"],
|
|
),
|
|
ROW,
|
|
)
|
|
got = sd.patched(ROW, doc)
|
|
why = {x["cid"]: x["까닭"] for x in got["짝잃음"]}
|
|
assert why["labor:LB999999#0"].startswith("덮개") and "비목 다름" in why["labor:LB000033#0"]
|
|
assert why["labor:LB000033#1"].startswith("고름") and why["material:없음#0"].startswith("뺀줄")
|
|
assert "labor:LB999999#0" in doc["덮개"]
|
|
|
|
|
|
def test_chain_moves_base_picks():
|
|
"""바탕 SK 사슬 — 바탕 쪽 차례 고름이 아래 SK 뺀줄 뒤에도 그 줄에."""
|
|
first = sd.patched(ROW, sd.clean(_doc(고름={"material:자갈#0": "MT000010"}), ROW))
|
|
child = sd.clean({**_doc(뺀줄=["labor:LB000033#0"]), "바탕": None}, first["줄"])
|
|
got = sd.patched(first["줄"], child, first["고름"])
|
|
assert got["고름"] == {"2": "MT000010"} and got["줄"]["호표"][2]["이름"] == "자갈"
|
|
|
|
|
|
def test_empty_base():
|
|
row = sd.empty_row({"키": "SK-000003", "이름": "자체", "단위": "식"})
|
|
doc = sd.clean(
|
|
{"키": "SK-000003", "이름": "자체", "단위": "식", "바탕": None,
|
|
"더한줄": [{"종류": "재료", "요소": "MT000010", "수량": 3, "단가": 1200}]},
|
|
row,
|
|
) # fmt: skip
|
|
got = sd.patched(row, doc)
|
|
assert got["줄"]["호표"][0]["단가"] == Decimal(1200) and got["줄"]["결과단위"] == "원/식"
|