refactor(B09): 불도저를 딴 파일로 분리, 장비 붙이는 자리를 한 켤레로 맞춤

- B09_Estimation_MachineProductivity_Dozer.py 신설 (8-2-1 전용).
  굴착기(8-1-4)와 한 파일에 두면 700줄을 넘고, 두 식이 섞여 읽힘.
- 굴착기 쪽 장비 붙이기를 attach_machine_share 로 올려 불도저 쪽
  attach_dozer_share 와 같은 모양으로 맞춤 — 자리를 나눠 쓰는 것이 눈에 보이게.
- 줄수: UnitPrice 652 · MachineProductivity 360 · Dozer 398.
- 224건 통과 (동작 변화 없음).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 11:54:12 +09:00
co-authored by Claude Opus 5
parent b97eb042b6
commit b3c6f29684
3 changed files with 436 additions and 398 deletions
+13 -56
View File
@@ -27,12 +27,14 @@ from B09_Estimation.B09_Estimation_MachineCost import load_machine_catalog
from B09_Estimation.B09_Estimation_MachineProductivity import (
CycleFactors,
FactorGap,
attach_machine_share,
extract_cycle_factors,
)
from B09_Estimation.B09_Estimation_MachineProductivity_Dozer import (
attach_dozer_share,
dozer_variants,
formula_machine_codes,
extract_cycle_factors,
extract_dozer_factors,
machine_hours_per_unit,
formula_machine_codes,
)
from B09_Estimation.B09_Estimation_MachineOperating import (
load_fuel_price,
@@ -429,7 +431,14 @@ def build_unit_prices(axis: AxisResult | None = None) -> UnitPriceBuild:
build.book, build.factor_gaps, master, work_item_code, title_code, variant
)
if not machine_share and not variant:
machine_share = _attach_machine_share(build, master, work_item_code, title_code)
machine_share = attach_machine_share(
build.book,
build.factor_gaps,
build.cycle_factors,
master,
work_item_code,
title_code,
)
# ⚠ **배분율이 있는 표는 「몇 %가 실제로 붙었나」를 세어 둔다.**
# 「인력(10%)·장비(90%)」 표에서 인력만 붙으면 단가가 10 % 몫만인데, 그 값이
@@ -477,58 +486,6 @@ def _share_of(row) -> Decimal:
return Decimal(1) if ratio is None else Decimal(str(ratio)) / Decimal(100)
def _attach_machine_share(
build: UnitPriceBuild,
master: dict,
work_item_code: str,
title_code: str,
) -> Decimal:
"""시공능력 공식으로 **장비 몫**을 붙인다. 붙인 비율(%)을 돌려준다.
계수가 다 안 서면 **아무것도 안 붙이고 0 을 돌려준다** — 그러면 그 공종은
`partial_ratio` 에 남아 내역서에서 금액이 안 붙는다(지어낸 값이 서는 것보다 낫다).
"""
node = next(
(w for w in master.get("work_items", []) if w.get("work_item_code") == work_item_code),
None,
)
if node is None:
return _ZERO
for table in node.get("tables", []):
factors = extract_cycle_factors(work_item_code, table)
if not isinstance(factors, CycleFactors):
if isinstance(factors, FactorGap):
build.factor_gaps[work_item_code] = factors
continue
hourly_code = f"X-{factors.machine_code}"
if hourly_code not in build.book.titles:
# 기계 층이 안 섰다 — 지어내지 않고 못 붙인 채로 둔다.
build.factor_gaps[work_item_code] = FactorGap(
work_item_code=work_item_code,
pum_table_id=factors.pum_table_id,
missing=("시간당 사용료",),
note=f"{factors.machine_name} 의 시간당 사용료가 아직 안 섰습니다.",
)
continue
share = (
Decimal(1)
if factors.machine_ratio_pct is None
else Decimal(str(factors.machine_ratio_pct)) / Decimal(100)
)
build.book.add_detail(
PriceDetail(
title_code,
hourly_code,
machine_hours_per_unit(factors) * share,
note=factors.formula_text,
)
)
build.cycle_factors[work_item_code] = factors
return share * Decimal(100)
return _ZERO
def _covered_ratio_pct(
rows: list, attached_refs: set[str], build: UnitPriceBuild
) -> Decimal | None: