feat(715-2): M02 단가산출 서고 종류 sangeun(SK-) · 참조 · 순환 422 · 지우기 막음 · 층 복사 + SK 셈 길(715-1b 몫)
- M02_Labor_Ref — PREFIX · TITLE · 키 SK · kind_of 셋째 갈래 · refs_in(담은로직 · 바탕 · 더한줄) 로 users · _no_loop · carry · frozen 이 SK 참조도 훑음 · clean_doc SK 갈래
- Expand 를 새 M02_Labor_Expand.py 로 뗌(refs.Expand 이름 그대로) · SK 갈래 = 1 단위 값
- 새 M02_Sangeun_Calc.py — 바탕 사슬 → 고친 줄 → M01 색인 덧씌움 + run_line(엔진 한 벌) · 저장 때 원값 · 바탕판 서버가 셈
- 새 M02_Sangeun_Router.py POST /api/m02/layers/{층}/sangeun/calc(저장 안 함 · 층 읽기 권한) · main.py 등록 한 줄
- Template_Layers LIBRARY_KINDS += sangeun · 뼈대 = 더한줄 · Labor_Router 서고 새로 길 Literal 한 줄
- 낡은 시험 test_m02_labor_ref 「LIBRARY_KINDS == (precombo,)」 → 새 종류 반영(점검 29)
- 새 시험 test_715_sangeun_store.py(임시 storage)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
"""M02 단가산출(SK) 서고 · 셈(PLAN 715-1b · 715-2 · `M02_Sangeun_Calc` · `M02_Labor_Ref`) — 임시 storage.
|
||||
|
||||
고친 줄 값 = 손셈 · 덧줄 % 엔진이 다시 셈 · 뺀줄 뒤 고름 제자리 · 짝 잃음 남김 · 원값 서버 ·
|
||||
순환 422 · 쓰는 곳 지우기 409 · 사전조합이 SK 를 담음 · 층 복사 왕복 같은 값.
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
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 COMPANY, MINE, world # noqa: F401 — 같은 임시 층
|
||||
|
||||
PCT = "GC001109" # 원/㎡ · 인력 둘 + 「노무비 * 3 / 100」
|
||||
TWO_MATS = "GC000565" # 원/m · 재료 줄 둘(검색어)
|
||||
|
||||
|
||||
def _sk(folder, **doc):
|
||||
with store.at(folder):
|
||||
return refs.create("sangeun", {"이름": "내 단가산출", "단위": "㎡", "더한줄": [], **doc})[
|
||||
"이름"
|
||||
]
|
||||
|
||||
|
||||
def _doc(folder, key):
|
||||
with store.at(folder):
|
||||
return store.read("sangeun", key)["문서"]
|
||||
|
||||
|
||||
def _calc(folder, key, given=None):
|
||||
with store.at(folder):
|
||||
return calc.calc_doc(_doc(folder, key), given)
|
||||
|
||||
|
||||
def _whole():
|
||||
return m01.loaded()[1]
|
||||
|
||||
|
||||
def _bare(code):
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
|
||||
return labor.run_line(_whole(), code, {})
|
||||
|
||||
|
||||
def test_qty_cover_hand_and_pct_recomputed(world):
|
||||
"""품 덮개 = 손셈 · 「노무비 * 3 / 100」 덧줄이 덮인 노무비에서 다시 셈 · 원값 = 서버가 그때 M01 값."""
|
||||
bare = _bare(PCT)
|
||||
first, second = bare["줄"][0], bare["줄"][1]
|
||||
key = _sk(
|
||||
world["personal"], 바탕={"로직": PCT}, 덮개={"labor:LB000036#0": {"qty": 0.5, "원값": 9}}
|
||||
)
|
||||
doc = _doc(world["personal"], key)
|
||||
assert doc["덮개"]["labor:LB000036#0"]["원값"]["qty"] == pytest.approx(float(first["수량"]))
|
||||
assert len(doc["바탕판"]) == 16
|
||||
got = _calc(world["personal"], key)
|
||||
labor_sum = Decimal("0.5") * Decimal(str(first["단가"])) + Decimal(str(second["금액"]))
|
||||
assert got["기준"]["노무비"] == pytest.approx(float(labor_sum))
|
||||
assert got["기준"]["경비"] == pytest.approx(float(labor_sum * 3 / 100), abs=1)
|
||||
assert got["줄"][0]["덮개"] == ["qty"] and got["줄"][0]["cid"] == "labor:LB000036#0"
|
||||
assert got["짝잃음"] == []
|
||||
# 덧줄 율 덮개 = 5 %
|
||||
key = _sk(
|
||||
world["personal"],
|
||||
바탕={"로직": PCT},
|
||||
덮개={"pct:" + bare["줄"][2]["이름"] + "#0": {"qty": 5}},
|
||||
)
|
||||
got = _calc(world["personal"], key)
|
||||
assert got["기준"]["경비"] == pytest.approx(float(bare["기준"]["노무비"]) * 0.05, abs=1)
|
||||
|
||||
|
||||
def test_drop_then_pick_stays_on_its_line(world):
|
||||
"""뺀줄 뒤 고름 제자리 — 둘째 재료 줄을 고른 자재가 첫 재료 줄을 뺀 뒤에도 그 줄 품목."""
|
||||
import master_pick as mp
|
||||
|
||||
whole = _whole()
|
||||
row = whole.logic(TWO_MATS)
|
||||
item = row["호표"][2]
|
||||
picks = mp.search(whole, item["요소"], {}, item.get("단위"))
|
||||
want = picks[1] if len(picks) > 1 else picks[0]
|
||||
first_cid = "material:" + row["호표"][1]["이름"] + "#0"
|
||||
second_cid = "material:" + item["이름"] + "#0"
|
||||
key = _sk(
|
||||
world["personal"],
|
||||
단위="m",
|
||||
바탕={"로직": TWO_MATS},
|
||||
뺀줄=[first_cid],
|
||||
고름={second_cid: want},
|
||||
자재표시={second_cid: {"별도계상": "직접계산"}}, # 기본 = 별도계상(222-1) 줄 빠짐
|
||||
)
|
||||
got = _calc(world["personal"], key)
|
||||
names = [x["이름"] for x in got["줄"]]
|
||||
assert row["호표"][1]["이름"] not in names
|
||||
line = next(x for x in got["줄"] if x["cid"] == second_cid)
|
||||
assert line["품목"]["키"] == want
|
||||
|
||||
|
||||
def test_lost_cover_kept_and_listed(world):
|
||||
key = _sk(world["personal"], 바탕={"로직": PCT}, 덮개={"labor:LB999999#0": {"qty": 1}})
|
||||
got = _calc(world["personal"], key)
|
||||
assert got["짝잃음"][0]["cid"] == "labor:LB999999#0"
|
||||
assert "labor:LB999999#0" in _doc(world["personal"], key)["덮개"]
|
||||
assert got["기준"]["계"] == pytest.approx(float(_bare(PCT)["기준"]["계"]))
|
||||
|
||||
|
||||
def test_chain_loop_422_and_delete_409(world):
|
||||
"""바탕 사슬 A ← B · B 를 A 의 바탕으로 = 422 · 쓰는 곳 있는 A 지우기 = 409(이름)."""
|
||||
a = _sk(world["personal"], 바탕={"로직": PCT})
|
||||
b = _sk(
|
||||
world["personal"], 바탕={"참조": {**MINE, "키": a}}, 덮개={"labor:LB000002#0": {"qty": 0}}
|
||||
)
|
||||
got = _calc(world["personal"], b)
|
||||
bare = _bare(PCT)
|
||||
assert got["기준"]["노무비"] == pytest.approx(float(bare["줄"][0]["금액"]))
|
||||
with store.at(world["personal"]):
|
||||
doc = store.read("sangeun", a)
|
||||
with pytest.raises(store.StoreError) as e:
|
||||
store.write(
|
||||
"sangeun", a, doc["판"], {**doc["문서"], "바탕": {"참조": {**MINE, "키": b}}}
|
||||
)
|
||||
assert e.value.status == 422
|
||||
with pytest.raises(store.StoreError) as e:
|
||||
store.delete("sangeun", a)
|
||||
assert e.value.status == 409 and "단가산출 개인 7/42" in str(e.value.detail)
|
||||
|
||||
|
||||
def test_precombo_holds_sangeun(world):
|
||||
"""사전조합 참조 줄이 SK 를 담음 — 1 단위 값 × 수량 · SK 지우기 막음."""
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
|
||||
sk = _sk(world["personal"], 바탕={"로직": PCT}, 덮개={"labor:LB000036#0": {"qty": 0.5}})
|
||||
with store.at(world["personal"]):
|
||||
sc = refs.create(
|
||||
"precombo",
|
||||
{"이름": "조합", "담은로직": [{"참조": {**MINE, "키": sk}, "수량": {"손": 2}}]},
|
||||
)["이름"]
|
||||
got = labor.calc({"담은로직": [{"참조": {**MINE, "키": sc}, "수량": {"손": 1}}]}, {})
|
||||
with pytest.raises(store.StoreError) as e:
|
||||
store.delete("sangeun", sk)
|
||||
one = _calc(world["personal"], sk)["1단위"]["계"]
|
||||
assert got["반영합"]["계"] == pytest.approx(one * 2, abs=1)
|
||||
assert e.value.status == 409 and "사전조합 개인 7/42" in str(e.value.detail)
|
||||
|
||||
|
||||
def test_layer_copy_round_trip(world):
|
||||
"""개인 B(바탕 = 개인 A) → 회사로 복사 = A 도 같이(새 키 · 본뜬) → 개인으로 다시 = 같은 값."""
|
||||
a = _sk(world["personal"], 바탕={"로직": PCT}, 덮개={"labor:LB000036#0": {"qty": 0.5}})
|
||||
b = _sk(world["personal"], 바탕={"참조": {**MINE, "키": a}}, 뺀줄=["labor:LB000002#0"])
|
||||
want = _calc(world["personal"], b)["기준"]
|
||||
src = _doc(world["personal"], b)
|
||||
with store.at(world["company"]):
|
||||
made = refs.create(
|
||||
"sangeun", refs.carry(refs.origin_mark(src, {**MINE, "키": b}), ("personal", "7/42"))
|
||||
)
|
||||
copy = _doc(world["company"], made["이름"])
|
||||
assert copy["바탕"]["참조"]["층"] == "company" and copy["본뜬"]["키"] == b
|
||||
assert _calc(world["company"], made["이름"])["기준"] == want
|
||||
with store.at(world["personal"]):
|
||||
back = refs.create(
|
||||
"sangeun",
|
||||
refs.carry(refs.origin_mark(copy, {**COMPANY, "키": made["이름"]}), ("company", "7")),
|
||||
)
|
||||
assert _doc(world["personal"], back["이름"])["바탕"]["참조"]["층"] == "company"
|
||||
assert _calc(world["personal"], back["이름"])["기준"] == want
|
||||
|
||||
|
||||
def test_refusals(world):
|
||||
with store.at(world["personal"]):
|
||||
for bad, status in (
|
||||
({"이름": "x", "더한줄": []}, 400), # 단위 빔
|
||||
({"이름": "x", "단위": "㎡", "더한줄": [], "바탕": {"로직": "GC999999"}}, 400),
|
||||
(
|
||||
{
|
||||
"이름": "x",
|
||||
"단위": "㎡",
|
||||
"더한줄": [],
|
||||
"바탕": {"참조": {**MINE, "키": "SK-000999"}},
|
||||
},
|
||||
400,
|
||||
),
|
||||
({"이름": "x", "단위": "㎡"}, 400), # 뼈대(더한줄) 없음
|
||||
):
|
||||
with pytest.raises(store.StoreError) as e:
|
||||
refs.create("sangeun", bad)
|
||||
assert e.value.status == status, bad
|
||||
key = _sk(world["personal"], 바탕={"로직": PCT})
|
||||
with store.at(world["personal"]), pytest.raises(store.StoreError) as e:
|
||||
calc.calc_doc({**_doc(world["personal"], key), "바탕": {"참조": {**MINE, "키": key}}})
|
||||
assert e.value.status == 422
|
||||
@@ -85,7 +85,7 @@ def _base(code):
|
||||
|
||||
# ── 자료 틀 ────────────────────────────────────────────────────────────
|
||||
def test_단가승격_서고는_없음_층_종류에서_뺌(world):
|
||||
assert "promote" not in layers.KINDS and layers.LIBRARY_KINDS == ("precombo",)
|
||||
assert "promote" not in layers.KINDS and layers.LIBRARY_KINDS == ("precombo", "sangeun")
|
||||
with store.at(world["system"]), pytest.raises(KeyError):
|
||||
refs.create("promote", {"담은로직": [{"로직": TEN}]})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user