"""일위대가 문서(`labor/<열id>.json` · PLAN 39-1a) — 정리 · 검사 · 계산 · 알약 · 저장 때 계산값. 로직 · 자재는 M01 마스터(읽기만) · 층은 견본 표(`write_sample_table`)를 임시 폴더에. """ import json import pytest from M01_MasterData import M01_MasterData_Store_Combo as combo_store from M02_MasterTemplete import M02_Labor_Doc as labor from M02_MasterTemplete import M02_MasterTemplete_Store as store from resources.tester.test_m02_structure import write_sample_table TEN = "GC000421" # 원/10㎡ — 배 10 THOUSAND = "GF000017" # 원/1,000본 — 배 1000 PLAIN = "GC000422" # ㎡/시간 — 돈 아닌 로직 STONE = "GF000219" # 원/㎡ · 입력 뒷길이 · 돌 · 쌓기 · 높이 · 초과증가율 MAT = "MT000008" # 자재품목 · 물가정보 83000 def _doc(**extra): return {"열": "rv", "담은로직": [], "자재": [], **extra} def test_정리_모르는_입력_빈_값_버림_자재_칸은_M01_것(): got = labor.clean( _doc( 담은로직=[ {"로직": STONE, "입력값": {"돌": "깬돌", "모름": 1, "높이": ""}, "수량": None} ], 자재=[{"코드": MAT, "이름": "아무 이름", "수량": {"손": "2"}}], 단위="개소", ) ) assert got["담은로직"] == [ {"로직": STONE, "별칭": None, "메모": None, "입력값": {"돌": "깬돌"}, "수량": None} ] assert got["자재"][0]["이름"] == "치 즐(0.2㎥)" and got["자재"][0]["수량"] == {"손": 2} assert (got["종류"], got["단위"], got["본뜬"]) == ("일위대가", "개소", None) @pytest.mark.parametrize( "bad", [ _doc(담은로직=[{"로직": STONE}, {"로직": STONE}]), # 같은 로직 두 번 _doc(담은로직=[{"로직": "GF999999"}]), # 없는 로직 _doc(담은로직=[{"로직": STONE, "수량": {"손": "열"}}]), # 수 아님 _doc(담은로직=[{"로직": STONE, "수량": 3}]), # 꼴 틀림 _doc(담은로직=[{"로직": STONE, "별칭": 3}]), # 별칭 글 아님(44-6) _doc(담은로직=[{"로직": STONE, "별칭": "가" * 41}]), # 별칭 40자 넘음 _doc(자재=[{"코드": "LB000001"}]), # 자재품목 아님 _doc(자재=[{"코드": MAT}, {"코드": MAT}]), # 같은 자재 두 번 "문서 아님", ], ) def test_틀린_문서는_400(bad): with pytest.raises(store.StoreError) as e: labor.clean(bad) assert e.value.status == 400 def test_별칭은_앞뒤_빈칸_떼고_빈_글은_null(): got = labor.clean( _doc(담은로직=[{"로직": STONE, "별칭": " 돌붙임 "}, {"로직": TEN, "별칭": " "}]) ) assert [r["별칭"] for r in got["담은로직"]] == ["돌붙임", None] def test_빈_일위대가도_됨(): got = labor.server(_doc()) assert got["계산값"]["기준합"]["계"] == 0 and got["계산값"]["반영합"]["계"] == 0 def test_PER_천_단위_쉼표는_배(): pills = combo_store.combo_pills({"담은로직": [{"로직": THOUSAND}, {"로직": TEN}]})["물량"] assert {p["단위"]: p.get("배") for p in pills} == {"본": 1000, "㎡": 10} def test_반영은_기준_곱_수량_나누기_배_돈_아닌_로직은_반영_없음(): doc = labor.clean( _doc(담은로직=[{"로직": TEN, "수량": {"손": 4}}, {"로직": PLAIN, "수량": None}]) ) got = labor.calc(doc, {}) ten, plain = got["줄"] assert (ten["배"], ten["수량"], ten["수량출처"]) == (10, 4, "손") for k in ("노무비", "재료비", "경비", "계"): assert ten["반영"][k] == pytest.approx(ten["기준"][k] * 4 / 10) assert plain["기준"] is None and plain["반영"] is None and "결과" in plain assert got["반영합"]["계"] == pytest.approx(ten["반영"]["계"]) def test_수량_손이_칸보다_앞섬_칸_값은_알약_키로(): doc = labor.clean( _doc( 담은로직=[{"로직": TEN, "수량": None}, {"로직": THOUSAND, "수량": {"손": 1}}], 자재=[{"코드": MAT, "수량": None}], ) ) keys = {p["키"] for p in labor.pills(doc)} ten_key = next(k for k in keys if k.endswith("|㎡")) thousand_key = next(k for k in keys if k.endswith("|본")) assert f"자재|{MAT}" in keys got = labor.calc(doc, {ten_key: "2.5", thousand_key: 99, f"자재|{MAT}": "3"}) ten, thousand = got["줄"] assert (ten["수량"], ten["수량출처"]) == (2.5, "칸") assert (thousand["수량"], thousand["수량출처"]) == (1, "손") mat = got["자재"][0] assert (mat["기준단가"], mat["수량"], mat["반영"]) == (83000, 3, 249000) # 43-3 — M01 값 열 다섯(빈 것 null) · 기준단가가 된 열 이름 assert list(mat["가격"]) == ["물가자료", "유통물가", "물가정보", "거래가격", "관급"] assert mat["기준칸"] == "물가정보" and mat["가격"]["물가정보"] == 83000 nothing = labor.calc(doc, {})["줄"][0] assert (nothing["수량출처"], nothing["반영"]) == ("없음", None) def test_면적_값_식_칸은_계산값_그_밖은_칸_값(): area = { "시트": [ { "id": "s1", "칸": {"B6": {"값": "1,200"}, "C1": {"값": "글"}}, "연결": [{"키": "a", "칸": "B6"}, {"키": "b", "칸": "D2"}, {"키": "c", "칸": "C1"}], } ], "계산값": {"s1": {"D2": {"수": "7.5"}}}, } got = labor.area_values(area) assert {k: float(v) for k, v in got.items()} == {"a": 1200, "b": 7.5} @pytest.fixture() def system(tmp_path, monkeypatch): write_sample_table(tmp_path) monkeypatch.setattr(store, "FOLDER", tmp_path) store.create_structure("rv") return tmp_path def test_구조물_만들면_빈_일위대가_같은_뒷자리(system): doc = store.read("labor", "rv")["문서"] assert doc["일위대가번"] == "SU" + store.read("structure", "rv")["문서"]["도번"][2:] assert doc["담은로직"] == [] and doc["자재"] == [] def test_저장_때_서버가_계산값_번호는_그대로_면적_저장이_계산값_새로(system): have = store.read("labor", "rv") sent = { **have["문서"], "일위대가번": "SU-999999", "담은로직": [{"로직": TEN, "수량": None}], "계산값": {"브라우저": "값"}, } store.write("labor", "rv", have["판"], sent) doc = store.read("labor", "rv")["문서"] assert doc["일위대가번"] == have["문서"]["일위대가번"] assert doc["계산값"]["줄"][0]["수량출처"] == "없음" pill = labor.pills(doc)[0]["키"] area = store.read("area", "rv") sheet = area["문서"]["시트"][0] sheet["칸"] = {"B6": {"값": "5"}} sheet["연결"] = [{"키": pill, "이름": "x", "칸": "B6", "단위": "㎡"}] store.write("area", "rv", area["판"], area["문서"]) line = store.read("labor", "rv")["문서"]["계산값"]["줄"][0] assert (line["수량"], line["수량출처"]) == (5, "칸") assert line["반영"]["계"] == pytest.approx(line["기준"]["계"] * 5 / 10) def test_틀린_일위대가_저장은_400_파일_그대로(system): have = store.read("labor", "rv") with pytest.raises(store.StoreError) as e: store.write("labor", "rv", have["판"], {**have["문서"], "담은로직": [{"로직": "GF999999"}]}) assert e.value.status == 400 assert json.loads((system / "labor" / "rv.json").read_bytes())["담은로직"] == [] # ── 길(`M02_Labor_Router`) ───────────────────────────────────────────── @pytest.fixture() def client(tmp_path, monkeypatch): 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_Template_Layers as layers 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") 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, } return TestClient(app) def test_길_calc_는_저장_안_한_문서와_알약_값으로_셈_알약도_같이(client): url = "/api/m02/layers/system/labor/rv/calc" sent = {"담은로직": [{"로직": TEN, "수량": None}], "자재": [{"코드": MAT, "수량": {"손": 1}}]} got = client.post(url, json={"doc": sent, "수량": {"사석 수상고르기|㎡": 20}}).json() assert (got["줄"][0]["수량출처"], got["자재"][0]["반영"]) == ("칸", 83000) assert {p["키"] for p in got["알약"]} == {"사석 수상고르기|㎡", f"자재|{MAT}"} saved = client.post(url, json={}).json() # 저장본 = 빈 일위대가 assert saved["줄"] == [] and saved["알약"] == [] assert client.get("/api/m02/layers/system/labor/rv/pills").json() == {"열": "rv", "알약": []} def test_길_틀린_문서_400_없는_열_404_남의_개인_층_403(client): url = "/api/m02/layers/{}/labor/{}/calc" bad = {"doc": {"담은로직": [{"로직": "GF999999"}]}} assert client.post(url.format("system", "rv"), json=bad).status_code == 400 assert client.post(url.format("system", "없는열"), json={}).status_code == 404 other = client.post(url.format("personal", "rv") + "?owner=7/99", json={}) assert other.status_code == 403 def test_로직_지우기_검사는_모든_층_일위대가를_봄(tmp_path, monkeypatch): from config import config_system from M01_MasterData import M01_MasterData_Store_Cache as cache system, storage = tmp_path / "system", tmp_path / "storage" for folder, code in ( (system / "labor", STONE), (storage / "7" / "templates" / "labor", TEN), # 회사 층 (storage / "7" / "42" / "templates" / "labor", STONE), # 개인 층 (storage / "7" / "42" / "p1" / "templates" / "labor", STONE), # 프로젝트 — 안 봄 ): folder.mkdir(parents=True) body = {"담은로직": [{"로직": code}]} (folder / "a.json").write_text(json.dumps(body), encoding="utf-8") from M02_MasterTemplete import M02_Template_Layers as layers monkeypatch.setattr(layers, "SYSTEM_ROOT", system) # 검사는 39-1d 한 벌(층 자리 = layers) monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(storage)) class Gone: # STONE 을 지운 마스터 def logic(self, code): if code == STONE: raise cache.cm.mf.FormulaError("없음") got = cache.labor_errors(Gone()) assert len(got) == 2 and all(STONE in x for x in got) assert any("마스터" in x for x in got) and any("개인 7/42" in x for x in got)