feat(master_data): 로직 재료 줄 고르기 조건 — 구분·상세구분·규격 후보 · 대표 줄 · 후보 목록 API
- `_틀.md` 7장에 재료 고르기 문법 — 호표 `요소` 를 조건 묶음으로 · 규격 낱말 조건 · 대표 줄 지정 - 엔진 `candidates`·`pick`·`check_pick` — 조건 안 후보 목록(화면·테스트 컨테이너 공용) · 시험 계산은 대표 줄, 없으면 값 있는 첫 줄(자재지역 우선) - 로직 검사에 고르기 조건 검사 — 칸 이름 · 자재품목에 없는 구분·상세구분 · 조건 밖 대표 · 후보 0 - M01 서버 `GET /api/m01/materials` — 구분·상세구분·규격·자재지역으로 좁힌 후보와 기본 줄 - 옛 방식 재료 줄(`MP…`·`MT…` 키)은 그대로 돎 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
This commit is contained in:
@@ -724,3 +724,145 @@ def test_품셈재료_자재지역_후보(whole):
|
||||
mf.element(whole, "MP:레미콘", {"자재지역": "울진"})
|
||||
value, source = mf.element(whole, "MP:모래", {"자재지역": "수원"})
|
||||
assert source.startswith("MT") and value > 0
|
||||
|
||||
|
||||
# ── 재료 고르기 조건 (`_틀.md` 7장) ────────────────────────────────────
|
||||
def _pipes() -> mf.Master:
|
||||
"""고르기 견본 — 자재품목 넉 줄 · 로직 한 줄(호표 요소가 고르기 조건)."""
|
||||
rows = [
|
||||
{
|
||||
"키": "MT900001",
|
||||
"구분": "관·수로",
|
||||
"상세구분": "흄관",
|
||||
"이름": "흄관",
|
||||
"규격": "D300 B종",
|
||||
"단위": "m",
|
||||
"물가자료": None,
|
||||
"관급": None,
|
||||
},
|
||||
{
|
||||
"키": "MT900002",
|
||||
"구분": "관·수로",
|
||||
"상세구분": "흄관",
|
||||
"이름": "흄관",
|
||||
"규격": "D 300 B종",
|
||||
"단위": "m",
|
||||
"물가자료": Decimal(7000),
|
||||
"관급": Decimal(6500),
|
||||
},
|
||||
{
|
||||
"키": "MT900003",
|
||||
"구분": "관·수로",
|
||||
"상세구분": "흄관",
|
||||
"이름": "흄관",
|
||||
"규격": "D600 B종",
|
||||
"단위": "m",
|
||||
"물가자료": Decimal(12000),
|
||||
"관급": None,
|
||||
},
|
||||
{
|
||||
"키": "MT900004",
|
||||
"구분": "관·수로",
|
||||
"상세구분": "파형강관",
|
||||
"이름": "파형강관",
|
||||
"규격": "D300",
|
||||
"단위": "m",
|
||||
"물가자료": Decimal(9000),
|
||||
"관급": None,
|
||||
},
|
||||
]
|
||||
logic = {
|
||||
"키": "GX900001",
|
||||
"이름": "흄관 부설",
|
||||
"입력": [],
|
||||
"호표": [
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": {"구분": "관·수로", "상세구분": "흄관", "규격": "D300"},
|
||||
"이름": "흄관",
|
||||
"단위": "m",
|
||||
"수량": "2",
|
||||
"비목": "재료비",
|
||||
}
|
||||
],
|
||||
}
|
||||
return mf.Master(
|
||||
{
|
||||
"재료_자재품목.json": {"그룹": "재료", "원문": "자재품목", "줄": rows},
|
||||
"로직_자체.json": {"그룹": "로직", "원문": "자체", "줄": [logic]},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_재료_고르기_조건이_후보를_좁힘():
|
||||
"""구분 → 상세구분 → 규격 낱말(빈칸·대소문자 안 가림) 차례로 좁아짐."""
|
||||
whole = _pipes()
|
||||
assert len(mm.candidates(whole, {"구분": "관·수로"})) == 4
|
||||
assert len(mm.candidates(whole, {"구분": "관·수로", "상세구분": "흄관"})) == 3
|
||||
narrow = mm.candidates(whole, {"구분": "관·수로", "상세구분": "흄관", "규격": "D300 B종"})
|
||||
assert narrow == ["MT900001", "MT900002"] # 「D 300 B종」 도 같은 줄로 봄
|
||||
assert mm.candidates(whole, {"구분": "없는구분"}) == []
|
||||
|
||||
|
||||
def test_재료_고르기_시험계산은_대표_줄_없으면_값_있는_첫_줄():
|
||||
whole = _pipes()
|
||||
cond = {"구분": "관·수로", "상세구분": "흄관", "규격": "D300"}
|
||||
assert mm.pick(whole, cond, {}) == "MT900002" # MT900001 은 값이 없어 건너뜀
|
||||
value, source = mf.element(whole, cond, {})
|
||||
assert value == 6500 and source == "MT900002.관급" # 값 열 다섯 가운데 낮은 값
|
||||
assert mm.pick(whole, {**cond, "대표": "MT900001"}, {}) == "MT900001"
|
||||
with pytest.raises(mf.FormulaError, match="후보 없음"):
|
||||
mm.pick(whole, {"구분": "없는구분"}, {})
|
||||
|
||||
|
||||
def test_재료_고르기_호표_줄이_돎():
|
||||
"""호표 `요소` 가 조건 묶음이어도 옛 방식과 같은 길로 금액이 섬."""
|
||||
got = mf.run(_pipes(), "GX900001", {})
|
||||
assert got["재료비"] == 13000 and got["줄"][0]["단가"] == 6500
|
||||
|
||||
|
||||
def test_재료_고르기_조건_검사():
|
||||
whole = _pipes()
|
||||
ok = {"구분": "관·수로", "상세구분": "흄관"}
|
||||
assert mm.check_pick(whole, "어디", ok) == []
|
||||
assert "조건 모양" in mm.check_pick(whole, "어디", {**ok, "지역": "울진"})[0]
|
||||
assert "조건 모양" in mm.check_pick(whole, "어디", {"상세구분": "흄관"})[0]
|
||||
assert "없는 구분" in mm.check_pick(whole, "어디", {"구분": "없는구분"})[0]
|
||||
assert "없는 상세구분" in mm.check_pick(whole, "어디", {**ok, "상세구분": "없음"})[0]
|
||||
assert "조건 밖" in mm.check_pick(whole, "어디", {**ok, "대표": "MT900004"})[0]
|
||||
assert "자재품목에 없음" in mm.check_pick(whole, "어디", {**ok, "대표": "MT000001"})[0]
|
||||
# 호표 줄 검사에도 같이 실림 — 종류가 재료가 아니면 잡음
|
||||
row = dict(whole.get("GX900001"))
|
||||
row["호표"] = [{**row["호표"][0], "종류": "기계"}]
|
||||
assert any("종류 「재료」" in x for x in mf.check_logic(whole, row)[0])
|
||||
|
||||
|
||||
def test_재료_고르기_자재지역():
|
||||
"""지역이 갈리는 품목 — 이름에 그 지역이 든 줄부터 · 그런 줄이 없으면 조건 안 전부."""
|
||||
rows = [
|
||||
{
|
||||
"키": "MT900011",
|
||||
"구분": "레미콘",
|
||||
"상세구분": None,
|
||||
"이름": "레미콘-경상북도(포항시)",
|
||||
"규격": "25-24-15",
|
||||
"단위": "㎥",
|
||||
"물가자료": Decimal(80000),
|
||||
"관급": None,
|
||||
},
|
||||
{
|
||||
"키": "MT900012",
|
||||
"구분": "레미콘",
|
||||
"상세구분": None,
|
||||
"이름": "레미콘-경상북도(울진군)",
|
||||
"규격": "25-24-15",
|
||||
"단위": "㎥",
|
||||
"물가자료": Decimal(90000),
|
||||
"관급": None,
|
||||
},
|
||||
]
|
||||
whole = mf.Master({"재료_자재품목.json": {"그룹": "재료", "원문": "자재품목", "줄": rows}})
|
||||
cond = {"구분": "레미콘"}
|
||||
assert mm.candidates(whole, cond, {mm.REGION: "울진"}) == ["MT900012"]
|
||||
assert len(mm.candidates(whole, cond, {mm.REGION: "제주"})) == 2 # 없으면 조건 안 전부
|
||||
assert mf.element(whole, cond, {mm.REGION: "울진"})[0] == 90000
|
||||
|
||||
Reference in New Issue
Block a user