Files
Aislo/resources/tester/test_830_2_material_merge.py
T
eomsangdonandClaude Opus 5.5 64affbb00e feat(830-2): 재료 DB 가격 열 재정의 · 항목 통합 이어 감 — 조달청 · 일반가격 · 상하차 · 재료 아닌 줄 삭제
- 열 한 벌 = 키 · 구분 · 상세구분 · 이름 · 규격 · 단위 · 상하차 · 조달청 · 물가자료 · 유통물가 · 물가정보 · 거래가격 · 일반가격 · 출처 · 비고 · 면수(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>
2026-11-14 18:36:51 +09:00

169 lines
6.1 KiB
Python

"""830-2 재료 표 — 항목 통합 · 가격 열 재정의(요구 827 ~ 832) 지킴.
열 한 벌(상하차 = 단위 뒤 · 조달청 · 물가지 넷 · 일반가격) · 조달청 값 0 · 이름에 덧정보 없음 ·
지역 기준 전국 › 서울 › 경기 › 그 밖 · 관급 사급 한 줄 · 철근 상차 줄 따로 · 재료 아닌 줄 0 ·
치즐 값 없음 · 남는 줄 키 · 물가지 값 = EST 원본 그 줄 · 빌더 = 정본 · 가장 낮은 값이 조달청 칸을 안 봄.
"""
import json
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "resources/master_data/scripts"))
import build_자재품목_분류 as cls # noqa: E402
import build_재료_자재품목 as builder # noqa: E402
import compare_estx as ce # noqa: E402
import master_keys as mk # noqa: E402
import master_material as mmat # noqa: E402
import material_merge as mm # noqa: E402
MASTER = ROOT / "resources/master_data"
COLS = (
"키",
"구분",
"상세구분",
"이름",
"규격",
"단위",
"상하차",
"조달청",
"물가자료",
"유통물가",
"물가정보",
"거래가격",
"일반가격",
"출처",
"비고",
"면수",
)
@pytest.fixture(scope="module")
def rows():
return json.loads((MASTER / "재료_자재품목.json").read_text(encoding="utf-8"))["줄"]
@pytest.fixture(scope="module")
def raw():
if not ce.ESTX.is_file():
pytest.skip("ESTX 원본 없음")
return ce.Estx().table("StuffcDat")
def test_열_한_벌_차례(rows):
assert mm.COLUMNS == COLS
assert {tuple(r) for r in rows} == {COLS}
def test_상하차_세_값(rows):
got = {r["상하차"] for r in rows}
assert got == {"-", "상차", "하차"}
def test_조달청_빔_일반가격_빔(rows):
assert all(r["조달청"] is None for r in rows)
assert all(
r["일반가격"] is None for r in rows
) # 처음엔 전부 빈칸 — 손 입력이 생기면 이 줄을 고침
def test_이름에_덧정보_없음(rows):
names = [r["이름"] for r in rows]
assert not [n for n in names if "관급" in n or "사급" in n]
assert not [n for n in names if mm.NAME_LOAD.search(n)]
remi = [r for r in rows if r["이름"].startswith("레미콘")]
assert remi and not [r for r in remi if "(" in r["이름"] or "," in r["이름"]]
def test_재료_아닌_줄_0_치즐_남고_값_없음(rows):
names = {r["이름"] for r in rows}
gone = ("골재시험", "콘크리트시험", "흙의물리시험", "아스팔트시험", "품질시험 현장출장비")
assert not names & set(gone)
assert not names & {"네오스톤블록", "네오스톤슬라브", "앤틱스톤블록"}
chisel = [r for r in rows if r["키"] in ("MT000009", "MT000011")] # 품셈(2016) 값 줄 둘
assert [r["이름"].replace(" ", "") for r in chisel] == ["치즐(0.4㎥)", "치즐(0.7㎥)"]
assert all(r[c] is None for r in chisel for c in (mm.GOVT, *mm.MARKET, mm.OWN))
assert not [
r
for r in rows
if "가격정보" in str(r["비고"] or "") or "품셈(2016)" in str(r["비고"] or "")
]
def test_철근_상차도_줄_따로(rows):
rebar = [r for r in rows if r["이름"].startswith("이형철근") and r["규격"].startswith("HD=")]
assert rebar and all(r["상하차"] == "상차" for r in rebar)
assert not [r for r in rebar if "상차도" in r["규격"]]
def test_지역_기준_전국_서울_경기_그밖():
def kept(names):
rows = [
{"열쇠": str(i), "이름": n, "규격": "25-21-150", "단위": "㎥", "물가자료": p}
for i, (n, p) in enumerate(names)
]
out, log = mm.merge(rows)
assert len(out) == 1
return log[0]["기준 지역"], out[0]["비고"]
base = [("레미콘(부산)-사급", 100), ("레미콘(수원)-사급", 200), ("레미콘(서울)-사급", 300)]
assert kept(base + [("레미콘(전국)-사급", 400)])[0] == "전국"
assert kept(base)[0] == "서울"
region, note = kept(base[:2])
assert region == "수원" and "기준 지역 수원" in note # 경기
region, _ = kept(
[("레미콘(부산)-사급", 100), ("레미콘(대구)-사급", 300), ("레미콘(광주)-사급", 200)]
)
assert region == "광주" # 그 밖 = 값 중간
def test_관급_사급_한_줄_조달청_빔():
rows = [
{
"열쇠": "1",
"이름": "VR관(관급)",
"규격": "D=300mm",
"단위": "본",
"물가자료": None,
mm.GOVT: 5,
},
{"열쇠": "2", "이름": "VR관", "규격": "D=300mm", "단위": "본", "물가자료": 7},
]
out, _ = mm.merge(rows)
assert [(r["이름"], r["물가자료"], r[mm.GOVT]) for r in out] == [("VR관", 7, None)]
def test_빌더_정본_키_물가지_원본_그대로(rows, raw):
book = mk.load_book()["키"]
code = {r["키"]: book[r["키"]]["원문번호"] for r in rows}
src = {r["Code"]: r for r in raw if r.get("Code")}
est = [r for r in rows if not str(r["출처"]).startswith(builder.CARRY)]
assert len(est) == 17573 and len(rows) == 17692
for r in est:
s = src[code[r["키"]]]
assert [r[c] or 0 for c in mm.MARKET] == [s.get(f"Cost{i}") or 0 for i in range(2, 6)]
assert not builder.dropped(s)
built, _ = mm.merge(builder.build(raw) + builder.carried()) # 다시 지어도 같은 표
assert [code[r["키"]] for r in rows] == [r["열쇠"] for r in built]
same = [
c for c in COLS if c not in ("키", "구분", "상세구분", "비고")
] # 분류 두 칸 = 분류기 몫
for r, b in zip(rows, built):
assert [r[c] for c in same] == [b[c] for c in same]
assert r["비고"] == b["비고"] or (r["비고"] == cls.NOTE_EMPTY and not r["이름"])
def test_가장_낮은_값은_조달청_안_봄():
assert mm.GOVT not in mmat.SLOTS and mm.OWN in mmat.SLOTS
row = {"키": "MT999999", mm.GOVT: 1, "물가자료": 50, mm.OWN: 40}
class M:
def get(self, key):
return row
assert mmat._lowest(M(), "MT999999") == (40, "MT999999.일반가격")