Files
Aislo/resources/tester/test_m02_labor_doc.py
T
eomsangdonandClaude Opus 5.5 f393abdeec merge(M01): origin/sub_laptop_7 받음 — 로직 지우기 검사 두 벌을 sub4 한 벌로(labor_errors · 실무문서 · 알림 글)
- sub7 의 LABOR_ROOTS · 겹친 _labor_files · labor_errors 지움 · sub7 시험은 층 자리(layers.SYSTEM_ROOT) · 알림 글만 맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
2026-10-04 18:30:14 +09:00

243 lines
10 KiB
Python

"""일위대가 문서(`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}
]
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(자재=[{"코드": "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_빈_일위대가도_됨():
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)
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)