Merge remote-tracking branch 'origin/dev' into sub_laptop_1
This commit is contained in:
@@ -414,11 +414,11 @@ from B09_Estimation.B09_Estimation_BillOfQuantities_Rows import ( # noqa: E402
|
||||
_composite_row,
|
||||
_excluded_row,
|
||||
_leaf_row,
|
||||
_material_row,
|
||||
_structure_price_row,
|
||||
_sum_groups,
|
||||
bill_line, # noqa: F401 — 내역 줄 성분별 절사(골든셋 시험이 여기서 부름)
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_BillOfQuantities_Materials import _material_row # noqa: E402
|
||||
|
||||
|
||||
def build_bill(
|
||||
|
||||
@@ -15,7 +15,13 @@ from __future__ import annotations
|
||||
from decimal import Decimal
|
||||
from typing import Any, Callable
|
||||
|
||||
from B09_Estimation.B09_Estimation_BillOfQuantities import BillResult, BillRow
|
||||
from B09_Estimation.B09_Estimation_BillOfQuantities import (
|
||||
SUPPLY_OWNER,
|
||||
SUPPLY_UNKNOWN,
|
||||
BillResult,
|
||||
BillRow,
|
||||
HandoffMaterial,
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_PriceBook import Money3, PriceKind
|
||||
from B09_Estimation.B09_Estimation_UnitPrice import FUEL_CODE_PREFIX, UnitPriceBuild
|
||||
|
||||
@@ -24,6 +30,71 @@ DOUBLE_COUNT_SUSPECT = "double_count_suspect"
|
||||
_ZERO = Decimal(0)
|
||||
|
||||
|
||||
def _material_row(
|
||||
material: HandoffMaterial, result: BillResult, manual: dict | None = None
|
||||
) -> BillRow:
|
||||
"""자재 한 줄. 공급 구분이 안 갈렸으면 **어느 쪽에도 안 넣는다**.
|
||||
|
||||
`manual` — 「자재 단가」 수동 단가(키 「이름 규격」). 사급 줄에 값이 있으면 빠진 목록에 안 올림
|
||||
(금액은 본체 「자재(사급)」 줄이 셈 — `BillOfQuantities_Materials`).
|
||||
"""
|
||||
row = BillRow(
|
||||
item_no="",
|
||||
level=1,
|
||||
code=None,
|
||||
name=material.material_name,
|
||||
spec=material.spec,
|
||||
unit=material.unit,
|
||||
quantity=material.total_amount,
|
||||
)
|
||||
# 할증 사유는 **수량**에 닿는다 — 할증이 곱해진 뒤의 수량이기 때문이다.
|
||||
row.add_note("quantity", material.surcharge_note)
|
||||
if material.supply_type == SUPPLY_UNKNOWN:
|
||||
row.add_note(
|
||||
"", "관급·사급이 안 갈렸습니다 — 관급자재대에도, 도급 재료비에도 넣지 않습니다."
|
||||
)
|
||||
result.missing.append(
|
||||
{
|
||||
"name": material.display_name,
|
||||
"unit": material.unit,
|
||||
"quantity": str(material.total_amount),
|
||||
"reason": "공급 구분 미정(unknown)",
|
||||
}
|
||||
)
|
||||
return row
|
||||
# ⚠ **관급을 「사급」이라 적으면 안 된다** (2026-09-08 메인 창 실측 — 물구멍·야면석이
|
||||
# `owner_supplied` 인데 「사급 자재 단가 미확보」로 뜨고 있었다). 갈래마다 **가는 자리도
|
||||
# 원천도 다르다** — 관급은 총원가 밖 관급자재대(나라장터), 사급은 도급 재료비(물가지).
|
||||
if material.supply_type == SUPPLY_OWNER:
|
||||
row.add_note(
|
||||
"unit_price_krw",
|
||||
"관급 자재 단가 미확보 — 나라장터 목록에 이 품목이 없습니다. "
|
||||
"관급자재대(총원가 밖 별도 표기)로 갑니다.",
|
||||
)
|
||||
reason = "관급 자재 단가 없음"
|
||||
elif f"{material.material_name} {material.spec}".strip() in (manual or {}):
|
||||
row.add_note(
|
||||
"unit_price_krw", "⚠ 사급 자재 수동 단가(미확정) — 본체 「자재(사급)」 줄로 섬"
|
||||
)
|
||||
return row
|
||||
else:
|
||||
row.add_note(
|
||||
"unit_price_krw", "사급 자재 단가 미확보 — 「자재 단가」 탭에서 수동 입력 대기."
|
||||
)
|
||||
reason = "사급 자재 단가 없음(미결 No.18)"
|
||||
|
||||
result.missing.append(
|
||||
{
|
||||
"name": material.display_name,
|
||||
"unit": material.unit,
|
||||
"quantity": str(material.total_amount),
|
||||
"reason": reason,
|
||||
"supply_type": material.supply_type,
|
||||
}
|
||||
)
|
||||
return row
|
||||
|
||||
|
||||
def _flat(text: Any) -> str:
|
||||
return "".join(str(text or "").split())
|
||||
|
||||
|
||||
@@ -13,11 +13,8 @@ import re
|
||||
from decimal import Decimal
|
||||
|
||||
from B09_Estimation.B09_Estimation_BillOfQuantities import (
|
||||
SUPPLY_OWNER,
|
||||
SUPPLY_UNKNOWN,
|
||||
BillResult,
|
||||
BillRow,
|
||||
HandoffMaterial,
|
||||
HandoffWorkItem,
|
||||
_BLOCKED_LABELS,
|
||||
_MasterNode,
|
||||
@@ -121,6 +118,7 @@ def _composite_row(
|
||||
in_bill=item.in_bill,
|
||||
)
|
||||
missing_parts: list[str] = []
|
||||
reasons: list[str] = []
|
||||
money = None
|
||||
for part in item.composite_parts:
|
||||
code = str(part.get("code") or "")
|
||||
@@ -128,12 +126,24 @@ def _composite_row(
|
||||
if not code or amount is None or f"B-{code}" not in unit_prices.book.titles:
|
||||
missing_parts.append(code or str(part.get("name") or "이름 없음"))
|
||||
continue
|
||||
# 조각도 보통 줄과 같은 두 검사 — 일부 몫만 선 단가·밑수 모르는 표를 묶음에 더하면
|
||||
# 묶음 줄만 온전한 금액처럼 섬(2026-09-14 ㉱ 구조 결함).
|
||||
plain = code.split("#", 1)[0]
|
||||
covered = unit_prices.partial_ratio.get(plain)
|
||||
basis = unit_prices.basis_missing.get(plain)
|
||||
if covered is not None or basis:
|
||||
missing_parts.append(code)
|
||||
reasons.append(
|
||||
f"{code}: 단가 일부만 섬(붙은 몫 {covered}%)"
|
||||
if covered is not None
|
||||
else f"{code}: 밑수 미확보 — 원문 {basis}"
|
||||
)
|
||||
continue
|
||||
# 묶음도 호표 한 장 — 조각 줄 0.1원 · 성분 소계 원 미만 절사(아래 `floored`, 명세 7장).
|
||||
scaled = unit_prices.book.resolve(f"B-{code}").scaled(amount).floored(Decimal("0.1"))
|
||||
money = scaled if money is None else money + scaled
|
||||
row.parts.append((f"B-{code}", amount))
|
||||
|
||||
reasons: list[str] = []
|
||||
for pending in item.composite_not_ready:
|
||||
# 「단가 없음」과 「물량 없음」을 가른다 — 사유가 다르면 할 일도 다르다.
|
||||
if isinstance(pending, str):
|
||||
@@ -621,71 +631,6 @@ def pending_formula_note(code: str | None) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _material_row(
|
||||
material: HandoffMaterial, result: BillResult, manual: dict | None = None
|
||||
) -> BillRow:
|
||||
"""자재 한 줄. 공급 구분이 안 갈렸으면 **어느 쪽에도 안 넣는다**.
|
||||
|
||||
`manual` — 「자재 단가」 수동 단가(키 「이름 규격」). 사급 줄에 값이 있으면 빠진 목록에 안 올림
|
||||
(금액은 본체 「자재(사급)」 줄이 셈 — `BillOfQuantities_Materials`).
|
||||
"""
|
||||
row = BillRow(
|
||||
item_no="",
|
||||
level=1,
|
||||
code=None,
|
||||
name=material.material_name,
|
||||
spec=material.spec,
|
||||
unit=material.unit,
|
||||
quantity=material.total_amount,
|
||||
)
|
||||
# 할증 사유는 **수량**에 닿는다 — 할증이 곱해진 뒤의 수량이기 때문이다.
|
||||
row.add_note("quantity", material.surcharge_note)
|
||||
if material.supply_type == SUPPLY_UNKNOWN:
|
||||
row.add_note(
|
||||
"", "관급·사급이 안 갈렸습니다 — 관급자재대에도, 도급 재료비에도 넣지 않습니다."
|
||||
)
|
||||
result.missing.append(
|
||||
{
|
||||
"name": material.display_name,
|
||||
"unit": material.unit,
|
||||
"quantity": str(material.total_amount),
|
||||
"reason": "공급 구분 미정(unknown)",
|
||||
}
|
||||
)
|
||||
return row
|
||||
# ⚠ **관급을 「사급」이라 적으면 안 된다** (2026-09-08 메인 창 실측 — 물구멍·야면석이
|
||||
# `owner_supplied` 인데 「사급 자재 단가 미확보」로 뜨고 있었다). 갈래마다 **가는 자리도
|
||||
# 원천도 다르다** — 관급은 총원가 밖 관급자재대(나라장터), 사급은 도급 재료비(물가지).
|
||||
if material.supply_type == SUPPLY_OWNER:
|
||||
row.add_note(
|
||||
"unit_price_krw",
|
||||
"관급 자재 단가 미확보 — 나라장터 목록에 이 품목이 없습니다. "
|
||||
"관급자재대(총원가 밖 별도 표기)로 갑니다.",
|
||||
)
|
||||
reason = "관급 자재 단가 없음"
|
||||
elif f"{material.material_name} {material.spec}".strip() in (manual or {}):
|
||||
row.add_note(
|
||||
"unit_price_krw", "⚠ 사급 자재 수동 단가(미확정) — 본체 「자재(사급)」 줄로 섬"
|
||||
)
|
||||
return row
|
||||
else:
|
||||
row.add_note(
|
||||
"unit_price_krw", "사급 자재 단가 미확보 — 「자재 단가」 탭에서 수동 입력 대기."
|
||||
)
|
||||
reason = "사급 자재 단가 없음(미결 No.18)"
|
||||
|
||||
result.missing.append(
|
||||
{
|
||||
"name": material.display_name,
|
||||
"unit": material.unit,
|
||||
"quantity": str(material.total_amount),
|
||||
"reason": reason,
|
||||
"supply_type": material.supply_type,
|
||||
}
|
||||
)
|
||||
return row
|
||||
|
||||
|
||||
#: 같은 단위의 다른 표기 — 표기만 다르고 뜻이 같은 것을 「다르다」고 하면 멀쩡한 줄이 멈춘다.
|
||||
_UNIT_ALIASES = {
|
||||
"㎥": "m3",
|
||||
|
||||
@@ -57,6 +57,11 @@ KNOWN_GAPS: dict[str, tuple[str, str]] = {
|
||||
# 2026-09-14 ㉮ — 사용횟수 갈래로 푼 뒤 남는 원문 몫. 값을 짓지 않고 말만.
|
||||
# 봉상후렉시블 셋의 표 나머지 줄은 판정표 자동 목록(`_unread_rows`)이 맡음 — 줄이 아닌 [주] 만 여기.
|
||||
"FP-12-12": ("원문 [주]", "ⓘ [주] 「성토부 날개벽 설치시 인건비 30% 할증」 은 안 걺(선택)."),
|
||||
"FP-08-11": (
|
||||
"원문 [주]",
|
||||
"ⓘ [주]③ 장비 운반비는 별도 계상(기계 수송비 칸) · [주]④ 우드그랩은 원목 규격에 따라 별도 · [주]② 추가"
|
||||
" 인력(파쇄 후 마대담기 등)은 조사해 반영 · [주]⑥ 이 규격 외 파쇄기는 견적 — 이 일위대가엔 안 넣음.",
|
||||
),
|
||||
# 원문 대 실무 어긋남 기록(2026-09-15 브레인 규칙 — 원문이 또렷하면 원문 · 어긋남은 늘 기록).
|
||||
"FP-09-15-02": (
|
||||
"실무 어긋남",
|
||||
|
||||
@@ -295,6 +295,14 @@ def _add_machine_layers(
|
||||
"""
|
||||
catalog = load_machine_catalog()
|
||||
operating = {r.machine_code: r for r in load_operating_records().records}
|
||||
# 8-4 칸이 「-」 라 레코드가 없는 이동식 임목파쇄기 93.25 — 8-11 [주]⑤·비고가 유일한 값(2026-09-15).
|
||||
from B09_Estimation.B09_Estimation_WoodChipping import MACHINE as CHIPPER
|
||||
from B09_Estimation.B09_Estimation_WoodChipping import operating_record as chipper_record
|
||||
|
||||
if CHIPPER in machine_codes and CHIPPER not in operating:
|
||||
chipper = chipper_record()
|
||||
if chipper is not None:
|
||||
operating[CHIPPER] = chipper
|
||||
wages = load_operator_wages()
|
||||
incomplete: list[str] = []
|
||||
|
||||
@@ -796,6 +804,9 @@ def build_unit_prices(
|
||||
|
||||
if dump_haul_m:
|
||||
machine_codes.add(DUMP_TRUCK_CODE)
|
||||
from B09_Estimation.B09_Estimation_WoodChipping import MACHINE as CHIPPER
|
||||
|
||||
machine_codes.add(CHIPPER) # 8-11 표가 자원 줄로 안 읽혀(Q 칸) 공종 모듈이 부름
|
||||
build.operator_wage_digits = operator_wage_digits
|
||||
build.incomplete_machines = _add_machine_layers(
|
||||
build.book, machine_codes, fuel_region, operator_wage_digits
|
||||
@@ -1099,6 +1110,10 @@ def build_unit_prices(
|
||||
from B09_Estimation.B09_Estimation_TopsoilRemoval import attach_topsoil_removal
|
||||
|
||||
attach_topsoil_removal(build, nodes_by_code)
|
||||
# 이동식 임목 파쇄(8-11) — 파쇄기 1/Q · 보통인부 2인/8h/Q · 파쇄기날(2026-09-15 ㉰).
|
||||
from B09_Estimation.B09_Estimation_WoodChipping import attach_wood_chipping
|
||||
|
||||
attach_wood_chipping(build, nodes_by_code)
|
||||
# ⚠ **마지막에 한 번** — 조합 사용 공종의 본체 기계를 잡재료 16% 층으로 바꿔 단다
|
||||
# (품셈 제8장 [주]⑤). 조립 도중에 바꾸면 어느 공종이 조합인지 아직 모른다.
|
||||
build.combined_swapped = _apply_combined_misc_rate(
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
"""B09 원가계산 — **이동식 임목 파쇄** 8-11 (2026-09-15 브레인 판정 · ㉰ 둘째).
|
||||
|
||||
원문 L4651 표: 이동식 임목 파쇄기 93.25KW · Q = 3.5 ㎥/hr · 비고 「잡재료 : 주연료비의 16% · 소모품비(파쇄기날)
|
||||
0.00125개/hr」 · [주]① 1일 8시간 ② 1일 기계운전자 1인·보통인부 2인 ③ 장비 운반비 별도 ④ 우드그랩 별도
|
||||
⑤ 연료 「10.8 + 16.3ℓ / 2 = 13.5ℓ (디젤)」.
|
||||
|
||||
⚠ 건설품셈 운전경비표(8-4 L3458) 7205-0125 93.25㎾ 줄은 연료·잡품 칸이 「-」 → 운전경비 레코드가 없어
|
||||
기계 층이 안 서고 일위대가도 조용히 없었음. 8-11 [주]⑤·비고가 **유일한 값**(부딪히는 원문 없음)이라 그대로 씀.
|
||||
기계운전자는 기계 층 조종원(건설기계운전사 · 8-4 잠정 규칙과 같은 직종)으로 한 번만 셈.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
CODE = "FP-08-11"
|
||||
MACHINE = "7205-0125"
|
||||
BLADE = "AR-M-249d0a01"
|
||||
TABLE = "F0237"
|
||||
#: [주]⑤ — 표 칸이 아니라 [주] 에만 있어 마스터 원문 줄에 안 실림. 원문 그대로 한 곳.
|
||||
FUEL_LITERS_PER_HOUR = Decimal("13.5")
|
||||
#: [주]① 1일 8시간 · [주]② 1일 보통인부 2인(기계운전자 1인은 기계 층 조종원).
|
||||
HOURS_PER_DAY = Decimal(8)
|
||||
LABORERS_PER_DAY = Decimal(2)
|
||||
LABORER_CODE = "1002" # 보통인부
|
||||
BLADE_MISSING = "파쇄기날 — 규격·단가 없음 · 「자재 단가」 탭에서 넣으면 0.00125개/hr ÷ Q 로 붙음"
|
||||
|
||||
_RE_Q = re.compile(r"Q\s*=\s*(\d+(?:\.\d+)?)")
|
||||
_RE_MISC = re.compile(r"주연료비의\s*(\d+(?:\.\d+)?)\s*%")
|
||||
_RE_BLADE = re.compile(r"(\d+(?:\.\d+)?)\s*개\s*/\s*hr")
|
||||
|
||||
|
||||
def _table_values(node: dict[str, Any]) -> tuple[Decimal, Decimal, Decimal] | None:
|
||||
"""(Q ㎥/hr, 잡재료 %, 파쇄기날 개/hr) — 표 한 줄에서. 칸이 달라지면 `None`."""
|
||||
table = next((t for t in node.get("tables") or [] if t.get("pum_table_id") == TABLE), {})
|
||||
text = " ".join(str(c) for row in table.get("raw_row") or [] for c in row)
|
||||
found = [pattern.search(text) for pattern in (_RE_Q, _RE_MISC, _RE_BLADE)]
|
||||
if not all(found):
|
||||
return None
|
||||
return tuple(Decimal(m.group(1)) for m in found) # type: ignore[return-value]
|
||||
|
||||
|
||||
def operating_record(node: dict[str, Any] | None = None):
|
||||
"""7205-0125 운전경비 — 8-11 [주]⑤ 연료 · 비고 잡재료 · [주]② 기계운전자 1인."""
|
||||
from B09_Estimation.B09_Estimation_MachineOperating import (
|
||||
_CATALOG_SUBPATH,
|
||||
OperatingRecord,
|
||||
_operator_code,
|
||||
_read_json,
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_ResourceAxis import load_work_item_master
|
||||
|
||||
if node is None:
|
||||
node = next(n for n in load_work_item_master()["work_items"] if n["work_item_code"] == CODE)
|
||||
values = _table_values(node)
|
||||
if values is None:
|
||||
return None
|
||||
aliases = _read_json(*_CATALOG_SUBPATH, "labor_const_2026-01-01.json")["variables"]["aliases"]
|
||||
return OperatingRecord(
|
||||
machine_code=MACHINE,
|
||||
machine_name="이동식 임목파쇄기",
|
||||
specification="93.25",
|
||||
fuel_liters_per_hour=FUEL_LITERS_PER_HOUR,
|
||||
fuel_kind="경유",
|
||||
misc_material_percent=values[1],
|
||||
operator_person_days=Decimal(1),
|
||||
operator_occupation_code=_operator_code("이동식 임목파쇄기", aliases),
|
||||
)
|
||||
|
||||
|
||||
def attach_wood_chipping(build: Any, nodes_by_code: dict[str, dict[str, Any]]) -> None:
|
||||
"""`B-FP-08-11` ㎥당 — 파쇄기 1/Q(D) · 보통인부 2인 ÷ 8시간 ÷ Q · 파쇄기날(단가가 들면)."""
|
||||
from B09_Estimation.B09_Estimation_PriceBook import PriceDetail, PriceKind, PriceTitle
|
||||
|
||||
node = nodes_by_code.get(CODE)
|
||||
values = _table_values(node or {})
|
||||
book = build.book
|
||||
uses = build.material_uses.setdefault(BLADE, [])
|
||||
if CODE not in uses:
|
||||
uses.append(CODE)
|
||||
if node is None or values is None or f"X-{MACHINE}" not in book.titles:
|
||||
build.component_gaps[CODE] = "8-11 표 칸이 달라졌거나 파쇄기 기계 층이 안 섬"
|
||||
return
|
||||
q, _misc, blade_per_hour = values
|
||||
title = f"B-{CODE}"
|
||||
book.add_title(
|
||||
PriceTitle(code=title, kind=PriceKind.UNIT_PRICE, name=str(node.get("name")), unit="㎥")
|
||||
)
|
||||
book.add_output_detail(
|
||||
title, f"X-{MACHINE}", Decimal(1) / q, f"Q = {q} ㎥/hr (산림품셈 8-11)", output=q
|
||||
)
|
||||
book.add_detail(
|
||||
PriceDetail(
|
||||
title,
|
||||
LABORER_CODE,
|
||||
LABORERS_PER_DAY / HOURS_PER_DAY / q,
|
||||
note=f"[주]② 1일 보통인부 {LABORERS_PER_DAY}인 ÷ [주]① {HOURS_PER_DAY}시간 ÷ Q {q}",
|
||||
)
|
||||
)
|
||||
labels = [label for label in build.unattached.get(CODE, []) if "파쇄기날" not in label]
|
||||
if BLADE in book.titles:
|
||||
book.add_detail(
|
||||
PriceDetail(
|
||||
title, BLADE, blade_per_hour / q, note=f"파쇄기날 {blade_per_hour}개/hr ÷ Q"
|
||||
)
|
||||
)
|
||||
else:
|
||||
labels.append(BLADE_MISSING)
|
||||
build.unattached[CODE] = labels
|
||||
if CODE in build.skipped:
|
||||
build.skipped.remove(CODE)
|
||||
@@ -381,6 +381,19 @@
|
||||
"F0393"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "AR-M-249d0a01",
|
||||
"kind": "material",
|
||||
"name": "파쇄기날",
|
||||
"spec": "이동식 임목파쇄기용",
|
||||
"unit": "개",
|
||||
"source": {
|
||||
"pum_edition": "2026-01-01",
|
||||
"pum_table_ids": [
|
||||
"F0237"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""조립(묶음) 줄이 조각의 「일부만」·「밑수 없음」 을 올림 — 2026-09-14 브레인 ㉱ 첫째(구조 결함).
|
||||
|
||||
종전 `_composite_row` 는 조각 제목이 **있기만 하면** 금액에 더했음 → 조각 단가가 일부 몫만 섰거나(인력만)
|
||||
밑수를 모르는 표여도 조립 줄이 **온전한 금액처럼** 섰음. 보통 줄은 그 둘을 막는데 조립 줄만 새던 자리.
|
||||
⇒ 조각마다 보통 줄과 같은 두 검사 — 걸리면 금액을 안 세우고 어느 조각이 왜인지 사유.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from B09_Estimation.B09_Estimation_BillOfQuantities import build_bill
|
||||
from B09_Estimation.B09_Estimation_UnitPrice import cached_build
|
||||
|
||||
|
||||
def _composite(parts: list[dict]) -> dict:
|
||||
return {
|
||||
"work_items": [
|
||||
{
|
||||
"work_item_code": None,
|
||||
"name": "시험 묶음",
|
||||
"spec": "",
|
||||
"unit": "개소",
|
||||
"quantity": "1",
|
||||
"in_bill": True,
|
||||
"composite_parts": parts,
|
||||
"composite_not_ready": [],
|
||||
}
|
||||
],
|
||||
"materials": [],
|
||||
}
|
||||
|
||||
|
||||
def _line(parts: list[dict]):
|
||||
rows = build_bill(_composite(parts)).rows
|
||||
return next(r for r in rows if r.code is None and r.name == "시험 묶음")
|
||||
|
||||
|
||||
def test_일부만_선_조각이_있으면_묶음_금액을_안_세우고_까닭() -> None:
|
||||
build = cached_build()
|
||||
partial = next(c for c in build.partial_ratio if f"B-{c}" in build.book.titles)
|
||||
line = _line([{"code": partial, "quantity": 1}])
|
||||
assert line.amount_krw is None, (partial, line.amount_krw)
|
||||
assert partial in line.note and "일부만" in line.note, line.note
|
||||
|
||||
|
||||
def test_밑수_없는_조각도_같이_막음() -> None:
|
||||
build = cached_build()
|
||||
missing = next(c for c in build.basis_missing if f"B-{c}" in build.book.titles)
|
||||
line = _line([{"code": missing, "quantity": 1}])
|
||||
assert line.amount_krw is None and "밑수" in line.note, (missing, line.note)
|
||||
|
||||
|
||||
def test_온전한_조각만이면_종전대로_금액() -> None:
|
||||
line = _line([{"code": "FP-09-15-02", "quantity": 2}])
|
||||
assert line.amount_krw and line.amount_krw > 0, line.note
|
||||
@@ -0,0 +1,63 @@
|
||||
"""이동식 임목 파쇄 8-11 — 2026-09-15 브레인 판정(㉰ 둘째).
|
||||
|
||||
원문 L4651: 이동식 임목 파쇄기 93.25KW · Q = 3.5 ㎥/hr · 비고 「잡재료 : 주연료비의 16% · 소모품비(파쇄기날) 0.00125개/hr」
|
||||
[주]① 1일 8시간 ② 1일 기계운전자 1인 · 보통인부 2인 ③ 장비 운반비 별도 ④ 우드그랩 별도 ⑤ 연료 13.5ℓ(디젤)
|
||||
건설품셈 운전경비표(8-4 L3458) 7205-0125 93.25㎾ 줄은 연료·잡품 칸이 「-」 → 우리 운전경비 레코드가 없어
|
||||
기계 층이 안 서고 일위대가도 조용히 없었음. 8-11 [주]⑤·비고가 **유일한 값**이라 그대로 씀(판정할 것 없음).
|
||||
파쇄기날은 규격·단가가 없어 「자재 단가」 칸 + 사유(화약류·치즐과 같은 통로).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from B09_Estimation.B09_Estimation_UnitPrice import cached_build
|
||||
|
||||
BLADE = "AR-M-249d0a01"
|
||||
Q = Decimal("3.5")
|
||||
|
||||
|
||||
def _rows(book, code: str) -> list:
|
||||
own = book.details.get(code) or []
|
||||
return [
|
||||
*own,
|
||||
*(r for d in own if d.ref_code.startswith("D-") for r in book.details[d.ref_code]),
|
||||
]
|
||||
|
||||
|
||||
def test_파쇄기_기계_층이_8_11_주5_연료와_잡재료_16퍼센트로_섬() -> None:
|
||||
book = cached_build().book
|
||||
rows = {r.ref_code: r for r in book.details["X-7205-0125"]}
|
||||
assert rows["M-FUEL-경유"].quantity == Decimal("13.5"), rows
|
||||
misc = next(r for r in book.details["X-7205-0125"] if r.percent_of_material is not None)
|
||||
assert misc.percent_of_material == Decimal(16)
|
||||
assert any(code.endswith("#시간") for code in rows), rows # 기계운전자 1인(조종원 시간당)
|
||||
|
||||
|
||||
def test_파쇄_일위대가는_기계_1_Q_와_보통인부_2인_8시간() -> None:
|
||||
build = cached_build()
|
||||
book = build.book
|
||||
assert book.titles["B-FP-08-11"].unit == "㎥"
|
||||
rows = _rows(book, "B-FP-08-11")
|
||||
machine = next(r for r in rows if r.ref_code == "X-7205-0125")
|
||||
assert machine.quantity == Decimal(1) / Q and machine.output == Q, machine
|
||||
labor = next(r for r in rows if r.ref_code == "1002")
|
||||
assert labor.quantity == Decimal(2) / Decimal(8) / Q, labor
|
||||
assert not any(r.ref_code.startswith("1048") for r in rows) # 운전자는 기계 층에 — 두 번 안 셈
|
||||
|
||||
|
||||
def test_파쇄기날은_단가가_없으면_사유_있으면_시간당_수량_나누기_Q() -> None:
|
||||
build = cached_build()
|
||||
assert "FP-08-11" in build.material_uses.get(BLADE, [])
|
||||
assert any("파쇄기날" in label for label in build.unattached.get("FP-08-11", []))
|
||||
priced = cached_build(material_prices=((BLADE, "150000", "견적"),))
|
||||
blade = next(r for r in _rows(priced.book, "B-FP-08-11") if r.ref_code == BLADE)
|
||||
assert blade.quantity == Decimal("0.00125") / Q, blade
|
||||
assert not any("파쇄기날" in label for label in priced.unattached.get("FP-08-11", []))
|
||||
|
||||
|
||||
def test_원문_주의_별도_계상은_사유로() -> None:
|
||||
from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note
|
||||
|
||||
note = known_gap_note("FP-08-11")
|
||||
assert "운반비" in note and "우드그랩" in note, note
|
||||
Reference in New Issue
Block a user