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"), ) # 토공·운반 표는 토적표 라우터의 것을 그대로 쓴다 — 여기서 다시 만들지 않는다.