feat(b09): 덤프 적재 10-12 「1. 적재」 — 굴착기 0.7㎥ Q1 호표(#적재) · E0 토사 0.60(본문 0.75 버림을 사유에)

- Q1 = 3600×0.7×K×f×E0/22초 · 토사 47.63 · 암절취 16.32 · 발파암 13.67 ㎥/hr
- E0 는 운반 식 Es 와 다른 기호 — 토사 0.60(표 「임도」·10-12-3 [주]③) 채택 · 본문 0.75 버림(브레인 판정)
- 거리 무관이라 늘 세움 · 내역은 인계 수단 표지 dump_loading 줄을 잎 #적재 로(B08 짝 줄은 다음 커밋)
- 시험 1건 보탬

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
2026-09-14 08:37:58 +09:00
co-authored by Claude Opus 5
parent 999aeac755
commit 58afe8f2d2
3 changed files with 103 additions and 8 deletions
@@ -24,8 +24,10 @@ from B09_Estimation.B09_Estimation_BillOfQuantities import (
) )
from B09_Estimation.B09_Estimation_MachineProductivity_Dump import ( from B09_Estimation.B09_Estimation_MachineProductivity_Dump import (
DUMP_PARENT, DUMP_PARENT,
LOADING_EQUIPMENT,
dump_child_for, dump_child_for,
dump_title_code, dump_title_code,
loading_title_code,
) )
from B09_Estimation.B09_Estimation_PriceBook import Money3 from B09_Estimation.B09_Estimation_PriceBook import Money3
from B09_Estimation.B09_Estimation_Rounding import OutputPlace, round_at from B09_Estimation.B09_Estimation_Rounding import OutputPlace, round_at
@@ -312,19 +314,26 @@ def _leaf_row(
return row return row
price_code = f"B-{node.code}" price_code = f"B-{node.code}"
if node.code == DUMP_PARENT and item.haul_equipment == "dump_truck": loading = item.haul_equipment == LOADING_EQUIPMENT
if node.code == DUMP_PARENT and (item.haul_equipment == "dump_truck" or loading):
# 덤프 운반(10-12) — 잎(토사·암절취·발파암) × 운반거리마다 호표 한 장. 거리 없으면 0원 금지. # 덤프 운반(10-12) — 잎(토사·암절취·발파암) × 운반거리마다 호표 한 장. 거리 없으면 0원 금지.
# 적재 짝 줄은 거리 무관 `#적재` 한 장.
child = dump_child_for(item.variant_value, _master_edition()) child = dump_child_for(item.variant_value, _master_edition())
why = "" why = ""
wanted = ""
if child is None: if child is None:
why = ( why = (
f"덤프 운반 갈래 「{item.variant_value or '미지정'}」를 10-12 잎" f"덤프 운반 갈래 「{item.variant_value or '미지정'}」를 10-12 잎"
"(토사·암절취·발파암)에 못 맞춤" "(토사·암절취·발파암)에 못 맞춤"
) )
elif loading:
wanted = loading_title_code(child)
elif item.haul_distance_m is None: elif item.haul_distance_m is None:
why = "입력이 필요합니다 — 운반거리 미입력(유토곡선·사토장 거리가 서면 섬)" why = "입력이 필요합니다 — 운반거리 미입력(유토곡선·사토장 거리가 서면 섬)"
elif dump_title_code(child, item.haul_distance_m) not in unit_prices.book.titles: else:
why = unit_prices.component_gaps.get(child) or "덤프 운반 일위대가를 못 세웠습니다" wanted = dump_title_code(child, item.haul_distance_m)
if wanted and wanted not in unit_prices.book.titles:
why = unit_prices.component_gaps.get(child) or "덤프 운반·적재 일위대가를 못 세웠습니다"
if why: if why:
row.add_note("unit_price_krw", why) row.add_note("unit_price_krw", why)
result.missing.append( result.missing.append(
@@ -337,8 +346,9 @@ def _leaf_row(
} }
) )
return row return row
price_code = dump_title_code(child, item.haul_distance_m) price_code = wanted
row.spec = f"{row.spec} L={item.haul_distance_m}m".strip() if not loading:
row.spec = f"{row.spec} L={item.haul_distance_m}m".strip()
if price_code not in unit_prices.book.titles: if price_code not in unit_prices.book.titles:
# B08 은 **의미**(어느 공종·어느 제원)만 보내고 갈래 키는 우리가 만든다. # B08 은 **의미**(어느 공종·어느 제원)만 보내고 갈래 키는 우리가 만든다.
# 못 맞추면 후보를 보이는 길로 내려간다 — 가까운 갈래를 임의로 고르지 않는다. # 못 맞추면 후보를 보이는 길로 내려간다 — 가까운 갈래를 임의로 고르지 않는다.
@@ -82,6 +82,40 @@ DUMP_MATERIALS: dict[str, DumpMaterial] = {
} # fmt: skip } # fmt: skip
#: 「1. 적재」 — Q1 = 3600 × q0 × K × f × E0 / ㎝ · 굴착기(무한궤도) 0.7㎥ · ㎝ 22초(180°).
#: ⚠ E0 는 운반 식의 Es 와 **다른 기호**다(판정 ④⑥). 토사 본문 「E0=0.75」 는 표(10-12-1 [주]⑤
#: 「E0 토사 0.60(불량) — 임도」)·10-12-3 [주]③(「토사 0.6」)과 어긋나 **0.60 채택 · 0.75 버림**.
LOADER_CODE = "0201-0070"
_LOADING_CYCLE_SEC = Decimal(22)
_LOADING_E0 = {
"FP-10-12-01": Decimal("0.60"),
"FP-10-12-02": Decimal("0.35"),
"FP-10-12-03": Decimal("0.35"),
}
_LOADING_NOTES = {
"FP-10-12-01": "E0 = 0.60(표 「임도」·10-12-3 [주]③) — 본문 표기 0.75 버림",
"FP-10-12-02": "E0 = 0.35(본문·표 「파쇄암」 같음)",
"FP-10-12-03": "E0 = 0.35(10-12-3 [주]③ 「파쇄암 0.35」)",
} # fmt: skip
def loading_output(code: str) -> Decimal:
"""적재 Q1 (㎥/hr, 자연상태) — f·Q1 소수 2자리 확정(명세 7장)."""
material = DUMP_MATERIALS[code]
return fix2(
Decimal(3600)
* _BUCKET_M3
* material.bucket_factor
* fix2(Decimal(1) / material.loose_factor)
* _LOADING_E0[code]
/ _LOADING_CYCLE_SEC
)
def loading_title_code(code: str) -> str:
return f"B-{code}#적재"
@dataclass(frozen=True) @dataclass(frozen=True)
class DumpHaul: class DumpHaul:
"""덤프 운반 한 벌 — 재료 + 운반거리.""" """덤프 운반 한 벌 — 재료 + 운반거리."""
@@ -143,13 +177,39 @@ def attach_dump_hauls(build: Any, master: dict[str, Any], distances_m: tuple[Dec
""" """
from B09_Estimation.B09_Estimation_PriceBook import PriceKind, PriceTitle from B09_Estimation.B09_Estimation_PriceBook import PriceKind, PriceTitle
if not distances_m:
return
hourly = f"X-{DUMP_TRUCK_CODE}"
names = { names = {
str(node.get("work_item_code")): str(node.get("name") or "") str(node.get("work_item_code")): str(node.get("name") or "")
for node in master.get("work_items", []) for node in master.get("work_items", [])
} }
# 적재 — 거리와 무관해 늘 세움(굴착기 0.7㎥ 사용료 층이 있으면). 운반과 한 벌이라 여기 둠.
loader = f"X-{LOADER_CODE}"
for code in DUMP_MATERIALS:
title_code = loading_title_code(code)
if loader not in build.book.titles or title_code in build.book.titles:
continue
output = loading_output(code)
material = DUMP_MATERIALS[code]
label = names.get(code) or material.label
build.book.add_title(
PriceTitle(
code=title_code,
kind=PriceKind.UNIT_PRICE,
name=f"{names.get(DUMP_PARENT) or '덤프운반'} {label} 적재",
spec="굴착기(무한궤도) 0.7㎥",
unit="",
)
)
build.book.add_output_detail(
title_code,
loader,
Decimal(1) / output,
f"Q1 = 3600×0.7×{material.bucket_factor}×{fix2(Decimal(1) / material.loose_factor)}"
f"×{_LOADING_E0[code]}/22 = {output}㎥/hr · {_LOADING_NOTES[code]}",
output=output,
)
if not distances_m:
return
hourly = f"X-{DUMP_TRUCK_CODE}"
for code, material in DUMP_MATERIALS.items(): for code, material in DUMP_MATERIALS.items():
if hourly not in build.book.titles: if hourly not in build.book.titles:
build.component_gaps[code] = "덤프트럭(15ton) 시간당 사용료가 아직 안 섰습니다" build.component_gaps[code] = "덤프트럭(15ton) 시간당 사용료가 아직 안 섰습니다"
@@ -192,6 +252,9 @@ def dump_haul_distances(payload: dict[str, Any]) -> tuple[str, ...]:
_RE_DUMP_CODE = re.compile(rf"^[BD]-{DUMP_PARENT}-0[1-3]#L(\d+(?:\.\d+)?)m$") _RE_DUMP_CODE = re.compile(rf"^[BD]-{DUMP_PARENT}-0[1-3]#L(\d+(?:\.\d+)?)m$")
#: 인계 「덤프 적재」 짝 줄의 수단 표지 — 운반 줄(`dump_truck`)과 갈라 잎 `#적재` 로 감.
LOADING_EQUIPMENT = "dump_loading"
def dump_distance_from_code(code: str) -> tuple[str, ...]: def dump_distance_from_code(code: str) -> tuple[str, ...]:
"""호표 한 장을 여는 쪽 — 코드 `#L164.23m` 에서 거리를 되읽음(인계를 다시 안 셈).""" """호표 한 장을 여는 쪽 — 코드 `#L164.23m` 에서 거리를 되읽음(인계를 다시 안 셈)."""
+22
View File
@@ -74,6 +74,23 @@ def test_인계_거리와_갈래_잇기() -> None:
assert dump_child_for("모르는암", "2026-01-01") is None assert dump_child_for("모르는암", "2026-01-01") is None
def test_적재는_거리_무관_한_장이고_E0_는_0_60() -> None:
from B09_Estimation.B09_Estimation_MachineProductivity_Dump import (
loading_output,
loading_title_code,
)
# 토사 3600×0.7×0.9×0.77×0.60/22 = 47.628 → 47.63
# 암절취 3600×0.7×0.55×0.74×0.35/22 = 16.317 → 16.32
assert loading_output("FP-10-12-01") == Decimal("47.63")
assert loading_output("FP-10-12-02") == Decimal("16.32")
build = cached_build() # 거리 없어도 섬
code = loading_title_code("FP-10-12-01")
assert build.book.resolve(code).total > 0
basis = build.book.details[f"D-{code[2:]}"][0].note
assert "0.75 버림" in basis
def test_내역_줄_거리_없으면_운반거리_미입력() -> None: def test_내역_줄_거리_없으면_운반거리_미입력() -> None:
from B09_Estimation.B09_Estimation_BillOfQuantities import build_bill from B09_Estimation.B09_Estimation_BillOfQuantities import build_bill
@@ -102,3 +119,8 @@ def test_내역_줄_거리_없으면_운반거리_미입력() -> None:
priced = build_bill(payload(164.23), build=cached_build(dump_haul_m=("164.23",))) priced = build_bill(payload(164.23), build=cached_build(dump_haul_m=("164.23",)))
row = next(r for r in priced.rows if r.name == "dump_truck 운반") row = next(r for r in priced.rows if r.name == "dump_truck 운반")
assert row.price_code == "B-FP-10-12-01#L164.23m" and row.amount_krw > 0 assert row.price_code == "B-FP-10-12-01#L164.23m" and row.amount_krw > 0
loading = payload(None)
loading["work_items"][0].update(name="덤프 적재", haul_equipment="dump_loading")
loaded = build_bill(loading, build=cached_build())
row = next(r for r in loaded.rows if r.name == "덤프 적재")
assert row.price_code == "B-FP-10-12-01#적재" and row.amount_krw > 0