feat(b09): 초류종자살포 5-24 둘이 섬 — 트럭 → 덤프트럭 4.5 · 종자살포기 → 취부기 11.94㎾ 손료만(8-4 줄 없음 · 거창 실무) · 종자 후보 넷 칸 · 어긋남 사유 나란히(브레인 판정 ①②′④)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
@@ -114,6 +114,15 @@ KNOWN_GAPS: dict[str, tuple[str, str]] = {
|
||||
"ⓘ 재료(합판·못 카탈로그 없음 · 각재·철선·박리제 규격 미정)는 못 붙은 줄 — 지금 값은"
|
||||
" **인력 품만** · [주]③ 동바리 별도 · [주]⑥ 소형구조물 인력품 30% 할증(선택)은 안 걺.",
|
||||
),
|
||||
"FP-05-24": (
|
||||
"실무 역산",
|
||||
"ⓘ 원문이 애매해 실무(거창 「초류종자살포(씨드스프레이)」)를 따른 자리 둘 — 뒤집을 수 있게 어긋남을"
|
||||
" 나란히: ① 「트럭 4.5ton」 → 덤프트럭 4.5(원문은 트럭 · 카탈로그에 트럭 없음 · 거창이 덤프트럭 4.5Ton)"
|
||||
" ② 「종자살포기 2,500-3,000ℓ」 → 취부기 11.94㎾(원문 규격은 ℓ 탱크 용량 · 카탈로그는 ㎾ · 건설 4-1-3"
|
||||
"·거창이 취부기) · 취부기는 손료만(8-4 운전경비표에 줄 없음 · 거창도 손료만) · ⚠ 거창은 디젤엔진 11.19㎾"
|
||||
" 를 따로 세움 — 산림 5-24 · 건설 4-1-3 표 둘 다에 없어 안 넣음 · 물탱크 조종원은 화물차운전사(8-1-2 5호"
|
||||
" 살수차) · [주]⑦ 물주기(인력) 보통인부 0.0005인은 필요시라 안 걺(2026-09-15 브레인).",
|
||||
),
|
||||
"FP-12-25": (
|
||||
"운반거리 미정",
|
||||
"⚠ 이 값에는 **운반 몫이 빠져 있습니다** — 품셈 12-25 는 「운반 | 덤프트럭(15ton)」 줄을 "
|
||||
|
||||
@@ -81,6 +81,7 @@ def machine_expense_sheets(build: Any) -> list[dict[str, Any]]:
|
||||
"""**내역에 실제로 선 기종만** 한 장씩. 안 쓰는 613 기종을 다 뿌리지 않는다."""
|
||||
from B09_Estimation.B09_Estimation_MachineOperating import load_fuel_price
|
||||
from B09_Estimation.B09_Estimation_MachineOperating import (
|
||||
LOSS_ONLY_MACHINES,
|
||||
load_operating_records,
|
||||
load_operator_wages,
|
||||
)
|
||||
@@ -120,7 +121,8 @@ def machine_expense_sheets(build: Any) -> list[dict[str, Any]]:
|
||||
money = build.book.resolve(code)
|
||||
|
||||
gaps: list[str] = []
|
||||
attachment = machine_code.startswith(_ATTACHMENT_PREFIXES)
|
||||
loss_only = LOSS_ONLY_MACHINES.get(machine_code, "") # 8-4 에 줄 없음 · 손료만(②′)
|
||||
attachment = machine_code.startswith(_ATTACHMENT_PREFIXES) or bool(loss_only)
|
||||
fuel_liters = getattr(record, "fuel_liters_per_hour", None)
|
||||
# 연료 종류대로 그 유가(휘발유 기계가 경유값으로 보이던 자리 · 661 뒤 ②).
|
||||
fuel_price, fuel_meta = (
|
||||
@@ -181,7 +183,7 @@ def machine_expense_sheets(build: Any) -> list[dict[str, Any]]:
|
||||
"expense_krw": _money(money.expense),
|
||||
"total_krw": _money(money.total),
|
||||
"attachment": attachment,
|
||||
"attachment_note": ATTACHMENT_NOTE if attachment else "",
|
||||
"attachment_note": loss_only or (ATTACHMENT_NOTE if attachment else ""),
|
||||
"gaps": gaps,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -62,6 +62,15 @@ MERGED_CELL_OPERATING_FIXES: dict[str, str] = {
|
||||
"7204-0065": "9.4",
|
||||
"7204-0160": "12.9",
|
||||
}
|
||||
#: **운전경비 줄이 원문에 없는 기계** — 손료만으로 층을 세움(부착 장비 길 · 2026-09-15 브레인 판정 ②′).
|
||||
#: ⚠ 이 목록 밖으로 넓히지 말 것 — 연료가 빠진 채 조용히 싼 값이 섬. 줄마다 원문 없음 · 실무 실증을 적음.
|
||||
LOSS_ONLY_MACHINES: dict[str, str] = {
|
||||
"7750-0016": (
|
||||
"손료만 — 건설품셈 8-4 운전경비표에 취부기(7750) 줄이 없음(원문 없음) · 거창 실무 「취부기(녹생토)"
|
||||
" 11.94kW」 도 손료 20,295 만 · ⚠ 거창은 디젤엔진 11.19kW 를 따로 세움 — 산림 5-24 · 건설 4-1-3"
|
||||
" 표 둘 다에 없어 안 넣음"
|
||||
),
|
||||
}
|
||||
_WATER_TANK_MISC_PERCENT = Decimal(30)
|
||||
_WATER_TANK_OPERATORS = Decimal(1)
|
||||
|
||||
|
||||
@@ -128,14 +128,20 @@ def scoped_alias_entry(
|
||||
흐려짐 — 별칭이 코드를 적었으면 그 코드가 곧 답(2026-09-14 · 9-19-3 소형브레이커).
|
||||
⚠ 별칭은 **빈 곳을 채움** — 옆 칸이 규격을 적었으면 안 덮음(같은 9-19-1 성토면 「굴착기 |
|
||||
0.6㎥」 가 절토면 [주]① 0.7 로 덮이던 자리 · 2026-09-14 판정 Ⓐ).
|
||||
단, 대상 규격이 옆 칸 규격과 **같으면** 덮는 것이 아님 — 이름만 이음(5-24 「트럭 | 4.5ton」 →
|
||||
덤프트럭 4.5 · 2026-09-15 판정 ①).
|
||||
"""
|
||||
if _writes_spec(side_cells):
|
||||
return None
|
||||
wanted = _normalize(name_cell)
|
||||
for row in catalog.scoped_aliases:
|
||||
if _normalize(row["from"]) != wanted or not in_scope(work_item_code, row["scope"]):
|
||||
continue
|
||||
return next((entry for entry in catalog.entries if entry.code == row["to"]), None)
|
||||
entry = next((entry for entry in catalog.entries if entry.code == row["to"]), None)
|
||||
if entry is not None and _writes_spec(side_cells):
|
||||
same = any(
|
||||
_RE_SIDE_SPEC.match(_normalize(c)) and same_spec(entry.spec, c) for c in side_cells
|
||||
)
|
||||
return entry if same else None
|
||||
return entry
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""B09 원가계산 — **초류종자살포 5-24 종자**(2026-09-15 브레인 판정 ④ · 화약류 길).
|
||||
|
||||
표 「종자 kg 0.025」 는 규격을 안 적음 — 자원 목록에 종자 후보가 넷(생태복원형 초본류·목본류 · 초본류 · 목본류)이라
|
||||
조인 규칙(규격이 같아야)에 걸려 못 붙었음. 배합은 [주]①③ 이 현장 판단(도로비탈면 녹화 지침 · 방향·고도·계절)으로 넘김.
|
||||
⇒ 넷 모두 「자재 단가」 탭에 칸이 서고 **설계자가 넣은 하나**가 표 수량으로 붙음 · 안 넣으면 「규격 미정」 ·
|
||||
둘 이상 넣으면 고르지 않고 사유(임의로 규격을 고르지 않음).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
CODES = ("FP-05-24-01", "FP-05-24-02")
|
||||
NAME = "종자"
|
||||
#: (자원 목록 코드, 규격) — 자원 목록 `resource_catalog_ext` 의 종자 넷.
|
||||
SEEDS: tuple[tuple[str, str], ...] = (
|
||||
("AR-M-f89c57d4", "생태복원형,초본류"),
|
||||
("AR-M-d52b8fc1", "생태복원형,목본류"),
|
||||
("AR-M-8f852514", "초본류"),
|
||||
("AR-M-e39ee36e", "목본류"),
|
||||
)
|
||||
SEED_MISSING = (
|
||||
"종자 — 규격 미정(원문 표가 규격을 안 적음 · 후보 넷 "
|
||||
+ " · ".join(spec for _, spec in SEEDS)
|
||||
+ ") · 「자재 단가」 탭에서 하나를 넣으면 붙음"
|
||||
)
|
||||
SEED_TWO = "종자 — 후보가 둘 이상 들어옴({specs}) · 하나만 넣을 것"
|
||||
|
||||
|
||||
def attach_seed_spray(build: Any, nodes_by_code: dict[str, dict[str, Any]]) -> None:
|
||||
"""칸 넷을 세우고(`material_uses`) · 넣은 하나만 붙이고 · 없거나 둘이면 사유로 갈아 끼움."""
|
||||
from B09_Estimation.B09_Estimation_Explosives import _table_amount
|
||||
from B09_Estimation.B09_Estimation_PriceBook import PriceDetail
|
||||
|
||||
book = build.book
|
||||
for code in CODES:
|
||||
for material, _ in SEEDS:
|
||||
uses = build.material_uses.setdefault(material, [])
|
||||
if code not in uses:
|
||||
uses.append(code)
|
||||
labels = [
|
||||
label for label in build.unattached.get(code) or [] if "".join(label.split()) != NAME
|
||||
]
|
||||
amount = _table_amount(nodes_by_code.get(code) or {}, NAME)
|
||||
picked = [(material, spec) for material, spec in SEEDS if material in book.titles]
|
||||
titles = [t for t in book.titles if t == f"B-{code}" or t.startswith(f"B-{code}#")]
|
||||
if len(picked) == 1 and amount is not None and titles:
|
||||
material, spec = picked[0]
|
||||
for title in titles:
|
||||
book.add_detail(
|
||||
PriceDetail(title, material, amount, note=f"종자 {spec} — 설계자 선택")
|
||||
)
|
||||
elif len(picked) > 1:
|
||||
labels.append(SEED_TWO.format(specs=" · ".join(spec for _, spec in picked)))
|
||||
else:
|
||||
labels.append(SEED_MISSING)
|
||||
build.unattached[code] = labels
|
||||
@@ -45,6 +45,7 @@ from B09_Estimation.B09_Estimation_MachineProductivity_Dozer import (
|
||||
formula_machine_codes,
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_MachineOperating import (
|
||||
LOSS_ONLY_MACHINES,
|
||||
load_fuel_price,
|
||||
load_operating_records,
|
||||
LABOR_RELIABILITY_LABEL,
|
||||
@@ -354,7 +355,8 @@ def _add_machine_layers(
|
||||
if machine is None or machine.loss_coefficient_per_hour is None:
|
||||
incomplete.append(code)
|
||||
continue
|
||||
if record is None and not attachment:
|
||||
loss_only = LOSS_ONLY_MACHINES.get(code) # 8-4 에 줄 없는 기계 — 원문 없음 · 실무 실증(②′)
|
||||
if record is None and not attachment and not loss_only:
|
||||
incomplete.append(code)
|
||||
continue
|
||||
|
||||
@@ -394,7 +396,8 @@ def _add_machine_layers(
|
||||
hourly_code,
|
||||
base_code,
|
||||
Decimal(0),
|
||||
note=(
|
||||
note=loss_only
|
||||
or (
|
||||
"부착용 장비 — 연료·조종원은 본체(굴착기·불도저)에 듭니다"
|
||||
" (품셈 제8장 [주]⑤)."
|
||||
),
|
||||
@@ -1106,6 +1109,10 @@ def build_unit_prices(
|
||||
from B09_Estimation.B09_Estimation_Explosives import attach_explosives
|
||||
|
||||
attach_explosives(build, nodes_by_code)
|
||||
# 초류종자살포 5-24 종자 — 후보 넷 칸 · 넣은 하나만 붙음(2026-09-15 ④ 화약류 길).
|
||||
from B09_Estimation.B09_Estimation_SeedSpray import attach_seed_spray
|
||||
|
||||
attach_seed_spray(build, nodes_by_code)
|
||||
# 표토제거 답외구간(9-15-2) — 계수표를 불도저 식 + T 로(2026-09-14 ㉰).
|
||||
from B09_Estimation.B09_Estimation_TopsoilRemoval import attach_topsoil_removal
|
||||
|
||||
|
||||
@@ -146,6 +146,22 @@
|
||||
"scope": "FP-10-12",
|
||||
"pum_edition": "2026-01-01",
|
||||
"basis": "품셈 10-12 덤프 운반 잎이 토사·암절취·발파암으로 갈리고 [주]③ 환산계수가 「암절취 1.35」(10-11 f 파쇄암 1/1.35 와 같은 값) — 우리 리핑암(리퍼 절취) = 품셈 암절취. 옛 매핑표 ground_aliases 에서 범위를 붙여 옮김(2026-09-13). ⚠ 쓰는 곳: 덤프 운반 잎 고르기(아직 없음 — 갈래 고르기형 부모 판정 대기)"
|
||||
},
|
||||
{
|
||||
"axis": "resource",
|
||||
"from": "트럭",
|
||||
"to": "0602-0045",
|
||||
"scope": "FP-05-24",
|
||||
"pum_edition": "2026-01-01",
|
||||
"basis": "산림 5-24 표 「트럭 4.5ton」 — 카탈로그에 「트럭」 이 없음(덤프트럭·크레인만) = 원문 애매 → 실무. 거창 실무 호표 「51.초류종자살포(씨드스프레이) 나. 덤프트럭 (4.5Ton)」(건설 4-1-3 표 「트럭 4.5ton」 을 덤프트럭 4.5 로 셈). ⚠ 어긋남: 원문은 트럭(건설 4-1-3 [주]① 트럭에 종자살포기 장착) · 카탈로그는 덤프트럭. 2026-09-15 브레인 판정 ①"
|
||||
},
|
||||
{
|
||||
"axis": "resource",
|
||||
"from": "종자살포기",
|
||||
"to": "7750-0016",
|
||||
"scope": "FP-05-24",
|
||||
"pum_edition": "2026-01-01",
|
||||
"basis": "산림 5-24 표 「종자살포기 2,500-3,000ℓ」 — 원문 규격은 ℓ(탱크 용량) · 카탈로그는 취부기(녹생토 암절개면 보호식재용) 11.94㎾ 로 같은 기계라는 원문 근거 없음 = 애매 → 실무. 5-24-1 [주]⑤ 가 건설 4-1-2 초류종자 살포를 참조 · 건설 4-1-3 표 「취부기 11.94kW」 · 거창 실무 「종자살포기 : 취부기(녹생토) (11.94kW)」. 2026-09-15 브레인 판정 ②"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""초류종자살포 5-24-1·2 뿜어붙이기 — 2026-09-15 브레인 판정 ①②′④ (③ 물탱크는 `test_b09_water_tank_operating`).
|
||||
|
||||
산림 5-24 표(㎡당): 종자 0.025 · 복합비료 0.1 · 화이버 0.18 · 합성접착제 0.1 · 색소 0.002 kg ·
|
||||
종자살포기 2,500-3,000ℓ 0.0024h · 트럭 4.5ton 0.0024h · 물탱크 5,500ℓ 0.0036h · 조경공 0.0007 · 보통인부 0.0004
|
||||
[주]⑤ 「건설공사 표준품셈 공통부문 4-1-2 초류종자 살포」 참조
|
||||
① 트럭 4.5ton → 덤프트럭 4.5(0602-0045) — 카탈로그에 「트럭」 없음(원문 애매) · 거창 실무 「덤프트럭 (4.5Ton)」
|
||||
②′ 종자살포기 → 취부기 11.94㎾(7750-0016) · **손료만** — 원문 규격은 ℓ(탱크 용량) · 건설 4-1-3 「취부기 11.94kW」 ·
|
||||
거창 실무 「종자살포기 : 취부기(녹생토)」 · 8-4 운전경비표에 7750 줄 없음(원문 없음) · 거창도 손료 20,295 만
|
||||
디젤엔진 11.19㎾ 는 거창만 따로 세움 — 산림 5-24 · 건설 4-1-3 표 둘 다에 없어 안 넣고 어긋남만 기록
|
||||
④ 종자 규격 후보 넷(생태복원형 초본·목본 · 초본류 · 목본류) — 화약류 길: 「자재 단가」 칸 · 넣은 쪽 하나가 붙음
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import ROUND_FLOOR, Decimal
|
||||
|
||||
from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note
|
||||
from B09_Estimation.B09_Estimation_MachineExpenseSheet import machine_expense_sheets
|
||||
from B09_Estimation.B09_Estimation_UnitPrice import cached_build
|
||||
|
||||
CODES = ("FP-05-24-01", "FP-05-24-02")
|
||||
SEEDS = ("AR-M-f89c57d4", "AR-M-d52b8fc1", "AR-M-8f852514", "AR-M-e39ee36e")
|
||||
|
||||
|
||||
def _rows(book, code: str) -> dict[str, Decimal]:
|
||||
return {r.ref_code: r.quantity for r in book.details[f"B-{code}"]}
|
||||
|
||||
|
||||
def test_트럭은_덤프트럭_종자살포기는_취부기_물탱크까지_표_시간대로() -> None:
|
||||
build = cached_build()
|
||||
for code in CODES:
|
||||
rows = _rows(build.book, code)
|
||||
assert rows["X-0602-0045"] == Decimal("0.0024"), rows
|
||||
assert rows["X-7750-0016"] == Decimal("0.0024"), rows
|
||||
assert rows["X-7204-0055"] == Decimal("0.0036"), rows
|
||||
left = " ".join(build.unattached.get(code, []))
|
||||
assert "트 럭" not in left and "종자살포기" not in left, left
|
||||
assert code not in build.component_gaps, build.component_gaps.get(code)
|
||||
|
||||
|
||||
def test_취부기는_손료만으로_서고_사유가_붙음() -> None:
|
||||
build = cached_build()
|
||||
assert not any(m.startswith("7750-0016") for m in build.incomplete_machines)
|
||||
money = build.book.resolve("X-7750-0016")
|
||||
assert money.material == 0 and money.labor == 0
|
||||
assert (
|
||||
money.expense.quantize(Decimal(1), rounding=ROUND_FLOOR) == 20295
|
||||
) # 거창 46,603,000 × 4,355
|
||||
notes = " ".join(r.note for r in build.book.details["X-7750-0016"])
|
||||
assert "8-4" in notes and "거창" in notes, notes
|
||||
sheet = next(s for s in machine_expense_sheets(build) if s["code"] == "X-7750-0016")
|
||||
assert sheet["gaps"] == [] and "8-4" in sheet["attachment_note"], sheet
|
||||
|
||||
|
||||
def test_종자는_후보_넷_칸_넣은_하나만_붙고_둘이면_사유() -> None:
|
||||
none = cached_build()
|
||||
for code in CODES:
|
||||
assert all(code in none.material_uses.get(seed, []) for seed in SEEDS)
|
||||
left = " ".join(none.unattached.get(code, []))
|
||||
assert "종자 — 규격 미정" in left and "종 자" not in left, left
|
||||
one = cached_build(material_prices=(("AR-M-8f852514", "20000", "견적"),))
|
||||
assert _rows(one.book, "FP-05-24-01")["AR-M-8f852514"] == Decimal("0.025")
|
||||
assert "종자" not in " ".join(one.unattached.get("FP-05-24-01", []))
|
||||
two = cached_build(
|
||||
material_prices=(("AR-M-8f852514", "20000", "견적"), ("AR-M-e39ee36e", "30000", "견적"))
|
||||
)
|
||||
rows = _rows(two.book, "FP-05-24-01")
|
||||
assert "AR-M-8f852514" not in rows and "AR-M-e39ee36e" not in rows
|
||||
assert "하나만" in " ".join(two.unattached.get("FP-05-24-01", []))
|
||||
|
||||
|
||||
def test_사유는_어긋남을_나란히() -> None:
|
||||
note = known_gap_note("FP-05-24-01")
|
||||
for word in ("트럭", "덤프트럭", "ℓ", "취부기", "손료만", "디젤엔진"):
|
||||
assert word in note, (word, note)
|
||||
assert known_gap_note("FP-05-24-02") == note
|
||||
Reference in New Issue
Block a user