- 열 한 벌 = 키 · 구분 · 상세구분 · 이름 · 규격 · 단위 · 상하차 · 조달청 · 물가자료 · 유통물가 · 물가정보 · 거래가격 · 일반가격 · 출처 · 비고 · 면수(material_merge.COLUMNS 한 곳 · check_master 가 씀)
- 「관급」 칸 → 「조달청」(값 0 · EST 가격정보 값 안 씀) · 새 칸 「일반가격」(빈칸 · 시스템 관리자 손 입력 · 빌더가 원문번호로 이어 받음) · 상하차 = 단위 바로 뒤
- 남는 줄 지역 기준 = 전국 › 지역 없음 › 서울 › 서울 권역 › 경기 › 그 밖(값 중간) · 기준 지역은 비고 「통합: 기준 지역 …」(지역 없는 묶음은 안 적음)
- 재료 아닌 줄 삭제(요구 832) — 품질시험 수수료 60(가격정보 출처 「2026(경북)」) · 일반 도로용 블록 5(「~26.10.16」) · 치즐 2 줄은 남김 · 값 없음
- 재료_자재품목 24,092 → 17,692 줄(통합 6,335 · 재료 아닌 줄 65) · 남는 줄 키 = 830-1 키(7565ee787 이 지운 대장 키를 되살려 다시 지음 · 새 키 0) · 남는 줄 물가지 값 바뀐 것 0 · 빌더 두 번 = 같은 파일
- 가장 낮은 값 = 사급 칸(물가지 넷 + 일반가격) — master_material.SLOTS · M02_Elem_Alias.PRICES · M02 PRICE_SLOTS · 조달청 칸 안 봄 · M01 고르기 「관급」 표시 = 조달청 칸 · B09 대비표 「관급」 출처 = M01 조달청 칸(이름 그대로)
- M01 재료 화면 상하차 = 고르기(부품 createSelectField) · 새 줄 기본 「-」 · 로직 고르기 표 · 시험 계산 값 줄에 일반가격
- check_master 틀 · 로직 · 조합 0 · 단위 27 · 배 7(전과 같음) · _틀.md · _갱신절차.md 열 정의
- 시험: 새 test_830_2_material_merge · test_830_1 지움(EST 그대로 상태 = 830-2 로 끝남 · 남는 검사는 830_2 로) · 지역 줄 · 관급 이름 · 관급 칸을 보던 시험 기대값 고침(m01_api · m01_pick · masterdata_check · 814_1 · 815_1 · 827_2 · 827_3 · 750_1 · m02_hopyo_match · m02_labor_doc) · 「시멘트 전국」 검색어 시험 = xfail(830-3 연결 할 일)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 부산", "㎡")) # 830-2 지역 줄 통합 — 한 줄
|
||
assert busan["출처"].startswith("MT000018") and busan["단가"] == 11354
|
||
assert "단위경고" not in seoul
|
||
|
||
|
||
def test_고름은_그_줄만_그_품목으로_셈(client) -> None:
|
||
row = _sample("합판(내수) 12t 서울", "㎡")
|
||
got = _line(client, row, {"0": "MT000037"})
|
||
assert got["출처"].startswith("MT000037") and got["단가"] == 8465
|
||
assert _line(client, row)["단가"] == 11354 # 요청마다 — 저장 안 함
|
||
text = _line(client, row, {"0": "MT000037"}, "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): "MT000037"}}).json()[
|
||
"lines"
|
||
]
|
||
assert lines[idx]["품목"]["키"] == "MT000037" 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
|