Compare commits
7
Commits
sub_laptop_1
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
419d70d898 | ||
|
|
2269266910 | ||
|
|
329bdc5c90 | ||
|
|
ab8a010985 | ||
|
|
30fc514f3f | ||
|
|
cbd3b87ed5 | ||
|
|
3d896dec50 |
@@ -114,6 +114,15 @@ KNOWN_GAPS: dict[str, tuple[str, str]] = {
|
|||||||
"ⓘ 재료(합판·못 카탈로그 없음 · 각재·철선·박리제 규격 미정)는 못 붙은 줄 — 지금 값은"
|
"ⓘ 재료(합판·못 카탈로그 없음 · 각재·철선·박리제 규격 미정)는 못 붙은 줄 — 지금 값은"
|
||||||
" **인력 품만** · [주]③ 동바리 별도 · [주]⑥ 소형구조물 인력품 30% 할증(선택)은 안 걺.",
|
" **인력 품만** · [주]③ 동바리 별도 · [주]⑥ 소형구조물 인력품 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": (
|
"FP-12-25": (
|
||||||
"운반거리 미정",
|
"운반거리 미정",
|
||||||
"⚠ 이 값에는 **운반 몫이 빠져 있습니다** — 품셈 12-25 는 「운반 | 덤프트럭(15ton)」 줄을 "
|
"⚠ 이 값에는 **운반 몫이 빠져 있습니다** — 품셈 12-25 는 「운반 | 덤프트럭(15ton)」 줄을 "
|
||||||
|
|||||||
@@ -81,12 +81,21 @@ def machine_expense_sheets(build: Any) -> list[dict[str, Any]]:
|
|||||||
"""**내역에 실제로 선 기종만** 한 장씩. 안 쓰는 613 기종을 다 뿌리지 않는다."""
|
"""**내역에 실제로 선 기종만** 한 장씩. 안 쓰는 613 기종을 다 뿌리지 않는다."""
|
||||||
from B09_Estimation.B09_Estimation_MachineOperating import load_fuel_price
|
from B09_Estimation.B09_Estimation_MachineOperating import load_fuel_price
|
||||||
from B09_Estimation.B09_Estimation_MachineOperating import (
|
from B09_Estimation.B09_Estimation_MachineOperating import (
|
||||||
|
LOSS_ONLY_MACHINES,
|
||||||
|
OPERATOR_PROVISIONAL_NOTE,
|
||||||
load_operating_records,
|
load_operating_records,
|
||||||
load_operator_wages,
|
load_operator_wages,
|
||||||
)
|
)
|
||||||
|
from B09_Estimation.B09_Estimation_WoodChipping import MACHINE as CHIPPER
|
||||||
|
from B09_Estimation.B09_Estimation_WoodChipping import operating_record as chipper_record
|
||||||
|
|
||||||
catalog = load_machine_catalog()
|
catalog = load_machine_catalog()
|
||||||
operating = {row.machine_code: row for row in load_operating_records().records}
|
operating = {row.machine_code: row for row in load_operating_records().records}
|
||||||
|
# 파쇄기는 8-4 칸이 「-」 라 8-11 [주]⑤ 레코드로 섬(단가와 같은 값) — 없으면 장이 「연료·조종원 없음」 으로 틀리게 뜸.
|
||||||
|
if f"X-{CHIPPER}" in build.book.titles and CHIPPER not in operating:
|
||||||
|
chipper = chipper_record()
|
||||||
|
if chipper is not None:
|
||||||
|
operating[CHIPPER] = chipper
|
||||||
wages = load_operator_wages()
|
wages = load_operator_wages()
|
||||||
loss = _loss_records()
|
loss = _loss_records()
|
||||||
|
|
||||||
@@ -120,7 +129,8 @@ def machine_expense_sheets(build: Any) -> list[dict[str, Any]]:
|
|||||||
money = build.book.resolve(code)
|
money = build.book.resolve(code)
|
||||||
|
|
||||||
gaps: list[str] = []
|
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)
|
fuel_liters = getattr(record, "fuel_liters_per_hour", None)
|
||||||
# 연료 종류대로 그 유가(휘발유 기계가 경유값으로 보이던 자리 · 661 뒤 ②).
|
# 연료 종류대로 그 유가(휘발유 기계가 경유값으로 보이던 자리 · 661 뒤 ②).
|
||||||
fuel_price, fuel_meta = (
|
fuel_price, fuel_meta = (
|
||||||
@@ -168,6 +178,13 @@ def machine_expense_sheets(build: Any) -> list[dict[str, Any]]:
|
|||||||
str(COMBINED_MISC_PERCENT) if variant == "조합" else _money(misc_percent)
|
str(COMBINED_MISC_PERCENT) if variant == "조합" else _money(misc_percent)
|
||||||
),
|
),
|
||||||
"operator_code": occupation,
|
"operator_code": occupation,
|
||||||
|
# 8-1-2 5호가 이 기종을 이름으로 안 가르면 잠정 사유(원문이 안 가르는 것을 우리가 안 가름).
|
||||||
|
"operator_note": (
|
||||||
|
OPERATOR_PROVISIONAL_NOTE
|
||||||
|
if wage is not None
|
||||||
|
and getattr(record, "operator_mapping_is_provisional", False)
|
||||||
|
else ""
|
||||||
|
),
|
||||||
"operator_daily_wage": _money(wage),
|
"operator_daily_wage": _money(wage),
|
||||||
# 식 좌→우 순차 + 원 미만 절사(명세 7장) — 계수로 접으면 1원 틀림.
|
# 식 좌→우 순차 + 원 미만 절사(명세 7장) — 계수로 접으면 1원 틀림.
|
||||||
"operator_krw_per_hour": _money(
|
"operator_krw_per_hour": _money(
|
||||||
@@ -181,7 +198,7 @@ def machine_expense_sheets(build: Any) -> list[dict[str, Any]]:
|
|||||||
"expense_krw": _money(money.expense),
|
"expense_krw": _money(money.expense),
|
||||||
"total_krw": _money(money.total),
|
"total_krw": _money(money.total),
|
||||||
"attachment": attachment,
|
"attachment": attachment,
|
||||||
"attachment_note": ATTACHMENT_NOTE if attachment else "",
|
"attachment_note": loss_only or (ATTACHMENT_NOTE if attachment else ""),
|
||||||
"gaps": gaps,
|
"gaps": gaps,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -47,6 +47,32 @@ _RE_FUEL_WITH_KIND = re.compile(r"^(휘발유|중유|경유)?(\d+(?:\.\d+)?)$")
|
|||||||
_OPERATOR_TRUCK_WORDS = ("덤프트럭", "트럭", "트레일러", "화물", "살수차")
|
_OPERATOR_TRUCK_WORDS = ("덤프트럭", "트럭", "트레일러", "화물", "살수차")
|
||||||
_OPERATOR_ALIAS_TRUCK = "labor_op_truck"
|
_OPERATOR_ALIAS_TRUCK = "labor_op_truck"
|
||||||
_OPERATOR_ALIAS_CONSTRUCTION = "labor_op_const"
|
_OPERATOR_ALIAS_CONSTRUCTION = "labor_op_const"
|
||||||
|
_OPERATOR_ALIAS_GENERAL = "labor_op_general"
|
||||||
|
|
||||||
|
#: ⭐ 건설품셈 8-1-2 5호 「운전사의 구분」 — **원문이 이름으로 가르는 기종만** (2026-09-15 브레인).
|
||||||
|
#: 옛 잠정 규칙이 덤프 15t 을 화물차 · 콤팩터·래머·소형믹서를 건설기계로 세우던 틀림 12(조립에 선 것 2).
|
||||||
|
#: 거창 실무 「덤프 15Ton 건설기계 · 8·4.5Ton 화물차 · 플레이트 콤팩터 일반기계운전사」 = 원문.
|
||||||
|
#: ⚠ 원문이 안 가르는 기종(콘크리트 펌프차 · 트랙터(타이어) · 믹서트럭 …)은 **우리가 가르지 않음** — 옛 규칙 + 잠정 사유.
|
||||||
|
_CLASS_CONSTRUCTION_WORDS = (
|
||||||
|
"불도저", "굴착기", "로더", "지게차", "스크레이퍼", "기중기", "모터그레이더", "롤러",
|
||||||
|
"노상안정기", "콘크리트배치플랜트", "콘크리트피니셔", "콘크리트스프레더", "아스팔트믹싱플랜트",
|
||||||
|
"아스팔트피니셔", "아스팔트살포기", "슬러리실", "골재살포기", "쇄석기", "천공기", "사리채취기",
|
||||||
|
"노면파쇄기",
|
||||||
|
) # fmt: skip
|
||||||
|
_CLASS_TRUCK_WORDS = ("화물트럭", "살수차", "제설차", "노면청소차", "트럭탑재형크레인")
|
||||||
|
_CLASS_GENERAL_WORDS = (
|
||||||
|
"양수기",
|
||||||
|
"윈치",
|
||||||
|
"벨트컨베이어",
|
||||||
|
"발전기",
|
||||||
|
"래머",
|
||||||
|
"콤팩터",
|
||||||
|
"콘크리트파쇄기",
|
||||||
|
)
|
||||||
|
OPERATOR_PROVISIONAL_NOTE = (
|
||||||
|
"조종원 직종 잠정 — 건설품셈 8-1-2 5호 「운전사의 구분」 이 이 기종을 이름으로 안 가름"
|
||||||
|
" (이름에 트럭이 들면 화물차운전사 · 그 밖은 건설기계운전사로 둠)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#: ⚠ **셀 병합으로 줄째 버려지는 표에서 원문 값을 되살린다** (2026-09-15 초류종자살포 ③ 브레인).
|
#: ⚠ **셀 병합으로 줄째 버려지는 표에서 원문 값을 되살린다** (2026-09-15 초류종자살포 ③ 브레인).
|
||||||
@@ -62,6 +88,15 @@ MERGED_CELL_OPERATING_FIXES: dict[str, str] = {
|
|||||||
"7204-0065": "9.4",
|
"7204-0065": "9.4",
|
||||||
"7204-0160": "12.9",
|
"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_MISC_PERCENT = Decimal(30)
|
||||||
_WATER_TANK_OPERATORS = Decimal(1)
|
_WATER_TANK_OPERATORS = Decimal(1)
|
||||||
|
|
||||||
@@ -172,14 +207,45 @@ def _parse_person_days(token: str) -> Decimal | None:
|
|||||||
return Decimal(text) if _RE_DECIMAL.match(text) else None
|
return Decimal(text) if _RE_DECIMAL.match(text) else None
|
||||||
|
|
||||||
|
|
||||||
def _operator_code(machine_name: str, aliases: dict[str, str]) -> str:
|
def operator_class(machine_name: str, specification: str = "") -> str | None:
|
||||||
"""기종 이름으로 운전사 직종을 고른다 — **잠정 규칙**.
|
"""8-1-2 5호가 이름·규격으로 가르는 직종 별칭 키 — 원문이 안 가르면 `None`."""
|
||||||
|
name = re.sub(r"\s", "", machine_name)
|
||||||
|
found = re.search(r"\d+(?:\.\d+)?", str(specification).replace(",", ""))
|
||||||
|
size = Decimal(found.group(0)) if found else None
|
||||||
|
if name.startswith("덤프트럭"):
|
||||||
|
# 「덤프트럭(12ton이상)」 건설기계 · 「12ton미만의 덤프트럭」 화물차
|
||||||
|
if size is None:
|
||||||
|
return None
|
||||||
|
return _OPERATOR_ALIAS_CONSTRUCTION if size >= 12 else _OPERATOR_ALIAS_TRUCK
|
||||||
|
if name.startswith("콘크리트믹서") and "트럭" not in name:
|
||||||
|
# 「콘크리트 믹서(0.55㎥ 이상)」 건설기계 · 「소형믹서」 일반기계
|
||||||
|
if size is None:
|
||||||
|
return None
|
||||||
|
return _OPERATOR_ALIAS_CONSTRUCTION if size >= Decimal("0.55") else _OPERATOR_ALIAS_GENERAL
|
||||||
|
if name.startswith("공기압축기"):
|
||||||
|
# 「공기압축기(이동식, 2.83㎥/min 이상)」 건설기계 · 「소형의 공기압축기」 일반기계
|
||||||
|
if size is None or "이동식" not in name:
|
||||||
|
return None
|
||||||
|
return _OPERATOR_ALIAS_CONSTRUCTION if size >= Decimal("2.83") else _OPERATOR_ALIAS_GENERAL
|
||||||
|
# 괄호 앞 이름의 **끝말**로 — 속에 든 글자로 보면 「크롤러드릴」 이 롤러 · 「타워크레인」 이 기중기로 걸림.
|
||||||
|
base = re.sub(r"[((].*", "", name)
|
||||||
|
if any(word in name for word in _CLASS_TRUCK_WORDS): # 「물탱크(살수차)」 — 괄호 속 이름
|
||||||
|
return _OPERATOR_ALIAS_TRUCK
|
||||||
|
if base.endswith(_CLASS_GENERAL_WORDS):
|
||||||
|
return _OPERATOR_ALIAS_GENERAL
|
||||||
|
if base == "크레인" or base.endswith(_CLASS_CONSTRUCTION_WORDS): # 「기중기(차륜 및 무한궤도)」
|
||||||
|
return _OPERATOR_ALIAS_CONSTRUCTION
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _operator_code(machine_name: str, aliases: dict[str, str], specification: str = "") -> str:
|
||||||
|
"""기종 이름으로 운전사 직종을 고른다 — 8-1-2 5호가 가르면 원문, 아니면 **잠정 규칙**.
|
||||||
|
|
||||||
품셈 8-4 표는 「조종원 인/일」 수만 주고 직종명을 안 준다. 노임표의 `aliases` 가
|
품셈 8-4 표는 「조종원 인/일」 수만 주고 직종명을 안 준다. 노임표의 `aliases` 가
|
||||||
운전사 직종 셋(`labor_op_const`·`labor_op_truck`·`labor_op_general`)을 들고 있어
|
운전사 직종 셋(`labor_op_const`·`labor_op_truck`·`labor_op_general`)을 들고 있음.
|
||||||
트럭 계열만 화물차운전사로, 나머지는 건설기계운전사로 **잠정** 매핑한다.
|
원문이 안 가르는 기종은 트럭 계열만 화물차운전사로, 나머지는 건설기계운전사로 **잠정** 매핑한다.
|
||||||
"""
|
"""
|
||||||
key = (
|
key = operator_class(machine_name, specification) or (
|
||||||
_OPERATOR_ALIAS_TRUCK
|
_OPERATOR_ALIAS_TRUCK
|
||||||
if any(word in machine_name for word in _OPERATOR_TRUCK_WORDS)
|
if any(word in machine_name for word in _OPERATOR_TRUCK_WORDS)
|
||||||
else _OPERATOR_ALIAS_CONSTRUCTION
|
else _OPERATOR_ALIAS_CONSTRUCTION
|
||||||
@@ -279,7 +345,13 @@ def enrich_with_catalog(
|
|||||||
fuel_kind=record.fuel_kind,
|
fuel_kind=record.fuel_kind,
|
||||||
misc_material_percent=record.misc_material_percent,
|
misc_material_percent=record.misc_material_percent,
|
||||||
operator_person_days=record.operator_person_days,
|
operator_person_days=record.operator_person_days,
|
||||||
operator_occupation_code=_operator_code(machine.name, aliases),
|
operator_occupation_code=_operator_code(
|
||||||
|
machine.name, aliases, machine.specification or record.specification
|
||||||
|
),
|
||||||
|
operator_mapping_is_provisional=operator_class(
|
||||||
|
machine.name, machine.specification or record.specification
|
||||||
|
)
|
||||||
|
is None,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
result.records = enriched
|
result.records = enriched
|
||||||
|
|||||||
@@ -128,14 +128,20 @@ def scoped_alias_entry(
|
|||||||
흐려짐 — 별칭이 코드를 적었으면 그 코드가 곧 답(2026-09-14 · 9-19-3 소형브레이커).
|
흐려짐 — 별칭이 코드를 적었으면 그 코드가 곧 답(2026-09-14 · 9-19-3 소형브레이커).
|
||||||
⚠ 별칭은 **빈 곳을 채움** — 옆 칸이 규격을 적었으면 안 덮음(같은 9-19-1 성토면 「굴착기 |
|
⚠ 별칭은 **빈 곳을 채움** — 옆 칸이 규격을 적었으면 안 덮음(같은 9-19-1 성토면 「굴착기 |
|
||||||
0.6㎥」 가 절토면 [주]① 0.7 로 덮이던 자리 · 2026-09-14 판정 Ⓐ).
|
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)
|
wanted = _normalize(name_cell)
|
||||||
for row in catalog.scoped_aliases:
|
for row in catalog.scoped_aliases:
|
||||||
if _normalize(row["from"]) != wanted or not in_scope(work_item_code, row["scope"]):
|
if _normalize(row["from"]) != wanted or not in_scope(work_item_code, row["scope"]):
|
||||||
continue
|
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
|
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
|
||||||
@@ -30,6 +30,7 @@ export interface MachineExpenseDto {
|
|||||||
fuel_scope: string;
|
fuel_scope: string;
|
||||||
misc_material_percent: string | null;
|
misc_material_percent: string | null;
|
||||||
operator_code: string;
|
operator_code: string;
|
||||||
|
operator_note: string;
|
||||||
operator_daily_wage: string | null;
|
operator_daily_wage: string | null;
|
||||||
operator_krw_per_hour: string | null;
|
operator_krw_per_hour: string | null;
|
||||||
material_krw: string | null;
|
material_krw: string | null;
|
||||||
@@ -110,6 +111,7 @@ function machineExpenseSheet(sheet: MachineExpenseDto["sheets"][number]): HTMLEl
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (sheet.attachment_note) box.append(note(sheet.attachment_note));
|
if (sheet.attachment_note) box.append(note(sheet.attachment_note));
|
||||||
|
if (sheet.operator_note) box.append(note(sheet.operator_note));
|
||||||
for (const gap of sheet.gaps) box.append(note(`⚠ ${gap}`));
|
for (const gap of sheet.gaps) box.append(note(`⚠ ${gap}`));
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ from B09_Estimation.B09_Estimation_MachineProductivity_Dozer import (
|
|||||||
formula_machine_codes,
|
formula_machine_codes,
|
||||||
)
|
)
|
||||||
from B09_Estimation.B09_Estimation_MachineOperating import (
|
from B09_Estimation.B09_Estimation_MachineOperating import (
|
||||||
|
LOSS_ONLY_MACHINES,
|
||||||
load_fuel_price,
|
load_fuel_price,
|
||||||
load_operating_records,
|
load_operating_records,
|
||||||
LABOR_RELIABILITY_LABEL,
|
LABOR_RELIABILITY_LABEL,
|
||||||
@@ -354,7 +355,8 @@ def _add_machine_layers(
|
|||||||
if machine is None or machine.loss_coefficient_per_hour is None:
|
if machine is None or machine.loss_coefficient_per_hour is None:
|
||||||
incomplete.append(code)
|
incomplete.append(code)
|
||||||
continue
|
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)
|
incomplete.append(code)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -394,7 +396,8 @@ def _add_machine_layers(
|
|||||||
hourly_code,
|
hourly_code,
|
||||||
base_code,
|
base_code,
|
||||||
Decimal(0),
|
Decimal(0),
|
||||||
note=(
|
note=loss_only
|
||||||
|
or (
|
||||||
"부착용 장비 — 연료·조종원은 본체(굴착기·불도저)에 듭니다"
|
"부착용 장비 — 연료·조종원은 본체(굴착기·불도저)에 듭니다"
|
||||||
" (품셈 제8장 [주]⑤)."
|
" (품셈 제8장 [주]⑤)."
|
||||||
),
|
),
|
||||||
@@ -1106,6 +1109,10 @@ def build_unit_prices(
|
|||||||
from B09_Estimation.B09_Estimation_Explosives import attach_explosives
|
from B09_Estimation.B09_Estimation_Explosives import attach_explosives
|
||||||
|
|
||||||
attach_explosives(build, nodes_by_code)
|
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 ㉰).
|
# 표토제거 답외구간(9-15-2) — 계수표를 불도저 식 + T 로(2026-09-14 ㉰).
|
||||||
from B09_Estimation.B09_Estimation_TopsoilRemoval import attach_topsoil_removal
|
from B09_Estimation.B09_Estimation_TopsoilRemoval import attach_topsoil_removal
|
||||||
|
|
||||||
|
|||||||
+5
-3
@@ -7,14 +7,16 @@
|
|||||||
|
|
||||||
### 남은 것
|
### 남은 것
|
||||||
- [ ] ⚠ 자원 이름이 겹치면 남의 공종 풀이가 바뀜(명세 2장 이름 맞춤의 부작용) — 자원 목록에 「양수기」 를 넣자 12-26 물푸기 「양수기(150m/m)」 줄 풀이가 바뀌어 제목이 사라짐 → 지금은 「유인헬기방제 양수기」 로 피함(임시) · 언젠가 공종 범위·코드로 맞출 자리
|
- [ ] ⚠ 자원 이름이 겹치면 남의 공종 풀이가 바뀜(명세 2장 이름 맞춤의 부작용) — 자원 목록에 「양수기」 를 넣자 12-26 물푸기 「양수기(150m/m)」 줄 풀이가 바뀌어 제목이 사라짐 → 지금은 「유인헬기방제 양수기」 로 피함(임시) · 언젠가 공종 범위·코드로 맞출 자리
|
||||||
- [ ] ㉯ 나머지 — 제목 없음 32(4-1 수확베기·4-2 부모·6-2 풀베기(예취기)·13-10-2 말뚝박기 등 · 표 읽기에서 먼저 막혀 드러남) · 2장 표 읽는 한 벌(`B09_Estimation_Consumables`)로 더할 자리
|
- [ ] ㉯ 나머지 — 제목 없음(지금 셈 34 · 같은 자) 가르기: ⓐ 참고 표 19(1-2 단위표준·운반 · 2장 소요재료·기계손료 17 — 다른 공종이 읽는 표라 제목이 안 서는 게 맞음) ⓑ choose_one 부모 2(4-1·4-2 — 자식이 섬 · 단어는 부록 설계예시표 F0475·F0476 에서 걸림) ⓒ 단어는 설계예시표에서만 · 자기 표는 인력뿐 3(3-1 경계표시 **사유 없음** · 3-2 작업로 선정 초급기술자 못 풂 · 3-3 작업로 설치 비율 인력 못 풂) ⓓ **예취기·분무기 인력 표 한 모양 4**(6-2-2 줄베기 · 6-2-3 모두베기 · 6-4-1 덩굴걷기 **사유 없음** · 6-4-2 덩굴 약제 — 「이름 | 도구 | 인/ha | 값 | 직종」 줄을 못 읽음 · 풀리면 2장 예취기·배부식분무기 1일 몫을 체인톱처럼) ⓔ 이름에만 드론·윈치 3(3-8 드론 영상 촬영 인력만 **사유 없음** · 7-3 아키야윈치 **사유 없음** · 8-6-2 드론방제 인력 줄 못 풂) ⓕ 거리 표 2(10-13-1 드론운반 · 10-13-2 인력운반 — 형식 미정으로 건너뜀 · **사유 없음**) ⓖ 13-10-2 나무 말뚝박기 1(뭉친 칸) · 사유 없이 버려지는 표 6(F0075 · F0082 · F0158 · F0174 · F0322 · F0323)
|
||||||
- [ ] 막힌 셋 풀기(원문에 값이 없음 · 입력·견적 칸이 서야 함): 12-26 물푸기 가동시간 · 10-7-4 모노레일 ps·임대료·운반거리 · 10-8-3 케이블크레인 ℓ/kWh · 8-6-3 지상방제 1일 작업량·1톤 트럭
|
- [ ] 막힌 셋 풀기(원문에 값이 없음 · 입력·견적 칸이 서야 함): 12-26 물푸기 가동시간 · 10-7-4 모노레일 ps·임대료·운반거리 · 10-8-3 케이블크레인 ℓ/kWh · 8-6-3 지상방제 1일 작업량·1톤 트럭
|
||||||
- [ ] 손료계수 없는 기계 224종 가르기 — 파서가 놓친 것(크롤러드릴(공기식) 5401 1,548·2,506 · 크레인(무한궤도) 15 등)과 원문에 없는 것이 섞임
|
- [ ] 손료계수 없는 기계 222종 되살림(미룸 · 브레인 승인 — 지금 막는 공종 0) — 가르기 끝: 원문 없음 0 · 원천 한 줄 누락 1(3201-0003 「-0003」 표기) · 뭉친 칸이지만 열마다 개수 맞고 상각+정비+관리=계 검증 통과 196(크레인(무한궤도) 15 · 골재생산(컨베이어·크러셔·스크린·빈) 55 · 해상(준설선·예선·대선 등) 66 · 그 밖 60) · 글줄로 붙음 25(3601 콘크리트 피니셔 5 · 9020 준설선 7 · 9060 토운선 6 · 7) · 원문 md 한 줄 읽기가 기존 계수 383/383 일치 · 8-4 운전경비 버려진 줄 3 = 81기종(포장 27 · 기타 36 · 기타 18) 중 공종이 쓰는 것은 물탱크 하나(되살림 끝)
|
||||||
- [ ] 초류종자살포 B09 둘 — 트럭 4.5ton 카탈로그 없음(덤프·크레인만) · 종자 규격 미정 후보 넷(화약류 모양 설계자 선택 칸 + 사유)
|
- [ ] ⚠ 조종원 직종 잠정 규칙이 8-1-2 5호와 어긋남 — 원문: 덤프트럭 12ton 이상·공기압축기(이동식 2.83㎥/min 이상) = 건설기계운전사 · 12ton 미만·살수차 = 화물차운전사 · 소형믹서·래머·콤팩터·양수기·발전기 = 일반기계운전사 ↔ 코드: 이름에 트럭 들면 화물차 · 나머지 전부 건설기계. 잼(2026-09-15): 조종원 있는 레코드 217 중 원문 또렷 맞음 167 · **틀림 12**(덤프트럭 15·20·24·32t 화물차→건설기계 4 · 콘크리트 믹서 0.10~0.45 건설기계→일반 6 · 래머 1 · 플레이트 콤팩터 1.5 1) · 원문으로 못 가름 38(콘크리트 펌프차 8 · 파일천공전용장비 6 · 트랙터(타이어) 4 · 콘크리트 믹서트럭 2 …) · **조립에 선 것 2** — 덤프트럭 15t(10-12 덤프 운반 세 갈래 · 시간당 조종원 49,778 → 59,020) · 플레이트 콤팩터 1.5(9-14-2 · 59,020 → 36,248 · 본당 노무 13,854 → 약 8,509) · 프로젝트는 덤프 운반 토사 40.44㎥ 에서 약 +1만 원 · ⭐ 거창 실무 기계경비 「덤프트럭 15Ton 건설기계운전사 · 8Ton·4.5Ton 화물차운전사 · 플레이트 콤팩터 일반기계운전사」 원문과 같음 · 앞서 적은 「공기압축기 3.5·10.3 틀림」 은 틀린 말(2.83 이상이라 건설기계 맞음)
|
||||||
- [ ] 10-7-4 모노레일 제목의 보통인부 두 번 셈(4인) — 막힌 채 사유만 적음 · 풀리는 날 운전 단가표 2인을 빼고 셀 것
|
- [ ] 10-7-4 모노레일 제목의 보통인부 두 번 셈(4인) — 막힌 채 사유만 적음 · 풀리는 날 운전 단가표 2인을 빼고 셀 것
|
||||||
- [ ] 소형 장비 1일 소모 표(㉯) 가운데 2-1-2 경유 표(집재장비) · 천공기 · 예취기 — 쓰는 공종이 풀릴 때 같은 한 벌로
|
- [ ] 소형 장비 1일 소모 표(㉯) 가운데 2-1-2 경유 표(집재장비) · 천공기 · 예취기 — 쓰는 공종이 풀릴 때 같은 한 벌로
|
||||||
|
|
||||||
### 검증완료(2026-09-14 밤 ~ 09-15 · 커밋 해시)
|
### 검증완료(2026-09-14 밤 ~ 09-15 · 커밋 해시)
|
||||||
|
- [X] 초류종자살포 5-24-1·2 둘이 섬(브레인 판정 ①②′④) — ① 트럭 4.5ton → 덤프트럭 4.5(범위 별칭 · 옆 칸 규격이 대상 규격과 같으면 덮는 것 아님) ②′ 종자살포기 → 취부기 11.94㎾ 손료만(`LOSS_ONLY_MACHINES` · 8-4 에 줄 없음 · 거창도 손료 20,295 만 · 디젤엔진은 원문 둘 다에 없어 안 넣음) ④ 종자 후보 넷 칸 · 넣은 하나만 · 둘이면 사유(`B09_Estimation_SeedSpray`) · 어긋남 사유 나란히 · 233 → 527(③) → 744원/㎡(재료 단가 전) · 다른 공종·프로젝트 내역 변화 0. 검증완료(3d896dec)
|
||||||
|
- [X] ③ 물탱크(살수차) 운전경비 되살림 — 8-4-8 셀 병합으로 줄째 버려지던 7204 다섯 원문 값(8.2·8.6·9.3·9.4·12.9ℓ · 잡재료 30 · 1인) · 조종원 8-1-2 5호 살수차 = 화물차운전사 · 거창 5,500ℓ 일치. 검증완료(078af37d)
|
||||||
- [X] ㉡-3 ㉠ 으로 막은 다섯 표 읽기(브레인 판정 ①②③) — 판정표 「시간 줄」(`hour_rows` · F0450·F0451): 장비 h 그대로 · 인력 h ÷ 8(총칙 L530) · ÷10본 · 굴착기 0.8㎥ 무한궤도·타이어 두 갈래(제안 없음) · 14-1 은 부착용 집게 7206-0070 과 조합(본체 `#조합`) · 사유(이름 다름 · 시간 단위 · 옛 값 8배) · 막힌 셋은 옛 값 까닭 사유(12-26 보통인부 1인뿐 · 10-7-4 같은 사람 두 번 셈 · 10-8-3 특별인부 1인뿐). 옛↔새(본당): 14-2 11,356 → 무한궤도 4,431 · 타이어 4,631 · 14-1 8,603 → 3,192 · 3,316 · 12-26 172,068 · 10-7-4 688,272 · 10-8-3 226,122 은 막힌 채 · 마스터 14-1·14-2 갈래 키만 · 전체 1721 + 392. 검증완료(42e8b4f0)
|
- [X] ㉡-3 ㉠ 으로 막은 다섯 표 읽기(브레인 판정 ①②③) — 판정표 「시간 줄」(`hour_rows` · F0450·F0451): 장비 h 그대로 · 인력 h ÷ 8(총칙 L530) · ÷10본 · 굴착기 0.8㎥ 무한궤도·타이어 두 갈래(제안 없음) · 14-1 은 부착용 집게 7206-0070 과 조합(본체 `#조합`) · 사유(이름 다름 · 시간 단위 · 옛 값 8배) · 막힌 셋은 옛 값 까닭 사유(12-26 보통인부 1인뿐 · 10-7-4 같은 사람 두 번 셈 · 10-8-3 특별인부 1인뿐). 옛↔새(본당): 14-2 11,356 → 무한궤도 4,431 · 타이어 4,631 · 14-1 8,603 → 3,192 · 3,316 · 12-26 172,068 · 10-7-4 688,272 · 10-8-3 226,122 은 막힌 채 · 마스터 14-1·14-2 갈래 키만 · 전체 1721 + 392. 검증완료(42e8b4f0)
|
||||||
- [X] 갈래마다 밑수가 다른 표 — `_match_two_row_table` 이 밑수로 안 나눠 8-6-1 유인헬기 160배·400배 부풂 → 갈래 「(Nha당)」 밑수 · 그 길 표 다섯 중 밑수 1 아닌 것 8-6-1 하나 · 전체에서 갈래마다 밑수 다른 표도 8-6-1 하나 · 소형 1,591,855 → 9,949원/ha · 대형 2,780,567 → 6,951. 검증완료(e15e9fc2)
|
- [X] 갈래마다 밑수가 다른 표 — `_match_two_row_table` 이 밑수로 안 나눠 8-6-1 유인헬기 160배·400배 부풂 → 갈래 「(Nha당)」 밑수 · 그 길 표 다섯 중 밑수 1 아닌 것 8-6-1 하나 · 전체에서 갈래마다 밑수 다른 표도 8-6-1 하나 · 소형 1,591,855 → 9,949원/ha · 대형 2,780,567 → 6,951. 검증완료(e15e9fc2)
|
||||||
- [X] ㉡-2 유인헬기방제 8-6-1 — `AreaSet`: 양수기 1대1일 호표(휘발유 10ℓ + 잡품 95% + 손료 칸) ÷ 1일 면적 · 깃발 20ha당 1개(칸) · 헬기 임차료(견적 칸) · 소방관서 급수 단서 · 자원 이름 「유인헬기방제 양수기·깃발」 · 소형 +227원/ha · 대형 +90 · 8-6-3 은 막힌 채 사유 둘(1일 작업량 없음 · 15ha 는 연료 설명 / 1톤 트럭 카탈로그 없음). 검증완료(9563d060)
|
- [X] ㉡-2 유인헬기방제 8-6-1 — `AreaSet`: 양수기 1대1일 호표(휘발유 10ℓ + 잡품 95% + 손료 칸) ÷ 1일 면적 · 깃발 20ha당 1개(칸) · 헬기 임차료(견적 칸) · 소방관서 급수 단서 · 자원 이름 「유인헬기방제 양수기·깃발」 · 소형 +227원/ha · 대형 +90 · 8-6-3 은 막힌 채 사유 둘(1일 작업량 없음 · 15ha 는 연료 설명 / 1톤 트럭 카탈로그 없음). 검증완료(9563d060)
|
||||||
|
|||||||
@@ -146,6 +146,22 @@
|
|||||||
"scope": "FP-10-12",
|
"scope": "FP-10-12",
|
||||||
"pum_edition": "2026-01-01",
|
"pum_edition": "2026-01-01",
|
||||||
"basis": "품셈 10-12 덤프 운반 잎이 토사·암절취·발파암으로 갈리고 [주]③ 환산계수가 「암절취 1.35」(10-11 f 파쇄암 1/1.35 와 같은 값) — 우리 리핑암(리퍼 절취) = 품셈 암절취. 옛 매핑표 ground_aliases 에서 범위를 붙여 옮김(2026-09-13). ⚠ 쓰는 곳: 덤프 운반 잎 고르기(아직 없음 — 갈래 고르기형 부모 판정 대기)"
|
"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,70 @@
|
|||||||
|
"""조종원 직종 — 건설품셈 8-1-2 5호 「운전사의 구분」 (2026-09-15 브레인 · 잰 틀림 12 · 조립에 선 것 2).
|
||||||
|
|
||||||
|
원문: 건설기계운전사 「…덤프트럭(12ton이상) … 콘크리트 믹서(0.55㎥ 이상) … 공기압축기(이동식, 2.83㎥/min 이상)」
|
||||||
|
화물차운전사 「12ton미만의 덤프트럭, 화물트럭, 살수차 …」
|
||||||
|
일반기계운전사 「소형의 공기압축기, 양수기, 소형믹서, 윈치 … 발전기, 래머, 콤팩터 …」
|
||||||
|
⚠ 옛 잠정 규칙(이름에 트럭 들면 화물차 · 그 밖 전부 건설기계) — 덤프 15t 이 화물차 · 콤팩터·래머·소형믹서가 건설기계로 섰음.
|
||||||
|
거창 실무 기계경비 「덤프트럭 15Ton 건설기계운전사 · 8Ton·4.5Ton 화물차운전사 · 플레이트 콤팩터 일반기계운전사」 = 원문.
|
||||||
|
원문이 이름으로 안 가르는 기종(콘크리트 펌프차 · 트랙터(타이어) · 콘크리트 믹서트럭 …)은 **잠정 그대로 + 사유**.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from B09_Estimation.B09_Estimation_MachineExpenseSheet import machine_expense_sheets
|
||||||
|
from B09_Estimation.B09_Estimation_MachineOperating import (
|
||||||
|
_CATALOG_SUBPATH,
|
||||||
|
_read_json,
|
||||||
|
load_operating_records,
|
||||||
|
)
|
||||||
|
from B09_Estimation.B09_Estimation_UnitPrice import cached_build
|
||||||
|
|
||||||
|
ALIASES = _read_json(*_CATALOG_SUBPATH, "labor_const_2026-01-01.json")["variables"]["aliases"]
|
||||||
|
CONST, TRUCK, GENERAL = (
|
||||||
|
ALIASES[k] for k in ("labor_op_const", "labor_op_truck", "labor_op_general")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _records():
|
||||||
|
return {r.machine_code: r for r in load_operating_records().records}
|
||||||
|
|
||||||
|
|
||||||
|
def test_원문이_가르는_직종() -> None:
|
||||||
|
records = _records()
|
||||||
|
expected = {
|
||||||
|
"0602-0150": CONST, # 덤프트럭 15t — 12ton 이상
|
||||||
|
"0602-0320": CONST,
|
||||||
|
"0602-0045": TRUCK, # 덤프트럭 4.5t — 12ton 미만
|
||||||
|
"7204-0055": TRUCK, # 살수차
|
||||||
|
"1730-0015": GENERAL, # 플레이트 콤팩터
|
||||||
|
"1630-0080": GENERAL, # 래머
|
||||||
|
"4205-0045": GENERAL, # 콘크리트 믹서 0.45㎥ — 소형믹서(0.55 미만)
|
||||||
|
"0201-0070": CONST, # 굴착기
|
||||||
|
"5205-0035": CONST, # 공기압축기(이동식) 3.5 — 2.83 이상
|
||||||
|
}
|
||||||
|
for code, occupation in expected.items():
|
||||||
|
record = records[code]
|
||||||
|
assert record.operator_occupation_code == occupation, record
|
||||||
|
assert record.operator_mapping_is_provisional is False, record
|
||||||
|
|
||||||
|
|
||||||
|
def test_원문이_안_가르는_기종은_잠정_그대로() -> None:
|
||||||
|
records = _records()
|
||||||
|
for code in ("2602-0015", "4304-0060"): # 트랙터(타이어) · 콘크리트 믹서트럭
|
||||||
|
assert records[code].operator_mapping_is_provisional is True, records[code]
|
||||||
|
assert records["2602-0015"].operator_occupation_code == CONST # 옛 규칙 그대로
|
||||||
|
assert records["4304-0060"].operator_occupation_code == TRUCK
|
||||||
|
|
||||||
|
|
||||||
|
def test_조립에_선_둘이_원문_직종으로_섬() -> None:
|
||||||
|
build = cached_build(dump_haul_m=("164.23",))
|
||||||
|
dump = {r.ref_code for r in build.book.details["X-0602-0150"]}
|
||||||
|
compactor = {r.ref_code for r in build.book.details["X-1730-0015"]}
|
||||||
|
assert f"{CONST}#시간" in dump, dump
|
||||||
|
assert f"{GENERAL}#시간" in compactor, compactor
|
||||||
|
|
||||||
|
|
||||||
|
def test_잠정_직종은_중기경비_장에_사유() -> None:
|
||||||
|
sheets = {s["machine_code"]: s for s in machine_expense_sheets(cached_build())}
|
||||||
|
assert sheets["0201-0070"]["operator_note"] == "" # 굴착기 — 원문이 가름
|
||||||
|
note = sheets["7205-0125"]["operator_note"] # 이동식 임목파쇄기 — 원문 목록에 없음
|
||||||
|
assert "잠정" in note and "8-1-2" in note, note
|
||||||
@@ -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