Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
152 lines
6.6 KiB
Python
152 lines
6.6 KiB
Python
"""M01 재료 기본 검색어 · 고름 · 단위 환산 · 단위 경고 — PLAN 8-1 · 8-2 계약.
|
||
|
||
검색어 든 견본 로직 줄을 시험 안에서 만들어(`POST /calc` 의 `row` — 저장 안 함) 돌림. 정본 폴더는 읽기만.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from decimal import Decimal
|
||
|
||
import pytest
|
||
from fastapi import FastAPI
|
||
from fastapi.testclient import TestClient
|
||
|
||
from M01_MasterData import M01_MasterData_Router as router_module
|
||
from M01_MasterData import M01_MasterData_Store as store
|
||
|
||
mf = store.cm.mf
|
||
mp = mf.mp
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def client() -> TestClient:
|
||
app = FastAPI()
|
||
app.include_router(router_module.router)
|
||
return TestClient(app)
|
||
|
||
|
||
def _sample(term: str, unit: str, **cond) -> dict:
|
||
"""재료 줄 하나짜리 견본 로직 — 검색어 줄 하나 · 수량 1."""
|
||
return {
|
||
"이름": "검색어 견본",
|
||
"구분": "자체",
|
||
"입력": [],
|
||
"호표": [
|
||
{
|
||
"종류": "재료",
|
||
"요소": {"검색어": term, **cond},
|
||
"이름": "견본 재료",
|
||
"단위": unit,
|
||
"수량": "1",
|
||
"비목": "재료비",
|
||
}
|
||
],
|
||
}
|
||
|
||
|
||
def _line(client: TestClient, row: dict, picks: dict | None = None, path: str = "calc") -> dict:
|
||
body = {"key": "GF999999", "inputs": {}, "row": row, **({"고름": picks} if picks else {})}
|
||
got = client.post(f"/api/m01/{path}", json=body).json()
|
||
assert got.get("ok", True) is True, got
|
||
return got["lines"][0] if path == "calc" else got
|
||
|
||
|
||
def test_검색어로_찾고_지역_낱말이_맞는_줄이_먼저(client) -> None:
|
||
seoul = _line(client, _sample("합판(내수) 12t 서울", "㎡"))
|
||
assert seoul["출처"].startswith("MT000018") and seoul["단가"] == 11354 # 값 열 가운데 낮은 값
|
||
busan = _line(client, _sample("합판(내수) 12t 부산", "㎡"))
|
||
assert busan["출처"].startswith("MT000020") and busan["단가"] == 11488
|
||
assert "단위경고" not in seoul
|
||
|
||
|
||
def test_고름은_그_줄만_그_품목으로_셈(client) -> None:
|
||
row = _sample("합판(내수) 12t 서울", "㎡")
|
||
got = _line(client, row, {"0": "MT000019"})
|
||
assert got["출처"].startswith("MT000019") and got["단가"] == 11556
|
||
assert _line(client, row)["단가"] == 11354 # 요청마다 — 저장 안 함
|
||
text = _line(client, row, {"0": "MT000019"}, "text")
|
||
assert text["ok"] is True # 읽는 식도 같은 몸으로 돎
|
||
|
||
|
||
def test_같은_성격_단위는_줄_단위로_환산(client) -> None:
|
||
got = _line(client, _sample("어닐링철선 서울 4.0mm", "톤")) # 자재 kg → 줄 톤
|
||
assert got["단가"] == 1610000 and "단위경고" not in got
|
||
got = _line(client, _sample("어닐링철선 서울 4.0mm", "g"))
|
||
assert got["단가"] == pytest.approx(1.61)
|
||
|
||
|
||
def test_성격이_다르면_계산은_하고_단위경고(client) -> None:
|
||
got = _line(client, _sample("어닐링철선 서울 4.0mm", "m"))
|
||
assert got["단가"] == 1610 and got["단위경고"] == "줄 m · 자재 kg"
|
||
assert got["금액"] == 1610
|
||
|
||
|
||
def test_검색어_후보가_없으면_그_줄만_비움(client) -> None:
|
||
got = client.post(
|
||
"/api/m01/calc", json={"key": "GF999999", "inputs": {}, "row": _sample("없는물건zzz", "개")}
|
||
).json()
|
||
assert got["ok"] is True and got["lines"][0]["금액"] is None
|
||
assert "후보 없음" in got["lines"][0]["까닭"]
|
||
|
||
|
||
def test_기계_입력은_목록_밖_원문번호도_받음(client) -> None:
|
||
key = "GC000267"
|
||
inputs = {**store.auto(key)["값"], "기계": "0101-0007"} # 고르기 목록 밖 · 기계 마스터엔 있음
|
||
got = client.post("/api/m01/calc", json={"key": key, "inputs": inputs}).json()
|
||
assert got["ok"] is True, got
|
||
bad = client.post("/api/m01/calc", json={"key": key, "inputs": {**inputs, "기계": "9999-9999"}})
|
||
assert bad.json()["ok"] is False # 기계 마스터에 없는 번호는 그대로 막힘
|
||
|
||
|
||
def test_단위_환산_표() -> None:
|
||
assert mp.convert(Decimal(1610), "kg", "톤") == 1610000
|
||
assert mp.convert(Decimal(5), "Ton", "kg") == Decimal("0.005")
|
||
assert mp.convert(Decimal(100), "㎡", "㎠") == Decimal("0.01")
|
||
assert mp.convert(Decimal(7), "kg", "m") == 7 # 성격이 다르면 그대로
|
||
assert mp.same_kind("ℓ", "㎥") and not mp.same_kind("kg", "m")
|
||
|
||
|
||
def test_check_master_검색어_후보0_단위경고(client) -> None:
|
||
whole = store.loaded()[1]
|
||
assert mf.check_pick(whole, "줄", {"검색어": "합판(내수) 12t 서울"}) == []
|
||
assert "후보 0" in mf.check_pick(whole, "줄", {"검색어": "없는물건zzz"})[0]
|
||
assert mf.check_unit(whole, "줄", {"검색어": "어닐링철선 서울 4.0mm"}, "kg") == []
|
||
assert "성격이 다름" in mf.check_unit(whole, "줄", {"검색어": "어닐링철선 서울 4.0mm"}, "m")[0]
|
||
|
||
|
||
def test_기계_입력은_EQ_키도_원문번호로_풀어_받음(client) -> None:
|
||
key = "GC000267"
|
||
number = "0101-0007" # 고르기 목록 밖
|
||
eq = next(k for k, r in store.loaded()[1].index["EQ"].items() if r.get("원문번호") == number)
|
||
base = {**store.auto(key)["값"]}
|
||
by_number = client.post(
|
||
"/api/m01/calc", json={"key": key, "inputs": {**base, "기계": number}}
|
||
).json()
|
||
by_key = client.post("/api/m01/calc", json={"key": key, "inputs": {**base, "기계": eq}}).json()
|
||
assert by_key["ok"] is True, by_key
|
||
assert by_key["sums"] == by_number["sums"] # EQ 키 = 원문번호와 같은 계산
|
||
bad = client.post("/api/m01/calc", json={"key": key, "inputs": {**base, "기계": "EQ999999"}})
|
||
assert bad.json()["ok"] is False
|
||
|
||
|
||
def test_재료_줄마다_품목_칸(client) -> None:
|
||
key = "GF000160"
|
||
body = {"key": key, "inputs": {k: str(v) for k, v in store.auto(key)["값"].items()}}
|
||
lines = client.post("/api/m01/calc", json=body).json()["lines"]
|
||
idx = next(
|
||
i for i, x in enumerate(lines) if x.get("품목", {}).get("이름", "").startswith("합판(내수)")
|
||
)
|
||
first = lines[idx]["품목"]
|
||
assert first["이름"] == "합판(내수), 서울" and first["값칸"]
|
||
assert {"키", "이름", "규격", "단위", "값칸"} <= set(first)
|
||
lines = client.post("/api/m01/calc", json={**body, "고름": {str(idx): "MT000019"}}).json()[
|
||
"lines"
|
||
]
|
||
assert lines[idx]["품목"]["키"] == "MT000019" and "인천" in lines[idx]["품목"]["이름"]
|
||
|
||
|
||
def test_목록_검색은_띄어쓰기를_무시(client) -> None:
|
||
for q in ("함석", "골 함 석", "골함석"):
|
||
got = client.get("/api/m01/rows", params={"file": "재료_자재품목.json", "q": q}).json()
|
||
assert any(r["이름"] == "골 함 석" for r in got["rows"]), q
|