"""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