Files
Aislo/resources/tester/test_204_2_b09_bill_library.py
T

245 lines
12 KiB
Python

"""204-2 B09 설계내역서 연결 고르기 — 프로젝트 호표 + 서고(마스터 · 회사 · 개인)
구조물조합 · 사전조합.
- 서고 고름 = 프로젝트 사본 들여옴(`B09_Estimation/v1/library/{폴더}`) + 새 대가 호표(`libs`) +
줄 연결 `{tab: dg, key: 블록 id, lib: 폴더}` · 다시 고르면 같은 사본(만듦 거짓 · 같은 호표)
- 사본 단가 묶음(로직 · 사전조합 자재 · 고른 자재) · 원본을 지워도 그대로 · 단가 수요 키에 듦
- [저장] = 줄이 안 잇는 사본 지움 · 덮개 저장 꼴이 `lib` 를 지킴
- 층 권한 = `M02_Layer_Access`(남의 회사 403 · 회사 없는 개인 409)
- 화면 — 연결 메뉴 출처 다섯 · 사전조합 창(서고 줄만) → 들여오기 → 원가 자료 바꿔 끼움 · 줄 연결
임시 층(견본 마스터 · 임시 storage) · 실제 마스터 · 프로젝트는 안 씀.
"""
import asyncio
import json
import re
import subprocess
import sys
from pathlib import Path
import pytest
from fastapi import HTTPException
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from B09_Estimation import B09_Estimation_Bill_Library as library # noqa: E402
from B09_Estimation import B09_Estimation_Router as router # noqa: E402
from B09_Estimation import B09_Estimation_Store as store_b09 # noqa: E402
from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs # noqa: E402
from B09_Estimation.B09_Estimation_Linked import linked_resource_keys # noqa: E402
from config import config_system # noqa: E402
from M02_MasterTemplete import M02_Labor_Ref as refs # noqa: E402
from M02_MasterTemplete import M02_MasterTemplete_Store as store # noqa: E402
from M02_MasterTemplete import M02_Template_Layers as layers # noqa: E402
from resources.tester.helper_m02_master_sample import copy_master_sample # noqa: E402
STONE = "GF000219" # 돌쌓기 메쌓기 원/㎡
NET = "GC000337" # 낙석방지망 — 고름 4 = NET_ITEM
NET_ITEM = "MT006697"
MAT = "MT000008"
COMBO = "SS000011" # 견본 관공 관매설(옛 UA 조합 줄)
IMPORTS = re.compile(r"^import[^;]*;\r?\n", re.MULTILINE)
@pytest.fixture()
def world(tmp_path, monkeypatch):
system = copy_master_sample(tmp_path / "master")
monkeypatch.setattr(store, "FOLDER", system)
monkeypatch.setattr(layers, "SYSTEM_ROOT", system)
monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(tmp_path / "storage"))
lines = [
{"로직": STONE, "수량": {"손": 2}},
{"로직": NET, "수량": {"손": 1}, "고름": {"4": NET_ITEM}},
]
with store.at(system):
key = refs.create(
"precombo",
{"이름": "서고 조합", "단위": "㎡", "담은로직": lines,
"자재": [{"코드": MAT, "수량": {"손": 3}}]},
)["이름"] # fmt: skip
company = layers.company_dir(7)
company.mkdir(parents=True)
with store.at(company):
other = refs.create("precombo", {"이름": "회사 조합", "담은로직": lines[:1]})["이름"]
project = tmp_path / "storage" / "7" / "1" / "proj"
project.mkdir(parents=True)
return {"system": system, "key": key, "company": other, "root": project}
def _block(root, folder):
got = daega_inputs(root)
bid = got["libs"][folder]
return got, next(b for b in got["blocks"] if b["id"] == bid)
def test_사전조합_고름_사본_들여옴_새_호표_다시_고르면_재사용(world):
root = world["root"]
made = library.import_copy(root, "precombo", "system", None, world["key"])
folder = made["폴더"]
assert made == {"폴더": f"precombo@system@-@{world['key']}", "만듦": True}
copy = library.library_dir(root) / folder
assert {p.name for p in copy.iterdir()} == {"일위대가.json", "단가.json", "manifest.json"}
got, block = _block(root, folder)
assert (block["kind"], block["name"], block["unit"]) == ("사전조합", "서고 조합", "㎡")
kinds = [(ln["kind"], ln.get("qty")) for ln in block["lines"]]
assert kinds == [("sangeun", 2.0), ("sangeun", 1.0), ("material", 3.0)]
assert got["owners"] == {} # 구조물 사본 없어도 호표
# 단가 묶음 = 담은 로직 · 사전조합 자재 · 고른 자재(사본 셈이 마스터를 안 읽음)
bundle = json.loads((copy / "단가.json").read_text("utf-8"))
text = json.dumps(bundle, ensure_ascii=False)
assert all(code in text for code in (STONE, NET, MAT, NET_ITEM))
keys = linked_resource_keys(root)
assert MAT in keys and any(k.startswith("LB") for k in keys) # 단가 수요에 듦
# 다시 고름 = 새로 안 들여옴 · 같은 호표
again = library.import_copy(root, "precombo", "system", None, world["key"])
assert again == {"폴더": folder, "만듦": False}
assert _block(root, folder)[1]["id"] == block["id"]
# 원본을 지워도 굳힌 사본 그대로
(world["system"] / "precombo" / f"{world['key']}.json").unlink()
assert _block(root, folder)[1]["id"] == block["id"]
def test_구조물조합_고름_사본_조합_호표(world):
root = world["root"]
made = library.import_copy(root, "combo", "system", None, COMBO)
copy = library.library_dir(root) / made["폴더"]
names = {p.name for p in copy.iterdir()}
assert {"집계.json", "일위대가.json", "단가.json", "입력값.json", "manifest.json"} <= names
manifest = json.loads((copy / "manifest.json").read_text("utf-8"))
assert manifest["종류"] == "combo" and manifest["원본"]["key"] == COMBO
assert manifest["원본"]["경로"] # 가져온 때 경로
_, block = _block(root, made["폴더"])
assert block["kind"] == "조합" and block["lines"]
assert block["origin"] == {"layer": "system", "owner": None, "key": COMBO}
def test_없는_것_모양_틀림(world):
root = world["root"]
with pytest.raises(LookupError):
library.import_copy(root, "precombo", "system", None, "SC-999999")
with pytest.raises(LookupError):
library.import_copy(root, "combo", "system", None, "SS999999")
with pytest.raises(ValueError):
library.import_copy(root, "precombo", "system", None, "../x")
with pytest.raises(ValueError):
library.import_copy(root, "other", "system", None, world["key"])
assert library.copies(root) == []
def test_저장_때_안_잇는_사본_지움_lib_지킴(world):
root = world["root"]
a = library.import_copy(root, "precombo", "system", None, world["key"])["폴더"]
b = library.import_copy(root, "combo", "system", None, COMBO)["폴더"]
ref = {"tab": "dg", "key": "DG:x", "lib": a}
user = store_b09.clean_user(
{"bill": {"v": 2, "rows": [{"key": "hand:1", "name": "줄", "qty": 1, "ref": ref}]}}
)
assert user["bill"]["rows"][0]["ref"] == ref # 저장 꼴이 lib 를 지킴
assert library.prune(root, user) == [b]
assert [f.name for f, _ in library.copies(root)] == [a]
assert library.prune(root, {}) == [a] and library.copies(root) == []
def _call(monkeypatch, root, session, **body):
async def fake_root(_):
return root
async def fake_payload(_, at):
return {"inputs": {"daega": daega_inputs(at)}, "sheets": {}}
monkeypatch.setattr(router, "_project_root", fake_root)
monkeypatch.setattr(router, "payload", fake_payload)
req = router.LibraryBody(**body)
return asyncio.run(router.import_library("00000000-0000-0000-0000-000000000001", req, session))
def test_층_권한_길(world, monkeypatch):
root = world["root"]
mine = {"user_id": 1, "company_id": 7, "role": "USER", "name": "갑"}
got = _call(monkeypatch, root, mine, kind="precombo", layer="company", key=world["company"])
assert got["만듦"] and got["폴더"] == f"precombo@company@7@{world['company']}"
assert got["key"] == got["payload"]["inputs"]["daega"]["libs"][got["폴더"]]
assert json.loads((library.library_dir(root) / got["폴더"] / "manifest.json").read_bytes())[
"가져온이"
] == {"id": 1, "이름": "갑"}
other = {"user_id": 2, "company_id": 8, "role": "USER"}
with pytest.raises(HTTPException) as error:
_call(monkeypatch, root, other, kind="precombo", layer="company", owner="7",
key=world["company"]) # fmt: skip
assert error.value.status_code == 403
lonely = {"user_id": 3, "company_id": None, "role": "USER"}
with pytest.raises(HTTPException) as error:
_call(monkeypatch, root, lonely, kind="precombo", layer="personal", key=world["key"])
assert error.value.status_code == 409
with pytest.raises(HTTPException) as error:
_call(monkeypatch, root, lonely, kind="precombo", layer="system", key="SC-999999")
assert error.value.status_code == 404
got = _call(monkeypatch, root, lonely, kind="precombo", layer="system", key=world["key"])
assert got["만듦"] and got["key"] # 마스터 = 누구나 읽음
# ── 화면 ──────────────────────────────────────────────────────────────
LINK_JS = """
const toasts = [];
const sent = [];
globalThis.showToast = (m, k) => toasts.push([k, m]);
globalThis.API_BASE_URL = "/api";
globalThis.CURRENT_PROJECT_ID_KEY = "pid";
globalThis.localStorage = { getItem: () => "P1" };
globalThis.openLogicPicker = (done, lib) => { sent.push(["pre", lib.layer, lib.libOnly, lib.title]);
lib.pick({ 층: "company", 주인: "7", 키: "SC-000002" }, "회사 조합"); };
globalThis.openItemPicker = async () => ({ action: "apply",
item: { layer: "system", owner: null, key: "SS000011", name: "관매설" } });
const answer = { 폴더: "precombo@company@7@SC-000002", 만듦: true, key: "DG:new",
payload: { user: { bill: "서버 것" }, inputs: { daega: { libs: {} } },
sheets: { daega: [{ key: "DG:new", name: "회사 조합", spec: "", unit: "㎡" }] } } };
globalThis.fetch = async (url, init) => { sent.push([url, JSON.parse(init.body)]);
return { ok: true, status: 200, json: async () => answer }; };
const ctx = { sheets: {}, payload: { inputs: { old: 1 } }, user: { keep: 1 }, setUser() {} };
const applied = [];
const apply = (ref, from) => applied.push([ref, from && from.name]);
const [picks, more] = linkItems(ctx, { tab: "dg", key: "DG:a" }, apply);
const out = { picks: picks.map((x) => x[0]), more: more.map((x) => x[0]) };
picks.find((x) => x[0].startsWith("서고 일위대가"))[1]();
await new Promise((r) => setTimeout(r, 0));
picks.find((x) => x[0].startsWith("서고 구조물"))[1]();
await new Promise((r) => setTimeout(r, 0));
await new Promise((r) => setTimeout(r, 0));
Object.assign(out, { sent, applied, payload: ctx.payload, toasts });
console.log(JSON.stringify(out));
"""
def test_화면_출처_다섯_사전조합_창_들여오기_줄_연결_TS():
src = IMPORTS.sub(
"", (ROOT / "B09_Estimation/B09_Estimation_UI_Tabs_Bill_Link.ts").read_text("utf-8")
)
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript"],
input=src + LINK_JS, capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
out = json.loads(result.stdout)
assert out["picks"] == [
"일위대가 호표",
"단가산출 호표",
"서고 구조물조합(마스터 · 회사 · 개인)",
"서고 일위대가 사전조합(마스터 · 회사 · 개인)",
"단가산출 로직(M01 · 새 산근)",
]
assert out["more"] == ["연결 풀기"]
pre, (url, body) = out["sent"][0], out["sent"][1]
assert pre == ["pre", "personal", True, "일위대가 사전조합 고르기"] # 서고 줄만 · 층 셋
assert url == "/api/projects/P1/estimation/library"
assert body == {"kind": "precombo", "layer": "company", "owner": "7", "key": "SC-000002"}
assert out["sent"][2][1] == {"kind": "combo", "layer": "system", "owner": None,
"key": "SS000011"} # fmt: skip
ref = {"tab": "dg", "key": "DG:new", "lib": "precombo@company@7@SC-000002"}
assert out["applied"][0] == [ref, "회사 조합"]
# 원가 자료만 바꿔 끼움 — 사용자 값(서버 user)은 안 받음
assert "user" not in out["payload"] and out["payload"]["inputs"] == {"daega": {"libs": {}}}
assert out["toasts"][-1][0] == "success"