Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sDZSjFvoiDjdGpgj3muc3
232 lines
9.8 KiB
Python
232 lines
9.8 KiB
Python
"""M02 개인 자재(PLAN 148) — 개인 층 파일 `개인자재.json` · 서버 길(목록 · 추가) ·
|
|
품번 `PM` + 6자리.
|
|
|
|
- 품번 = 모든 사람 통틀어 한 줄 번호(두 사람 · 두 회사 안 겹침) · 화면이 보낸 키는 안 받음
|
|
- 품명 · 단위 빈 = 400 · 단가 = 0 이상 수 또는 빈 값 · 같은 품명 + 규격 + 단위 = 409
|
|
- 개인 층만(회사 = 빈 목록 · 추가 400) · 남의 개인 층 = 403
|
|
층은 임시 폴더(마스터 · storage).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from common_util.common_util_auth import verify_session
|
|
from config import config_system
|
|
from M02_MasterTemplete import M02_Labor_Router as labor_router
|
|
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
|
from M02_MasterTemplete import M02_Material_Own as own
|
|
from M02_MasterTemplete import M02_Template_Layers as layers
|
|
from resources.tester.test_m02_structure import write_sample_table
|
|
|
|
URL = "/api/m02/layers/{}/materials"
|
|
BOLT = {"이름": "앵커볼트", "규격": "M16", "단위": "EA", "값": 1200}
|
|
|
|
|
|
@pytest.fixture()
|
|
def world(tmp_path, monkeypatch) -> dict[str, Any]:
|
|
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"))
|
|
session: dict[str, Any] = {"user_id": 42, "company_id": 7, "role": "USER", "is_master": False}
|
|
app = FastAPI()
|
|
app.include_router(labor_router.router)
|
|
app.dependency_overrides[verify_session] = lambda: session
|
|
return {"client": TestClient(app), "session": session}
|
|
|
|
|
|
def _as(world, user: int, company: int = 7) -> TestClient:
|
|
world["session"].update(user_id=user, company_id=company)
|
|
return world["client"]
|
|
|
|
|
|
def test_추가하면_PM_6자리_키_목록에_보임_화면_키_무시(world):
|
|
c = _as(world, 42)
|
|
got = c.post(URL.format("personal"), json={**BOLT, "키": "MT000008"})
|
|
assert got.status_code == 200, got.text
|
|
row = got.json()
|
|
assert row == {"키": "PM000001", "이름": "앵커볼트", "규격": "M16", "단위": "EA", "값": 1200}
|
|
assert c.get(URL.format("personal")).json()["줄"] == [row]
|
|
path = layers.personal_dir(7, 42) / own.FILE
|
|
assert path.is_file()
|
|
assert own.read(path.parent)["그룹"] == "재료"
|
|
|
|
|
|
def test_두_사람_두_회사_번호_안_겹침(world):
|
|
a = _as(world, 42).post(URL.format("personal"), json=BOLT).json()["키"]
|
|
b = _as(world, 43).post(URL.format("personal"), json=BOLT).json()["키"]
|
|
c = _as(world, 50, company=8).post(URL.format("personal"), json=BOLT).json()["키"]
|
|
d = _as(world, 42).post(URL.format("personal"), json={**BOLT, "규격": "M20"}).json()["키"]
|
|
assert [a, b, c, d] == ["PM000001", "PM000002", "PM000003", "PM000004"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"body",
|
|
[
|
|
{**BOLT, "이름": " "},
|
|
{**BOLT, "단위": ""},
|
|
{**BOLT, "값": -1},
|
|
{**BOLT, "값": "abc"},
|
|
{**BOLT, "값": True},
|
|
],
|
|
)
|
|
def test_빈_품명_단위_틀린_단가_400(world, body):
|
|
assert _as(world, 42).post(URL.format("personal"), json=body).status_code == 400
|
|
|
|
|
|
def test_단가_빈_값_받음_같은_자재_409(world):
|
|
c = _as(world, 42)
|
|
row = c.post(URL.format("personal"), json={**BOLT, "값": ""}).json()
|
|
assert row["값"] is None
|
|
assert c.post(URL.format("personal"), json={**BOLT, "값": 5}).status_code == 409
|
|
|
|
|
|
def test_회사_층은_빈_목록_추가_400_남의_개인_403(world):
|
|
c = _as(world, 42)
|
|
c.post(URL.format("personal"), json=BOLT)
|
|
assert c.get(URL.format("company")).json() == {"줄": []}
|
|
assert c.post(URL.format("company"), json=BOLT).status_code == 400
|
|
other = _as(world, 43)
|
|
assert other.get(URL.format("personal"), params={"owner": "7/42"}).status_code == 403
|
|
assert other.post(URL.format("personal") + "?owner=7/42", json=BOLT).status_code == 403
|
|
|
|
|
|
# ── 148-3 — 저장 검사 · 셈 · 사본 · 층 사이 · 개인에 저장 ──────────────
|
|
|
|
|
|
def _personal(company: int, user: int, column: str = "a"):
|
|
return _layer(layers.personal_dir(company, user), column)
|
|
|
|
|
|
def _layer(folder, column):
|
|
"""층 + 구조물 열 하나(빈 일위대가)."""
|
|
from M02_MasterTemplete import M02_MasterTemplete_Items as items_store
|
|
|
|
items_store.ensure(folder)
|
|
with store.at(folder):
|
|
got = store.read("table", store.STRUCTURE_TABLE)
|
|
got["문서"]["열"] = [{"id": column, "머리": ["돌", "가"], "꼴": "수"}]
|
|
store.write("table", store.STRUCTURE_TABLE, got["판"], got["문서"])
|
|
return folder
|
|
|
|
|
|
def _put_labor(folder, column, mats):
|
|
with store.at(folder):
|
|
have = store.read("labor", column)
|
|
store.write("labor", column, have["판"], {**have["문서"], "자재": mats})
|
|
return store.read("labor", column)["문서"]
|
|
|
|
|
|
def test_개인_층_일위대가가_개인_자재를_받고_셈은_값(world):
|
|
from M02_MasterTemplete import M02_Labor_Doc as labor
|
|
|
|
a = _personal(7, 42)
|
|
row = own.add(a, BOLT)
|
|
bare = own.add(a, {**BOLT, "규격": "M20", "값": None})
|
|
doc = _put_labor(a, "a", [{"코드": row["키"], "수량": {"손": 2}}, {"코드": bare["키"]}])
|
|
assert [(m["코드"], m["이름"], m["규격"]) for m in doc["자재"]] == [
|
|
("PM000001", "앵커볼트", "M16"),
|
|
("PM000002", "앵커볼트", "M20"),
|
|
]
|
|
with store.at(a):
|
|
got = labor.calc(doc, {})["자재"]
|
|
assert got[0]["기준단가"] == 1200 and got[0]["반영"] == 2400
|
|
assert got[1]["기준단가"] is None and got[1]["까닭"] == "단가 없음"
|
|
|
|
|
|
def test_회사_층_문서엔_개인_자재_400(world):
|
|
from M02_MasterTemplete import M02_Labor_Doc as labor
|
|
|
|
a = _personal(7, 42)
|
|
key = own.add(a, BOLT)["키"]
|
|
with store.at(layers.company_dir(7)), pytest.raises(store.StoreError) as e:
|
|
labor.clean({"열": "a", "담은로직": [], "자재": [{"코드": key}]})
|
|
assert e.value.status == 400 and "개인 층에서만" in str(e.value.detail)
|
|
with pytest.raises(store.StoreError): # 기본(마스터) 층도
|
|
labor.clean({"열": "a", "담은로직": [], "자재": [{"코드": key}]})
|
|
|
|
|
|
def test_사본_단가에_개인_줄_굳힘_사본_셈은_층_셈(world):
|
|
from M02_MasterTemplete import M02_Item_Prices as prices
|
|
from M02_MasterTemplete import M02_Item_Source as source
|
|
from M02_MasterTemplete import M02_Labor_Doc as labor
|
|
|
|
a = _personal(7, 42)
|
|
key = own.add(a, BOLT)["키"]
|
|
doc = _put_labor(a, "a", [{"코드": key, "수량": {"손": 3}}])
|
|
with store.at(a):
|
|
ss = next(r["키"] for r in store.read_combos()["줄"] if r.get("열") == "a")
|
|
layer_calc = labor.calc(doc, {})
|
|
files = source.build_source({"layer": "personal", "owner": "7/42", "key": ss})
|
|
assert files["단가.json"][own.BUNDLE]["줄"] == [{"키": key, **{k: BOLT[k] for k in BOLT}}]
|
|
whole = prices.load_prices(files["단가.json"])
|
|
copy_calc = labor.calc(doc, {}, whole=whole, read=lambda ref: None, pills=[])
|
|
assert copy_calc["자재"][0]["기준단가"] == layer_calc["자재"][0]["기준단가"] == 1200
|
|
# 원본 목록이 바뀌어도 사본 그대로 — 사본 단가는 굳힌 것
|
|
(a / own.FILE).write_text(json.dumps(own._blank()), encoding="utf-8")
|
|
assert (
|
|
labor.calc(doc, {}, whole=whole, read=lambda ref: None, pills=[])["자재"][0]["기준단가"]
|
|
== 1200
|
|
)
|
|
|
|
|
|
def test_층_사이_복사_받는_개인_층에_같은_키_회사로는_400(world):
|
|
a = _personal(7, 42)
|
|
b = _personal(7, 43, "b")
|
|
key = own.add(a, BOLT)["키"]
|
|
_put_labor(a, "a", [{"코드": key}])
|
|
with store.at(b):
|
|
store.copy_from("labor", "b", "a", source_root=a)
|
|
assert store.read("labor", "b")["문서"]["자재"][0]["코드"] == key
|
|
assert [r["키"] for r in own.rows(b)] == [key]
|
|
company = _layer(layers.company_dir(7), "c")
|
|
with store.at(company):
|
|
with pytest.raises(store.StoreError) as e:
|
|
store.copy_from("labor", "c", "a", source_root=a)
|
|
assert e.value.status == 400
|
|
|
|
|
|
def test_개인에_저장은_사본_개인_줄을_내_목록에_같은_키(world, tmp_path):
|
|
from M02_MasterTemplete import M02_Item_Personal as personal
|
|
from M02_MasterTemplete import M02_Item_Source as source
|
|
|
|
a = _personal(7, 42)
|
|
key = own.add(a, BOLT)["키"]
|
|
_put_labor(a, "a", [{"코드": key}])
|
|
with store.at(a):
|
|
ss = next(r["키"] for r in store.read_combos()["줄"] if r.get("열") == "a")
|
|
files = source.build_source({"layer": "personal", "owner": "7/42", "key": ss})
|
|
copy = tmp_path / "copy"
|
|
copy.mkdir()
|
|
for name, body in files.items():
|
|
(copy / name).write_text(json.dumps(body, ensure_ascii=False), encoding="utf-8")
|
|
b = _personal(7, 43, "b")
|
|
made = personal.create_item(b, ["돌", "나"], "볼트", "개", copy)
|
|
with store.at(b):
|
|
doc = store.read("labor", made["열"])["문서"]
|
|
assert doc["자재"][0]["코드"] == key
|
|
assert own.rows(b) == own.rows(a)
|
|
|
|
|
|
def test_화면_신규_자재는_머리_단추_개인_층만():
|
|
"""148-3 — [신규 자재] = 공용 모달 `actions`(개인 층만) · 품번은 안 보냄 · 상세는 개인 층에만 넘김."""
|
|
import os
|
|
|
|
here = os.path.join(os.path.dirname(__file__), "..", "..", "M02_MasterTemplete")
|
|
pick = open(os.path.join(here, "M02_Material_Pick.ts"), encoding="utf-8").read()
|
|
assert "...(own ? { actions: [newButton] } : {})" in pick
|
|
assert (
|
|
"`/layers/personal/materials" in pick and "키:" not in pick.split("JSON.stringify")[1][:200]
|
|
)
|
|
detail = open(os.path.join(here, "M02_MasterTemplete_Labor_Detail.ts"), encoding="utf-8").read()
|
|
assert 'opt.layer === "personal" ? { owner: library.owner } : undefined' in detail
|