From a57e1bbe6dcb3213e768a0a2d126a8834ee1dcfc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 9 Sep 2026 06:24:58 +0900 Subject: [PATCH] =?UTF-8?q?feat(B08):=20=EC=BD=98=ED=81=AC=EB=A6=AC?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=9E=90=EC=9E=AC=20=EC=B6=95=EC=97=90=20?= =?UTF-8?q?=EC=84=B8=EC=9B=80=20+=20=ED=95=A0=EC=A6=9D=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EC=9E=87=EA=B8=B0=20(=ED=99=95=EC=A0=95=203?= =?UTF-8?q?=EC=B0=A8=20=E2=91=A5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 타설 줄(품셈 12-1)은 품만 주고 재료를 안 줌(서브 일위대가도 재료 0원) — 콘크리트를 자재 축에 안 보내면 재료비가 통째로 빠짐. destination 을 material 로 옮김 - 배합 분해 금지(㉢)는 그대로 — 「콘크리트 ㎥」에서 멈춤 - 할증 이름 잇기(값은 그대로, 찾을 때만 다른 이름): · 이형철근 D13·D16 → 「이형철근」 (품셈 1-3-1, 규격 안 가림) · 콘크리트 셋 → 「레미콘」 **레미콘 타설일 때만**. 비빔은 시멘트·골재가 각각 할증되는 자리라 붙이지 않고 사유를 냄 - 할증률 미확보 사유를 비고에 적음 — 「표에 이름 없음」과 「이 방식엔 안 붙임」은 할 일이 다름 - tmp/tests/test_b08_material_concrete.py 신설(6건) Co-Authored-By: Claude Opus 5 (1M context) --- .../B08_Quantity_Engine_MaterialSummary.py | 35 +++++++++++++++++-- .../B08_Quantity_Engine_UnitQuantity.py | 10 ++++-- B08_Quantity/B08_Quantity_Router_Material.py | 6 +++- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/B08_Quantity/B08_Quantity_Engine_MaterialSummary.py b/B08_Quantity/B08_Quantity_Engine_MaterialSummary.py index c8283641..1f821dfd 100644 --- a/B08_Quantity/B08_Quantity_Engine_MaterialSummary.py +++ b/B08_Quantity/B08_Quantity_Engine_MaterialSummary.py @@ -165,7 +165,11 @@ class MaterialRow: if self.surcharge_included: parts.append(NOTE_INCLUDED) elif self.surcharge_pct is None: + # ⚠ **왜 미확보인지**를 함께 적는다 — 「표에 이름이 없음」과 「이 방식엔 안 붙임」은 + # 할 일이 다르다(2026-09-09 콘크리트에서 갈린 자리). parts.append(NOTE_RATE_MISSING) + if self.basis: + parts.append(self.basis) elif self.basis: parts.append(self.basis) if self.supply == SUPPLY_OWNER and self.install_by is None: @@ -215,6 +219,31 @@ def verify_single_surcharge(unit_quantity_table: dict[str, Any] | None) -> list[ return [] +#: 할증표 이름과 우리 성분 이름이 **다른 자리** — 이름만 잇는다(값은 그대로). +#: ⚠ 리핑암↔파쇄암 때와 같은 처방이다. 이름을 바꾸면 다른 쪽(타설 줄·묶음 조각)이 어긋난다. +#: 철근: 품셈 1-3-1 의 「이형철근 3 %」는 **규격을 가리지 않는다** — D13·D16 이 같은 줄이다. +#: 콘크리트: **레미콘일 때만** 잇는다. 기계·인력 비빔은 시멘트·골재가 각각 할증되는 자리라 +#: 레미콘 할증을 붙이면 틀린다 — 그때는 미확보로 두고 사유를 낸다. +REBAR_ALIASES = {"이형철근 D13": "이형철근", "이형철근 D16": "이형철근"} +CONCRETE_NAMES = ("콘크리트", "채움콘크리트", "버림콘크리트") +READY_MIXED = "ready_mixed" +CONCRETE_ALIAS = "레미콘" +CONCRETE_NOT_READY_NOTE = ( + "타설 방식이 레미콘이 아니라 레미콘 할증을 붙이지 않음 — 비빔은 시멘트·골재가 각각 할증됨" +) + + +def surcharge_lookup_name(name: str, concrete_placing_method: str | None) -> tuple[str, str]: + """(할증표에서 찾을 이름, 사유). 이름이 그대로면 사유는 빈 문자열이다.""" + if name in REBAR_ALIASES: + return REBAR_ALIASES[name], "품셈 1-3-1 「이형철근」 — 규격을 가리지 않음" + if name in CONCRETE_NAMES: + if concrete_placing_method == READY_MIXED: + return CONCRETE_ALIAS, "타설 방식이 레미콘 — 할증표의 「레미콘」 줄로 봄" + return name, CONCRETE_NOT_READY_NOTE + return name, "" + + def _collect( unit_quantity_table: dict[str, Any], ) -> tuple[dict[tuple[str, str], MaterialRow], dict[str, int]]: @@ -245,6 +274,7 @@ def build_table( surcharge_table: SurchargeTable | None = None, supply_map: dict[str, Any] | None = None, extra_materials: Iterable[dict[str, Any]] = (), + concrete_placing_method: str | None = None, ) -> dict[str, Any]: """화면·API 가 그대로 쓰는 모양. @@ -280,9 +310,10 @@ def build_table( missing_install_by.append(name) if row.surcharge_included: continue - rate, basis = table.rate_for(name) + lookup, alias_note = surcharge_lookup_name(name, concrete_placing_method) + rate, basis = table.rate_for(lookup) row.surcharge_pct = rate - row.basis = basis + row.basis = " · ".join(part for part in (basis, alias_note) if part) if rate is None: missing_rate.append(name) diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py index 2ba791ac..c63af84c 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py @@ -288,9 +288,13 @@ DESTINATION = { "야면석": "material", "고임돌": "material", "막자갈": "material", - "콘크리트": "unit_price", - "채움콘크리트": "unit_price", - "버림콘크리트": "unit_price", + # ⭐ 2026-09-09 사용자 확정 3차 ⑥ — **콘크리트는 자재 축에 세운다**(사용자 명시). + # ⚠ 타설 줄(품셈 12-1)은 **품만** 주고 재료를 안 준다(서브 일위대가도 재료 0원). + # 그래서 자재로 안 보내면 **재료비가 통째로 빠진다** — B09 매김에서 드러난 자리다. + # ⚠ 배합을 분해하지 않는 규칙(㉢)은 그대로다 — 「콘크리트 ㎥」에서 멈춘다. + "콘크리트": "material", + "채움콘크리트": "material", + "버림콘크리트": "material", "모르터": "unit_price", "거푸집": "unit_price", "물구멍관": "material", diff --git a/B08_Quantity/B08_Quantity_Router_Material.py b/B08_Quantity/B08_Quantity_Router_Material.py index 7a42dfbc..d9e2cd49 100644 --- a/B08_Quantity/B08_Quantity_Router_Material.py +++ b/B08_Quantity/B08_Quantity_Router_Material.py @@ -157,6 +157,8 @@ async def get_material_summary(project_id: UUID) -> JSONResponse: material_table = build_material_table( unit_table, supply_map=settings.get("material_supply") or {}, + # 콘크리트 할증은 **레미콘일 때만** 붙는다 — 방식이 이름을 가른다(확정 3차 ⑥). + concrete_placing_method=settings.get("concrete_placing_method"), ) # 묶음으로 서는 구조물의 조각을 화면에도 보인다 — 코드만으로는 사람이 검증 못 한다. handoff = build_handoff(unit_quantity_table=unit_table) @@ -241,7 +243,9 @@ async def get_handoff(project_id: UUID) -> JSONResponse: settings.get("rubble_base_thickness_m"), ) material_table = build_material_table( - unit_table, supply_map=settings.get("material_supply") or {} + unit_table, + supply_map=settings.get("material_supply") or {}, + concrete_placing_method=settings.get("concrete_placing_method"), ) # 토공·운반 표는 토적표 라우터의 것을 그대로 쓴다 — 여기서 다시 만들지 않는다.