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:
@@ -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` 에서 거리를 되읽음(인계를 다시 안 셈)."""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user