feat(B08): 지장목제거를 두 줄로 가름 (확정 5차 2번) + 작업 갈래를 지반 갈래로 안 읽음

- 실무(영월 1.9)가 한 면적에 두 작업을 얹음 — 뿌리뽑기 @475 · 잡관목제거 @882 가
  같은 11,035㎡. ⚠ 이중계상이 아니라 서식이 그러함(사유에 적음)
- 잡관목제거는 품셈에 이름이 없음(실무 D00033 별도 단가) — 공종 보류(확정 5차 3번)라
  코드 없이 서고 사유가 붙음
- ⚠ 곁들여 잡은 것: `item` 칸을 **늘 지반 갈래로 읽고 있었음** — 지장목제거의 작업 갈래에
  「시공법 미지정으로 공종을 못 고름」이라는 틀린 사유가 붙었음. 갈래로 읽는 공종을
  GROUND_SPLIT_GROUPS 로 한정하고 나머지는 규격 칸으로 보냄
- tmp/tests/test_b08_tree_removal_split.py 신설(5건)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-09 12:12:30 +09:00
co-authored by Claude Opus 5
parent 691b356e2a
commit 857a93f08c
3 changed files with 58 additions and 19 deletions
@@ -170,24 +170,49 @@ def build_rows(source: SummaryInput) -> list[SummaryRow]:
) )
) )
removal = slope.get("tree_removal_fill", 0.0) + slope.get("tree_removal_cut", 0.0) removal = slope.get("tree_removal_fill", 0.0) + slope.get("tree_removal_cut", 0.0)
rows.append( # ⭐ 2026-09-09 **사용자 확정 5차 2번** — 지장목제거를 **두 줄로 가른다**(실무 서식).
SummaryRow( # 영월 설계내역서 1.9 지장목제거가 두 줄이고 **같은 면적을 나눠 쓴다**:
group="지장목제거", # 1.9.1 뿌리뽑기(장비+인력) 11,035㎡ @475
unit="", # 1.9.2 잡관목제거 벌목(5m미만) 11,035㎡ @882 ← 같은 11,035㎡
amount=removal * _ratio(source, "obstacle_removal"), # ⚠⚠ **이중계상이 아니다** — 한 면적에 **다른 두 작업**이 얹히는 것이라 실무가 그렇게 적는다.
amount_gross=removal, # (같은 작업을 두 축에서 두 번 세는 것과는 다른 자리다.)
application_ratio_pct=_ratio(source, "obstacle_removal") * 100.0, # ⚠ 잡관목제거는 **품셈에 그 이름이 없다** — 실무는 별도 단가(영월 D00033)를 씀.
application_ratio_breakdown={ # 공종 없는 줄 보류(확정 5차 3번)에 걸리므로 **코드 없이 서고 사유가 붙는다.**
"fill": _ratio(source, "obstacle_removal") * 100.0, for item, why in (
"cut": _ratio(source, "obstacle_removal") * 100.0, (
}, "뿌리뽑기",
quantity_breakdown={ "확정 5차 2번 — 실무가 뿌리뽑기·잡관목제거 두 줄로 가름(같은 면적을 나눠 씀 · 이중계상 아님)",
"fill": slope.get("tree_removal_fill", 0.0) * _ratio(source, "obstacle_removal"), ),
"cut": slope.get("tree_removal_cut", 0.0) * _ratio(source, "obstacle_removal"), (
}, "잡관목제거",
note=_ratio_note(source, "obstacle_removal", "성토면+절토면"), "확정 5차 2번 — 같은 면적에 얹히는 다른 작업(이중계상 아님)."
" ⚠ 품셈에 그 이름이 없어 실무는 별도 단가를 씀(영월 D00033) — 공종 보류 대상",
),
):
rows.append(
SummaryRow(
group="지장목제거",
item=item,
unit="",
amount=removal * _ratio(source, "obstacle_removal"),
amount_gross=removal,
application_ratio_pct=_ratio(source, "obstacle_removal") * 100.0,
application_ratio_breakdown={
"fill": _ratio(source, "obstacle_removal") * 100.0,
"cut": _ratio(source, "obstacle_removal") * 100.0,
},
quantity_breakdown={
"fill": slope.get("tree_removal_fill", 0.0)
* _ratio(source, "obstacle_removal"),
"cut": slope.get("tree_removal_cut", 0.0) * _ratio(source, "obstacle_removal"),
},
note=" · ".join(
part
for part in (_ratio_note(source, "obstacle_removal", "성토면+절토면"), why)
if part
),
)
) )
)
rows.append( rows.append(
SummaryRow( SummaryRow(
group="층따기", spec="백호우", unit="", amount=slope.get("bench_cut_fill", 0.0) group="층따기", spec="백호우", unit="", amount=slope.get("bench_cut_fill", 0.0)
@@ -134,6 +134,13 @@ BLOCKED_UNIT_DATA_MISSING = "unit_data_missing" # 원단위·표준 물량 자
BLOCKED_FORMULA_MISSING = "formula_missing" # 수량 산출식 자체가 없음 BLOCKED_FORMULA_MISSING = "formula_missing" # 수량 산출식 자체가 없음
#: 사면 계열 — 토공집계에 함께 실리지만 출처가 사면적이다. #: 사면 계열 — 토공집계에 함께 실리지만 출처가 사면적이다.
#: ⚠ `item` 칸이 **지반 갈래**인 공종 — 그 밖의 공종에서 `item` 은 **작업 갈래**다
#: (지장목제거의 「뿌리뽑기·잡관목제거」). 갈래로 읽으면 「시공법 미지정」이라는 **틀린 사유**가
#: 붙는다(2026-09-09 실측). 정의처는 `EarthworkSummary` 이고 여기서 그대로 가져다 쓴다.
from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import ( # noqa: E402
GROUND_SPLIT_GROUPS as GROUND_SPLIT_GROUPS,
)
SLOPE_GROUPS = frozenset({"성토면다짐", "초류종자살포", "지장목제거", "층따기", "면고르기"}) SLOPE_GROUPS = frozenset({"성토면다짐", "초류종자살포", "지장목제거", "층따기", "면고르기"})
#: 집계 합계 줄 — 내역 줄이 아니라 검산용이다. #: 집계 합계 줄 — 내역 줄이 아니라 검산용이다.
@@ -14,6 +14,7 @@ from B08_Quantity.B08_Quantity_Engine_Handoff_Mapping import (
BLOCKED_FORMULA_MISSING, BLOCKED_FORMULA_MISSING,
BLOCKED_INPUT_MISSING, BLOCKED_INPUT_MISSING,
BLOCKED_UNIT_DATA_MISSING, BLOCKED_UNIT_DATA_MISSING,
GROUND_SPLIT_GROUPS,
HAUL_SUMMARY_GROUPS, HAUL_SUMMARY_GROUPS,
METHOD_TO_GROUND, METHOD_TO_GROUND,
NOTE_HAUL_IN_SUMMARY, NOTE_HAUL_IN_SUMMARY,
@@ -114,7 +115,12 @@ def _earthwork_rows(
group = str(row.get("group") or "") group = str(row.get("group") or "")
if not group: if not group:
continue continue
ground = row.get("item") or None # ⚠ `item` 이 **지반 갈래인 공종**은 정해져 있다(흙깎기·측구터파기·구조물터파기).
# 그 밖(지장목제거의 「뿌리뽑기·잡관목제거」 같은 **작업 갈래**)을 갈래로 읽으면
# 「시공법 미지정으로 공종을 못 고름」이라는 **틀린 사유**가 붙는다(2026-09-09 실측).
is_ground_split = group in GROUND_SPLIT_GROUPS
ground = (row.get("item") or None) if is_ground_split else None
work_kind = None if is_ground_split else (row.get("item") or None)
origin = ORIGIN_SLOPE if group in SLOPE_GROUPS else ORIGIN_EARTHWORK origin = ORIGIN_SLOPE if group in SLOPE_GROUPS else ORIGIN_EARTHWORK
# ⚠ 운반은 **집계에도 오르고 운반표에도 오른다** — 내역 줄은 운반표 쪽 하나뿐이다. # ⚠ 운반은 **집계에도 오르고 운반표에도 오른다** — 내역 줄은 운반표 쪽 하나뿐이다.
is_subtotal = group in SUBTOTAL_GROUPS or group in HAUL_SUMMARY_GROUPS is_subtotal = group in SUBTOTAL_GROUPS or group in HAUL_SUMMARY_GROUPS
@@ -142,7 +148,8 @@ def _earthwork_rows(
{ {
"work_item_code": code, "work_item_code": code,
"name": group, "name": group,
"spec": str(row.get("spec") or ""), # 작업 갈래가 있으면 **규격 칸**에 적는다 — 갈래 축(`ground_class`)이 아니다.
"spec": str(work_kind or row.get("spec") or ""),
"unit": unit, "unit": unit,
"quantity": amount, "quantity": amount,
# 반영률 — 곱하기는 여기가 끝이다. 받는 쪽은 적기만 한다. # 반영률 — 곱하기는 여기가 끝이다. 받는 쪽은 적기만 한다.