Compare commits
8
Commits
sub_laptop_1
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d54d52b6eb | ||
|
|
9f1adeab49 | ||
|
|
05e88998c1 | ||
|
|
a3950a189b | ||
|
|
387af3d888 | ||
|
|
42e8b4f0a9 | ||
|
|
d64becb12c | ||
|
|
e0c5e36433 |
@@ -45,28 +45,30 @@ from B09_Estimation.B09_Estimation_UnitPrice import (
|
|||||||
|
|
||||||
_ZERO = Decimal(0)
|
_ZERO = Decimal(0)
|
||||||
|
|
||||||
|
# ⬇ 인계 읽기는 `_Input` 으로 옮겼다(2026-09-15 700줄 분리) — 쓰던 이름이 그대로 살게 다시 내보낸다.
|
||||||
|
from B09_Estimation.B09_Estimation_BillOfQuantities_Input import ( # noqa: E402,F401
|
||||||
|
BLOCKED_UNCONFIRMED,
|
||||||
|
SUPPLY_OWNER,
|
||||||
|
SUPPLY_UNKNOWN,
|
||||||
|
BillError,
|
||||||
|
HandoffMaterial,
|
||||||
|
HandoffWorkItem,
|
||||||
|
_decimal,
|
||||||
|
parse_handoff,
|
||||||
|
)
|
||||||
|
|
||||||
#: 총 절취량으로 셀 공종 — 9-3 토사깎기 · 9-4 암절취 · 9-5 발파암(인계 대응표 「흙깎기」 셋).
|
#: 총 절취량으로 셀 공종 — 9-3 토사깎기 · 9-4 암절취 · 9-5 발파암(인계 대응표 「흙깎기」 셋).
|
||||||
#: ⚠ 판에 묶인 절 번호임(명세 17장) — 판이 바뀌면 대응표와 함께 고칠 것.
|
#: ⚠ 판에 묶인 절 번호임(명세 17장) — 판이 바뀌면 대응표와 함께 고칠 것.
|
||||||
CUT_CODE_PREFIXES = ("FP-09-03", "FP-09-04", "FP-09-05")
|
CUT_CODE_PREFIXES = ("FP-09-03", "FP-09-04", "FP-09-05")
|
||||||
#: 구조물도 양식 호표 코드 머리(명세 2장 자체 확장) — 금액은 B08 일위대가 엔진을 B09 단가표로 셈.
|
#: 구조물도 양식 호표 코드 머리(명세 2장 자체 확장) — 금액은 B08 일위대가 엔진을 B09 단가표로 셈.
|
||||||
STRUCTURE_PRICE_PREFIX = "AX-ST-"
|
STRUCTURE_PRICE_PREFIX = "AX-ST-"
|
||||||
|
|
||||||
#: 자재 공급 구분이 안 갈린 값. B08 이 실제로 이 값을 보낸다(2026-09-08 실물 확인).
|
|
||||||
#: **관급자재대에도, 도급 재료비에도 넣지 않는다** — 어느 쪽에 넣어도 총액이 틀린다.
|
|
||||||
SUPPLY_UNKNOWN = "unknown"
|
|
||||||
|
|
||||||
#: 관급 — 총원가 밖 별도 표기라 사급과 **가는 자리가 다르다**(PLAN 8-2).
|
|
||||||
SUPPLY_OWNER = "owner_supplied"
|
|
||||||
|
|
||||||
#: 막힘 갈래를 사람 말로. **할 일이 다르므로 화면에서 갈라 보인다.**
|
#: 막힘 갈래를 사람 말로. **할 일이 다르므로 화면에서 갈라 보인다.**
|
||||||
#: ⚠ `blocked_kind` 가 **`None` 인데 `in_bill=False`** 면 **막힌 줄이 아니다** —
|
#: ⚠ `blocked_kind` 가 **`None` 인데 `in_bill=False`** 면 **막힌 줄이 아니다** —
|
||||||
#: 「다른 표에서 이미 섬」(벌목·지장목제거)이거나 「이 노선엔 없음」(사방 시설)이다.
|
#: 「다른 표에서 이미 섬」(벌목·지장목제거)이거나 「이 노선엔 없음」(사방 시설)이다.
|
||||||
#: 그것을 「우리가 만들어야 하는 것」에 얹으면 **결국 이중계상으로 간다**(㉠~㉦ 규칙).
|
#: 그것을 「우리가 만들어야 하는 것」에 얹으면 **결국 이중계상으로 간다**(㉠~㉦ 규칙).
|
||||||
_NOT_OUR_ROW = "not_our_row"
|
_NOT_OUR_ROW = "not_our_row"
|
||||||
|
|
||||||
#: B08 `BLOCKED_UNCONFIRMED` 와 같은 글 — 치수 없이 기본값으로 선 줄.
|
|
||||||
BLOCKED_UNCONFIRMED = "unconfirmed"
|
|
||||||
|
|
||||||
_BLOCKED_LABELS = {
|
_BLOCKED_LABELS = {
|
||||||
_NOT_OUR_ROW: "여기서 세지 않는 줄",
|
_NOT_OUR_ROW: "여기서 세지 않는 줄",
|
||||||
BLOCKED_UNCONFIRMED: "미확정 — 금액에 안 들어감",
|
BLOCKED_UNCONFIRMED: "미확정 — 금액에 안 들어감",
|
||||||
@@ -76,84 +78,6 @@ _BLOCKED_LABELS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class BillError(ValueError):
|
|
||||||
"""내역서를 세울 수 없는 경우. 빈 표를 돌려주지 않고 멈춘다."""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class HandoffWorkItem:
|
|
||||||
"""B08 인계 공종 한 줄. **수량은 B08 것이 정본이다** — 여기서 다시 세지 않는다."""
|
|
||||||
|
|
||||||
work_item_code: str | None
|
|
||||||
name: str
|
|
||||||
spec: str
|
|
||||||
unit: str
|
|
||||||
quantity: Decimal
|
|
||||||
in_bill: bool
|
|
||||||
#: 운반 줄에만 있다 — 거리(m)와 수단. 무대(20 m 이내)는 `free_haul` 로 온다.
|
|
||||||
haul_distance_m: Decimal | None = None
|
|
||||||
haul_equipment: str | None = None
|
|
||||||
in_bill_reason: str = ""
|
|
||||||
origin: str = ""
|
|
||||||
ground_class: str = ""
|
|
||||||
#: 반영률(%) — B08 이 이미 곱했으면 산출근거에만 적고 **여기서 또 곱하지 않는다**.
|
|
||||||
application_ratio_pct: Decimal | None = None
|
|
||||||
#: 반영률 적용 **전** 수량. 산출근거에만 쓴다.
|
|
||||||
quantity_gross: Decimal | None = None
|
|
||||||
#: 성·절토처럼 율이 갈리는 경우의 몫별 율·수량 — 문장 파싱 없이 그대로 그린다.
|
|
||||||
application_ratio_breakdown: dict | None = None
|
|
||||||
quantity_breakdown: dict | None = None
|
|
||||||
#: 묶음 줄(옹벽처럼 품셈에 그 공종이 없는 것) — 무엇으로 이루어지는지.
|
|
||||||
composite_parts: tuple = ()
|
|
||||||
#: 묶음인데 아직 못 채운 조각 — 「단가 없음」과 「물량 없음」을 갈라 적는다.
|
|
||||||
composite_not_ready: tuple = ()
|
|
||||||
structure_kind: str = ""
|
|
||||||
#: B08 이 적어 보낸 막힘 사유 — **문구는 B08 것을 그대로 쓴다**(두 벌로 짜지 않는다).
|
|
||||||
blocked_reason: str = ""
|
|
||||||
#: 막힘 갈래 — `input_missing`(사용자가 입력하면 풀림) /
|
|
||||||
#: `unit_data_missing`·`formula_missing`(우리가 만들어야 함). 할 일이 다르므로 가른다.
|
|
||||||
blocked_kind: str = ""
|
|
||||||
#: 갈래 축·원본값 — 「stone_cm」·「60~80」. **키 문자열은 우리가 만든다**(두 창 합의).
|
|
||||||
variant_axis: str = ""
|
|
||||||
variant_value: str = ""
|
|
||||||
#: 규격 갈래를 B08 이 판정해 보낸 것(「철근구조물」)과 그 근거 문구.
|
|
||||||
#: **근거는 산출근거 칸에 그대로 적는다** — 우리가 다시 지어내지 않는다.
|
|
||||||
spec_class: str = ""
|
|
||||||
spec_class_basis: str = ""
|
|
||||||
#: 이 줄의 공종 코드가 본 품셈 판 — 마스터 판과 다르면 값을 안 씀(명세 17장).
|
|
||||||
pum_edition: str = ""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def display_name(self) -> str:
|
|
||||||
return f"{self.name} {self.spec}".strip()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def unconfirmed(self) -> bool:
|
|
||||||
"""B08 이 치수 없이 기본값으로 세운 줄(2026-09-14 브레인 판정) — 내역 제자리에
|
|
||||||
**빈 금액 + 빨간 테두리**로 서고 합계에 안 듦 · 머리에 「미확정 N건 — 금액에 안 들어감」."""
|
|
||||||
return self.blocked_kind == BLOCKED_UNCONFIRMED
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class HandoffMaterial:
|
|
||||||
"""B08 인계 자재 한 줄. `work_item_code` 칸이 **아예 없는** 별도 벌이다(계약 확정)."""
|
|
||||||
|
|
||||||
material_name: str
|
|
||||||
spec: str
|
|
||||||
unit: str
|
|
||||||
net_amount: Decimal
|
|
||||||
total_amount: Decimal
|
|
||||||
supply_type: str
|
|
||||||
surcharge_pct: Decimal | None = None
|
|
||||||
surcharge_note: str = ""
|
|
||||||
install_by: str | None = None
|
|
||||||
source_structure: tuple[str, ...] = ()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def display_name(self) -> str:
|
|
||||||
return f"{self.material_name} {self.spec}".strip()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BillRow:
|
class BillRow:
|
||||||
"""내역서 한 줄. 머리(그룹)줄은 `is_group=True` 이고 수량·단가가 없다."""
|
"""내역서 한 줄. 머리(그룹)줄은 `is_group=True` 이고 수량·단가가 없다."""
|
||||||
@@ -308,66 +232,6 @@ class BillResult:
|
|||||||
return sum((r.amount_krw or _ZERO for r in self.rows if not r.is_group), _ZERO)
|
return sum((r.amount_krw or _ZERO for r in self.rows if not r.is_group), _ZERO)
|
||||||
|
|
||||||
|
|
||||||
def _decimal(value: Any, default: Decimal | None = _ZERO) -> Decimal | None:
|
|
||||||
if value is None or value == "":
|
|
||||||
return default
|
|
||||||
return Decimal(str(value))
|
|
||||||
|
|
||||||
|
|
||||||
def parse_handoff(payload: dict[str, Any]) -> tuple[list[HandoffWorkItem], list[HandoffMaterial]]:
|
|
||||||
"""인계 응답을 우리 자료형으로 옮긴다. **모르는 칸을 채우지 않는다.**"""
|
|
||||||
if "work_items" not in payload or "materials" not in payload:
|
|
||||||
raise BillError("인계 응답에 `work_items`·`materials` 두 벌이 다 있어야 합니다.")
|
|
||||||
|
|
||||||
work_items = [
|
|
||||||
HandoffWorkItem(
|
|
||||||
work_item_code=row.get("work_item_code"),
|
|
||||||
name=row.get("name", ""),
|
|
||||||
spec=row.get("spec") or "",
|
|
||||||
unit=row.get("unit") or "",
|
|
||||||
quantity=_decimal(row.get("quantity")) or _ZERO,
|
|
||||||
in_bill=bool(row.get("in_bill", True)),
|
|
||||||
in_bill_reason=row.get("in_bill_reason") or "",
|
|
||||||
origin=row.get("origin") or "",
|
|
||||||
ground_class=row.get("ground_class") or "",
|
|
||||||
haul_distance_m=_decimal(row.get("haul_distance_m"), None),
|
|
||||||
haul_equipment=row.get("haul_equipment"),
|
|
||||||
# ⚠ 있으면 **적기만** 한다 — 곱하기는 B08 한 곳에서만(2026-09-08 이견 ①).
|
|
||||||
application_ratio_pct=_decimal(row.get("application_ratio_pct"), None),
|
|
||||||
quantity_gross=_decimal(row.get("quantity_gross"), None),
|
|
||||||
application_ratio_breakdown=row.get("application_ratio_breakdown"),
|
|
||||||
quantity_breakdown=row.get("quantity_breakdown"),
|
|
||||||
composite_parts=tuple(row.get("composite_parts") or ()),
|
|
||||||
composite_not_ready=tuple(row.get("composite_not_ready") or ()),
|
|
||||||
structure_kind=row.get("structure_kind") or "",
|
|
||||||
blocked_reason=row.get("blocked_reason") or "",
|
|
||||||
blocked_kind=row.get("blocked_kind") or "",
|
|
||||||
variant_axis=row.get("variant_axis") or "",
|
|
||||||
variant_value=str(row.get("variant_value") or ""),
|
|
||||||
spec_class=row.get("spec_class") or "",
|
|
||||||
spec_class_basis=row.get("spec_class_basis") or "",
|
|
||||||
pum_edition=str(row.get("pum_edition") or ""),
|
|
||||||
)
|
|
||||||
for row in payload["work_items"]
|
|
||||||
]
|
|
||||||
materials = [
|
|
||||||
HandoffMaterial(
|
|
||||||
material_name=row.get("material_name", ""),
|
|
||||||
spec=row.get("spec") or "",
|
|
||||||
unit=row.get("unit") or "",
|
|
||||||
net_amount=_decimal(row.get("net_amount")) or _ZERO,
|
|
||||||
total_amount=_decimal(row.get("total_amount")) or _ZERO,
|
|
||||||
supply_type=row.get("supply_type") or SUPPLY_UNKNOWN,
|
|
||||||
surcharge_pct=_decimal(row.get("surcharge_pct"), None),
|
|
||||||
surcharge_note=row.get("surcharge_note") or "",
|
|
||||||
install_by=row.get("install_by"),
|
|
||||||
source_structure=tuple(row.get("source_structure") or ()),
|
|
||||||
)
|
|
||||||
for row in payload["materials"]
|
|
||||||
]
|
|
||||||
return work_items, materials
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class _MasterNode:
|
class _MasterNode:
|
||||||
code: str
|
code: str
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
"""B09 원가계산 — ④ 예산내역서 **인계 읽기**(자료형 · 파싱).
|
||||||
|
|
||||||
|
`B09_Estimation_BillOfQuantities` 가 700줄에 닿아 **그대로 옮겨 온 자리**다
|
||||||
|
(2026-09-15 · 순수 분리 — 글자 한 자 안 고쳤다). 조판·계층은 그쪽에 그대로 있다.
|
||||||
|
|
||||||
|
⚠ **모르는 칸을 채우지 않는다** — B08 이 안 보낸 값은 `None`·빈 문자열로 남긴다.
|
||||||
|
⚠ 쓰던 자리가 안 바뀌도록 본 모듈이 다시 내보낸다 —
|
||||||
|
`from ...BillOfQuantities import HandoffWorkItem` 가 그대로 돈다.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
_ZERO = Decimal(0)
|
||||||
|
|
||||||
|
#: 자재 공급 구분이 안 갈린 값. B08 이 실제로 이 값을 보낸다(2026-09-08 실물 확인).
|
||||||
|
#: **관급자재대에도, 도급 재료비에도 넣지 않는다** — 어느 쪽에 넣어도 총액이 틀린다.
|
||||||
|
SUPPLY_UNKNOWN = "unknown"
|
||||||
|
|
||||||
|
#: 관급 — 총원가 밖 별도 표기라 사급과 **가는 자리가 다르다**(PLAN 8-2).
|
||||||
|
SUPPLY_OWNER = "owner_supplied"
|
||||||
|
|
||||||
|
#: B08 `BLOCKED_UNCONFIRMED` 와 같은 글 — 치수 없이 기본값으로 선 줄.
|
||||||
|
BLOCKED_UNCONFIRMED = "unconfirmed"
|
||||||
|
|
||||||
|
|
||||||
|
class BillError(ValueError):
|
||||||
|
"""내역서를 세울 수 없는 경우. 빈 표를 돌려주지 않고 멈춘다."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class HandoffWorkItem:
|
||||||
|
"""B08 인계 공종 한 줄. **수량은 B08 것이 정본이다** — 여기서 다시 세지 않는다."""
|
||||||
|
|
||||||
|
work_item_code: str | None
|
||||||
|
name: str
|
||||||
|
spec: str
|
||||||
|
unit: str
|
||||||
|
quantity: Decimal
|
||||||
|
in_bill: bool
|
||||||
|
#: 운반 줄에만 있다 — 거리(m)와 수단. 무대(20 m 이내)는 `free_haul` 로 온다.
|
||||||
|
haul_distance_m: Decimal | None = None
|
||||||
|
haul_equipment: str | None = None
|
||||||
|
in_bill_reason: str = ""
|
||||||
|
origin: str = ""
|
||||||
|
ground_class: str = ""
|
||||||
|
#: 반영률(%) — B08 이 이미 곱했으면 산출근거에만 적고 **여기서 또 곱하지 않는다**.
|
||||||
|
application_ratio_pct: Decimal | None = None
|
||||||
|
#: 반영률 적용 **전** 수량. 산출근거에만 쓴다.
|
||||||
|
quantity_gross: Decimal | None = None
|
||||||
|
#: 성·절토처럼 율이 갈리는 경우의 몫별 율·수량 — 문장 파싱 없이 그대로 그린다.
|
||||||
|
application_ratio_breakdown: dict | None = None
|
||||||
|
quantity_breakdown: dict | None = None
|
||||||
|
#: 묶음 줄(옹벽처럼 품셈에 그 공종이 없는 것) — 무엇으로 이루어지는지.
|
||||||
|
composite_parts: tuple = ()
|
||||||
|
#: 묶음인데 아직 못 채운 조각 — 「단가 없음」과 「물량 없음」을 갈라 적는다.
|
||||||
|
composite_not_ready: tuple = ()
|
||||||
|
structure_kind: str = ""
|
||||||
|
#: B08 이 적어 보낸 막힘 사유 — **문구는 B08 것을 그대로 쓴다**(두 벌로 짜지 않는다).
|
||||||
|
blocked_reason: str = ""
|
||||||
|
#: 막힘 갈래 — `input_missing`(사용자가 입력하면 풀림) /
|
||||||
|
#: `unit_data_missing`·`formula_missing`(우리가 만들어야 함). 할 일이 다르므로 가른다.
|
||||||
|
blocked_kind: str = ""
|
||||||
|
#: 갈래 축·원본값 — 「stone_cm」·「60~80」. **키 문자열은 우리가 만든다**(두 창 합의).
|
||||||
|
variant_axis: str = ""
|
||||||
|
variant_value: str = ""
|
||||||
|
#: 규격 갈래를 B08 이 판정해 보낸 것(「철근구조물」)과 그 근거 문구.
|
||||||
|
#: **근거는 산출근거 칸에 그대로 적는다** — 우리가 다시 지어내지 않는다.
|
||||||
|
spec_class: str = ""
|
||||||
|
spec_class_basis: str = ""
|
||||||
|
#: 이 줄의 공종 코드가 본 품셈 판 — 마스터 판과 다르면 값을 안 씀(명세 17장).
|
||||||
|
pum_edition: str = ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_name(self) -> str:
|
||||||
|
return f"{self.name} {self.spec}".strip()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unconfirmed(self) -> bool:
|
||||||
|
"""B08 이 치수 없이 기본값으로 세운 줄(2026-09-14 브레인 판정) — 내역 제자리에
|
||||||
|
**빈 금액 + 빨간 테두리**로 서고 합계에 안 듦 · 머리에 「미확정 N건 — 금액에 안 들어감」."""
|
||||||
|
return self.blocked_kind == BLOCKED_UNCONFIRMED
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class HandoffMaterial:
|
||||||
|
"""B08 인계 자재 한 줄. `work_item_code` 칸이 **아예 없는** 별도 벌이다(계약 확정)."""
|
||||||
|
|
||||||
|
material_name: str
|
||||||
|
spec: str
|
||||||
|
unit: str
|
||||||
|
net_amount: Decimal
|
||||||
|
total_amount: Decimal
|
||||||
|
supply_type: str
|
||||||
|
surcharge_pct: Decimal | None = None
|
||||||
|
surcharge_note: str = ""
|
||||||
|
install_by: str | None = None
|
||||||
|
source_structure: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_name(self) -> str:
|
||||||
|
return f"{self.material_name} {self.spec}".strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _decimal(value: Any, default: Decimal | None = _ZERO) -> Decimal | None:
|
||||||
|
if value is None or value == "":
|
||||||
|
return default
|
||||||
|
return Decimal(str(value))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_handoff(payload: dict[str, Any]) -> tuple[list[HandoffWorkItem], list[HandoffMaterial]]:
|
||||||
|
"""인계 응답을 우리 자료형으로 옮긴다. **모르는 칸을 채우지 않는다.**"""
|
||||||
|
if "work_items" not in payload or "materials" not in payload:
|
||||||
|
raise BillError("인계 응답에 `work_items`·`materials` 두 벌이 다 있어야 합니다.")
|
||||||
|
|
||||||
|
work_items = [
|
||||||
|
HandoffWorkItem(
|
||||||
|
work_item_code=row.get("work_item_code"),
|
||||||
|
name=row.get("name", ""),
|
||||||
|
spec=row.get("spec") or "",
|
||||||
|
unit=row.get("unit") or "",
|
||||||
|
quantity=_decimal(row.get("quantity")) or _ZERO,
|
||||||
|
in_bill=bool(row.get("in_bill", True)),
|
||||||
|
in_bill_reason=row.get("in_bill_reason") or "",
|
||||||
|
origin=row.get("origin") or "",
|
||||||
|
ground_class=row.get("ground_class") or "",
|
||||||
|
haul_distance_m=_decimal(row.get("haul_distance_m"), None),
|
||||||
|
haul_equipment=row.get("haul_equipment"),
|
||||||
|
# ⚠ 있으면 **적기만** 한다 — 곱하기는 B08 한 곳에서만(2026-09-08 이견 ①).
|
||||||
|
application_ratio_pct=_decimal(row.get("application_ratio_pct"), None),
|
||||||
|
quantity_gross=_decimal(row.get("quantity_gross"), None),
|
||||||
|
application_ratio_breakdown=row.get("application_ratio_breakdown"),
|
||||||
|
quantity_breakdown=row.get("quantity_breakdown"),
|
||||||
|
composite_parts=tuple(row.get("composite_parts") or ()),
|
||||||
|
composite_not_ready=tuple(row.get("composite_not_ready") or ()),
|
||||||
|
structure_kind=row.get("structure_kind") or "",
|
||||||
|
blocked_reason=row.get("blocked_reason") or "",
|
||||||
|
blocked_kind=row.get("blocked_kind") or "",
|
||||||
|
variant_axis=row.get("variant_axis") or "",
|
||||||
|
variant_value=str(row.get("variant_value") or ""),
|
||||||
|
spec_class=row.get("spec_class") or "",
|
||||||
|
spec_class_basis=row.get("spec_class_basis") or "",
|
||||||
|
pum_edition=str(row.get("pum_edition") or ""),
|
||||||
|
)
|
||||||
|
for row in payload["work_items"]
|
||||||
|
]
|
||||||
|
materials = [
|
||||||
|
HandoffMaterial(
|
||||||
|
material_name=row.get("material_name", ""),
|
||||||
|
spec=row.get("spec") or "",
|
||||||
|
unit=row.get("unit") or "",
|
||||||
|
net_amount=_decimal(row.get("net_amount")) or _ZERO,
|
||||||
|
total_amount=_decimal(row.get("total_amount")) or _ZERO,
|
||||||
|
supply_type=row.get("supply_type") or SUPPLY_UNKNOWN,
|
||||||
|
surcharge_pct=_decimal(row.get("surcharge_pct"), None),
|
||||||
|
surcharge_note=row.get("surcharge_note") or "",
|
||||||
|
install_by=row.get("install_by"),
|
||||||
|
source_structure=tuple(row.get("source_structure") or ()),
|
||||||
|
)
|
||||||
|
for row in payload["materials"]
|
||||||
|
]
|
||||||
|
return work_items, materials
|
||||||
@@ -18,7 +18,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass, field, replace
|
from dataclasses import dataclass, field, replace
|
||||||
from decimal import ROUND_CEILING, ROUND_FLOOR, Decimal
|
from decimal import ROUND_CEILING, ROUND_FLOOR, Decimal
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from B09_Estimation.B09_Estimation_Engine_Cost_Options import (
|
from B09_Estimation.B09_Estimation_Engine_Cost_Options import (
|
||||||
CUT_BASES,
|
CUT_BASES,
|
||||||
@@ -45,9 +44,12 @@ from B09_Estimation.B09_Estimation_Statutory import (
|
|||||||
_ZERO = Decimal(0)
|
_ZERO = Decimal(0)
|
||||||
_HUNDRED = Decimal(100)
|
_HUNDRED = Decimal(100)
|
||||||
|
|
||||||
#: 기본으로 켜는 비목 = **그 해 요율 데이터에 있는 것 전부**.
|
# ⬇ 입력 한 벌은 `_Input` 으로 옮겼다(2026-09-15 700줄 분리) —
|
||||||
#: 사용자 확정(2026-09-07, PLAN 8-14): 실무 서류에 없다고 빼지 않는다.
|
# 쓰던 이름이 그대로 살게 다시 내보낸다.
|
||||||
DEFAULT_ITEMS = "ALL_AVAILABLE"
|
from B09_Estimation.B09_Estimation_Engine_Cost_Input import ( # noqa: E402,F401
|
||||||
|
DEFAULT_ITEMS,
|
||||||
|
CostInput,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def floor_won(value: Decimal) -> Decimal:
|
def floor_won(value: Decimal) -> Decimal:
|
||||||
@@ -60,122 +62,6 @@ def ceil_thousand(value: Decimal) -> Decimal:
|
|||||||
return (value / 1000).quantize(Decimal(1), rounding=ROUND_CEILING) * 1000
|
return (value / 1000).quantize(Decimal(1), rounding=ROUND_CEILING) * 1000
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class CostInput:
|
|
||||||
"""원가계산 입력. 금액은 전부 원 단위 `Decimal`."""
|
|
||||||
|
|
||||||
direct_material_krw: Decimal
|
|
||||||
direct_labor_krw: Decimal
|
|
||||||
direct_expense_krw: Decimal
|
|
||||||
indirect_material_krw: Decimal = _ZERO
|
|
||||||
|
|
||||||
#: 구간 판정용 공종·기간. `work_type` 값은 요율 데이터의 표기를 그대로 쓴다.
|
|
||||||
work_type_indirect_labor: str = "civil"
|
|
||||||
work_type_safety: str = "civil"
|
|
||||||
duration_days: int = 183
|
|
||||||
pension_year: int = 2026
|
|
||||||
|
|
||||||
#: 관급자재 — **순자재대**와 조달수수료를 나눠 받는다(순환 정의 방지, 원가계산_체계 §1).
|
|
||||||
#: ⚠ `owner_supplied_material_krw` 는 **수수료를 뺀 순자재대**다. 실무 서류의
|
|
||||||
#: 「관급자재대」는 이미 `순자재대 + 수수료` 를 천원 올림한 값이므로 그대로 넣으면 안 된다
|
|
||||||
#: (2026-09-07 실측 정정 — 울진 순자재대 69,474,220 + 수수료 375,160 = 69,849,380 →
|
|
||||||
#: 천원 올림 69,850,000). 안전관리비 관급항도 **순자재대만** 쓴다.
|
|
||||||
owner_supplied_material_krw: Decimal = _ZERO
|
|
||||||
procurement_fee_krw: Decimal = _ZERO
|
|
||||||
include_fee_in_owner_material_total: bool = True
|
|
||||||
|
|
||||||
#: 안전관리비 대상액에 들어가는 **도급자설치 관급금액**. None 이면 관급 전액.
|
|
||||||
owner_supplied_for_safety_krw: Decimal | None = None
|
|
||||||
#: 그 금액이 부가세 포함인가 — 포함이면 1.1 로 나눈다.
|
|
||||||
#: ⚠ 근거는 **실무**다 — 고시(산업안전보건관리비 계상 및 사용기준) 제4조① 단서는 「해당
|
|
||||||
#: 재료비를 **대상액에 포함**」까지만 적고 부가세를 말하지 않는다. ÷1.1 은 부가세 제외
|
|
||||||
#: 환산이며, 실무 원가계산서 **6건이 모두** 「관급재/1.1」로 적었다(2026-09-14 전수 확인).
|
|
||||||
owner_supplied_includes_vat: bool = True
|
|
||||||
#: 규모 구간 판정에 쓸 **추정가격**. 주면 그 값으로 한 번만 판정한다.
|
|
||||||
#: 없으면 직접공사비를 씨앗으로 **반복 수렴**한다 (`calculate_cost` 참조).
|
|
||||||
#: 근거 — 국가계약법 시행령 제7조 1호 「공사계약의 경우에는 관급자재로 공급될
|
|
||||||
#: 부분의 가격을 제외한 금액」. 우리 계산의 그 값은 **총원가**(부가세 전, 관급 밖).
|
|
||||||
estimated_price_krw: Decimal | None = None
|
|
||||||
|
|
||||||
#: 이윤 수동 조정액 — 설계자 명시 입력일 때만. 프로그램이 스스로 채우지 않는다.
|
|
||||||
profit_adjustment_krw: Decimal = _ZERO
|
|
||||||
#: 조정액 줄 산식 칸 — 비면 「설계자 명시 입력」. 절사 자동보정이 제 이름을 적는 자리.
|
|
||||||
profit_adjustment_label: str = ""
|
|
||||||
|
|
||||||
#: 폐기물처리비 — 요율이 아니라 **실비**(수량 × 처리단가).
|
|
||||||
#: ⭐ 2026-09-14 판정 — **비목은 경비**(예정가격작성기준 제19조③18호)라 순공사원가에 들고
|
|
||||||
#: **일반관리비·이윤 밑수에 든다.** 빠지면 총액이 조용히 작아진다.
|
|
||||||
#: ⚠ 법정경비 밑수(직접공사비·노무비)에는 안 넣는다 — 그 밑수는 요율 데이터가 정한 합이다.
|
|
||||||
waste_disposal_krw: Decimal = _ZERO
|
|
||||||
#: 분리발주 — 켜면 공사와 따로 발주하는 용역이라 **총원가 밖**, 총공사비에만 더한다
|
|
||||||
#: (거창 원가계산서 `총공사비 = 도급액 + 관급자재대 + 폐기물처리비` 모양). 기본 꺼짐.
|
|
||||||
waste_separate_order: bool = False
|
|
||||||
#: 폐기물처리비 자리 — `expense`(기본 · 법 문언: 경비 · 일반관리비·이윤 밑수 안) ·
|
|
||||||
#: `after_profit`(실무 관행: 이윤 뒤 · 총원가 안 · 부가세 안 · 승률 밖 — 울진소광·실정보고).
|
|
||||||
#: 분리발주(`waste_separate_order`)가 켜지면 그쪽이 먼저(총원가 밖).
|
|
||||||
waste_placement: str = "expense"
|
|
||||||
|
|
||||||
#: ── 기준 입력 10·11·12 (규칙은 `Engine_Cost_Options`) — 기본값은 종전 계산과 같음 ──
|
|
||||||
#: 원가계산 형식 — 일반관리비 밑수가 형식마다 다름(지금은 `general` 만).
|
|
||||||
form: str = "general"
|
|
||||||
#: 10. 일반관리비 — 요율 표 이름((주)공사 / 전문공사).
|
|
||||||
overhead_class: str = "civil_landscape_industrial"
|
|
||||||
#: 일반 형식 일반관리비 밑수에 더하는 관리품목 자재대.
|
|
||||||
overhead_managed_material_krw: Decimal = _ZERO
|
|
||||||
#: 12. 부가세 방식(`VAT_MODES`) · 산림조합-면세품이면 면세품 금액.
|
|
||||||
vat_mode: str = "supply"
|
|
||||||
tax_exempt_material_krw: Decimal = _ZERO
|
|
||||||
#: 11. 절사 — `grand_total`(총공사비에서 조정) · `supply`(공급가액) · `none`·빈 값은 안 자름.
|
|
||||||
#: ⭐ 2026-09-14 **기본이 켜짐** — 근거 산림청고시 제2025-82호 「금액의 단위표준」
|
|
||||||
#: 「설계서의 총액 · 원 · 1,000 · 미만버림」.
|
|
||||||
#: 실무 6건이 여섯 다 `000` 으로 끝나는 것이 증거.
|
|
||||||
#: ⚠ 2026-09-08 「조용히 깎지 않음」 합의의 정신은 살린다 — 깎은 몫과
|
|
||||||
#: **까닭(고시 번호)** 을 이윤 조정액 줄과 결과 메모에 적고,
|
|
||||||
#: 설계자가 `none` 으로 **끌 수 있게** 둔다.
|
|
||||||
cut_basis: str = "grand_total"
|
|
||||||
cut_unit_krw: int = 1000
|
|
||||||
#: 4. 고용보험 등급 — `auto`(추정금액 구간) · `none`(없음) · `1`~`7`(등급 직접).
|
|
||||||
employment_insurance_grade: str = "auto"
|
|
||||||
#: 5. 퇴직공제부금비 — `auto`(추정금액 1억 이상) · `apply`(적용) · `none`(미적용).
|
|
||||||
#: ⚠ STmate 는 토목·준설·건축·기타로 가르나 현행 제비율(2026-04-13)은 공종 구분 없이 2.3% —
|
|
||||||
#: 율이 안 갈리는 구분은 칸으로 안 세움.
|
|
||||||
retirement_mutual_aid_mode: str = "auto"
|
|
||||||
#: 13. 사급비 위치 — `material`(재료비에 포함) 뿐. 사급은 이미 내역 재료비에 들어 오므로
|
|
||||||
#: 계산에 안 씀(실무 거창·영월 · STmate 기본). 다른 자리는 사급 금액 칸이 설 때 엶.
|
|
||||||
private_material_position: str = "material"
|
|
||||||
#: 14. 낙찰방식 — `not_comprehensive`(종합심사 외) · `comprehensive`(종합심사 대상) · `turnkey`.
|
|
||||||
#: 하도급대금 지급보증 요율 줄만 고름.
|
|
||||||
bid_method: str = "not_comprehensive"
|
|
||||||
#: 하도급대금 지급보증수수료 — `off`(기본) · `on`. 실무 여섯 건에 줄 없음 · 건설산업기본법
|
|
||||||
#: 제34조 대상이면 켬(「대상 아님」인지 「안 적음」인지는 실무로 못 가름).
|
|
||||||
subcontract_guarantee: str = "off"
|
|
||||||
#: 15. 이행보증 — `general`(일반계약: 추정가격 300억 이상만) ·
|
|
||||||
#: `lowest_price_tech`(최저가·기술제안: 규모 무관).
|
|
||||||
performance_guarantee_mode: str = "general"
|
|
||||||
#: 16. 적용기준 — `national`·`local`·`moi`. 계산에 안 씀: 현행 조달청 표 한 벌뿐 · 국가/지방
|
|
||||||
#: 구간 차이(예정가격작성기준 제20조 표)는 원문 이미지라 미확인 · 행자부 표 없음. 칸만 받음.
|
|
||||||
contract_law_basis: str = ""
|
|
||||||
#: 프로젝트 요율 덮어쓰기(`RateOverride` 한 줄씩, 사유 포함) — 발주처별 별도요율.
|
|
||||||
rate_overrides: tuple[dict[str, Any], ...] = ()
|
|
||||||
|
|
||||||
#: 환경보전비 공종 (`rate_environment.all_work_types` 의 값).
|
|
||||||
#: TODO(미결 PLAN 9-6): 임도가 「도로 0.9 %」인지 「기타 토목 0.8 %」인지 미확정.
|
|
||||||
#: 잠정 = 도로(0.9 %). 요율 데이터가 `pending` 을 달고 있어 결과 줄에 경고가 붙는다.
|
|
||||||
environment_work_type: str = "civil_road"
|
|
||||||
#: 건설기계대여대금 지급보증 공종.
|
|
||||||
equipment_guarantee_work_type: str = "civil_general"
|
|
||||||
#: 하도급대금 지급보증 — 30억 이상 구간이 공종으로 갈린다(토목·산업설비 / 건축).
|
|
||||||
subcontract_guarantee_variant: str = "integrated_civil_or_industrial"
|
|
||||||
|
|
||||||
#: 켤 비목. 기본은 「그 해 요율 데이터에 있는 것 전부」.
|
|
||||||
enabled_items: tuple[str, ...] | str = DEFAULT_ITEMS
|
|
||||||
|
|
||||||
#: 요율 데이터 파일명. **연도를 갈아끼우는 자리.**
|
|
||||||
rate_file_name: str = "rates_2026.json"
|
|
||||||
#: 매니페스트 밖 요율 파일(옛 연도 재현 검산 전용). 주면 이쪽이 우선.
|
|
||||||
rate_file_path: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CostLine:
|
class CostLine:
|
||||||
"""원가계산서 한 줄 — 화면이 「비목·금액·요율·산출근거」를 다 보이므로 넷을 다 든다."""
|
"""원가계산서 한 줄 — 화면이 「비목·금액·요율·산출근거」를 다 보이므로 넷을 다 든다."""
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
"""B09 원가계산 — ⑤ 공사원가계산서 **입력 한 벌**(`CostInput`).
|
||||||
|
|
||||||
|
`B09_Estimation_Engine_Cost` 가 700줄에 닿아 **그대로 옮겨 온 자리**다
|
||||||
|
(2026-09-15 · 순수 분리 — 글자 한 자 안 고쳤다). 계산 사슬은 그쪽에 그대로 있다.
|
||||||
|
|
||||||
|
⚠ 쓰던 자리가 안 바뀌도록 엔진이 다시 내보낸다 —
|
||||||
|
`from ...Engine_Cost import CostInput` 가 그대로 돈다.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
_ZERO = Decimal(0)
|
||||||
|
|
||||||
|
#: 기본으로 켜는 비목 = **그 해 요율 데이터에 있는 것 전부**.
|
||||||
|
#: 사용자 확정(2026-09-07, PLAN 8-14): 실무 서류에 없다고 빼지 않는다.
|
||||||
|
DEFAULT_ITEMS = "ALL_AVAILABLE"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CostInput:
|
||||||
|
"""원가계산 입력. 금액은 전부 원 단위 `Decimal`."""
|
||||||
|
|
||||||
|
direct_material_krw: Decimal
|
||||||
|
direct_labor_krw: Decimal
|
||||||
|
direct_expense_krw: Decimal
|
||||||
|
indirect_material_krw: Decimal = _ZERO
|
||||||
|
|
||||||
|
#: 구간 판정용 공종·기간. `work_type` 값은 요율 데이터의 표기를 그대로 쓴다.
|
||||||
|
work_type_indirect_labor: str = "civil"
|
||||||
|
work_type_safety: str = "civil"
|
||||||
|
duration_days: int = 183
|
||||||
|
pension_year: int = 2026
|
||||||
|
|
||||||
|
#: 관급자재 — **순자재대**와 조달수수료를 나눠 받는다(순환 정의 방지, 원가계산_체계 §1).
|
||||||
|
#: ⚠ `owner_supplied_material_krw` 는 **수수료를 뺀 순자재대**다. 실무 서류의
|
||||||
|
#: 「관급자재대」는 이미 `순자재대 + 수수료` 를 천원 올림한 값이므로 그대로 넣으면 안 된다
|
||||||
|
#: (2026-09-07 실측 정정 — 울진 순자재대 69,474,220 + 수수료 375,160 = 69,849,380 →
|
||||||
|
#: 천원 올림 69,850,000). 안전관리비 관급항도 **순자재대만** 쓴다.
|
||||||
|
owner_supplied_material_krw: Decimal = _ZERO
|
||||||
|
procurement_fee_krw: Decimal = _ZERO
|
||||||
|
include_fee_in_owner_material_total: bool = True
|
||||||
|
|
||||||
|
#: 안전관리비 대상액에 들어가는 **도급자설치 관급금액**. None 이면 관급 전액.
|
||||||
|
owner_supplied_for_safety_krw: Decimal | None = None
|
||||||
|
#: 그 금액이 부가세 포함인가 — 포함이면 1.1 로 나눈다.
|
||||||
|
#: ⚠ 근거는 **실무**다 — 고시(산업안전보건관리비 계상 및 사용기준) 제4조① 단서는 「해당
|
||||||
|
#: 재료비를 **대상액에 포함**」까지만 적고 부가세를 말하지 않는다. ÷1.1 은 부가세 제외
|
||||||
|
#: 환산이며, 실무 원가계산서 **6건이 모두** 「관급재/1.1」로 적었다(2026-09-14 전수 확인).
|
||||||
|
owner_supplied_includes_vat: bool = True
|
||||||
|
#: 규모 구간 판정에 쓸 **추정가격**. 주면 그 값으로 한 번만 판정한다.
|
||||||
|
#: 없으면 직접공사비를 씨앗으로 **반복 수렴**한다 (`calculate_cost` 참조).
|
||||||
|
#: 근거 — 국가계약법 시행령 제7조 1호 「공사계약의 경우에는 관급자재로 공급될
|
||||||
|
#: 부분의 가격을 제외한 금액」. 우리 계산의 그 값은 **총원가**(부가세 전, 관급 밖).
|
||||||
|
estimated_price_krw: Decimal | None = None
|
||||||
|
|
||||||
|
#: 이윤 수동 조정액 — 설계자 명시 입력일 때만. 프로그램이 스스로 채우지 않는다.
|
||||||
|
profit_adjustment_krw: Decimal = _ZERO
|
||||||
|
#: 조정액 줄 산식 칸 — 비면 「설계자 명시 입력」. 절사 자동보정이 제 이름을 적는 자리.
|
||||||
|
profit_adjustment_label: str = ""
|
||||||
|
|
||||||
|
#: 폐기물처리비 — 요율이 아니라 **실비**(수량 × 처리단가).
|
||||||
|
#: ⭐ 2026-09-14 판정 — **비목은 경비**(예정가격작성기준 제19조③18호)라 순공사원가에 들고
|
||||||
|
#: **일반관리비·이윤 밑수에 든다.** 빠지면 총액이 조용히 작아진다.
|
||||||
|
#: ⚠ 법정경비 밑수(직접공사비·노무비)에는 안 넣는다 — 그 밑수는 요율 데이터가 정한 합이다.
|
||||||
|
waste_disposal_krw: Decimal = _ZERO
|
||||||
|
#: 분리발주 — 켜면 공사와 따로 발주하는 용역이라 **총원가 밖**, 총공사비에만 더한다
|
||||||
|
#: (거창 원가계산서 `총공사비 = 도급액 + 관급자재대 + 폐기물처리비` 모양). 기본 꺼짐.
|
||||||
|
waste_separate_order: bool = False
|
||||||
|
#: 폐기물처리비 자리 — `expense`(기본 · 법 문언: 경비 · 일반관리비·이윤 밑수 안) ·
|
||||||
|
#: `after_profit`(실무 관행: 이윤 뒤 · 총원가 안 · 부가세 안 · 승률 밖 — 울진소광·실정보고).
|
||||||
|
#: 분리발주(`waste_separate_order`)가 켜지면 그쪽이 먼저(총원가 밖).
|
||||||
|
waste_placement: str = "expense"
|
||||||
|
|
||||||
|
#: ── 기준 입력 10·11·12 (규칙은 `Engine_Cost_Options`) — 기본값은 종전 계산과 같음 ──
|
||||||
|
#: 원가계산 형식 — 일반관리비 밑수가 형식마다 다름(지금은 `general` 만).
|
||||||
|
form: str = "general"
|
||||||
|
#: 10. 일반관리비 — 요율 표 이름((주)공사 / 전문공사).
|
||||||
|
overhead_class: str = "civil_landscape_industrial"
|
||||||
|
#: 일반 형식 일반관리비 밑수에 더하는 관리품목 자재대.
|
||||||
|
overhead_managed_material_krw: Decimal = _ZERO
|
||||||
|
#: 12. 부가세 방식(`VAT_MODES`) · 산림조합-면세품이면 면세품 금액.
|
||||||
|
vat_mode: str = "supply"
|
||||||
|
tax_exempt_material_krw: Decimal = _ZERO
|
||||||
|
#: 11. 절사 — `grand_total`(총공사비에서 조정) · `supply`(공급가액) · `none`·빈 값은 안 자름.
|
||||||
|
#: ⭐ 2026-09-14 **기본이 켜짐** — 근거 산림청고시 제2025-82호 「금액의 단위표준」
|
||||||
|
#: 「설계서의 총액 · 원 · 1,000 · 미만버림」.
|
||||||
|
#: 실무 6건이 여섯 다 `000` 으로 끝나는 것이 증거.
|
||||||
|
#: ⚠ 2026-09-08 「조용히 깎지 않음」 합의의 정신은 살린다 — 깎은 몫과
|
||||||
|
#: **까닭(고시 번호)** 을 이윤 조정액 줄과 결과 메모에 적고,
|
||||||
|
#: 설계자가 `none` 으로 **끌 수 있게** 둔다.
|
||||||
|
cut_basis: str = "grand_total"
|
||||||
|
cut_unit_krw: int = 1000
|
||||||
|
#: 4. 고용보험 등급 — `auto`(추정금액 구간) · `none`(없음) · `1`~`7`(등급 직접).
|
||||||
|
employment_insurance_grade: str = "auto"
|
||||||
|
#: 5. 퇴직공제부금비 — `auto`(추정금액 1억 이상) · `apply`(적용) · `none`(미적용).
|
||||||
|
#: ⚠ STmate 는 토목·준설·건축·기타로 가르나 현행 제비율(2026-04-13)은 공종 구분 없이 2.3% —
|
||||||
|
#: 율이 안 갈리는 구분은 칸으로 안 세움.
|
||||||
|
retirement_mutual_aid_mode: str = "auto"
|
||||||
|
#: 13. 사급비 위치 — `material`(재료비에 포함) 뿐. 사급은 이미 내역 재료비에 들어 오므로
|
||||||
|
#: 계산에 안 씀(실무 거창·영월 · STmate 기본). 다른 자리는 사급 금액 칸이 설 때 엶.
|
||||||
|
private_material_position: str = "material"
|
||||||
|
#: 14. 낙찰방식 — `not_comprehensive`(종합심사 외) · `comprehensive`(종합심사 대상) · `turnkey`.
|
||||||
|
#: 하도급대금 지급보증 요율 줄만 고름.
|
||||||
|
bid_method: str = "not_comprehensive"
|
||||||
|
#: 하도급대금 지급보증수수료 — `off`(기본) · `on`. 실무 여섯 건에 줄 없음 · 건설산업기본법
|
||||||
|
#: 제34조 대상이면 켬(「대상 아님」인지 「안 적음」인지는 실무로 못 가름).
|
||||||
|
subcontract_guarantee: str = "off"
|
||||||
|
#: 15. 이행보증 — `general`(일반계약: 추정가격 300억 이상만) ·
|
||||||
|
#: `lowest_price_tech`(최저가·기술제안: 규모 무관).
|
||||||
|
performance_guarantee_mode: str = "general"
|
||||||
|
#: 16. 적용기준 — `national`·`local`·`moi`. 계산에 안 씀: 현행 조달청 표 한 벌뿐 · 국가/지방
|
||||||
|
#: 구간 차이(예정가격작성기준 제20조 표)는 원문 이미지라 미확인 · 행자부 표 없음. 칸만 받음.
|
||||||
|
contract_law_basis: str = ""
|
||||||
|
#: 프로젝트 요율 덮어쓰기(`RateOverride` 한 줄씩, 사유 포함) — 발주처별 별도요율.
|
||||||
|
rate_overrides: tuple[dict[str, Any], ...] = ()
|
||||||
|
|
||||||
|
#: 환경보전비 공종 (`rate_environment.all_work_types` 의 값).
|
||||||
|
#: TODO(미결 PLAN 9-6): 임도가 「도로 0.9 %」인지 「기타 토목 0.8 %」인지 미확정.
|
||||||
|
#: 잠정 = 도로(0.9 %). 요율 데이터가 `pending` 을 달고 있어 결과 줄에 경고가 붙는다.
|
||||||
|
environment_work_type: str = "civil_road"
|
||||||
|
#: 건설기계대여대금 지급보증 공종.
|
||||||
|
equipment_guarantee_work_type: str = "civil_general"
|
||||||
|
#: 하도급대금 지급보증 — 30억 이상 구간이 공종으로 갈린다(토목·산업설비 / 건축).
|
||||||
|
subcontract_guarantee_variant: str = "integrated_civil_or_industrial"
|
||||||
|
|
||||||
|
#: 켤 비목. 기본은 「그 해 요율 데이터에 있는 것 전부」.
|
||||||
|
enabled_items: tuple[str, ...] | str = DEFAULT_ITEMS
|
||||||
|
|
||||||
|
#: 요율 데이터 파일명. **연도를 갈아끼우는 자리.**
|
||||||
|
rate_file_name: str = "rates_2026.json"
|
||||||
|
#: 매니페스트 밖 요율 파일(옛 연도 재현 검산 전용). 주면 이쪽이 우선.
|
||||||
|
rate_file_path: str | None = None
|
||||||
@@ -57,6 +57,33 @@ KNOWN_GAPS: dict[str, tuple[str, str]] = {
|
|||||||
# 2026-09-14 ㉮ — 사용횟수 갈래로 푼 뒤 남는 원문 몫. 값을 짓지 않고 말만.
|
# 2026-09-14 ㉮ — 사용횟수 갈래로 푼 뒤 남는 원문 몫. 값을 짓지 않고 말만.
|
||||||
# 봉상후렉시블 셋의 표 나머지 줄은 판정표 자동 목록(`_unread_rows`)이 맡음 — 줄이 아닌 [주] 만 여기.
|
# 봉상후렉시블 셋의 표 나머지 줄은 판정표 자동 목록(`_unread_rows`)이 맡음 — 줄이 아닌 [주] 만 여기.
|
||||||
"FP-12-12": ("원문 [주]", "ⓘ [주] 「성토부 날개벽 설치시 인건비 30% 할증」 은 안 걺(선택)."),
|
"FP-12-12": ("원문 [주]", "ⓘ [주] 「성토부 날개벽 설치시 인건비 30% 할증」 은 안 걺(선택)."),
|
||||||
|
# ㉡-3(2026-09-15) — 시간 줄 표 둘 · 막힌 셋은 옛 값이 왜 틀렸는지(풀리는 날 두 번 세지 않게 · 브레인 ③).
|
||||||
|
"FP-14-01": (
|
||||||
|
"원문 표",
|
||||||
|
"ⓘ 원문 「0.8㎥ 굴착기(우드그랩 부착)」 가 굴착기 형식을 안 적어 무한궤도·타이어 두 갈래 · 우드그랩 ↔ 카탈로그"
|
||||||
|
" 「부착용 집게 7206-0070(0.6∼0.8)」 — 이름이 다름 · 인력 「h」 는 시간이라 총칙 일일 작업시간 8시간으로 ÷8.",
|
||||||
|
),
|
||||||
|
"FP-14-02": (
|
||||||
|
"원문 표",
|
||||||
|
"ⓘ 원문 「0.8㎥ 굴착기」 가 형식을 안 적어 무한궤도·타이어 두 갈래 · 인력 「h」 는 시간이라 총칙 일일 작업시간"
|
||||||
|
" 8시간으로 ÷8 · 옛 11,356원은 0.66h 를 인으로 셈(8배) + 굴착기 줄이 빠진 값.",
|
||||||
|
),
|
||||||
|
"FP-12-26": (
|
||||||
|
"막힘 까닭",
|
||||||
|
"ⓘ 양수기(150㎜)·디젤엔진(15HP 11.19kW) 가동시간이 원문 표에 없음 · 카탈로그 양수기는 1.49㎾ 뿐 · 엔진(디젤)"
|
||||||
|
" 손료계수 없음 → 금액 안 세움 · 옛 172,068원은 보통인부 1인 값뿐(기계·목도 4인 빠짐).",
|
||||||
|
),
|
||||||
|
"FP-10-07-04": (
|
||||||
|
"막힘 까닭",
|
||||||
|
"ⓘ 운전 단가표(1일) 보통인부 2 는 적재·짐부리기 보통인부 2인(「운전조작」 포함)과 같은 사람 — 두 번 세지 말 것 ·"
|
||||||
|
" 연료 「ps × 0.253ℓ × 6h」 의 ps 입력 없음 · 본기계·레일은 임대료·견적 → 금액 안 세움 · 옛 688,272원은"
|
||||||
|
" 보통인부를 두 번 센 4인 값.",
|
||||||
|
),
|
||||||
|
"FP-10-08-03": (
|
||||||
|
"막힘 까닭",
|
||||||
|
"ⓘ 연료 = 윈치 기관 출력([주]② 36~73kW) × 운전시간([주]① 4.3~6.7h) × ℓ/kWh — ℓ/kWh 가 원문에 없음 · 보통인부는"
|
||||||
|
" 10-8-1 짐내리기 품 · 운반기구 손료 별도 → 금액 안 세움 · 옛 226,122원은 특별인부 1인 값뿐.",
|
||||||
|
),
|
||||||
"FP-08-06-01": (
|
"FP-08-06-01": (
|
||||||
"원문 [주]",
|
"원문 [주]",
|
||||||
"ⓘ 2-1-8 [주]② 「소방관서의 급수 지원을 받은 경우에는 휘발유(양수기)는 미반영」 — 해당하면 양수기 몫을"
|
"ⓘ 2-1-8 [주]② 「소방관서의 급수 지원을 받은 경우에는 휘발유(양수기)는 미반영」 — 해당하면 양수기 몫을"
|
||||||
|
|||||||
@@ -96,7 +96,27 @@ JUDGED_TABLES: dict[str, dict[str, Any]] = {
|
|||||||
"same_machine": ("봉상후렉시블(45mm)",),
|
"same_machine": ("봉상후렉시블(45mm)",),
|
||||||
"why": "원문 L6839 12-34-1 「인력 콘크리트공·보통인부 인 · 기계 대 (Q=5.4㎥/hr)」",
|
"why": "원문 L6839 12-34-1 「인력 콘크리트공·보통인부 인 · 기계 대 (Q=5.4㎥/hr)」",
|
||||||
},
|
},
|
||||||
|
# 시간 줄 표 — 장비·인력이 다 「h」(2026-09-15 브레인 ㉡-3). 굴착기 형식을 원문이 안 적어 두 갈래 · 제안 없음(①).
|
||||||
|
"F0450": {
|
||||||
|
"code": "FP-14-01",
|
||||||
|
"shape": "hour_rows",
|
||||||
|
"prefix": "입목뿌리 밑막이",
|
||||||
|
"forms": (("무한궤도", "0201-0080"), ("타이어", "0211-0080")),
|
||||||
|
# 원문 「우드그랩 부착」 ↔ 카탈로그 「부착용 집게 0.6∼0.8」 — 이름이 다름(②).
|
||||||
|
"attachment": "7206-0070",
|
||||||
|
"why": "원문 L7798 14-1 「0.8㎥ 굴착기(우드그랩 부착) h · 보통인부 h」(10본당)",
|
||||||
|
},
|
||||||
|
"F0451": {
|
||||||
|
"code": "FP-14-02",
|
||||||
|
"shape": "hour_rows",
|
||||||
|
"prefix": "근주이식",
|
||||||
|
"forms": (("무한궤도", "0201-0080"), ("타이어", "0211-0080")),
|
||||||
|
"why": "원문 L7809 14-2 「0.8㎥ 굴착기 h · 보통인부 h」(10본당)",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
#: 총칙 L530 「본 품셈에서 제시된 품은 일일 작업시간 8시간을 기준」 — 인력 시간 ÷ 8 = 인.
|
||||||
|
HOURS_PER_DAY = Decimal(8)
|
||||||
|
_RE_SPEC_FIRST_MACHINE = re.compile(r"^\d+(?:\.\d+)?㎥굴착기")
|
||||||
#: 줄 첫 칸이 분류 딱지인 표(12-34-1 「자재 | 콘크리트(레미콘)」) — 이름은 다음 칸.
|
#: 줄 첫 칸이 분류 딱지인 표(12-34-1 「자재 | 콘크리트(레미콘)」) — 이름은 다음 칸.
|
||||||
_ROW_CATEGORIES = ("자재", "인력", "기계")
|
_ROW_CATEGORIES = ("자재", "인력", "기계")
|
||||||
#: 자원이 아닌 머리 줄(12-04 「횟수별 | 재료별(%) | 노무비(%)」).
|
#: 자원이 아닌 머리 줄(12-04 「횟수별 | 재료별(%) | 노무비(%)」).
|
||||||
@@ -206,6 +226,8 @@ def match_judged_table(
|
|||||||
staged = _use_count(code, table, rows, catalog, basis_quantity, unit)
|
staged = _use_count(code, table, rows, catalog, basis_quantity, unit)
|
||||||
elif judged["shape"] == "remark_labor":
|
elif judged["shape"] == "remark_labor":
|
||||||
staged = _remark_labor(code, table, judged, rows, catalog)
|
staged = _remark_labor(code, table, judged, rows, catalog)
|
||||||
|
elif judged["shape"] == "hour_rows":
|
||||||
|
staged = _hour_rows(code, table, judged, rows, catalog, basis_quantity)
|
||||||
elif judged["shape"] == "per_m3_rows":
|
elif judged["shape"] == "per_m3_rows":
|
||||||
staged = _per_m3_rows(code, table, judged, rows, catalog)
|
staged = _per_m3_rows(code, table, judged, rows, catalog)
|
||||||
else:
|
else:
|
||||||
@@ -370,3 +392,46 @@ def _per_m3_rows(code, table, judged, rows, catalog) -> list | str:
|
|||||||
amount = amount / Decimal(found_q.group(1))
|
amount = amount / Decimal(found_q.group(1))
|
||||||
staged.append(_row(code, table, entry, amount, "㎥", index, ""))
|
staged.append(_row(code, table, entry, amount, "㎥", index, ""))
|
||||||
return staged
|
return staged
|
||||||
|
|
||||||
|
|
||||||
|
def _hour_rows(code, table, judged, rows, catalog, basis_quantity) -> list | str:
|
||||||
|
"""장비·인력이 다 시간인 표 — 장비 h 그대로 · 인력 h ÷ 8 · ÷ 밑수 · 굴착기 형식마다 갈래."""
|
||||||
|
entries = {entry.code: entry for entry in catalog.entries}
|
||||||
|
unit = str(table.get("basis_unit") or "")
|
||||||
|
machine: tuple[int, Decimal] | None = None
|
||||||
|
labor: list = []
|
||||||
|
for index, cells in enumerate(rows):
|
||||||
|
if len(cells) < 3 or "".join(cells[1].split()) != "h":
|
||||||
|
continue
|
||||||
|
hours = parse_amount(cells[2])
|
||||||
|
if hours is None:
|
||||||
|
return f"{cells[0]} 시간"
|
||||||
|
if _RE_SPEC_FIRST_MACHINE.match("".join(cells[0].split())):
|
||||||
|
machine = (index, hours)
|
||||||
|
continue
|
||||||
|
entry = _entry(catalog, cells[0], code)
|
||||||
|
if entry is None:
|
||||||
|
return f"{cells[0]} 줄"
|
||||||
|
labor.append((index, entry, hours / HOURS_PER_DAY))
|
||||||
|
if machine is None:
|
||||||
|
return "장비 줄"
|
||||||
|
staged: list = []
|
||||||
|
for form, machine_code in judged["forms"]:
|
||||||
|
codes = [machine_code, *([judged["attachment"]] if judged.get("attachment") else [])]
|
||||||
|
if any(c not in entries for c in codes):
|
||||||
|
return f"{form} 기종 코드"
|
||||||
|
for machine_code_ in codes:
|
||||||
|
staged.append(
|
||||||
|
_row(
|
||||||
|
code,
|
||||||
|
table,
|
||||||
|
entries[machine_code_],
|
||||||
|
machine[1] / basis_quantity,
|
||||||
|
unit,
|
||||||
|
machine[0],
|
||||||
|
form,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for index, entry, amount in labor:
|
||||||
|
staged.append(_row(code, table, entry, amount / basis_quantity, unit, index, form))
|
||||||
|
return staged
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# PLAN — 데스크탑 서브 (B09 원가계산서)
|
||||||
|
|
||||||
|
> **임시 · 2026-09-15 시놀로지 덮어쓰기 때문 · 사용자 확인 전.**
|
||||||
|
> `PLAN.md` 는 두 PC 가 번갈아 덮어 창마다 제 파일에 적기로 함(브레인 지시).
|
||||||
|
> 나중에 브레인이 넷을 합쳐 `PLAN.md` 한 벌로 되돌림. `PLAN.md` 는 **읽기만** 함.
|
||||||
|
>
|
||||||
|
> ⚠ **사용자 지침을 어기는 자리**임 — 지침은 「`PLAN.md` 는 git 밖」·「한 파일로만 운영」인데
|
||||||
|
> 이 파일은 **git 안**이고 창마다 따로임. 오늘 밤 계획서를 거듭 잃은 까닭이 **git 밖이라 이력이
|
||||||
|
> 없어서**라 임시로 이렇게 둠. 사용자가 깨면 「PLAN 을 아예 git 으로 옮길지」를 브레인이 물음 —
|
||||||
|
> 합치고 나면 이 파일은 지움. `.gitignore` 는 사용자 파일이라 **안 건드림**.
|
||||||
|
|
||||||
|
## 닫은 것 — 2026-09-14 ~ 15
|
||||||
|
|
||||||
|
- [X] **골든셋을 원가계산서까지 넓힘** (`deac8200`) — 새 `resources/tester/test_b09_golden_cost_sheet.py`.
|
||||||
|
실무 6건 「공사원가계산서」 시트를 우리 엔진으로 되풂 · **요율은 원본 비고에서 뽑아 그 원본 전용 판**을 지어
|
||||||
|
「요율이 달라서 틀림」을 없앰 · 제비율 줄마다 **54자리 전수 일치**(안전관리비 A·B·채택 포함) ·
|
||||||
|
사슬은 온전한 셋(봉화·영월·울진 공통) **33자리 일치** · 이윤은 자동절사가 맞춘 둘(봉화 339·영월 632)과
|
||||||
|
설계자 조정 하나(울진 905,980) · 영월 도급공사비 차 762,000 = 원본에만 있는 국가지점번호판검증수수료.
|
||||||
|
검증완료(전체 1782)
|
||||||
|
|
||||||
|
- [X] **총액 1,000원 미만 버림을 기본으로 켬 · 절사 진동 고침 · 고정값 벌 이관** (`aeb18266`) —
|
||||||
|
근거 산림청고시 제2025-82호 「금액의 단위표준」 · 실무 6건이 여섯 다 `000` ·
|
||||||
|
**조용히 안 깎음**(이윤 조정액 줄·메모에 고시 번호와 「11 에서 끌 수 있음」) ·
|
||||||
|
끄는 값을 `none` 으로 바꿈(빈 값은 저장이 안 돼 끌 수가 없었음) ·
|
||||||
|
진동은 걸음마다 ÷1.1 로 고침 · `tmp/tests/test_b09_cost_fixtures.py` → `resources/tester/` 이관(사본은 안 지움).
|
||||||
|
검증완료(전체 1782)
|
||||||
|
|
||||||
|
- [X] **안전관리비 B 절사 자리를 STmate 실증본 쪽으로 · SA 안전관리비 별도내역 · 고시 대조 벌** (`00919e86`) —
|
||||||
|
1.2배의 대상을 **호별 산정이 끝난 금액**으로(고시 제4조① 단서 문언 + 영월 2024 B 20,330,639 → **20,330,638**) ·
|
||||||
|
SA 「확인 대기」 → 별도내역 · 새 `test_b09_safety_notice.py`(별표1 표를 **원문 md 에서 읽어** 16칸 전수 대조).
|
||||||
|
⚠ **두 실증본이 갈림** — 거창 2025(STmate 출력 아님)는 옛 셈이라 B 가 1원 위, 사유로만 남기고 채택 안 함.
|
||||||
|
검증완료(전체 1911)
|
||||||
|
|
||||||
|
- [X] **제3조 적용 하한을 엔진이 읽게 함 · 1.2배 차례와 관급 ÷1.1 사유 명기** (`5a016979`) —
|
||||||
|
「총공사금액 2천만 원 이상」 미달이면 **줄 자체를 안 만듦**(A·B 곁줄도 · 0 원으로 안 채움) ·
|
||||||
|
견주는 값은 규모 기준액(고시의 「총공사금액」과 딱 같은 말은 아니나 계산 차례상 그것뿐) ·
|
||||||
|
관급 ÷1.1 은 고시에 없고 **실무 6건 공통**임을 두 자리에 적음. 검증완료(전체 1952)
|
||||||
|
|
||||||
|
- [X] **기계 수송비 회수(대수 × 왕복) 설계 입력 칸** (`1400b7b7`) —
|
||||||
|
산림품셈 10-4 · 건설품셈 8-1-3 이 **회당 단가 산출식만** 주고 대수·횟수를 안 정함(전수 확인) ·
|
||||||
|
`parse_trips`(비거나 0 이면 없음 · 기본값 안 둠) · `transport_amount`(곱 하나뿐) · `TRIPS_NOTE` ·
|
||||||
|
도는 자리 넷(저장·산출조건·화면·못 채운 자리). 검증완료(전체 1976)
|
||||||
|
|
||||||
|
- [X] **절사 자동보정을 이분으로 앉힘** (`47196780`) —
|
||||||
|
이윤 1원이 총공사비 1~2원이라 ÷1.1 어림이 지나쳐 거창 꼴 40 자리 중 16 이 끝자리 999 로 남았음 ·
|
||||||
|
목표를 못 박고 **가장 작은 보정액을 이분으로** 찾음 · 못 밟는 배수면 한 칸 아래(실측 0건) ·
|
||||||
|
40/40 앉음 · 깎인 몫 356~423원으로 늘 한 칸 안쪽 · 실무 6건 보정액 그대로. 검증완료(전체 2043)
|
||||||
|
|
||||||
|
- [X] **B09 700줄 넘은 모듈 둘 쪼갬** (`e0c5e364`) — **순수 분리**, 그 김에 아무것도 안 고침 ·
|
||||||
|
`BillOfQuantities.py` **733 → 597** + 새 `_BillOfQuantities_Input.py` 162 ·
|
||||||
|
`Engine_Cost.py` **725 → 609** + 새 `_Engine_Cost_Input.py` 136 ·
|
||||||
|
쓰던 이름은 본 모듈이 다시 내보내 그대로 돎. 검증완료(전체 **2102** · 두 번 돌려 같음 · B09 부분 398)
|
||||||
|
|
||||||
|
## 재기만 한 것 (고치는 것은 배정 대기)
|
||||||
|
|
||||||
|
- [X] **[저장]·[확정] 때 B09 서버 재계산** — 큰 그림은 규칙대로임. **저장물이 없고** 쓰기 열 자리가 전부
|
||||||
|
**입력만** 저장, 금액은 GET 마다 서버가 다시 계산. 브라우저 금액을 받아 적는 자리 **없음**.
|
||||||
|
- ⭐ **결함 하나** — `GET /estimation/cost-sheet` 의 `settings` 가 **엔진이 푼 값**이고 화면이 그대로
|
||||||
|
되돌려 보내 [저장] 한 번에 **23칸 중 22칸**이 박힘(안 고른 `cut_basis` · `vat_mode` · `duration_days` 까지).
|
||||||
|
기본값 판정이 바뀌어도 그 프로젝트엔 **안 닿음**. 계약·실행·기성은 `stored` 를 줘서 이 병이 없음.
|
||||||
|
고치는 법 두 갈래 — ㉠ GET 이 `stored` 를 draft 로 · ㉡ 화면이 기본값과 같은 칸을 안 보냄. **판정 대기**
|
||||||
|
- ⚠ **[확정]은 아무것도 다시 계산 안 함**(stage 만 COMPLETE) — 「반만 계산해 덮음」 병은 없으나
|
||||||
|
**확정 시점 값이 안 박힘**. 둘 것인지 박을 것인지 **판정 대기**
|
||||||
|
|
||||||
|
- [X] **BOX암거 막힘 셋 가름** — **정말 막힌 것은 하나뿐**.
|
||||||
|
- 12-01-01 — 막힘 아님. 호표가 **세 갈래 다 섬**(무근 58,635 · 철근 65,826 · 소형 117,270 원/㎥).
|
||||||
|
갈래 없이 부르면 제목이 없어 「일위대가 없음」으로 적힌 것
|
||||||
|
- 12-38 유로폼 — 막힘 아님. 설치·해체는 이미 섬 · 안 선 자리는 `12-38-02` 자재비 하나 ·
|
||||||
|
「자재 단가」 탭에 **패널+내부 패널(손료)** 또는 **임대료 ㎡당** 중 **하나**만 넣으면 섬
|
||||||
|
- ⭐ 12-19 강관비계 — 정말 막힘(아래)
|
||||||
|
|
||||||
|
- [X] **12-19 강관비계 밑수** — 원문에 **이미 적혀 있었고 못 읽은 쪽은 우리**였음.
|
||||||
|
- ① 고시 6612줄 `(단위: 공㎥당)` — 파서가 그 줄 꼴은 읽으나 **「공㎥」를 공종 단위로 안 받아**
|
||||||
|
(`_QUANTITY_UNITS` 에 `㎥` 만) `unit_of('FP-12-19')=None` · 원문 전체에서 「공㎥」는 **이 한 절뿐**
|
||||||
|
- ② **부피 못 냄** — B08 이 비계를 **㎡** 로 보냄(외벽 양면 + 시종점 두 면 · 울진 18.72㎡/m).
|
||||||
|
동바리는 이미 ㎥ 이고 12-20 도 `(단위: ㎥당)` 이라 **그쪽은 맞음**
|
||||||
|
- ③ 12-19 [주]는 「공사기간 손율」뿐 **폭 규정 없음** ⇒ 설계 입력 칸 필요
|
||||||
|
- **갈래 판정 대기** — ㉠ B08 이 공㎥로 보냄 / ㉡ B09 가 「비계 폭(m)」 칸으로 **단가**를 환산
|
||||||
|
(수량은 B08 정본 그대로 · `BORROWED_BASIS_PER` 꼴)
|
||||||
|
- 곁 — 12-19·12-20 [주] **「공사기간에 따른 손율」을 지금 안 걸고 있음**(유로폼 사용횟수와 다른 자리)
|
||||||
|
|
||||||
|
## 남은 것
|
||||||
|
|
||||||
|
- [ ] **`UnitPrice.py` 1403줄** — 700줄 규칙 밖. 쪼개기 **따로 배정 필요**
|
||||||
|
- [ ] 위 「판정 대기」 넷 — 기준 입력판 기본값 박힘 · 확정 못 박기 · 12-19 갈래 · 12-19/12-20 공사기간 손율
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# PLAN — 랩탑 메인 (1장 `[랩탑_메인]` 몫)
|
||||||
|
|
||||||
|
> ⚠ 임시 · 2026-09-15 시놀로지 덮어쓰기 때문 · 사용자 확인 전(브레인 지시). `PLAN.md` 는 읽기만 · 나중에 브레인이 창별 파일을 합쳐 `PLAN.md` 한 벌로 되돌림.
|
||||||
|
> git 에 명시 커밋으로 올림(PC 사이를 건너가고 이력이 남게 · 사용자 지침 「PLAN.md 는 git 밖 · 한 파일로만」 과 어긋나는 임시 조치).
|
||||||
|
|
||||||
|
## 1. 축 세우기 — 랩탑 메인 차례
|
||||||
|
|
||||||
|
### 남은 것
|
||||||
|
- [ ] ⚠ 자원 이름이 겹치면 남의 공종 풀이가 바뀜(명세 2장 이름 맞춤의 부작용) — 자원 목록에 「양수기」 를 넣자 12-26 물푸기 「양수기(150m/m)」 줄 풀이가 바뀌어 제목이 사라짐 → 지금은 「유인헬기방제 양수기」 로 피함(임시) · 언젠가 공종 범위·코드로 맞출 자리
|
||||||
|
- [ ] ㉯ 나머지 — 제목 없음 32(4-1 수확베기·4-2 부모·6-2 풀베기(예취기)·13-10-2 말뚝박기 등 · 표 읽기에서 먼저 막혀 드러남) · 2장 표 읽는 한 벌(`B09_Estimation_Consumables`)로 더할 자리
|
||||||
|
- [ ] 막힌 셋 풀기(원문에 값이 없음 · 입력·견적 칸이 서야 함): 12-26 물푸기 가동시간 · 10-7-4 모노레일 ps·임대료·운반거리 · 10-8-3 케이블크레인 ℓ/kWh · 8-6-3 지상방제 1일 작업량·1톤 트럭
|
||||||
|
- [ ] 손료계수 없는 기계 224종 가르기 — 파서가 놓친 것(크롤러드릴(공기식) 5401 1,548·2,506 · 크레인(무한궤도) 15 등)과 원문에 없는 것이 섞임
|
||||||
|
- [ ] 초류종자살포 B09 둘 — 트럭 4.5ton 카탈로그 없음(덤프·크레인만) · 종자 규격 미정 후보 넷(화약류 모양 설계자 선택 칸 + 사유)
|
||||||
|
- [ ] 10-7-4 모노레일 제목의 보통인부 두 번 셈(4인) — 막힌 채 사유만 적음 · 풀리는 날 운전 단가표 2인을 빼고 셀 것
|
||||||
|
- [ ] 소형 장비 1일 소모 표(㉯) 가운데 2-1-2 경유 표(집재장비) · 천공기 · 예취기 — 쓰는 공종이 풀릴 때 같은 한 벌로
|
||||||
|
|
||||||
|
### 검증완료(2026-09-14 밤 ~ 09-15 · 커밋 해시)
|
||||||
|
- [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] ㉡-2 유인헬기방제 8-6-1 — `AreaSet`: 양수기 1대1일 호표(휘발유 10ℓ + 잡품 95% + 손료 칸) ÷ 1일 면적 · 깃발 20ha당 1개(칸) · 헬기 임차료(견적 칸) · 소방관서 급수 단서 · 자원 이름 「유인헬기방제 양수기·깃발」 · 소형 +227원/ha · 대형 +90 · 8-6-3 은 막힌 채 사유 둘(1일 작업량 없음 · 15ha 는 연료 설명 / 1톤 트럭 카탈로그 없음). 검증완료(9563d060)
|
||||||
|
- [X] ㉡-1 체인톱 — `B09_Estimation_Consumables`(2장 표 읽는 한 벌): 체인톱 1대1일 호표 = 휘발유 5.6ℓ + 잡품 40% + 손료 0.0084 × 구입가(칸) · 4-2-2 「벌목부」·6-5 「특별인부 (체인톱 사용)」 인원만큼 · 체인오일 넣은 쪽 · 4-2-2 +31·+41·+53원/㎡ · 6-5 +43,821원/ha · 프로젝트 지장목제거 +548,652(본체 109,122,948 → 109,671,600). 검증완료(852ad6c7)
|
||||||
|
- [X] ㉠ 말없이 버려지던 기계·연료 줄 — 자원 축 줄 읽기 세 자리(규격 앞 장비 이름 · 값·이름 둘 다 못 읽음 · 수량 빈 연료 줄) → 「못 맞춤 + 일부만」 · 새로 사유 붙은 줄 8 · 새로 막힌 공종 5 · 처음 승인받은 `_names_a_machine` 넓히기는 재어 보니 0 을 막아 정정. 검증완료(2f9d42ac)
|
||||||
|
- [X] ㉯ 셈 — 소형 장비·연료 이름 나오는 공종 41 · 제목 선 9 모두 기계·연료 줄 0 · 금액까지 서던 7 · 4-2-2 는 부모 표에만 체인톱(제목 선 자식은 그 하나)
|
||||||
|
- [X] ㉰ 이동식 임목 파쇄 8-11 — 8-4 운전경비 「-」 라 없던 파쇄기 기계 층을 8-11 [주]⑤ 13.5ℓ·잡재료 16%·운전자 1인으로 · ㎥당 = 1/3.5 + 보통인부 2인÷8h÷Q · 파쇄기날 칸 · 51,029원/㎥. 검증완료(0fc64fa7)
|
||||||
|
- [X] ㉱ 조립 줄이 조각 「일부만」·「밑수 없음」 올림 · `_material_row` 순수 이동(710→658). 검증완료(16d34615)
|
||||||
|
- [X] ㉰ 표토제거 답외구간 9-15-2 — 불도저 식 Q1 47.92 ÷ T 0.2 = 239.60 ㎡/hr · 618원/㎡ · 영월 호표 E 자리에 e(0.96) 기록(원문 또렷하면 원문) · 프로젝트 +6,151,768(102,971,180 → 109,122,948). 검증완료(ac66a25c)
|
||||||
|
- [X] 판정표 안 읽은 줄 자동 목록(`_unread_rows`) — 9표 늘어난 줄 0·+1·+4·+8·+6·+7·+1. 검증완료(9deaf479)
|
||||||
|
- [X] 봉상후렉시블 셋 + 12-34-1 — 판정표 F0350·F0351·F0354·F0385 · 별칭 넷 → 엔진식 4611-0350 · 실무 영월 「콘크리트타설(5-5)」 같은 모양 · 새 제목 다섯. 검증완료(15d91be7)
|
||||||
|
- [X] 672 품 할증 겹침 — 합산 근거 건설 공통 1-4-2(교차 참조) · 동일성격 단서 화면. 검증완료(8dddc768)
|
||||||
|
- [X] 진동기 + 연료 종류 — (4611) 원문값 되살림 · 12-15 → 엔진식 · `fuel_kind` 대로 휘발유 · 중기경비 장 `#암석` 뒤처리 · 휘발유 기계 10종 전수 대조. 검증완료(7ea25982)
|
||||||
|
- [X] 661 암석 손료보정(8-1-7 1) — `B09_Estimation_RockLoss` · 봉화 암석 계수 셋 재현 · 바뀐 제목 17 · E1~E3 원문에 없음 · 착암기 8-3 에도 손료 줄 없음. 검증완료(a8427666)
|
||||||
|
- [X] 301 유로폼 12-38-2 — `B09_Estimation_Euroform` · 손료 표 수량 곧장 · 임대료 한 줄 · 가드 ③ 에서 12-38-02 뺌 · 봉화 재료비 4,500·3,697 일치. 검증완료(efaf2403)
|
||||||
|
- [X] 300 발파 9-5-1 — 화약류 자재 단가 칸 · 착암기 부수물 손료 고시 없음 사유. 검증완료(09247fd1)
|
||||||
|
- [X] ⑭ `form_basis` 화면 — 판정표 까닭 회색 한 줄. 검증완료(c7f34b8c)
|
||||||
|
|
||||||
|
### 규칙(오늘 세운 것)
|
||||||
|
- 원문이 또렷하면 원문 · 애매하면 실무 역산 · 어긋남은 늘 기록
|
||||||
|
- 버릴 때는 사유를 남길 것(읽는 자리에서 조용히 버리지 않음)
|
||||||
|
- 빨간 시험은 구현까지 한 호흡에 · autopush 전 git status
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schema_version": "1.0",
|
"schema_version": "1.0",
|
||||||
"dataset_id": "data_work_item_master_manifest",
|
"dataset_id": "data_work_item_master_manifest",
|
||||||
"generated_at": "2026-09-14T23:14:39+09:00",
|
"generated_at": "2026-09-15T03:10:55+09:00",
|
||||||
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
|
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
|
||||||
"source": {
|
"source": {
|
||||||
"dataset_id": "pum_forest",
|
"dataset_id": "pum_forest",
|
||||||
@@ -12,8 +12,8 @@
|
|||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"file": "work_item_master_2026-01-01.json",
|
"file": "work_item_master_2026-01-01.json",
|
||||||
"sha256": "708a6a80d016c42d3402bd2a6cb5266fa18e1e2b5301bac823e6ff79d17fb2b6",
|
"sha256": "480f9510cdd2fe223b54818fb7180f8c6114d6e33e45cd3c6aa829c64de27eb3",
|
||||||
"size_bytes": 838969
|
"size_bytes": 839153
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "form_undetermined_2026-01-01.json",
|
"file": "form_undetermined_2026-01-01.json",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"dataset_id": "work_item_master_forest",
|
"dataset_id": "work_item_master_forest",
|
||||||
"effective_date": "2026-01-01",
|
"effective_date": "2026-01-01",
|
||||||
"pum_edition": "2026-01-01",
|
"pum_edition": "2026-01-01",
|
||||||
"generated_at": "2026-09-14T23:14:39+09:00",
|
"generated_at": "2026-09-15T03:10:55+09:00",
|
||||||
"dataset_version": {
|
"dataset_version": {
|
||||||
"dataset_id": "pum_forest",
|
"dataset_id": "pum_forest",
|
||||||
"effective_date": "2026-01-01",
|
"effective_date": "2026-01-01",
|
||||||
@@ -40210,7 +40210,10 @@
|
|||||||
"formula_rows": [],
|
"formula_rows": [],
|
||||||
"special_glyphs": [],
|
"special_glyphs": [],
|
||||||
"capacity_formula_here": false,
|
"capacity_formula_here": false,
|
||||||
"variant_key": [],
|
"variant_key": [
|
||||||
|
"무한궤도",
|
||||||
|
"타이어"
|
||||||
|
],
|
||||||
"condition_note": [
|
"condition_note": [
|
||||||
"장 비",
|
"장 비",
|
||||||
"단 위",
|
"단 위",
|
||||||
@@ -40233,7 +40236,10 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"variant_keys": []
|
"variant_keys": [
|
||||||
|
"무한궤도",
|
||||||
|
"타이어"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"work_item_code": "FP-14-02",
|
"work_item_code": "FP-14-02",
|
||||||
@@ -40260,7 +40266,10 @@
|
|||||||
"formula_rows": [],
|
"formula_rows": [],
|
||||||
"special_glyphs": [],
|
"special_glyphs": [],
|
||||||
"capacity_formula_here": false,
|
"capacity_formula_here": false,
|
||||||
"variant_key": [],
|
"variant_key": [
|
||||||
|
"무한궤도",
|
||||||
|
"타이어"
|
||||||
|
],
|
||||||
"condition_note": [
|
"condition_note": [
|
||||||
"장 비",
|
"장 비",
|
||||||
"단 위",
|
"단 위",
|
||||||
@@ -42099,7 +42108,10 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"variant_keys": []
|
"variant_keys": [
|
||||||
|
"무한궤도",
|
||||||
|
"타이어"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""시간 줄 판정표 — 14-1 입목뿌리 밑막이 · 14-2 근주이식(2026-09-15 브레인 ㉡-3 판정 ①②③).
|
||||||
|
|
||||||
|
원문(10본당): 14-2 「0.8㎥ 굴착기 | h | 0.25 · 보통인부 | h | 0.66」 · 14-1 「0.8㎥ 굴착기(우드그랩 부착) | h | 0.17 · 보통인부 | h | 0.5」
|
||||||
|
⚠ 인력 단위가 **시간** — 옛 14-2 11,356원은 0.66 을 인으로 셈(8배) + 굴착기 줄이 말없이 빠짐.
|
||||||
|
장비 h 그대로 · 인력 h ÷ 8(총칙 「일일 작업시간 8시간」) · ÷ 10본
|
||||||
|
① 「0.8㎥ 굴착기」 형식을 원문이 안 적음 — 무한궤도 0201-0080 · 타이어 0211-0080 두 갈래 · 제안 없음
|
||||||
|
② 우드그랩 = 부착용 집게 7206-0070(규격 「0.6∼0.8」 이 0.8 을 품음) — 이름이 다름을 사유에
|
||||||
|
③ 막힌 셋(12-26 · 10-7-4 · 10-8-3)은 옛 값이 왜 틀렸는지 사유에
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note
|
||||||
|
from B09_Estimation.B09_Estimation_UnitPrice import cached_build
|
||||||
|
|
||||||
|
|
||||||
|
def _rows(book, title: str) -> dict[str, Decimal]:
|
||||||
|
return {r.ref_code: r.quantity for r in book.details[title]}
|
||||||
|
|
||||||
|
|
||||||
|
def test_근주이식은_두_갈래_장비_시간과_인력_시간_나누기_8() -> None:
|
||||||
|
build = cached_build()
|
||||||
|
book = build.book
|
||||||
|
crawler = _rows(book, "B-FP-14-02#무한궤도")
|
||||||
|
tire = _rows(book, "B-FP-14-02#타이어")
|
||||||
|
assert crawler["X-0201-0080"] == Decimal("0.25") / 10, crawler
|
||||||
|
assert tire["X-0211-0080"] == Decimal("0.25") / 10, tire
|
||||||
|
assert crawler["1002"] == Decimal("0.66") / 8 / 10, crawler
|
||||||
|
assert "FP-14-02" not in build.partial_ratio
|
||||||
|
assert "FP-14-02" not in build.default_variants # 제안 없음(①)
|
||||||
|
|
||||||
|
|
||||||
|
def test_입목뿌리_밑막이는_부착용_집게를_같은_시간만큼() -> None:
|
||||||
|
book = cached_build().book
|
||||||
|
rows = _rows(book, "B-FP-14-01#무한궤도")
|
||||||
|
# 부착용 집게와 조합 — 본체는 잡재료 16% 층(`#조합` · 품셈 제8장 [주]⑤)
|
||||||
|
assert (
|
||||||
|
rows["X-0201-0080#조합"] == Decimal("0.17") / 10
|
||||||
|
and rows["X-7206-0070"] == Decimal("0.17") / 10
|
||||||
|
)
|
||||||
|
assert rows["1002"] == Decimal("0.5") / 8 / 10
|
||||||
|
note = known_gap_note("FP-14-01")
|
||||||
|
assert "우드그랩" in note and "부착용 집게" in note and "7206-0070" in note, note
|
||||||
|
|
||||||
|
|
||||||
|
def test_사유는_시간_단위와_옛_값의_잘못을_밝히고_무한궤도를_권하지_않음() -> None:
|
||||||
|
note = known_gap_note("FP-14-02")
|
||||||
|
assert "시간" in note and "8시간" in note and "8배" in note, note
|
||||||
|
assert "영월" not in note and "제안" not in note and "10-12" not in note, note
|
||||||
|
|
||||||
|
|
||||||
|
def test_막힌_셋은_옛_값이_왜_틀렸는지를_사유에() -> None:
|
||||||
|
build = cached_build()
|
||||||
|
assert "보통인부 1인 값뿐" in known_gap_note("FP-12-26")
|
||||||
|
assert "같은 사람" in known_gap_note("FP-10-07-04") and "두 번" in known_gap_note("FP-10-07-04")
|
||||||
|
assert "특별인부 1인 값뿐" in known_gap_note("FP-10-08-03")
|
||||||
|
for code in ("FP-12-26", "FP-10-07-04", "FP-10-08-03"):
|
||||||
|
assert code in build.partial_ratio, code
|
||||||
@@ -15,7 +15,8 @@ from B09_Estimation.B09_Estimation_UnitPrice import cached_build
|
|||||||
|
|
||||||
def test_기계_연료_줄이_안_붙은_다섯_공종은_일부만으로_막히고_사유() -> None:
|
def test_기계_연료_줄이_안_붙은_다섯_공종은_일부만으로_막히고_사유() -> None:
|
||||||
build = cached_build()
|
build = cached_build()
|
||||||
for code in ("FP-12-26", "FP-14-02", "FP-14-01", "FP-10-08-03", "FP-10-07-04"):
|
# 14-1·14-2 는 뒤이어 시간 줄 판정표가 읽어 풀림(`test_b09_hour_rows` · 2026-09-15 ㉡-3)
|
||||||
|
for code in ("FP-12-26", "FP-10-08-03", "FP-10-07-04"):
|
||||||
assert code in build.partial_ratio, code
|
assert code in build.partial_ratio, code
|
||||||
assert "기계·연료 줄" in (build.component_gaps.get(code) or ""), (
|
assert "기계·연료 줄" in (build.component_gaps.get(code) or ""), (
|
||||||
code,
|
code,
|
||||||
@@ -23,7 +24,7 @@ def test_기계_연료_줄이_안_붙은_다섯_공종은_일부만으로_막히
|
|||||||
)
|
)
|
||||||
left = " ".join(build.unattached.get("FP-12-26", []))
|
left = " ".join(build.unattached.get("FP-12-26", []))
|
||||||
assert "디젤엔진" in left and "양수기" in left, left
|
assert "디젤엔진" in left and "양수기" in left, left
|
||||||
assert "0.8㎥ 굴착기" in " ".join(build.unattached.get("FP-14-02", []))
|
assert "FP-14-02" not in build.partial_ratio and "FP-14-01" not in build.partial_ratio
|
||||||
|
|
||||||
|
|
||||||
def test_머리_줄과_인력만인_정상_공종은_안_막힘() -> None:
|
def test_머리_줄과_인력만인_정상_공종은_안_막힘() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user