Files
Aislo/resources/tester/test_715_sangeun_doc.py
T
eomsangdonandClaude Opus 5.5 a9242671da feat(715-1a): M02 단가산출(SK) 문서 꼴 · 검사 · 줄 정체 kind:키#n · 덮개 · 더한줄 · 뺀줄 · 고친 줄 순수 함수
- 새 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>
2026-11-01 14:44:14 +09:00

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["줄"]["결과단위"] == "원/식"