feat(B09): 열이 자원인 표(전치형) 읽기 + 오독 3종 차단
품셈에는 자원이 행이 아니라 **열 머리**에 오는 표가 39건 있음 (「구 분 | 콘크리트공(인) | 보통인부(인)」, 행은 무근·철근·소형구조물). 행-자원으로 읽어 콘크리트 타설(12-1)이 하나도 안 서고 있었음 - 갈래(variant)마다 따로 세움 — 품이 달라 뭉치면 어느 것도 안 맞음. 코드는 `B-FP-12-01-01#무근구조물`, 이름에 갈래를 적어 사람이 고를 수 있음 - 레디믹스트콘크리트 타설: 무근 58,635.0 / 철근 65,826.4 / 소형 117,270.0 원 읽다가 잡은 오독 3종 — 전부 **고쳐 읽지 않고 버림** (자리를 단정할 수 없음) - 병합 셀로 한 칸 밀린 행: 목재틀흙막이가 등급 글자를 건축목공 자리에 두어 단가 503만원으로 섰음 - 좌우 두 판이 붙은 표: 소운반에서 거리값 60 이 「보통인부 60인」으로 읽혀 1,553만원이 섰음. 열 머리가 되풀이되면 표째 버림 - 같은 갈래 이름 중복: 목재틀흙막이 「상」이 8.760 + 13.767 로 합산돼 667만원이 섰음. 그 줄들을 버림 - 갈래 이름에 자원용 머리글 필터를 쓰던 것도 고침 — 「중」·「상」 등급이 통째로 지워지고 있었음. 합계 줄만 걸러 냄 기준 단위 미상 드러내기 (막지 않음 — 막으면 125 중 122 가 멈춤) - `unknown_basis` 122건, 그중 100만원 넘는 10건을 목록으로 냄 (떼채취 평떼 1,032,408원 등 — 「100매당」류 묶음 기준 의심) - 내역서 줄 비고에도 「기준 단위가 표에 없습니다 — 수량 단위와 같다고 보고 곱했습니다」를 적음. 반영률 문구를 덮지 않고 이어 붙임 700줄 제한으로 전치형 처리를 `_Transposed.py` 로 분리 (647 + 138줄) 검증: pytest 163 통과(신규 5). 자원 축 198줄·갈래 54, 일위대가 85 → 125, 분포 최소 233.4 · 중앙 72,268.5 · 최대 5,030,954.6 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -423,6 +423,21 @@ def _leaf_row(
|
|||||||
)
|
)
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
title = unit_prices.book.title(price_code)
|
||||||
|
if not title.unit:
|
||||||
|
# ⚠ 품셈 표가 기준 단위를 안 준 단가다 — 「10㎡당」 같은 묶음 기준일 수 있다.
|
||||||
|
# 값을 막지는 않되(막으면 대부분이 멈춘다) **모르는 채 곱했다는 사실을 적는다**.
|
||||||
|
# 비고를 **덮지 않고 잇는다** — 반영률 문구가 먼저 적혀 있을 수 있다.
|
||||||
|
row.note = " / ".join(
|
||||||
|
part
|
||||||
|
for part in (
|
||||||
|
row.note,
|
||||||
|
f"단가의 기준 단위가 표에 없습니다 — B08 수량 단위({row.unit})와 같다고 "
|
||||||
|
"보고 곱했습니다. 확인 필요.",
|
||||||
|
)
|
||||||
|
if part
|
||||||
|
)
|
||||||
|
|
||||||
unit_money = unit_prices.book.resolve(price_code)
|
unit_money = unit_prices.book.resolve(price_code)
|
||||||
line = unit_money.scaled(item.quantity)
|
line = unit_money.scaled(item.quantity)
|
||||||
row.unit_price_krw = round_at(unit_money.total, OutputPlace.UNIT_PRICE_ROW)
|
row.unit_price_krw = round_at(unit_money.total, OutputPlace.UNIT_PRICE_ROW)
|
||||||
|
|||||||
@@ -366,6 +366,9 @@ class ResourceRow:
|
|||||||
amount: Decimal
|
amount: Decimal
|
||||||
amount_unit: str
|
amount_unit: str
|
||||||
raw_row_index: int
|
raw_row_index: int
|
||||||
|
#: 규격 갈래 — 열이 자원인 표에서 행 이름(「무근구조물」). 없으면 빈 문자열.
|
||||||
|
#: **갈래마다 품이 다르므로 한 일위대가로 뭉치지 않는다.**
|
||||||
|
variant: str = ""
|
||||||
#: 분류 딱지가 달고 온 배분율 — 「인력(10%)」이면 `10`. 없으면 `None`.
|
#: 분류 딱지가 달고 온 배분율 — 「인력(10%)」이면 `10`. 없으면 `None`.
|
||||||
#: ⚠ **이 값을 안 보면 단가가 조용히 틀린다** — 인력 몫 원단위를 전량에 곱하게 된다
|
#: ⚠ **이 값을 안 보면 단가가 조용히 틀린다** — 인력 몫 원단위를 전량에 곱하게 된다
|
||||||
#: (2026-09-08 실측: 측구터파기 39,575.6원/㎥ 이 인력 10 % 몫만이었다).
|
#: (2026-09-08 실측: 측구터파기 39,575.6원/㎥ 이 인력 10 % 몫만이었다).
|
||||||
@@ -383,6 +386,7 @@ class ResourceRow:
|
|||||||
"amount": str(self.amount),
|
"amount": str(self.amount),
|
||||||
"amount_unit": self.amount_unit,
|
"amount_unit": self.amount_unit,
|
||||||
"raw_row_index": self.raw_row_index,
|
"raw_row_index": self.raw_row_index,
|
||||||
|
"variant": self.variant,
|
||||||
"group_ratio_pct": None if self.group_ratio_pct is None else str(self.group_ratio_pct),
|
"group_ratio_pct": None if self.group_ratio_pct is None else str(self.group_ratio_pct),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,6 +460,15 @@ def match_table(
|
|||||||
basis_quantity = None if basis is None else Decimal(str(basis))
|
basis_quantity = None if basis is None else Decimal(str(basis))
|
||||||
unit = table.get("basis_unit") or ""
|
unit = table.get("basis_unit") or ""
|
||||||
|
|
||||||
|
# ⚠ **자원이 열 머리에 오는 표가 따로 있다** (2026-09-08 발견, 39 표).
|
||||||
|
# 「구 분 | 콘크리트공(인) | 보통인부(인)」처럼 **열이 자원**이고 행은 규격 갈래
|
||||||
|
# (무근·철근·소형구조물)다. 행을 자원으로 읽는 길로 보내면 통째로 못 맞춘다 —
|
||||||
|
# 콘크리트 타설(12-1)이 그래서 하나도 안 서고 있었다.
|
||||||
|
from B09_Estimation.B09_Estimation_ResourceAxis_Transposed import match_transposed_table
|
||||||
|
|
||||||
|
if match_transposed_table(node, table, catalog, result, basis_quantity, unit):
|
||||||
|
return
|
||||||
|
|
||||||
for index, row in enumerate(table.get("raw_row", [])):
|
for index, row in enumerate(table.get("raw_row", [])):
|
||||||
cells = [str(c) for c in row]
|
cells = [str(c) for c in row]
|
||||||
if not cells:
|
if not cells:
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
"""B09 원가계산 — **열이 자원인 표** 읽기 (자원 축 보조, 2026-09-08).
|
||||||
|
|
||||||
|
품셈 표에는 자원이 **행**이 아니라 **열 머리**에 오는 모양이 따로 있다 (39 표).
|
||||||
|
|
||||||
|
구 분 | 콘크리트공(인) | 보통인부(인)
|
||||||
|
무근구조물 | 0.12 | 0.15
|
||||||
|
철근구조물 | 0.14 | 0.16
|
||||||
|
|
||||||
|
행은 **규격 갈래**(무근·철근·소형구조물)이고 갈래마다 품이 다르다. 이 모양을 행-자원
|
||||||
|
표로 읽으면 통째로 안 맞는다 — 콘크리트 타설(12-1)이 그래서 하나도 안 서고 있었다.
|
||||||
|
|
||||||
|
⚠ **자리 밀림을 고쳐 읽지 않는다.** 첫 칸이 병합된 표는 값이 한 칸씩 밀려 오는데
|
||||||
|
(목재틀흙막이가 「건축목공 8.760」 자리에 등급 글자를 두어 단가가 503만원으로 섰다),
|
||||||
|
밀린 행은 **버리고 `unmatched` 에 남긴다.** 어느 칸이 어느 자원인지 단정할 수 없다.
|
||||||
|
|
||||||
|
`B09_Estimation_ResourceAxis` 가 700줄 제한에 걸려 이 표 모양만 떼어 낸 파일이다.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from B09_Estimation.B09_Estimation_ResourceAxis import (
|
||||||
|
AxisResult,
|
||||||
|
ResourceCatalog,
|
||||||
|
ResourceRow,
|
||||||
|
UnmatchedRow,
|
||||||
|
parse_amount,
|
||||||
|
split_name_and_spec,
|
||||||
|
)
|
||||||
|
|
||||||
|
#: 갈래 이름 자리에서 걸러 낼 말 — **합계 줄만**이다. 넓게 잡으면 등급이 지워진다.
|
||||||
|
_TOTAL_LABELS = ("계", "합계", "소계", "총계", "구분")
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_label(text: str) -> str:
|
||||||
|
return "".join(str(text).split())
|
||||||
|
|
||||||
|
|
||||||
|
def transposed_columns(table: dict[str, Any], catalog: ResourceCatalog) -> list[tuple[int, Any]]:
|
||||||
|
"""열 머리에서 자원을 찾는다. `[(열 번호, 카탈로그 줄)]`.
|
||||||
|
|
||||||
|
첫 칸은 갈래 이름(「구 분」)이라 **1번 열부터** 본다. 카탈로그에 있는 이름만
|
||||||
|
자원으로 본다 — 필터로 거르지 않는다(넓은 필터가 정상 자원을 지운 전례).
|
||||||
|
"""
|
||||||
|
headers = table.get("condition_note") or []
|
||||||
|
found: list[tuple[int, Any]] = []
|
||||||
|
for position, header in enumerate(headers[1:], start=1):
|
||||||
|
name, spec = split_name_and_spec(str(header))
|
||||||
|
entry = catalog.resolve(name, spec)
|
||||||
|
if entry is None and not spec:
|
||||||
|
candidates = catalog.by_name(name)
|
||||||
|
entry = candidates[0] if len(candidates) == 1 else None
|
||||||
|
if entry is not None:
|
||||||
|
found.append((position, entry))
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def match_transposed_table(
|
||||||
|
node: dict[str, Any],
|
||||||
|
table: dict[str, Any],
|
||||||
|
catalog: ResourceCatalog,
|
||||||
|
result: AxisResult,
|
||||||
|
basis_quantity: Decimal | None,
|
||||||
|
unit: str,
|
||||||
|
) -> bool:
|
||||||
|
"""열이 자원인 표를 읽는다. 그런 표가 아니면 `False` 를 돌려 원래 길로 보낸다.
|
||||||
|
|
||||||
|
행마다 **규격 갈래 하나**가 되므로 `variant` 를 달아 둔다 — 「무근구조물」과
|
||||||
|
「철근구조물」은 품이 달라 **한 일위대가로 뭉치면 안 된다**.
|
||||||
|
"""
|
||||||
|
columns = transposed_columns(table, catalog)
|
||||||
|
if not columns:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ⚠ **좌우로 두 판이 붙은 표는 통째로 버린다.** 열 머리가 되풀이되면
|
||||||
|
# (「거리 | 보통인부 | 거리 | 보통인부」) 오른쪽 판의 **거리값이 인원으로** 읽힌다
|
||||||
|
# — 2026-09-08 실측: 소운반이 「보통인부 60인」이 되어 단가가 1,553만원으로 섰다.
|
||||||
|
# 판 경계를 짐작해 읽지 않는다.
|
||||||
|
codes = [entry.code for _, entry in columns]
|
||||||
|
if len(codes) != len(set(codes)):
|
||||||
|
result.unmatched.append(
|
||||||
|
UnmatchedRow(
|
||||||
|
work_item_code=node.get("work_item_code", ""),
|
||||||
|
pum_table_id=str(table.get("pum_table_id", "")),
|
||||||
|
cell=" | ".join(str(c) for c in (table.get("condition_note") or [])),
|
||||||
|
reason="열 머리가 되풀이되는 두 판 짜리 표 — 자리를 단정할 수 없어 버렸습니다.",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ⚠ **갈래 이름이 되풀이되면 그 줄들을 버린다.** 첫 칸이 병합된 표에서 상위 등급이
|
||||||
|
# 떨어져 나가면 「상」이 두 번 나오고, 그대로 두면 서로 다른 등급의 품이 **합산**된다
|
||||||
|
# (2026-09-08 실측: 목재틀흙막이 「상」이 8.760 + 13.767 = 22.5 인이 되어 단가가
|
||||||
|
# 667만원으로 섰다). 어느 등급인지 단정할 수 없으므로 고쳐 읽지 않는다.
|
||||||
|
labels = [str(row[0]).strip() for row in (table.get("raw_row") or []) if row]
|
||||||
|
repeated = {label for label in labels if label and labels.count(label) > 1}
|
||||||
|
|
||||||
|
work_item_code = node.get("work_item_code", "")
|
||||||
|
table_id = str(table.get("pum_table_id", ""))
|
||||||
|
form = str(table.get("pum_form", ""))
|
||||||
|
matched_any = False
|
||||||
|
|
||||||
|
for index, row in enumerate(table.get("raw_row", [])):
|
||||||
|
cells = [str(c) for c in row]
|
||||||
|
if not cells:
|
||||||
|
continue
|
||||||
|
variant = cells[0].strip()
|
||||||
|
# ⚠ 첫 칸은 **갈래 이름**이지 자원 이름이 아니다 — 자원용 머리글 필터를 여기 쓰면
|
||||||
|
# 「중」·「상」 같은 정상 등급이 통째로 지워진다(2026-09-08 실측: 목재틀흙막이의
|
||||||
|
# 중·상 등급이 사라지고 상등구조만 남았다). 합계 줄만 걸러 낸다.
|
||||||
|
if not variant or _normalize_label(variant) in _TOTAL_LABELS:
|
||||||
|
continue
|
||||||
|
if variant in repeated:
|
||||||
|
result.unmatched.append(
|
||||||
|
UnmatchedRow(
|
||||||
|
work_item_code=work_item_code,
|
||||||
|
pum_table_id=table_id,
|
||||||
|
cell=variant,
|
||||||
|
reason="같은 갈래 이름이 두 번 나오는 표 — 등급을 단정할 수 없어 버렸습니다.",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
# ⚠ **자리 밀림 검사** — 자원 열 가운데 하나라도 수가 아니면 그 행은 밀린 것이다.
|
||||||
|
# 첫 칸이 병합된 표에서 값이 한 칸씩 밀려 들어온다(2026-09-08 실측: 목재틀흙막이가
|
||||||
|
# 「건축목공 8.760」 자리에 등급 글자를 두어 단가가 503만원으로 섰다).
|
||||||
|
# **밀린 행은 고쳐 읽지 않고 버린다** — 어느 칸이 어느 자원인지 단정할 수 없다.
|
||||||
|
readable = [
|
||||||
|
parse_amount(cells[position]) if position < len(cells) else None
|
||||||
|
for position, _ in columns
|
||||||
|
]
|
||||||
|
if any(value is None for value in readable) and any(
|
||||||
|
value is not None for value in readable
|
||||||
|
):
|
||||||
|
result.unmatched.append(
|
||||||
|
UnmatchedRow(
|
||||||
|
work_item_code=work_item_code,
|
||||||
|
pum_table_id=table_id,
|
||||||
|
cell=variant,
|
||||||
|
reason="자원 열의 값이 한 칸 밀린 행 — 자리를 단정할 수 없어 버렸습니다.",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for position, entry in columns:
|
||||||
|
if position >= len(cells):
|
||||||
|
# 칸이 모자란 행 — **자리를 밀어 읽지 않는다**. 밀려 읽으면 다른 직종의
|
||||||
|
# 품이 붙는다(기계경비 표에서 실제로 겪은 사고).
|
||||||
|
result.unmatched.append(
|
||||||
|
UnmatchedRow(
|
||||||
|
work_item_code=work_item_code,
|
||||||
|
pum_table_id=table_id,
|
||||||
|
cell=f"{variant} / {entry.name}",
|
||||||
|
reason="칸 수가 열 머리와 안 맞아 버렸습니다(자리 밀림 방지).",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
amount = parse_amount(cells[position])
|
||||||
|
if amount is None:
|
||||||
|
continue
|
||||||
|
if basis_quantity not in (None, 0, Decimal(1)):
|
||||||
|
amount = amount / basis_quantity
|
||||||
|
result.rows.append(
|
||||||
|
ResourceRow(
|
||||||
|
work_item_code=work_item_code,
|
||||||
|
pum_table_id=table_id,
|
||||||
|
pum_form=form,
|
||||||
|
resource_kind=entry.kind,
|
||||||
|
resource_code=entry.code,
|
||||||
|
resource_name=entry.name,
|
||||||
|
resource_spec=entry.spec,
|
||||||
|
amount=amount,
|
||||||
|
amount_unit=unit,
|
||||||
|
raw_row_index=index,
|
||||||
|
variant=variant,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
matched_any = True
|
||||||
|
return matched_any
|
||||||
@@ -56,6 +56,11 @@ _ZERO = Decimal(0)
|
|||||||
FUEL_CODE_PREFIX = "M-FUEL-"
|
FUEL_CODE_PREFIX = "M-FUEL-"
|
||||||
#: 일위대가 총액이 이보다 작으면 **성분이 빠졌을 가능성**이 크다 — 값이 있어도 경고한다.
|
#: 일위대가 총액이 이보다 작으면 **성분이 빠졌을 가능성**이 크다 — 값이 있어도 경고한다.
|
||||||
SUSPICIOUSLY_LOW_KRW = Decimal(100)
|
SUSPICIOUSLY_LOW_KRW = Decimal(100)
|
||||||
|
#: 기준 단위를 모르는 채 이 금액을 넘으면 **사람이 한 번 봐야 한다**.
|
||||||
|
#: 품셈 표가 「10㎡당」처럼 묶음 기준일 수 있어 값 자체는 맞고 기준만 모르는 경우가 많다
|
||||||
|
#: (2026-09-08: 목재틀흙막이 상등구조 = 건축목공 16.975인 → 503만원. 값은 품셈대로다).
|
||||||
|
#: **막지 않고 드러내기만 한다** — 막으면 120 중 117 이 멈춘다.
|
||||||
|
SUSPICIOUSLY_HIGH_KRW = Decimal(1_000_000)
|
||||||
|
|
||||||
|
|
||||||
def _slots(value: Decimal) -> list[Decimal | None]:
|
def _slots(value: Decimal) -> list[Decimal | None]:
|
||||||
@@ -76,6 +81,8 @@ class UnitPriceBuild:
|
|||||||
partial_ratio: dict[str, Decimal] = field(default_factory=dict)
|
partial_ratio: dict[str, Decimal] = field(default_factory=dict)
|
||||||
#: 시공능력 공식으로 장비 몫을 세운 공종 — 산출근거를 화면에 그대로 보인다.
|
#: 시공능력 공식으로 장비 몫을 세운 공종 — 산출근거를 화면에 그대로 보인다.
|
||||||
cycle_factors: dict[str, CycleFactors] = field(default_factory=dict)
|
cycle_factors: dict[str, CycleFactors] = field(default_factory=dict)
|
||||||
|
#: 공종 하나가 낳은 규격 갈래들 — 「무근구조물」·「철근구조물」·「소형구조물」.
|
||||||
|
variants: dict[str, list[str]] = field(default_factory=dict)
|
||||||
#: 계수를 못 세운 표 — **무엇이 없는지**를 들고 있는다.
|
#: 계수를 못 세운 표 — **무엇이 없는지**를 들고 있는다.
|
||||||
factor_gaps: dict[str, FactorGap] = field(default_factory=dict)
|
factor_gaps: dict[str, FactorGap] = field(default_factory=dict)
|
||||||
|
|
||||||
@@ -208,12 +215,14 @@ def build_unit_prices(axis: AxisResult | None = None) -> UnitPriceBuild:
|
|||||||
machine_codes = {r.resource_code for r in axis.rows if r.resource_kind == "machine"}
|
machine_codes = {r.resource_code for r in axis.rows if r.resource_kind == "machine"}
|
||||||
build.incomplete_machines = _add_machine_layers(build.book, machine_codes)
|
build.incomplete_machines = _add_machine_layers(build.book, machine_codes)
|
||||||
|
|
||||||
by_item: dict[str, list] = {}
|
# 규격 갈래(`variant`)가 있으면 **갈래마다 따로 세운다** — 무근·철근·소형구조물은
|
||||||
|
# 품이 달라 한 일위대가로 뭉치면 어느 것도 안 맞는다.
|
||||||
|
by_item: dict[tuple[str, str], list] = {}
|
||||||
for row in axis.rows:
|
for row in axis.rows:
|
||||||
by_item.setdefault(row.work_item_code, []).append(row)
|
by_item.setdefault((row.work_item_code, getattr(row, "variant", "")), []).append(row)
|
||||||
|
|
||||||
for work_item_code, rows in sorted(by_item.items()):
|
for (work_item_code, variant), rows in sorted(by_item.items()):
|
||||||
title_code = f"B-{work_item_code}"
|
title_code = f"B-{work_item_code}" + (f"#{variant}" if variant else "")
|
||||||
if title_code in build.book.titles:
|
if title_code in build.book.titles:
|
||||||
continue
|
continue
|
||||||
# ⚠ **붙을 상세를 먼저 모으고, 하나도 없으면 제목도 안 세운다.**
|
# ⚠ **붙을 상세를 먼저 모으고, 하나도 없으면 제목도 안 세운다.**
|
||||||
@@ -229,22 +238,27 @@ def build_unit_prices(axis: AxisResult | None = None) -> UnitPriceBuild:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
unit = next((r.amount_unit for r in rows if r.amount_unit), "")
|
unit = next((r.amount_unit for r in rows if r.amount_unit), "")
|
||||||
|
base_name = names.get(work_item_code) or work_item_code
|
||||||
build.book.add_title(
|
build.book.add_title(
|
||||||
PriceTitle(
|
PriceTitle(
|
||||||
code=title_code,
|
code=title_code,
|
||||||
kind=PriceKind.UNIT_PRICE,
|
kind=PriceKind.UNIT_PRICE,
|
||||||
name=names.get(work_item_code) or work_item_code,
|
name=f"{base_name} ({variant})" if variant else base_name,
|
||||||
spec=work_item_code,
|
spec=variant or work_item_code,
|
||||||
unit=unit,
|
unit=unit,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if variant:
|
||||||
|
build.variants.setdefault(work_item_code, []).append(variant)
|
||||||
# 배분율 표는 각 몫을 **그 비율만큼만** 센다 — 인력 원단위를 전량에 곱하면 틀린다.
|
# 배분율 표는 각 몫을 **그 비율만큼만** 센다 — 인력 원단위를 전량에 곱하면 틀린다.
|
||||||
for row, ref in attachable:
|
for row, ref in attachable:
|
||||||
share = _share_of(row)
|
share = _share_of(row)
|
||||||
build.book.add_detail(PriceDetail(title_code, ref, row.amount * share))
|
build.book.add_detail(PriceDetail(title_code, ref, row.amount * share))
|
||||||
|
|
||||||
# 장비 몫은 자원 수량이 아니라 **시공능력 공식**으로 온다 (품셈 8-1-4).
|
# 장비 몫은 자원 수량이 아니라 **시공능력 공식**으로 온다 (품셈 8-1-4).
|
||||||
machine_share = _attach_machine_share(build, master, work_item_code, title_code)
|
machine_share = (
|
||||||
|
_ZERO if variant else _attach_machine_share(build, master, work_item_code, title_code)
|
||||||
|
)
|
||||||
|
|
||||||
# ⚠ **배분율이 있는 표는 「몇 %가 실제로 붙었나」를 세어 둔다.**
|
# ⚠ **배분율이 있는 표는 「몇 %가 실제로 붙었나」를 세어 둔다.**
|
||||||
# 「인력(10%)·장비(90%)」 표에서 인력만 붙으면 단가가 10 % 몫만인데, 그 값이
|
# 「인력(10%)·장비(90%)」 표에서 인력만 붙으면 단가가 10 % 몫만인데, 그 값이
|
||||||
@@ -453,11 +467,27 @@ def build_summary(build: UnitPriceBuild) -> dict:
|
|||||||
and (money := build.book.resolve(code).total) < SUSPICIOUSLY_LOW_KRW
|
and (money := build.book.resolve(code).total) < SUSPICIOUSLY_LOW_KRW
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# 기준 단위를 모르는 채 큰 값 — 「10㎡당」 같은 묶음 기준일 수 있다.
|
||||||
|
high = [
|
||||||
|
{"code": code, "name": title.name, "total": _money_text(money)}
|
||||||
|
for code, title in build.book.titles.items()
|
||||||
|
if title.kind is PriceKind.UNIT_PRICE
|
||||||
|
and not title.unit
|
||||||
|
and (money := build.book.resolve(code).total) >= SUSPICIOUSLY_HIGH_KRW
|
||||||
|
]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"titles": len(build.book.titles),
|
"titles": len(build.book.titles),
|
||||||
"unit_price_totals": stats,
|
"unit_price_totals": stats,
|
||||||
# 값이 서기는 했는데 **크기가 이상한** 것 — 성분이 빠졌을 가능성이 크다.
|
# 값이 서기는 했는데 **크기가 이상한** 것 — 성분이 빠졌을 가능성이 크다.
|
||||||
"suspiciously_low": low,
|
"suspiciously_low": low,
|
||||||
|
# 기준 단위가 없는 채로 큰 값 — 값이 틀린 게 아니라 **기준을 모르는 것**이다.
|
||||||
|
"unknown_basis_high": high,
|
||||||
|
"unknown_basis": sum(
|
||||||
|
1
|
||||||
|
for code, title in build.book.titles.items()
|
||||||
|
if title.kind is PriceKind.UNIT_PRICE and not title.unit
|
||||||
|
),
|
||||||
"unit_prices": kinds.get(PriceKind.UNIT_PRICE.value, 0),
|
"unit_prices": kinds.get(PriceKind.UNIT_PRICE.value, 0),
|
||||||
"machine_hourly": kinds.get(PriceKind.MACHINE_HOURLY.value, 0),
|
"machine_hourly": kinds.get(PriceKind.MACHINE_HOURLY.value, 0),
|
||||||
"skipped_work_items": len(build.skipped),
|
"skipped_work_items": len(build.skipped),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -242,18 +242,6 @@
|
|||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
||||||
"work_item_code": "FP-04-02-02"
|
"work_item_code": "FP-04-02-02"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell": "줄떼",
|
|
||||||
"pum_table_id": "F0096",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-05-01-04"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "평떼",
|
|
||||||
"pum_table_id": "F0096",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-05-01-04"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"cell": "지게",
|
"cell": "지게",
|
||||||
"pum_table_id": "F0097",
|
"pum_table_id": "F0097",
|
||||||
@@ -350,66 +338,12 @@
|
|||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
||||||
"work_item_code": "FP-05-08"
|
"work_item_code": "FP-05-08"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell": "줄떼심기",
|
|
||||||
"pum_table_id": "F0118",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-05-13"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "띠떼심기",
|
|
||||||
"pum_table_id": "F0118",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-05-13"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "절 취",
|
|
||||||
"pum_table_id": "F0121",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-05-16-01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "수평잡기 및 단정리",
|
|
||||||
"pum_table_id": "F0121",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-05-16-01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "잡석 및 뿌리 정리",
|
|
||||||
"pum_table_id": "F0121",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-05-16-01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "절·성토면 고르기",
|
|
||||||
"pum_table_id": "F0121",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-05-16-01"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"cell": "표토절취",
|
"cell": "표토절취",
|
||||||
"pum_table_id": "F0126",
|
"pum_table_id": "F0126",
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
||||||
"work_item_code": "FP-05-19-01"
|
"work_item_code": "FP-05-19-01"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell": "채 취",
|
|
||||||
"pum_table_id": "F0129",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-05-21"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "운 반",
|
|
||||||
"pum_table_id": "F0129",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-05-21"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "붙이기",
|
|
||||||
"pum_table_id": "F0129",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-05-21"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"cell": "트럭",
|
"cell": "트럭",
|
||||||
"pum_table_id": "F0132",
|
"pum_table_id": "F0132",
|
||||||
@@ -1395,15 +1329,21 @@
|
|||||||
"work_item_code": "FP-09-19-03"
|
"work_item_code": "FP-09-19-03"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell": "콘크리트",
|
"cell": "거리 (m) | 보통인부(인) | 거리 (m) | 보통인부(인) | 비고",
|
||||||
|
"pum_table_id": "F0302",
|
||||||
|
"reason": "열 머리가 되풀이되는 두 판 짜리 표 — 자리를 단정할 수 없어 버렸습니다.",
|
||||||
|
"work_item_code": "FP-10-06-01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell": "〃",
|
||||||
"pum_table_id": "F0313",
|
"pum_table_id": "F0313",
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "같은 갈래 이름이 두 번 나오는 표 — 등급을 단정할 수 없어 버렸습니다.",
|
||||||
"work_item_code": "FP-10-08-01"
|
"work_item_code": "FP-10-08-01"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell": "제 자재",
|
"cell": "〃",
|
||||||
"pum_table_id": "F0313",
|
"pum_table_id": "F0313",
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "같은 갈래 이름이 두 번 나오는 표 — 등급을 단정할 수 없어 버렸습니다.",
|
||||||
"work_item_code": "FP-10-08-01"
|
"work_item_code": "FP-10-08-01"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1418,60 +1358,6 @@
|
|||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
||||||
"work_item_code": "FP-10-10-02"
|
"work_item_code": "FP-10-10-02"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell": "무근구조물",
|
|
||||||
"pum_table_id": "F0329",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-12-01-01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "철근구조물",
|
|
||||||
"pum_table_id": "F0329",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-12-01-01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "소형구조물",
|
|
||||||
"pum_table_id": "F0329",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-12-01-01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "무근구조물",
|
|
||||||
"pum_table_id": "F0330",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-12-01-02"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "철근구조물",
|
|
||||||
"pum_table_id": "F0330",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-12-01-02"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "소형구조물",
|
|
||||||
"pum_table_id": "F0330",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-12-01-02"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "무근구조물",
|
|
||||||
"pum_table_id": "F0331",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-12-01-03"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "철근구조물",
|
|
||||||
"pum_table_id": "F0331",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-12-01-03"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "소형구조물",
|
|
||||||
"pum_table_id": "F0331",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-12-01-03"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"cell": "간 단",
|
"cell": "간 단",
|
||||||
"pum_table_id": "F0335",
|
"pum_table_id": "F0335",
|
||||||
@@ -1790,30 +1676,6 @@
|
|||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
||||||
"work_item_code": "FP-13-03"
|
"work_item_code": "FP-13-03"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell": "두 께 3㎝",
|
|
||||||
"pum_table_id": "F0402",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-13-03-01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "두 께 6㎝",
|
|
||||||
"pum_table_id": "F0402",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-13-03-01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "자갈 기초다짐 지름 1~3㎝",
|
|
||||||
"pum_table_id": "F0402",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-13-03-01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "기초다짐 뒷채움",
|
|
||||||
"pum_table_id": "F0403",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-13-03-02"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"cell": "굴 삭 기 (무한궤도)",
|
"cell": "굴 삭 기 (무한궤도)",
|
||||||
"pum_table_id": "F0419",
|
"pum_table_id": "F0419",
|
||||||
@@ -1827,10 +1689,46 @@
|
|||||||
"work_item_code": "FP-13-07-02"
|
"work_item_code": "FP-13-07-02"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell": "직경 15㎝ 길이 4m",
|
"cell": "8 9",
|
||||||
"pum_table_id": "F0426",
|
"pum_table_id": "F0430",
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "자원 열의 값이 한 칸 밀린 행 — 자리를 단정할 수 없어 버렸습니다.",
|
||||||
"work_item_code": "FP-13-10-01"
|
"work_item_code": "FP-13-10-02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell": "12",
|
||||||
|
"pum_table_id": "F0430",
|
||||||
|
"reason": "자원 열의 값이 한 칸 밀린 행 — 자리를 단정할 수 없어 버렸습니다.",
|
||||||
|
"work_item_code": "FP-13-10-02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell": "15",
|
||||||
|
"pum_table_id": "F0430",
|
||||||
|
"reason": "자원 열의 값이 한 칸 밀린 행 — 자리를 단정할 수 없어 버렸습니다.",
|
||||||
|
"work_item_code": "FP-13-10-02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell": "18",
|
||||||
|
"pum_table_id": "F0430",
|
||||||
|
"reason": "자원 열의 값이 한 칸 밀린 행 — 자리를 단정할 수 없어 버렸습니다.",
|
||||||
|
"work_item_code": "FP-13-10-02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell": "21",
|
||||||
|
"pum_table_id": "F0430",
|
||||||
|
"reason": "자원 열의 값이 한 칸 밀린 행 — 자리를 단정할 수 없어 버렸습니다.",
|
||||||
|
"work_item_code": "FP-13-10-02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell": "24",
|
||||||
|
"pum_table_id": "F0430",
|
||||||
|
"reason": "자원 열의 값이 한 칸 밀린 행 — 자리를 단정할 수 없어 버렸습니다.",
|
||||||
|
"work_item_code": "FP-13-10-02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell": "27",
|
||||||
|
"pum_table_id": "F0430",
|
||||||
|
"reason": "자원 열의 값이 한 칸 밀린 행 — 자리를 단정할 수 없어 버렸습니다.",
|
||||||
|
"work_item_code": "FP-13-10-02"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell": "조약돌량(㎥)",
|
"cell": "조약돌량(㎥)",
|
||||||
@@ -1904,34 +1802,28 @@
|
|||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
||||||
"work_item_code": "FP-13-11-02"
|
"work_item_code": "FP-13-11-02"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell": "절 취(㎥)",
|
|
||||||
"pum_table_id": "F0437",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-13-12-01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "투 입(㎥)",
|
|
||||||
"pum_table_id": "F0437",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-13-12-01"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"cell": "보통구조",
|
"cell": "보통구조",
|
||||||
"pum_table_id": "F0440",
|
"pum_table_id": "F0440",
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "자원 열의 값이 한 칸 밀린 행 — 자리를 단정할 수 없어 버렸습니다.",
|
||||||
|
"work_item_code": "FP-13-13-01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell": "상",
|
||||||
|
"pum_table_id": "F0440",
|
||||||
|
"reason": "같은 갈래 이름이 두 번 나오는 표 — 등급을 단정할 수 없어 버렸습니다.",
|
||||||
"work_item_code": "FP-13-13-01"
|
"work_item_code": "FP-13-13-01"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell": "중등구조",
|
"cell": "중등구조",
|
||||||
"pum_table_id": "F0440",
|
"pum_table_id": "F0440",
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "자원 열의 값이 한 칸 밀린 행 — 자리를 단정할 수 없어 버렸습니다.",
|
||||||
"work_item_code": "FP-13-13-01"
|
"work_item_code": "FP-13-13-01"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell": "상등구조",
|
"cell": "상",
|
||||||
"pum_table_id": "F0440",
|
"pum_table_id": "F0440",
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "같은 갈래 이름이 두 번 나오는 표 — 등급을 단정할 수 없어 버렸습니다.",
|
||||||
"work_item_code": "FP-13-13-01"
|
"work_item_code": "FP-13-13-01"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1940,36 +1832,6 @@
|
|||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
||||||
"work_item_code": "FP-13-13-02"
|
"work_item_code": "FP-13-13-02"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell": "간 단 구 조",
|
|
||||||
"pum_table_id": "F0443",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-13-13-02"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "보 통 구 조",
|
|
||||||
"pum_table_id": "F0443",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-13-13-02"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "단 끊 기",
|
|
||||||
"pum_table_id": "F0444",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-13-14"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "흙채우기(마대채우기)",
|
|
||||||
"pum_table_id": "F0444",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-13-14"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell": "마대쌓기",
|
|
||||||
"pum_table_id": "F0444",
|
|
||||||
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
|
|
||||||
"work_item_code": "FP-13-14"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"cell": "어려운 조건",
|
"cell": "어려운 조건",
|
||||||
"pum_table_id": "F0447",
|
"pum_table_id": "F0447",
|
||||||
|
|||||||
Reference in New Issue
Block a user