362 lines
16 KiB
Python
362 lines
16 KiB
Python
"""일위대가 조합 서고(PLAN 50-3 · `M02_Labor_Ref`) — 단가승격 · 사전조합 · 참조 줄.
|
|
|
|
펼침(재귀 · 순환 · 깊이) · 층 규칙(위 층만) · 쓰는 곳 지우기 409 · 층 사이 따라 복사 ·
|
|
프로젝트 굳힘 · 로직 지우기 검사 · TS 짝 settle 거울. 층은 임시 폴더(마스터 · storage).
|
|
"""
|
|
|
|
import json
|
|
from decimal import Decimal
|
|
|
|
import pytest
|
|
|
|
from M02_MasterTemplete import M02_Labor_Doc as labor
|
|
from M02_MasterTemplete import M02_Labor_Ref as refs
|
|
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
|
from M02_MasterTemplete import M02_Template_Layers as layers
|
|
from resources.tester.test_m02_labor_calc_mirror import _same, _ts
|
|
from resources.tester.test_m02_structure import write_sample_table
|
|
|
|
TEN = "GC000421" # 원/10㎡ — 배 10
|
|
STONE = "GF000219" # 원/㎡
|
|
PLAIN = "GC000422" # 돈 아닌 로직
|
|
SYS = {"층": "system", "주인": None}
|
|
COMPANY = {"층": "company", "주인": "7"}
|
|
MINE = {"층": "personal", "주인": "7/42"}
|
|
|
|
|
|
@pytest.fixture()
|
|
def world(tmp_path, monkeypatch):
|
|
from config import config_system
|
|
|
|
system = tmp_path / "system"
|
|
system.mkdir()
|
|
write_sample_table(system)
|
|
monkeypatch.setattr(store, "FOLDER", system)
|
|
monkeypatch.setattr(layers, "SYSTEM_ROOT", system)
|
|
(tmp_path / "storage").mkdir()
|
|
monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(tmp_path / "storage"))
|
|
with store.at(system):
|
|
store.create_structure("rv")
|
|
return {
|
|
"system": system,
|
|
"company": layers.company_dir(7),
|
|
"personal": layers.personal_dir(7, 42),
|
|
}
|
|
|
|
|
|
def _promote(folder, code=TEN, **extra):
|
|
with store.at(folder):
|
|
return refs.create("promote", {"담은로직": [{"로직": code, "입력값": {}}], **extra})["이름"]
|
|
|
|
|
|
def _combo(folder, lines, name="조합"):
|
|
with store.at(folder):
|
|
return refs.create("precombo", {"이름": name, "담은로직": lines})["이름"]
|
|
|
|
|
|
def _read(folder, kind, key):
|
|
return json.loads((folder / kind / f"{key}.json").read_bytes())
|
|
|
|
|
|
def _base(code):
|
|
import M01_MasterData.M01_MasterData_Store as m01
|
|
|
|
return labor.run_line(m01.loaded()[1], code, {})["기준"]
|
|
|
|
|
|
# ── 자료 틀 ────────────────────────────────────────────────────────────
|
|
def test_승격은_로직_한_줄_단위_배_이름은_로직에서_키는_층마다_다음_번호(world):
|
|
assert _promote(world["system"]) == "SP-000001"
|
|
assert _promote(world["system"], STONE, 이름="돌") == "SP-000002"
|
|
assert _promote(world["personal"]) == "SP-000001" # 층마다 셈
|
|
doc = _read(world["system"], "promote", "SP-000001")
|
|
assert (doc["종류"], doc["단위"], doc["배"]) == ("단가승격", "㎡", 10)
|
|
assert doc["이름"] and doc["담은로직"] == [{"로직": TEN, "입력값": {}}]
|
|
assert _read(world["system"], "promote", "SP-000002")["이름"] == "돌"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"body",
|
|
[
|
|
{"담은로직": []},
|
|
{"담은로직": [{"로직": TEN}, {"로직": STONE}]},
|
|
{"담은로직": [{"로직": "GF999999"}]},
|
|
{"담은로직": [{"참조": {**SYS, "키": "SP-000001"}}]},
|
|
],
|
|
)
|
|
def test_틀린_승격은_400(world, body):
|
|
_promote(world["system"])
|
|
with store.at(world["system"]), pytest.raises(store.StoreError) as e:
|
|
refs.create("promote", body)
|
|
assert e.value.status == 400
|
|
|
|
|
|
def test_사전조합_틀린_줄_400_순환_422(world):
|
|
sp = _promote(world["system"])
|
|
one = {"참조": {**SYS, "키": sp}}
|
|
for lines in (
|
|
[{"로직": TEN}], # 로직은 못 담음(승격 · 사전조합만)
|
|
[one, one], # 같은 참조 두 번
|
|
[{"참조": {**SYS, "키": "SP-000009"}}], # 없는 참조
|
|
[{"참조": {**SYS, "키": "SU-000001"}}], # 서고 키 아님
|
|
):
|
|
with pytest.raises(store.StoreError) as e:
|
|
_combo(world["system"], lines)
|
|
assert e.value.status == 400
|
|
a = _combo(world["system"], [one], "가")
|
|
b = _combo(world["system"], [{"참조": {**SYS, "키": a}}], "나")
|
|
with store.at(world["system"]):
|
|
have = store.read("precombo", a)
|
|
with pytest.raises(store.StoreError) as e:
|
|
store.write(
|
|
"precombo",
|
|
a,
|
|
have["판"],
|
|
{**have["문서"], "담은로직": [{"참조": {**SYS, "키": b}}]},
|
|
)
|
|
assert e.value.status == 422 and f"{a} → {b} → {a}" in e.value.detail
|
|
|
|
|
|
def test_층_규칙은_위_층만(world):
|
|
_promote(world["system"])
|
|
_promote(world["company"])
|
|
_promote(world["personal"])
|
|
ok = [{"참조": {**s, "키": "SP-000001"}} for s in (SYS, COMPANY, MINE)]
|
|
assert _combo(world["personal"], ok) == "SC-000001" # 개인 → 개인 · 회사 · 마스터
|
|
for folder, bad in (
|
|
(world["company"], MINE), # 회사 → 개인 X
|
|
(world["system"], COMPANY), # 마스터 → 회사 X
|
|
(world["personal"], {"층": "personal", "주인": "7/43"}), # 남의 개인 X
|
|
(world["personal"], {"층": "company", "주인": "8"}), # 남의 회사 X
|
|
):
|
|
with pytest.raises(store.StoreError) as e:
|
|
_combo(folder, [{"참조": {**bad, "키": "SP-000001"}}])
|
|
assert e.value.status == 400 and "위 층만" in e.value.detail
|
|
|
|
|
|
def test_일위대가_참조_줄_정리_같은_참조_두_번_400(world):
|
|
sp = _promote(world["system"])
|
|
line = {"참조": {**SYS, "키": sp}, "별칭": " 승격 ", "수량": {"손": 3}}
|
|
with store.at(world["system"]):
|
|
got = labor.clean({"열": "rv", "담은로직": [line]})["담은로직"]
|
|
assert got == [{"참조": {**SYS, "키": sp}, "별칭": "승격", "메모": None, "수량": {"손": 3}}]
|
|
with pytest.raises(store.StoreError) as e:
|
|
labor.clean({"열": "rv", "담은로직": [line, line]})
|
|
assert e.value.status == 400
|
|
|
|
|
|
# ── 펼침(계산) ─────────────────────────────────────────────────────────
|
|
def test_calc_는_참조를_펼침_승격은_기준_나누기_배_사전조합은_수량_곱(world):
|
|
sp = _promote(world["system"])
|
|
sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}, "수량": {"손": 2}}], "둘")
|
|
ref = {**SYS, "키": sc}
|
|
with store.at(world["system"]):
|
|
doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 5}}]})
|
|
got = labor.calc(doc, {})
|
|
one = got["줄"][0]
|
|
want = Decimal(str(_base(TEN)["계"])) / 10 * 2
|
|
assert one["기준"]["계"] == pytest.approx(float(want))
|
|
assert (one["분모"], one["배"], one["알약"]) == (None, 1, refs.ref_id(ref))
|
|
assert one["반영"]["계"] == pytest.approx(float(want * 5))
|
|
assert one["안"][0]["수량"] == 2 and one["안"][0]["참조"]["키"] == sp
|
|
pills = labor.pills(doc)
|
|
assert pills == [{"키": refs.ref_id(ref), "이름": "둘", "단위": "", "로직": [], "참조": ref}]
|
|
|
|
|
|
def test_승격_줄_상세는_원_로직_키_표_사전조합은_안_줄만(world):
|
|
sp = _promote(world["system"])
|
|
sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}}], "하나")
|
|
with store.at(world["system"]):
|
|
doc = labor.clean({"열": "rv", "담은로직": [{"참조": {**SYS, "키": k}} for k in (sp, sc)]})
|
|
promote, combo = labor.calc(doc, {})["줄"]
|
|
assert promote["원로직"] == TEN and promote["줄"]
|
|
assert sum(x["금액"] for x in promote["줄"]) == pytest.approx(
|
|
promote["기준"]["계"]
|
|
) # 1 단위(51-2)
|
|
assert "줄" not in combo and [x["참조"]["키"] for x in combo["안"]] == [sp]
|
|
|
|
|
|
def test_펼침은_순환_깊이_없는_참조를_까닭으로_멈춤(world):
|
|
folder = world["system"] / "precombo"
|
|
for key, child in (("SC-000001", "SC-000002"), ("SC-000002", "SC-000001")):
|
|
body = {"이름": key, "담은로직": [{"참조": {**SYS, "키": child}}]}
|
|
(folder / f"{key}.json").write_text(json.dumps(body), encoding="utf-8")
|
|
import M01_MasterData.M01_MasterData_Store as m01
|
|
|
|
expand = refs.Expand(m01.loaded()[1])
|
|
got = expand({**SYS, "키": "SC-000001"})
|
|
assert got["기준"] is None and "순환" in got["까닭"]
|
|
assert "없는 참조" in expand({**SYS, "키": "SC-000009"})["까닭"]
|
|
deep = refs.Expand(
|
|
m01.loaded()[1], lambda r: {"담은로직": [{"참조": {**SYS, "키": "SC-000001"}}]}
|
|
)
|
|
assert deep.__call__({**SYS, "키": "SC-000001"}, ())["기준"] is None
|
|
|
|
|
|
def test_돈_아닌_승격은_까닭(world):
|
|
sp = _promote(world["system"], PLAIN)
|
|
import M01_MasterData.M01_MasterData_Store as m01
|
|
|
|
got = refs.Expand(m01.loaded()[1])({**SYS, "키": sp})
|
|
assert got["기준"] is None and got["까닭"]
|
|
|
|
|
|
def test_참조_줄_settle_파이썬과_TS_가_같은_답(world):
|
|
sp = _promote(world["system"])
|
|
ref = {**SYS, "키": sp}
|
|
with store.at(world["system"]):
|
|
doc = labor.clean(
|
|
{"열": "rv", "담은로직": [{"로직": STONE, "수량": None}, {"참조": ref, "수량": None}]}
|
|
)
|
|
answer = labor.calc(doc, {})
|
|
values = {refs.ref_id(ref): 4}
|
|
py = labor.settle(answer, doc, values)
|
|
assert py["줄"][1]["수량출처"] == "칸"
|
|
_same(py, _ts(answer, doc, values))
|
|
|
|
|
|
# ── 지우기 · 복사 · 굳힘 · 로직 지우기 ─────────────────────────────────
|
|
def test_쓰는_곳이_있으면_지우기_409_없으면_지움(world):
|
|
sp = _promote(world["system"])
|
|
with store.at(world["personal"]):
|
|
line = {"참조": {**SYS, "키": sp}}
|
|
store.write("labor", "x1", "", {"열": "x1", "담은로직": [line], "자재": []})
|
|
with store.at(world["system"]), pytest.raises(store.StoreError) as e:
|
|
store.delete("promote", sp)
|
|
assert e.value.status == 409 and "개인 7/42" in e.value.detail
|
|
(world["personal"] / "labor" / "x1.json").unlink()
|
|
with store.at(world["system"]):
|
|
store.delete("promote", sp)
|
|
assert not (world["system"] / "promote" / f"{sp}.json").exists()
|
|
|
|
|
|
def test_복사는_못_읽는_참조를_같이_복사_본뜬(world):
|
|
sp = _promote(world["personal"])
|
|
sys_sp = _promote(world["system"])
|
|
sc = _combo(
|
|
world["personal"],
|
|
[{"참조": {**MINE, "키": sp}}, {"참조": {**SYS, "키": sys_sp}, "수량": {"손": 2}}],
|
|
)
|
|
with store.at(world["personal"]):
|
|
doc = store.read("precombo", sc)["문서"]
|
|
with store.at(world["company"]):
|
|
made = refs.create(
|
|
"precombo", refs.carry({**doc, "본뜬": {**MINE, "키": sc}}, ("personal", "7/42"))
|
|
)
|
|
got = _read(world["company"], "precombo", made["이름"])
|
|
assert got["본뜬"] == {**MINE, "키": sc}
|
|
assert [one["참조"] for one in got["담은로직"]] == [
|
|
{**COMPANY, "키": "SP-000001"}, # 회사가 못 읽는 개인 승격 → 회사 층 새 키
|
|
{**SYS, "키": sys_sp}, # 마스터는 그대로
|
|
]
|
|
assert _read(world["company"], "promote", "SP-000001")["본뜬"] == {**MINE, "키": sp}
|
|
|
|
|
|
def test_길_새로_복사_권한(world):
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from common_util.common_util_auth import verify_session
|
|
from M02_MasterTemplete import M02_Labor_Router as labor_router
|
|
|
|
app = FastAPI()
|
|
app.include_router(labor_router.router)
|
|
app.dependency_overrides[verify_session] = lambda: {
|
|
"user_id": 42,
|
|
"company_id": 7,
|
|
"role": "USER",
|
|
"is_master": False,
|
|
}
|
|
client = TestClient(app)
|
|
url = "/api/m02/layers/{}/library/{}"
|
|
body = {"문서": {"담은로직": [{"로직": TEN}]}}
|
|
got = client.post(url.format("personal", "promote"), json=body).json()
|
|
assert got["이름"] == "SP-000001"
|
|
assert client.post(url.format("system", "promote"), json=body).status_code == 403
|
|
copy = {"본뜬": {**MINE, "키": "SP-000001"}}
|
|
assert client.post(url.format("personal", "promote"), json=copy).json()["이름"] == "SP-000002"
|
|
other = {"본뜬": {"층": "personal", "주인": "7/99", "키": "SP-000001"}}
|
|
assert client.post(url.format("personal", "promote"), json=other).status_code == 403
|
|
|
|
|
|
def test_프로젝트는_참조사본으로_굳힘_원본을_지워도_같은_값(world):
|
|
from M02_MasterTemplete import M02_Item_Prices as prices
|
|
from M02_MasterTemplete import M02_Item_Source as source
|
|
|
|
sp = _promote(world["system"])
|
|
sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}, "수량": {"손": 2}}])
|
|
ref = {**SYS, "키": sc}
|
|
with store.at(world["system"]):
|
|
doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 1}}]})
|
|
live = labor.calc(doc, {})["줄"][0]["기준"]["계"]
|
|
rows = source.labor_doc_rows(doc)
|
|
assert set(rows["참조사본"]) == {refs.ref_id(ref), refs.ref_id({**SYS, "키": sp})}
|
|
assert refs.frozen_logics(rows["참조사본"]) == [TEN]
|
|
bundle = prices.to_json(prices.collect([TEN]))
|
|
for key in (sc, sp): # 원본이 사라져도 프로젝트는 사본만 봄
|
|
kind = refs.kind_of(key)
|
|
(world["system"] / kind / f"{key}.json").unlink()
|
|
got = prices.calc(rows, bundle, {}, {}, None)
|
|
line = got["줄"][0]
|
|
assert line["ok"] and line["참조"] == ref
|
|
assert line["계"] == pytest.approx(live, rel=1e-9)
|
|
assert line["수량"] == 1 and line["반영"]["계"] == pytest.approx(live, rel=1e-9)
|
|
|
|
|
|
def test_로직_지우기_검사는_단가승격도_보고_참조_줄은_건너뜀(world):
|
|
from M01_MasterData import M01_MasterData_Store_Cache as cache
|
|
|
|
sp = _promote(world["system"], STONE)
|
|
with store.at(world["system"]):
|
|
have = store.read("labor", "rv")
|
|
line = {"참조": {**SYS, "키": sp}}
|
|
store.write("labor", "rv", have["판"], {**have["문서"], "담은로직": [line]})
|
|
|
|
class Gone: # STONE 을 지운 마스터
|
|
def logic(self, code):
|
|
if code == STONE:
|
|
raise cache.cm.mf.FormulaError("없음")
|
|
|
|
got = cache.labor_errors(Gone())
|
|
assert got == [f"단가승격 마스터 「{sp}」 이 로직 「{STONE}」 을 씀 — 거기서 먼저 뺌"]
|
|
|
|
|
|
def test_새_프로젝트_씨앗은_서고를_안_가져감(world, tmp_path):
|
|
_promote(world["system"])
|
|
got = layers.seed_project(tmp_path / "p1")
|
|
assert not any(k.startswith(layers.LIBRARY_KINDS) for k in got["작업본"])
|
|
|
|
|
|
def test_층_목록_서고_줄은_이름_단위_기준금액을_실음(world):
|
|
sp = _promote(world["system"])
|
|
sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}, "수량": {"손": 3}}], "셋")
|
|
rows = [
|
|
{"종류": "promote", "이름": sp},
|
|
{"종류": "precombo", "이름": sc},
|
|
{"종류": "labor", "이름": "rv"},
|
|
]
|
|
refs.summaries(world["system"], rows)
|
|
one = float(Decimal(str(_base(TEN)["계"])) / 10)
|
|
assert rows[0]["기준금액"] == pytest.approx(one)
|
|
assert (rows[1]["제목"], rows[1]["기준금액"]) == ("셋", pytest.approx(one * 3))
|
|
assert "기준금액" not in rows[2]
|
|
|
|
|
|
def test_사전조합이_자기_자신을_담으면_422_수량_기본_1(world):
|
|
sp = _promote(world["system"])
|
|
a = _combo(world["system"], [{"참조": {**SYS, "키": sp}}], "가")
|
|
with store.at(world["system"]):
|
|
have = store.read("precombo", a)
|
|
assert have["문서"]["담은로직"][0]["수량"] == {"손": 1}
|
|
with pytest.raises(store.StoreError) as e:
|
|
store.write(
|
|
"precombo",
|
|
a,
|
|
have["판"],
|
|
{
|
|
**have["문서"],
|
|
"담은로직": [*have["문서"]["담은로직"], {"참조": {**SYS, "키": a}}],
|
|
},
|
|
)
|
|
assert e.value.status == 422 and "자기 자신" in e.value.detail
|