Merge remote-tracking branch 'origin/sub_laptop_2' into sub_laptop_3

# Conflicts:
#	resources/master_data/재료_품셈재료.json
#	resources/tester/test_masterdata_check.py
This commit is contained in:
2026-09-22 13:38:53 +09:00
5 changed files with 55 additions and 15 deletions
+2 -1
View File
@@ -220,9 +220,10 @@
}
```
- 조건 칸 = `구분`(반드시) · `상세구분` · `규격` · `대표` · 그 밖의 칸은 두지 않음. `종류` 는 「재료」만.
- 조건 칸 = `구분`(반드시) · `상세구분` · `이름` · `규격` · `대표` · 그 밖의 칸은 두지 않음. `종류` 는 「재료」만.
- 구분·상세구분 = 자재품목 줄의 같은 이름 칸과 똑같이 적음(묶음 목록은 `ref/_묶음_자재품목.md`).
- 규격 조건 = 원문이 규격을 밝힌 것만 좁힘 — 낱말이 **모두** 든 줄이 후보(「D300」 · 「D300 B종」) · 대소문자·빈칸·`φ` 는 안 가림. 원문이 규격을 안 밝히면 칸을 두지 않음.
- 이름 조건 = 구분·상세구분·규격이 같아도 품명이 갈리는 자재품목을 가를 때만 둠(예: 파형강관 커플링밴드 vs 접속티 — 둘 다 관경만 규격에 있어 규격 조건으로는 안 갈림). 규격과 같은 방식(낱말이 **모두** 이름에 든 줄) — 대소문자·빈칸은 안 가림.
- 대표 줄 = 관리자가 고른 자재품목 키 — 시험 계산이 그 줄의 값을 씀. 조건 밖 키는 검사가 잡음.
- 후보는 **호표 줄 단위와 같은 자재품목 줄만** — 단위가 다르면 다른 물건(우드칩 ㎥ · 원목후로링 ㎡). 조건 안 줄이 모두 다른 단위면 `check_master 단위` 가 알림.
- 조건 값에 `{입력}` 을 넣을 수 있음 — `"규격": "{관경}"` 은 계산 때 입력값으로 채워짐(검사에서는 그 칸을 뺌).
@@ -16,7 +16,7 @@ LINKED = "MP" # 품셈재료 테이블ID
MARKET = "MT" # 자재품목 테이블ID
PRICED = "MO" # 유가전력 테이블ID — 값 한 칸
SLOTS = ("물가자료", "유통물가", "물가정보", "거래가격", "관급") # 자재품목 값 열
PICK_KEYS = ("구분", "상세구분", "규격", "대표") # 재료 고르기 조건 칸 (`_틀.md` 7장)
PICK_KEYS = ("구분", "상세구분", "이름", "규격", "대표") # 재료 고르기 조건 칸 (`_틀.md` 7장)
REGION = "자재지역" # 로직 입력 — 「지역」 은 유가 고르기가 씀
TARIFF = "전력 계약종별" # 로직 입력 — 품셈·계약예규에 계약종별 규정 없음
_UNITS = {"ea": "", "ton": "", "t": "", "l": "", "리터": "", "set": "", "세트": ""}
@@ -131,16 +131,18 @@ def by_class(master: mf.Master) -> dict[str, dict]:
def candidates(
master: mf.Master, cond: dict, env: dict | None = None, unit: str | None = None
) -> list[str]:
"""고르기 조건 안 자재품목 키 — 구분 · 상세구분 · 규격 낱말이 모두 든 줄 · 파일 차례대로.
"""고르기 조건 안 자재품목 키 — 구분 · 상세구분 · 이름 · 규격 낱말이 모두 든 줄 · 파일 차례대로.
`unit` 을 주면 그 단위(호표 줄 단위)와 같은 줄만 — 단위가 다르면 다른 물건.
입력 `자재지역` 이 있으면 이름에 그 지역이 든 줄만(그런 줄이 없으면 조건 안 전부)."""
want = [plain_spec(w) for w in str(cond.get("규격") or "").split()]
want_name = [plain(w) for w in str(cond.get("이름") or "").split()]
detail = cond.get("상세구분")
need = plain_unit(unit) if unit else ""
hits = [
key
for key, row in by_class(master).get(cond.get("구분"), {}).items()
if (not detail or row.get("상세구분") == detail)
and all(w in plain(row.get("이름")) for w in want_name)
and all(w in plain_spec(row.get("규격")) for w in want)
and (not need or plain_unit(row.get("단위")) == need)
]
@@ -2485,7 +2485,12 @@
},
{
"종류": "재료",
"요소": "MP:커플링밴드 ∅{관경}㎜",
"요소": {
"구분": "관·이음관",
"상세구분": "파형강관",
"이름": "파형강관 커플링밴드(관급)",
"규격": "Φ{관경}mm"
},
"이름": "커플링밴드",
"단위": "EA",
"수량": "만약(커플링밴드 == '필요', 1 / 6, 0)",
@@ -35,17 +35,17 @@
{ "키": "MP000024", "원문번호": "고무링 ∅800㎜", "이름": "고무링(VR관)", "규격": "∅800㎜", "단위": "개", "요구절": ["산림품셈 12-11-1"], "연결": "MT009721", "고르기": null, "비고": "연결: VR관고무링 D800 이름·규격 같음" },
{ "키": "MP000025", "원문번호": "흄관 ∅800㎜", "이름": "흄관", "규격": "∅800㎜", "단위": "m", "요구절": ["산림품셈 12-11-2-1"], "연결": null, "고르기": {"구분": "관·이음관", "상세구분": "흄관", "규격": "Φ800mm", "대표": "MT014981"}, "비고": "A·B형 갈림 · 값은 「본」 — m 아님" },
{ "키": "MP000026", "원문번호": "파형강관 ∅800㎜", "이름": "파형강관", "규격": "∅800㎜", "단위": "m", "요구절": ["산림품셈 12-11-3"], "연결": null, "고르기": {"구분": "관·이음관", "상세구분": "파형강관"}, "비고": "두께 1.6·2.0·2.7T 갈림 — 원문 없음" },
{ "키": "MP000027", "원문번호": "커플링밴드 ∅800㎜", "이름": "커플링밴드(파형강관)", "규격": "∅800㎜", "단위": "EA", "요구절": ["산림품셈 12-11-3 주①"], "연결": "MT015309", "고르기": null, "비고": "연결: 파형강관 커플링밴드 Φ800 관급 줄" },
{ "키": "MP000027", "원문번호": "커플링밴드 ∅800㎜", "이름": "커플링밴드(파형강관)", "규격": "∅800㎜", "단위": "EA", "요구절": ["산림품셈 12-11-3 주①"], "연결": null, "고르기": {"구분": "관·이음관", "상세구분": "파형강관", "이름": "파형강관 커플링밴드(관급)", "규격": "Φ800mm", "대표": "MT015309"}, "비고": "일감35 — 규격(관경)만으론 접속티와 안 갈려 이름 조건을 더함" },
{ "키": "MP000028", "원문번호": "VR관 ∅1000㎜", "이름": "VR관(소켓식)", "규격": "∅1000㎜", "단위": "m", "요구절": ["산림품셈 12-11-1"], "연결": null, "고르기": {"구분": "관·이음관", "상세구분": "PVC관"}, "비고": "VR관 값은 「본」 — m 환산 안 함" },
{ "키": "MP000029", "원문번호": "고무링 ∅1000㎜", "이름": "고무링(VR관)", "규격": "∅1000㎜", "단위": "개", "요구절": ["산림품셈 12-11-1"], "연결": "MT009723", "고르기": null, "비고": "연결: VR관고무링 D1000 이름·규격 같음" },
{ "키": "MP000030", "원문번호": "흄관 ∅1000㎜", "이름": "흄관", "규격": "∅1000㎜", "단위": "m", "요구절": ["산림품셈 12-11-2-1"], "연결": null, "고르기": {"구분": "관·이음관", "상세구분": "흄관", "규격": "Φ1000mm", "대표": "MT014983"}, "비고": "A·B형 갈림 · 값은 「본」 — m 아님" },
{ "키": "MP000031", "원문번호": "파형강관 ∅1000㎜", "이름": "파형강관", "규격": "∅1000㎜", "단위": "m", "요구절": ["산림품셈 12-11-3"], "연결": null, "고르기": {"구분": "관·이음관", "상세구분": "파형강관"}, "비고": "두께 1.6·2.0·2.7T 갈림 — 원문 없음" },
{ "키": "MP000032", "원문번호": "커플링밴드 ∅1000㎜", "이름": "커플링밴드(파형강관)", "규격": "∅1000㎜", "단위": "EA", "요구절": ["산림품셈 12-11-3 주①"], "연결": "MT015311", "고르기": null, "비고": "연결: 파형강관 커플링밴드 Φ1000 관급 줄" },
{ "키": "MP000032", "원문번호": "커플링밴드 ∅1000㎜", "이름": "커플링밴드(파형강관)", "규격": "∅1000㎜", "단위": "EA", "요구절": ["산림품셈 12-11-3 주①"], "연결": null, "고르기": {"구분": "관·이음관", "상세구분": "파형강관", "이름": "파형강관 커플링밴드(관급)", "규격": "Φ1000mm", "대표": "MT015311"}, "비고": "일감35 — 규격(관경)만으론 접속티와 안 갈려 이름 조건을 더함" },
{ "키": "MP000033", "원문번호": "VR관 ∅1200㎜", "이름": "VR관(소켓식)", "규격": "∅1200㎜", "단위": "m", "요구절": ["산림품셈 12-11-1"], "연결": null, "고르기": {"구분": "관·이음관", "상세구분": "PVC관"}, "비고": "VR관 값은 「본」 — m 환산 안 함" },
{ "키": "MP000034", "원문번호": "고무링 ∅1200㎜", "이름": "고무링(VR관)", "규격": "∅1200㎜", "단위": "개", "요구절": ["산림품셈 12-11-1"], "연결": "MT009725", "고르기": null, "비고": "연결: VR관고무링 D1200 이름·규격 같음" },
{ "키": "MP000035", "원문번호": "흄관 ∅1200㎜", "이름": "흄관", "규격": "∅1200㎜", "단위": "m", "요구절": ["산림품셈 12-11-2-1"], "연결": null, "고르기": {"구분": "관·이음관", "상세구분": "흄관", "규격": "Φ1200mm", "대표": "MT014985"}, "비고": "A·B형 갈림 · 값은 「본」 — m 아님" },
{ "키": "MP000036", "원문번호": "파형강관 ∅1200㎜", "이름": "파형강관", "규격": "∅1200㎜", "단위": "m", "요구절": ["산림품셈 12-11-3"], "연결": null, "고르기": {"구분": "관·이음관", "상세구분": "파형강관"}, "비고": "두께 1.6·2.0·2.7T 갈림 — 원문 없음" },
{ "키": "MP000037", "원문번호": "커플링밴드 ∅1200㎜", "이름": "커플링밴드(파형강관)", "규격": "∅1200㎜", "단위": "EA", "요구절": ["산림품셈 12-11-3 주①"], "연결": "MT015313", "고르기": null, "비고": "연결: 파형강관 커플링밴드 Φ1200 관급 줄" },
{ "키": "MP000037", "원문번호": "커플링밴드 ∅1200㎜", "이름": "커플링밴드(파형강관)", "규격": "∅1200㎜", "단위": "EA", "요구절": ["산림품셈 12-11-3 주①"], "연결": null, "고르기": {"구분": "관·이음관", "상세구분": "파형강관", "이름": "파형강관 커플링밴드(관급)", "규격": "Φ1200mm", "대표": "MT015313"}, "비고": "일감35 — 규격(관경)만으론 접속티와 안 갈려 이름 조건을 더함" },
{ "키": "MP000038", "원문번호": "지수활제", "이름": "지수활제", "규격": "", "단위": "g", "요구절": ["산림품셈 12-11-1"], "연결": "MT030601", "고르기": null, "비고": "PLAN 7-4b: 자재품목 새 줄(MT030601)로 이음 · 값 미확보" },
{ "키": "MP000039", "원문번호": "모르타르 1:2", "이름": "접합몰탈", "규격": "1:2", "단위": "㎥", "요구절": ["산림품셈 12-11-2-1", "건설품셈 건축 2-1-4", "건설품셈 건축 3-2-2"], "연결": null, "고르기": {"구분": "시멘트·골재", "상세구분": "모르타르", "규격": "1:2"}, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000040", "원문번호": "스틸그레이팅", "이름": "집수정 뚜껑", "규격": "스틸그레이팅", "단위": "개", "요구절": ["산림품셈 12-15"], "연결": null, "고르기": null, "비고": "자재품목에 같은 품명 없음 · 고르기 없음(자재품목 그 묶음에 같은 단위·이름 줄이 없음)" },
+40 -8
View File
@@ -824,6 +824,40 @@ def test_재료_고르기_조건이_후보를_좁힘():
assert mm.candidates(whole, {"구분": "없는구분"}) == []
def test_재료_고르기_이름_조건이_같은_규격_다른_품명을_가름():
"""일감35 — 규격(관경)만 같고 품명이 다른 줄(커플링밴드 vs 접속티)이 이름 칸으로 갈림."""
files = {
"재료_자재품목.json": {
"": [
{
"": "MT900010",
"구분": "관·이음관",
"상세구분": "파형강관",
"이름": "파형강관 커플링밴드(관급)",
"규격": "Φ800mm",
"단위": "",
"물가자료": None,
"관급": Decimal(19030),
},
{
"": "MT900011",
"구분": "관·이음관",
"상세구분": "파형강관",
"이름": "파형강관이음관(관급)-거양",
"규격": "접속티, Φ800mm, 아연도금",
"단위": "",
"물가자료": None,
"관급": Decimal(216770),
},
]
}
}
whole = mf.Master(files)
cond = {"구분": "관·이음관", "상세구분": "파형강관", "규격": "Φ800mm"}
assert sorted(mm.candidates(whole, cond)) == ["MT900010", "MT900011"] # 이름 없으면 둘 다 걸림
assert mm.candidates(whole, {**cond, "이름": "파형강관 커플링밴드(관급)"}) == ["MT900010"]
def test_재료_고르기_시험계산은_대표_줄_없으면_값_있는_첫_줄():
whole = _pipes()
cond = {"구분": "관·수로", "상세구분": "흄관", "규격": "D300"}
@@ -917,8 +951,8 @@ def test_품셈재료_고르기_칸_검사():
def test_고르기_조건_글자_열쇠():
"""화면 단가 미리보기가 조건 줄을 찾는 이름 — 빈 칸은 빈 글자."""
assert mm.cond_text({"구분": "관·수로", "상세구분": "흄관"}) == "관·수로|흄관||"
assert mm.cond_text({"구분": "레미콘", "대표": "MT900002"}) == "레미콘|||MT900002"
assert mm.cond_text({"구분": "관·수로", "상세구분": "흄관"}) == "관·수로|흄관|||"
assert mm.cond_text({"구분": "레미콘", "대표": "MT900002"}) == "레미콘||||MT900002"
def test_품셈재료_고르기_칸이_실파일에_있음():
@@ -1068,9 +1102,9 @@ def test_고르기_조건의_모르는_변수를_잡음(whole):
def test_변수_참조_줄은_고르기_조건으로_바뀜(whole):
"""일감24 — `MT:{철선}` 꼴 옛 참조 19줄을 고르기 조건으로 옮김.
VR관(GF000173)은 PLAN 7-4b 뒤 절반에서 1본=2.5m 환산으로 마저 옮김 — 남은 1줄은
커플링밴드(GF000176, 접속티와 안 갈리는 문제 — sub2 가 이름 칸을 더하는 중)뿐."""
"""일감24 — `MT:{철선}` 꼴 옛 참조 19줄을 고르기 조건으로 옮김. VR관(GF000173)은
PLAN 7-4b 뒤 절반에서 1본=2.5m 환산으로, 커플링밴드(GF000176)는 일감35 에서
「이름」 조건으로 마저 옮겨 남은 줄 없음."""
남음 = [
(str(row[""]), item["요소"])
for data in cm.load().values()
@@ -1081,6 +1115,4 @@ def test_변수_참조_줄은_고르기_조건으로_바뀜(whole):
and item["요소"][:2] in ("MP", "MT")
and "{" in item["요소"]
]
assert sorted(남음) == [
("GF000176", "MP:커플링밴드 ∅{관경}"),
]
assert sorted(남음) == []