From 490cfe1d08be456de94c085e15d3318261146d75 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 15 Sep 2026 01:08:43 +0900 Subject: [PATCH] =?UTF-8?q?feat(b08):=20=ED=92=88=EC=85=88=20=EA=B3=B5?= =?UTF-8?q?=EC=A2=85=20=EC=97=86=EB=8A=94=20=EB=B6=80=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EC=84=A4=20=EC=85=8B=EC=A7=B8=20=EC=83=81=ED=83=9C=20=E3=80=8C?= =?UTF-8?q?=ED=92=88=EC=85=88=20=EA=B3=B5=EC=A2=85=20=EC=97=86=EC=9D=8C=20?= =?UTF-8?q?=E2=80=94=20=EB=8B=A8=EA=B0=80=EB=A5=BC=20=EC=A7=81=EC=A0=91=20?= =?UTF-8?q?=EB=84=A3=EC=96=B4=EC=95=BC=20=ED=95=A8=E3=80=8D=20=C2=B7=20?= =?UTF-8?q?=E3=80=8C=EC=9E=90=EC=9E=AC=20=EB=8B=A8=EA=B0=80=E3=80=8D=20?= =?UTF-8?q?=ED=83=AD=20=ED=86=B5=EB=A1=9C(=EB=B8=8C=EB=A0=88=EC=9D=B8=20?= =?UTF-8?q?=ED=8C=90=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 개소를 넣어도 공종이 없어 단가가 안 서는 줄(국가지점번호판·안내판·차단기·수방대책 자재) — 「입력이 필요함」이면 넣고 기다려도 안 섬 - 개소가 선 줄은 자재총괄 사급 줄로 감 → 「자재 단가」 탭에 칸이 섬(화약류·치즐과 같은 통로) · 인계 공종 줄은 막힘 아님(두 번 안 셈) - 개소 안 넣은 줄은 종전대로 「입력이 필요함」 - 936be972: 국가지점번호판 3개소 넣음 → 셋째 상태 · 자재 단가 탭 「국가지점번호판 개소 3.0」 · 되돌림 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- .../B08_Quantity_Engine_Handoff_Rows_Prep.py | 12 ++++--- ...8_Quantity_Engine_Preparation_Ancillary.py | 31 +++++++++++++--- .../B08_Quantity_Engine_Preparation_Status.py | 4 +++ B08_Quantity/B08_Quantity_Router_Material.py | 5 ++- .../tester/test_b08_prep_status_input.py | 36 +++++++++++++++++-- 5 files changed, 75 insertions(+), 13 deletions(-) diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff_Rows_Prep.py b/B08_Quantity/B08_Quantity_Engine_Handoff_Rows_Prep.py index a6a76c91..01148e0b 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff_Rows_Prep.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff_Rows_Prep.py @@ -24,6 +24,9 @@ from B08_Quantity.B08_Quantity_Engine_Preparation_Status import ( from B08_Quantity.B08_Quantity_Engine_Preparation_Status import ( STATUS_NEEDS_INPUT as PREP_NEEDS_INPUT, ) +from B08_Quantity.B08_Quantity_Engine_Preparation_Status import ( + STATUS_NO_WORK_ITEM as PREP_NO_WORK_ITEM, +) from B08_Quantity.B08_Quantity_Engine_Preparation_Status import ( STATUS_NOT_APPLICABLE as PREP_NOT_APPLICABLE, ) @@ -82,9 +85,7 @@ def _preparation_rows(preparation_table: dict[str, Any]) -> list[dict[str, Any]] "composite_parts": None, "structure_kind": None, # ⚠ 못 서는 까닭을 그대로 넘긴다 — 받는 쪽이 「만들어야 할 것」 목록에 얹는다. - "blocked_kind": None - if ready - else row.get("blocked_kind") or _prep_blocked_kind(status), + "blocked_kind": None if ready else _prep_blocked_kind(status), "blocked_reason": "" if ready else str(row.get("reason") or status), # ⚠ 준비공 줄도 갈래를 실어 보낸다(2026-09-09) — 종전에는 늘 `None` 이라 # 표토 운반처럼 **부모 공종코드**로 가는 줄이 B09 에서 「후보 N건」에 머물렀다. @@ -110,7 +111,7 @@ def _preparation_rows(preparation_table: dict[str, Any]) -> list[dict[str, Any]] def _prep_blocked_kind(status: str) -> str | None: - """준비공 줄의 상태를 **막힌 갈래 셋** 중 하나로 옮긴다. 줄이 갈래를 적어 오면 그것이 이긴다. + """준비공 줄의 상태를 **막힌 갈래 셋** 중 하나로 옮긴다. 모르는 상태는 단가 자료 없음. ⚠ 「근거 없음」을 `input_missing` 으로 보내면 사방 원단위처럼 **우리가 만들 줄**이 B09 에 「입력이 필요합니다」로 뜬다 — 사용자가 넣을 칸을 찾아 헤맨다(2026-09-14 브레인 ㉴). @@ -122,7 +123,8 @@ def _prep_blocked_kind(status: str) -> str | None: if status == PREP_COUNTED_ELSEWHERE: # 다른 표에서 이미 선 줄 — 막힌 것이 아니라 **여기서 세면 안 되는** 줄이다. return None - if status == PREP_NOT_APPLICABLE: + if status in (PREP_NOT_APPLICABLE, PREP_NO_WORK_ITEM): + # 공종 없는 줄은 자재총괄 줄로 금액이 섬(「자재 단가」 탭) — 여기서 막힘으로 세면 두 벌. return None return BLOCKED_UNIT_DATA_MISSING diff --git a/B08_Quantity/B08_Quantity_Engine_Preparation_Ancillary.py b/B08_Quantity/B08_Quantity_Engine_Preparation_Ancillary.py index 07ab5e1f..90329c4b 100644 --- a/B08_Quantity/B08_Quantity_Engine_Preparation_Ancillary.py +++ b/B08_Quantity/B08_Quantity_Engine_Preparation_Ancillary.py @@ -12,10 +12,13 @@ from typing import Any from B08_Quantity.B08_Quantity_Engine_Preparation_Status import ( REASON_NO_WORK_ITEM, STATUS_NEEDS_INPUT, - STATUS_PENDING, + STATUS_NO_WORK_ITEM, STATUS_READY, ) +#: 공종 없는 줄에 개소가 들어온 뒤 할 일 — 자재총괄(사급) 줄로 가서 「자재 단가」 탭에 칸이 섬. +NO_WORK_ITEM_PRICE_PATH = "「자재 단가」 탭에 단가를 넣으면 자재총괄(사급) 줄로 금액이 섬" + #: 부대시설·가설공사 — **법이 요구하는데 우리가 안 내던 다섯 줄**(2026-09-09 사용자 확정 ⑬). #: `key` 는 설정의 `ancillary_counts` 칸 이름, `code` 는 품셈 공종(없으면 `None`). #: ⚠ 다섯 중 **품셈에 공종이 있는 것은 가설창고 하나뿐**이다(마스터 전수 확인). @@ -96,8 +99,11 @@ def ancillary_rows(counts: dict[str, Any] | None = None) -> list[dict[str, Any]] if amount is None: reasons.append("개소가 아직 입력되지 않았습니다 — 넣으면 물량이 섭니다") status = STATUS_NEEDS_INPUT + elif spec["code"]: + status = STATUS_READY else: - status = STATUS_READY if spec["code"] else STATUS_PENDING + reasons.append(NO_WORK_ITEM_PRICE_PATH) + status = STATUS_NO_WORK_ITEM rows.append( { "group": "부대시설", @@ -107,9 +113,26 @@ def ancillary_rows(counts: dict[str, Any] | None = None) -> list[dict[str, Any]] "status": status, "work_item_code": spec["code"], "legal_required": bool(spec["legal"]), - # 품셈에 공종이 없는 줄 — 금액은 별도 단가(사람 입력)로만 서서 인계는 늘 입력 갈래. - **({} if spec["code"] else {"blocked_kind": "input_missing"}), "reason": " · ".join(reasons), } ) return rows + + +def ancillary_material_rows(preparation_rows: list[dict[str, Any]]) -> list[dict[str, Any]]: + """공종 없는 부대시설 중 개소가 선 줄 → 자재총괄 성분(사급 기본) — 「자재 단가」 탭 통로. + + ⚠ 인계 공종 줄은 막힘 없이 `in_bill: False` 로 가므로 **여기 한 곳에서만** 금액이 선다. + """ + return [ + { + "name": row["item"], + "spec": "", + "unit": row["unit"], + "amount": float(row["amount"]), + "destination": "material", + "source": str(row.get("group") or "부대시설"), + } + for row in preparation_rows + if row.get("status") == STATUS_NO_WORK_ITEM and row.get("amount") + ] diff --git a/B08_Quantity/B08_Quantity_Engine_Preparation_Status.py b/B08_Quantity/B08_Quantity_Engine_Preparation_Status.py index 54759640..1f168fd0 100644 --- a/B08_Quantity/B08_Quantity_Engine_Preparation_Status.py +++ b/B08_Quantity/B08_Quantity_Engine_Preparation_Status.py @@ -15,6 +15,10 @@ STATUS_READY = "값 있음" STATUS_NEEDS_INPUT = "입력이 필요함" #: 자료·산식이 우리에게 없는 줄 — 입력으로는 안 풀림. 인계 `unit_data_missing`. STATUS_PENDING = "값을 낼 근거가 없음" +#: 수량은 섰는데 **품셈에 공종이 없어** 단가가 영영 안 서는 줄 — 「자재 단가」 탭에 단가를 넣음 +#: (화약류·치즐과 같은 통로 · 자재총괄 사급 줄로 감). 인계는 막힘이 아님(자재 줄로 셈). +#: ⚠ 「입력이 필요함」으로 두면 개소를 넣고 기다려도 안 섬(2026-09-14 브레인 판정). +STATUS_NO_WORK_ITEM = "품셈 공종 없음 — 단가를 직접 넣어야 함" STATUS_COUNTED_ELSEWHERE = "다른 표에서 이미 섬" STATUS_NOT_APPLICABLE = "해당 없음" diff --git a/B08_Quantity/B08_Quantity_Router_Material.py b/B08_Quantity/B08_Quantity_Router_Material.py index 47cb756f..bf67ce4b 100644 --- a/B08_Quantity/B08_Quantity_Router_Material.py +++ b/B08_Quantity/B08_Quantity_Router_Material.py @@ -47,6 +47,7 @@ from common_util.common_util_project_settings import ( ) from B08_Quantity.B08_Quantity_Engine_HaulInputs import haul_inputs from B08_Quantity.B08_Quantity_Engine_Preparation import frame_material_rows +from B08_Quantity.B08_Quantity_Engine_Preparation_Ancillary import ancillary_material_rows from common_util.common_util_storage import resolve_stored_project_path from common_util.common_util_structure_lengths import structure_lengths from config.config_db import run_with_connection @@ -172,7 +173,9 @@ def material_table_for( surcharge_overrides=settings.get("material_surcharge") or {}, # 콘크리트 할증은 **레미콘일 때만** 붙는다 — 방식이 이름을 가른다(확정 3차 ⑥). concrete_placing_method=settings.get("concrete_placing_method"), - extra_materials=frame_material_rows(frame_rows, settings.get("frame_material") or {}), + extra_materials=frame_material_rows(frame_rows, settings.get("frame_material") or {}) + # 공종 없는 부대시설 개소 — 「자재 단가」 탭에 단가를 넣는 통로(2026-09-14 브레인 판정). + + ancillary_material_rows((preparation_table or {}).get("rows") or []), ) diff --git a/resources/tester/test_b08_prep_status_input.py b/resources/tester/test_b08_prep_status_input.py index 666cce31..23c9a11b 100644 --- a/resources/tester/test_b08_prep_status_input.py +++ b/resources/tester/test_b08_prep_status_input.py @@ -87,14 +87,44 @@ def test_인계_막힘_갈래가_상태를_따른다() -> None: assert handed["골막이"]["blocked_kind"] == "unit_data_missing" # 해당 없음은 막힘이 아니다. assert handed["비탈 규준틀"]["blocked_kind"] is None - # 공종이 품셈에 없는 부대시설은 개소를 넣어도 종전대로 「입력」(별도 단가) 갈래. - entered = _table(ancillary_counts={"national_point_sign": 3}) + # 개소를 안 넣은 부대시설은 입력 갈래(개소를 넣으면 수량이 섬). + assert handed["국가지점번호판"]["blocked_kind"] == "input_missing" + + +def test_품셈_공종이_없는_부대시설은_셋째_상태로_자재_단가에_잇는다() -> None: + """개소를 넣어도 공종이 없어 단가가 영영 안 선다 — 「입력이 필요함」으로 두면 사용자가 기다린다. + + 브레인 판정(2026-09-14): 「품셈 공종 없음 — 단가를 직접 넣어야 함」 · 「자재 단가」 탭 통로 + (화약류·치즐과 같은 자리) — 수량은 자재총괄(사급) 줄로 가고 단가를 넣으면 금액이 선다. + """ + from B08_Quantity.B08_Quantity_Engine_Preparation_Status import STATUS_NO_WORK_ITEM + from B08_Quantity.B08_Quantity_Router_Material import material_table_for + + entered = _table(ancillary_counts={"national_point_sign": 3, "site_container": 1}) + rows = _by_item(entered) + assert rows["국가지점번호판"]["status"] == STATUS_NO_WORK_ITEM + assert STATUS_NO_WORK_ITEM == "품셈 공종 없음 — 단가를 직접 넣어야 함" + assert "자재 단가" in rows["국가지점번호판"]["reason"] + # 공종이 있는 가설창고는 그대로 값 있음. + assert rows["가설창고(컨테이너)"]["status"] == "값 있음" + # 인계 — 막힘이 아니라 자재 줄로 가는 줄(두 번 세지 않음). sign = next( row for row in build_handoff(preparation_table=entered)["work_items"] if row["name"] == "국가지점번호판" ) - assert sign["blocked_kind"] == "input_missing" + assert sign["blocked_kind"] is None and sign["in_bill"] is False + # 자재총괄 사급 줄 — 「자재 단가」 탭이 이 이름으로 칸을 세운다. + materials = material_table_for({"rows": []}, {}, entered)["rows"] + board = [row for row in materials if row["name"] == "국가지점번호판"] + assert len(board) == 1 + assert (board[0]["unit"], board[0]["net_amount"], board[0]["supply"]) == ( + "개소", + 3.0, + "contractor_supplied", + ) + # 개소를 안 넣은 공종 없는 줄은 자재로 안 감(수량이 없음). + assert not [row for row in materials if row["name"] == "임도 안내판"] def test_표_머리가_두_갈래를_따로_센다() -> None: