- labor 종류 · 일위대가번 SU(도번 · 면적번과 같은 뒷자리) · 빈 문서 · 뼈대 검사
- M02_Labor_Doc: 정리 · 검사(같은 로직 두 번 · 없는 로직 · 자재 · 수량 꼴) · 기준(M01 엔진) · settle(수량 · 반영 · 합)
- [저장] · 복사 때 서버가 계산값 다시 적음 · 면적산출 저장 때 그 열 일위대가 계산값도
- 짝 M02_MasterTemplete_Labor_Calc.ts settle + 거울 시험
- 길 POST /layers/{층}/labor/{열}/calc · GET …/pills (main.py 등록)
- M01 _PER 「원/1,000본」 배 1000
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
214 lines
9.0 KiB
Python
214 lines
9.0 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
|