feat(B08): 콘크리트를 자재 축에 세움 + 할증 이름 잇기 (확정 3차 ⑥)

- 타설 줄(품셈 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) <noreply@anthropic.com>
This commit is contained in:
2026-09-09 06:24:58 +09:00
co-authored by Claude Opus 5
parent 99248440c6
commit a57e1bbe6d
3 changed files with 45 additions and 6 deletions
@@ -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)
@@ -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",
+5 -1
View File
@@ -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"),
)
# 토공·운반 표는 토적표 라우터의 것을 그대로 쓴다 — 여기서 다시 만들지 않는다.