diff --git a/B09_Estimation/B09_Estimation_BillOfQuantities.py b/B09_Estimation/B09_Estimation_BillOfQuantities.py new file mode 100644 index 00000000..b52bce61 --- /dev/null +++ b/B09_Estimation/B09_Estimation_BillOfQuantities.py @@ -0,0 +1,505 @@ +"""B09 원가계산 — ④ 예산내역서 조판 (PLAN 9-5 「B08 출력을 붙이는 자리」). + +**하는 일** — B08 인계 응답(공종 수량 + 자재)을 받아 **계층이 선 내역서 한 장**으로 +접는다. 수량은 B08 것을 그대로 쓰고(다시 세지 않는다), 단가는 ③ 일위대가에서 가져오며, +금액은 이 자리에서 `수량 × 단가` 로 만든다. + +**계층은 코드에 안 박는다** (PLAN 9-3 · STmate `BOQ11` 해부 결과). 공종 마스터가 이미 +`parent_code` · `level` · `sort_order`(256 간격)를 들고 있으므로, 쓰인 공종의 **조상만 +남긴 가지치기 나무**를 세우고 거기서 ITEM NO. 를 매긴다. 깊이를 코드 글자수로 세지 않는다 — +`FP-09-03-02` 가 3층이라는 보장이 없다. + +**빈칸을 지어내지 않는다** — 단가가 없는 줄, 관급/사급이 안 갈린 자재는 금액을 0 으로 +때우지 않고 `missing` 에 이름째 남긴다. 화면이 그것을 그대로 보인다. + +⚠ **`in_bill=false` 줄에는 단가를 붙이지 않는다** (PLAN 8-7 ㉡ 와 같은 성격). +보정량계·무대 같은 검산용 줄이라 금액을 매기면 같은 것을 두 번 세게 된다. 주석으로 +막지 않고 `check_excluded_rows_not_priced()` 가 수치로 멈춘다. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from decimal import Decimal +from typing import Any + +from B09_Estimation.B09_Estimation_Guards import check_excluded_rows_not_priced +from B09_Estimation.B09_Estimation_ResourceAxis import load_work_item_master +from B09_Estimation.B09_Estimation_Rounding import OutputPlace, round_at +from B09_Estimation.B09_Estimation_UnitPrice import UnitPriceBuild, cached_build + +_ZERO = Decimal(0) + +#: 자재 공급 구분이 안 갈린 값. B08 이 실제로 이 값을 보낸다(2026-09-08 실물 확인). +#: **관급자재대에도, 도급 재료비에도 넣지 않는다** — 어느 쪽에 넣어도 총액이 틀린다. +SUPPLY_UNKNOWN = "unknown" + + +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 + in_bill_reason: str = "" + origin: str = "" + ground_class: str = "" + #: 반영률(%) — B08 이 이미 곱했으면 산출근거에만 적고 **여기서 또 곱하지 않는다**. + application_ratio_pct: Decimal | None = None + + @property + def display_name(self) -> str: + return f"{self.name} {self.spec}".strip() + + +@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 +class BillRow: + """내역서 한 줄. 머리(그룹)줄은 `is_group=True` 이고 수량·단가가 없다.""" + + item_no: str + level: int + code: str | None + name: str + spec: str = "" + unit: str = "" + quantity: Decimal | None = None + unit_price_krw: Decimal | None = None + amount_krw: Decimal | None = None + #: 3분할 — ⑤ 로 넘길 때 **뭉치지 않고** 성분 그대로 간다(PLAN 8-9 규칙 2). + material_krw: Decimal = _ZERO + labor_krw: Decimal = _ZERO + expense_krw: Decimal = _ZERO + is_group: bool = False + in_bill: bool = True + note: str = "" + + def as_dict(self) -> dict[str, Any]: + def money(value: Decimal | None) -> str | None: + return None if value is None else str(value) + + return { + "item_no": self.item_no, + "level": self.level, + "code": self.code, + "name": self.name, + "spec": self.spec, + "unit": self.unit, + "quantity": money(self.quantity), + "unit_price_krw": money(self.unit_price_krw), + "amount_krw": money(self.amount_krw), + "material_krw": str(self.material_krw), + "labor_krw": str(self.labor_krw), + "expense_krw": str(self.expense_krw), + "is_group": self.is_group, + "in_bill": self.in_bill, + "note": self.note, + } + + +@dataclass +class BillResult: + """④ 예산내역서 한 장.""" + + rows: list[BillRow] = field(default_factory=list) + #: 금액을 못 세운 줄 — **0 으로 안 때우고 이름째 남긴다**. + missing: list[dict[str, str]] = field(default_factory=list) + #: `in_bill=false` 라 금액을 안 매긴 줄(보정량계 등). 수량은 보이되 합계에 안 든다. + excluded: list[BillRow] = field(default_factory=list) + #: 자재 벌 — 공급 구분이 갈린 것만 금액이 선다. + material_rows: list[BillRow] = field(default_factory=list) + notes: list[str] = field(default_factory=list) + + @property + def direct_material_krw(self) -> Decimal: + return sum((r.material_krw for r in self.rows if not r.is_group), _ZERO) + + @property + def direct_labor_krw(self) -> Decimal: + return sum((r.labor_krw for r in self.rows if not r.is_group), _ZERO) + + @property + def direct_expense_krw(self) -> Decimal: + return sum((r.expense_krw for r in self.rows if not r.is_group), _ZERO) + + @property + def body_total_krw(self) -> Decimal: + """내역서 **본체** 합계 — 줄마다 절사한 금액의 합. + + ⚠ 집계표(반올림) 합계와 원 단위로 어긋나는 것이 정상이다 + (`B09_Estimation_Rounding.SUMMARY_MISMATCH_NOTE`). + """ + 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 "", + # ⚠ 있으면 **적기만** 한다 — 곱하기는 B08 한 곳에서만(2026-09-08 이견 ①). + application_ratio_pct=_decimal(row.get("application_ratio_pct"), None), + ) + 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) +class _MasterNode: + code: str + name: str + level: int + parent_code: str | None + sort_order: int + + +def _master_index(master: dict[str, Any]) -> dict[str, _MasterNode]: + return { + node["work_item_code"]: _MasterNode( + code=node["work_item_code"], + name=node.get("name", ""), + level=int(node.get("level", 1)), + parent_code=node.get("parent_code"), + sort_order=int(node.get("sort_order", 0)), + ) + for node in master.get("work_items", []) + if node.get("work_item_code") + } + + +def _ancestor_chain(code: str, index: dict[str, _MasterNode]) -> list[_MasterNode]: + """뿌리 → 자기 순서의 조상 사슬. **코드 글자수로 깊이를 세지 않는다.**""" + chain: list[_MasterNode] = [] + seen: set[str] = set() + cursor: str | None = code + while cursor and cursor in index and cursor not in seen: + seen.add(cursor) + node = index[cursor] + chain.append(node) + cursor = node.parent_code + chain.reverse() + return chain + + +def _number_of(path: tuple[int, ...]) -> str: + """ITEM NO. — 자리마다 1 부터. 「1」·「1-2」·「1-2-3」 모양.""" + return "-".join(str(n) for n in path) + + +def build_bill( + payload: dict[str, Any], + *, + build: UnitPriceBuild | None = None, + master: dict[str, Any] | None = None, +) -> BillResult: + """인계 응답 한 벌을 ④ 예산내역서 한 장으로 접는다.""" + work_items, materials = parse_handoff(payload) + unit_prices = build or cached_build() + index = _master_index(master or load_work_item_master()) + result = BillResult() + + # ── 1) 쓰인 공종의 조상만 남긴 가지치기 나무 ──────────────────────────────── + # 정렬은 마스터의 `sort_order`(256 간격)를 그대로 따른다 — 우리가 다시 매기지 않는다. + used: list[tuple[tuple[int, ...], HandoffWorkItem, list[_MasterNode]]] = [] + orphans: list[HandoffWorkItem] = [] + for item in work_items: + if not item.in_bill: + # ⚠ 코드 유무보다 **먼저** 가른다. 보정량계는 공종코드가 없어서가 아니라 + # **검산용 줄이라서** 금액이 없는 것이다 — `missing` 으로 새면 「단가를 구해야 할 + # 줄」로 잘못 읽힌다. + result.excluded.append(_excluded_row(item)) + continue + if not item.work_item_code or item.work_item_code not in index: + orphans.append(item) + continue + used.append(((), item, _ancestor_chain(item.work_item_code, index))) + + def sort_key(entry: tuple[tuple[int, ...], HandoffWorkItem, list[_MasterNode]]) -> tuple: + return tuple(node.sort_order for node in entry[2]) + + used.sort(key=sort_key) + + emitted: dict[str, str] = {} # 코드 → ITEM NO. + counters: dict[str, int] = {} # 부모 ITEM NO. → 마지막 번호 + + def next_number(parent_no: str) -> str: + counters[parent_no] = counters.get(parent_no, 0) + 1 + return f"{parent_no}-{counters[parent_no]}" if parent_no else str(counters[parent_no]) + + for _, item, chain in used: + parent_no = "" + # 조상 줄(머리글)을 먼저 세운다 — 이미 선 것은 다시 안 세운다. + for node in chain[:-1]: + if node.code in emitted: + parent_no = emitted[node.code] + continue + parent_no = next_number(parent_no) + emitted[node.code] = parent_no + result.rows.append( + BillRow( + item_no=parent_no, + level=node.level, + code=node.code, + name=node.name, + is_group=True, + ) + ) + leaf = chain[-1] + item_no = emitted.get(leaf.code) or next_number(parent_no) + emitted[leaf.code] = item_no + result.rows.append(_leaf_row(item_no, leaf, item, unit_prices, result)) + + # ── 2) 공종을 못 고른 줄 — 이름째 남긴다 ──────────────────────────────────── + for item in orphans: + result.missing.append( + { + "name": item.display_name, + "unit": item.unit, + "quantity": str(item.quantity), + "reason": "공종을 못 골랐습니다 — B08 인계에 공종코드가 없습니다.", + } + ) + + # ── 3) 자재 벌 ──────────────────────────────────────────────────────────── + for material in materials: + result.material_rows.append(_material_row(material, result)) + + # ── 4) 검사 — `in_bill=false` 줄에 금액이 붙지 않았는가 ────────────────────── + check_excluded_rows_not_priced(rows=[r.as_dict() for r in result.excluded]) + + if any(m.surcharge_pct is None for m in materials): + result.notes.append( + "자재 할증률이 아직 없습니다 — 할증 전 값으로 섰습니다. " + "할증은 자재총괄에서 한 번만 붙습니다 (PLAN 8-7 ㉠)." + ) + return result + + +def _excluded_row(item: HandoffWorkItem) -> BillRow: + """검산용 줄(`in_bill=false`). **수량만 보이고 단가·금액을 안 붙인다.**""" + return BillRow( + item_no="", + level=1, + code=item.work_item_code, + name=item.name, + spec=item.spec, + unit=item.unit, + quantity=item.quantity, + in_bill=False, + note=item.in_bill_reason or "합계 검산용 줄 — 금액을 매기지 않습니다.", + ) + + +def _leaf_row( + item_no: str, + node: _MasterNode, + item: HandoffWorkItem, + unit_prices: UnitPriceBuild, + result: BillResult, +) -> BillRow: + """세부 공종 한 줄. 단가가 없으면 **금액을 비우고** `missing` 에 남긴다.""" + row = BillRow( + item_no=item_no, + level=node.level, + code=node.code, + name=item.name or node.name, + spec=item.spec, + unit=item.unit, + quantity=item.quantity, + in_bill=item.in_bill, + ) + if item.application_ratio_pct is not None: + # ⚠ 곱하지 않는다 — B08 이 이미 곱한 값이다. 산출근거로만 적는다. + row.note = f"반영률 {item.application_ratio_pct}% 적용 후 수량" + + if not item.in_bill: + # 검산용 줄 — 수량은 보이되 **단가를 안 붙인다**(PLAN 8-7 ㉡ 와 같은 성격). + row.quantity = item.quantity + row.note = item.in_bill_reason or "합계 검산용 줄 — 금액을 매기지 않습니다." + result.excluded.append(row) + return row + + price_code = f"B-{node.code}" + if price_code not in unit_prices.book.titles: + # 한 층 아래에 일위대가가 있으면 **후보로 보여준다** — 임의로 고르지 않는다 + # (CLAUDE.md 3장 「미결 항목 임의 확정 금지」, B08 `mapping_pending_user` 와 같은 태도). + children = sorted( + code + for code in unit_prices.book.titles + if code.startswith(f"{price_code}-") and code.count("-") == price_code.count("-") + 1 + ) + if children: + names = ", ".join(f"{c[2:]} {unit_prices.book.title(c).name}" for c in children) + row.note = f"이 공종엔 일위대가가 없고 한 층 아래에 있습니다 — 후보: {names}" + reason = f"일위대가가 하위 공종에 있음(후보 {len(children)}건)" + else: + row.note = "일위대가가 아직 없습니다 — 금액을 0 으로 때우지 않습니다." + reason = "일위대가 없음" + result.missing.append( + { + "name": row.name, + "code": node.code, + "unit": row.unit, + "quantity": str(item.quantity), + "reason": reason, + "candidates": ", ".join(children), + } + ) + return row + + covered = unit_prices.partial_ratio.get(node.code) + if covered is not None: + # ⚠ **일부 몫만 선 단가는 안 붙인다.** 「인력(10%)·장비(90%)」 표에서 인력만 + # 붙은 값을 전량에 곱하면 내역서가 조용히 틀린다 — 0 으로 때우는 것과 같은 사고다. + row.note = f"단가가 일부만 섰습니다 — 붙은 몫 {covered}% (나머지는 시공능력 공식 몫)." + result.missing.append( + { + "name": row.name, + "code": node.code, + "unit": row.unit, + "quantity": str(item.quantity), + "reason": f"단가 일부만 섬(붙은 몫 {covered}%)", + } + ) + return row + + unit_money = unit_prices.book.resolve(price_code) + line = unit_money.scaled(item.quantity) + row.unit_price_krw = round_at(unit_money.total, OutputPlace.UNIT_PRICE_ROW) + # 내역서 **본체** 행은 절사다 — 집계표(반올림)와 어긋나는 것이 정상. + row.amount_krw = round_at(line.total, OutputPlace.BOQ_ROW) + # 3분할은 전정밀로 들고 간다 — ⑤ 밑수가 비목마다 갈리므로 여기서 자르면 안 된다. + row.material_krw = line.material + row.labor_krw = line.labor + row.expense_krw = line.expense + return row + + +def _material_row(material: HandoffMaterial, result: BillResult) -> BillRow: + """자재 한 줄. 공급 구분이 안 갈렸으면 **어느 쪽에도 안 넣는다**.""" + row = BillRow( + item_no="", + level=1, + code=None, + name=material.material_name, + spec=material.spec, + unit=material.unit, + quantity=material.total_amount, + note=material.surcharge_note, + ) + if material.supply_type == SUPPLY_UNKNOWN: + row.note = "관급·사급이 안 갈렸습니다 — 관급자재대에도, 도급 재료비에도 넣지 않습니다." + result.missing.append( + { + "name": material.display_name, + "unit": material.unit, + "quantity": str(material.total_amount), + "reason": "공급 구분 미정(unknown)", + } + ) + return row + # 사급 자재 단가는 아직 원천이 없다(미결 No.18) — 여기서도 지어내지 않는다. + row.note = row.note or "사급 자재 단가 미확보 — 6번 슬롯(적용 단가) 수동 입력 대기." + result.missing.append( + { + "name": material.display_name, + "unit": material.unit, + "quantity": str(material.total_amount), + "reason": "자재 단가 없음(미결 No.18)", + } + ) + return row + + +def bill_summary(result: BillResult) -> dict[str, Any]: + """화면에 낼 요약 — **무엇이 비었는지**를 함께 낸다.""" + return { + "rows": len(result.rows), + "detail_rows": sum(1 for r in result.rows if not r.is_group), + "group_rows": sum(1 for r in result.rows if r.is_group), + "excluded_rows": len(result.excluded), + "material_rows": len(result.material_rows), + "missing": result.missing, + "body_total_krw": str(result.body_total_krw), + "direct_material_krw": str(result.direct_material_krw), + "direct_labor_krw": str(result.direct_labor_krw), + "direct_expense_krw": str(result.direct_expense_krw), + "notes": result.notes, + } + + +def cost_input_from_bill(result: BillResult, **cost_input_kwargs): + """④ 내역서 합계를 ⑤ 원가계산서 입력으로 접어 넣는다. + + **뭉치지 않는다** — 재료·노무·경비 성분이 그대로 간다(PLAN 8-9 규칙 2). + ⑤ 표에 찍히는 자리이므로 **자원 집계표 규칙(반올림)** 으로 자른다. + """ + from B09_Estimation.B09_Estimation_Engine_Cost import CostInput + + summary = OutputPlace.RESOURCE_SUMMARY + return CostInput( + direct_material_krw=round_at(result.direct_material_krw, summary), + direct_labor_krw=round_at(result.direct_labor_krw, summary), + direct_expense_krw=round_at(result.direct_expense_krw, summary), + **cost_input_kwargs, + ) diff --git a/B09_Estimation/B09_Estimation_Guards.py b/B09_Estimation/B09_Estimation_Guards.py index 315aeb44..e36d1f69 100644 --- a/B09_Estimation/B09_Estimation_Guards.py +++ b/B09_Estimation/B09_Estimation_Guards.py @@ -179,3 +179,29 @@ def check_column_sums( f"표시 {shown:,.2f}. 같은 성분을 두 층에서 셌을 수 있습니다 " "(행 방향 `TC=NC+GC+JC` 검사로는 안 잡힘)." ) + + +def check_excluded_rows_not_priced( + *, + rows: list[dict], + amount_field: str = "amount_krw", + unit_price_field: str = "unit_price_krw", + label: str = "내역서", +) -> None: + """㉡ 확장 — `in_bill=false` 줄(보정량계·무대 등)에 금액이 붙지 않았는가. + + B08 은 검산용 줄도 **수량을 그대로 실어 보낸다**(계약 확정). 수량이 있으니 + 조판이 무심코 단가를 붙이면 같은 것을 두 번 세게 된다 — 합계 줄과 그 아래 + 상세 줄이 함께 더해지는 모양이라 **행 검사로는 안 잡힌다**. + """ + for row in rows: + for field_name in (amount_field, unit_price_field): + value = row.get(field_name) + if value in (None, ""): + continue + if Decimal(str(value)) != 0: + raise DoubleCountError( + f"{label}: 합계·검산용 줄(`in_bill=false`)에 {field_name} " + f"{Decimal(str(value)):,.0f} 이 붙었습니다 — 그 줄은 수량만 보이고 " + "금액을 매기지 않습니다 (PLAN 8-7 ㉡ 와 같은 성격)." + ) diff --git a/B09_Estimation/B09_Estimation_ResourceAxis.py b/B09_Estimation/B09_Estimation_ResourceAxis.py index 84b72ba5..f48679dd 100644 --- a/B09_Estimation/B09_Estimation_ResourceAxis.py +++ b/B09_Estimation/B09_Estimation_ResourceAxis.py @@ -366,6 +366,10 @@ class ResourceRow: amount: Decimal amount_unit: str raw_row_index: int + #: 분류 딱지가 달고 온 배분율 — 「인력(10%)」이면 `10`. 없으면 `None`. + #: ⚠ **이 값을 안 보면 단가가 조용히 틀린다** — 인력 몫 원단위를 전량에 곱하게 된다 + #: (2026-09-08 실측: 측구터파기 39,575.6원/㎥ 이 인력 10 % 몫만이었다). + group_ratio_pct: Decimal | None = None def as_dict(self) -> dict[str, Any]: return { @@ -379,6 +383,7 @@ class ResourceRow: "amount": str(self.amount), "amount_unit": self.amount_unit, "raw_row_index": self.raw_row_index, + "group_ratio_pct": None if self.group_ratio_pct is None else str(self.group_ratio_pct), } @@ -458,7 +463,9 @@ def match_table( # 첫 칸이 분류 딱지(「자재」·「장비」)면 **이름은 둘째 칸**이다. name_cell = cells[0] value_cells = cells[1:] - if _normalize(name_cell) in _GROUP_LABELS and len(cells) > 1: + group_ratio = None + if _group_label_of(name_cell) is not None and len(cells) > 1: + group_ratio = _group_ratio_of(name_cell) name_cell = cells[1] value_cells = cells[2:] @@ -509,10 +516,41 @@ def match_table( amount=amount, amount_unit=unit, raw_row_index=index, + group_ratio_pct=group_ratio, ) ) +#: 분류 딱지가 **비율을 달고 오는** 모양 — 「인력(10%)」·「장비(90%)」. +#: 2026-09-08 실측: 측구터파기(FP-09-12-01) 표가 이 모양이라 딱지 판정이 빗나가 +#: **보통인부 0.23인이 통째로 빠지고 있었다**(그 공종의 자원 줄이 0 개였다). +#: 괄호 안이 **숫자·%·소수점뿐일 때만** 떼어 낸다 — 「보통인부(인)」 같은 단위 표기는 +#: 떼면 안 되므로 넓게 잡지 않는다. +_RE_RATIO_SUFFIX = re.compile(r"[((][\d.\s]*%?[))]$") + + +def _group_label_of(cell: str) -> str | None: + """첫 칸이 분류 딱지면 그 딱지를, 아니면 `None` 을 돌려준다. + + ⚠ 딱지 목록은 **정확 일치**를 유지한다(부분일치가 정상 자원을 지운 전례 — + `_NON_RESOURCE_WORDS` 주석). 비율 꼬리표만 떼고 다시 정확 일치로 본다. + """ + text = _normalize(cell) + if text in _GROUP_LABELS: + return text + stripped = _normalize(_RE_RATIO_SUFFIX.sub("", text)) + return stripped if stripped in _GROUP_LABELS else None + + +def _group_ratio_of(cell: str) -> Decimal | None: + """분류 딱지에 붙은 배분율. 「인력(10%)」 → `10`, 「자재」 → `None`.""" + found = _RE_RATIO_SUFFIX.search(_normalize(cell)) + if found is None: + return None + digits = found.group(0).strip("()()%").strip() + return Decimal(digits) if digits else None + + def build_resource_axis(master: dict[str, Any], catalog: ResourceCatalog) -> AxisResult: """공종 축 전체를 훑어 자원 축을 만든다.""" result = AxisResult() diff --git a/B09_Estimation/B09_Estimation_UnitPrice.py b/B09_Estimation/B09_Estimation_UnitPrice.py index c34a3a0f..61219251 100644 --- a/B09_Estimation/B09_Estimation_UnitPrice.py +++ b/B09_Estimation/B09_Estimation_UnitPrice.py @@ -66,6 +66,8 @@ class UnitPriceBuild: skipped: list[str] = field(default_factory=list) #: 기계 성분이 비어 있는 채로 쓰인 기종 (연료·조종원 미확보). incomplete_machines: list[str] = field(default_factory=list) + #: 배분율 표인데 일부 몫만 붙은 공종 — 「단가가 일부만 섬」. 값은 붙은 몫(%). + partial_ratio: dict[str, Decimal] = field(default_factory=dict) def _add_labor_titles(book: PriceBook, wages: dict[str, Decimal]) -> None: @@ -228,9 +230,39 @@ def build_unit_prices(axis: AxisResult | None = None) -> UnitPriceBuild: ) for row, ref in attachable: build.book.add_detail(PriceDetail(title_code, ref, row.amount)) + + # ⚠ **배분율이 있는 표는 「몇 %가 실제로 붙었나」를 세어 둔다.** + # 「인력(10%)·장비(90%)」 표에서 인력만 붙으면 단가가 10 % 몫만인데, 그 값이 + # 조용히 서면 내역서가 틀린 줄 모른다(2026-09-08 실측: 측구터파기 39,575.6원/㎥ + # 이 인력 10 % 몫만이었다). 0 으로 때우는 것과 같은 종류의 사고다. + covered = _covered_ratio_pct(rows, {ref for _, ref in attachable}, build) + if covered is not None and covered < Decimal(100): + build.partial_ratio[work_item_code] = covered return build +def _covered_ratio_pct( + rows: list, attached_refs: set[str], build: UnitPriceBuild +) -> Decimal | None: + """배분율 표에서 **실제로 붙은 몫**의 합계(%). 배분율이 없는 표면 `None`.""" + ratios = { + row.group_ratio_pct for row in rows if getattr(row, "group_ratio_pct", None) is not None + } + if not ratios: + return None + covered = Decimal(0) + seen: set[Decimal] = set() + for row in rows: + ratio = getattr(row, "group_ratio_pct", None) + if ratio is None or ratio in seen: + continue + ref = row.resource_code if row.resource_kind == "labor" else f"X-{row.resource_code}" + if ref in attached_refs: + seen.add(ratio) + covered += ratio + return covered + + def material_total_before_surcharge(build: UnitPriceBuild, code: str) -> Decimal: """일위대가 한 줄의 **할증 전** 재료비 합계 — ㉠ 가드에 넘길 값.""" return build.book.resolve(code).material diff --git a/resources/data_cost_resource_axis/resource_axis_2026-01-01.json b/resources/data_cost_resource_axis/resource_axis_2026-01-01.json index 7b3a2e23..6a986ebe 100644 --- a/resources/data_cost_resource_axis/resource_axis_2026-01-01.json +++ b/resources/data_cost_resource_axis/resource_axis_2026-01-01.json @@ -475,6 +475,30 @@ "resource_spec": "", "work_item_code": "FP-09-08-01" }, + { + "amount": "0.23", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0258", + "raw_row_index": 0, + "resource_code": "1002", + "resource_kind": "labor", + "resource_name": "보통인부", + "resource_spec": "", + "work_item_code": "FP-09-12-01" + }, + { + "amount": "1.6", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0259", + "raw_row_index": 0, + "resource_code": "1017", + "resource_kind": "labor", + "resource_name": "할석공", + "resource_spec": "", + "work_item_code": "FP-09-12-02" + }, { "amount": "0.8", "amount_unit": "", @@ -487,6 +511,18 @@ "resource_spec": "", "work_item_code": "FP-09-12-02" }, + { + "amount": "2.8", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0260", + "raw_row_index": 0, + "resource_code": "1017", + "resource_kind": "labor", + "resource_name": "할석공", + "resource_spec": "", + "work_item_code": "FP-09-12-03" + }, { "amount": "1.266", "amount_unit": "", @@ -499,6 +535,90 @@ "resource_spec": "", "work_item_code": "FP-09-12-03" }, + { + "amount": "0.23", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0261", + "raw_row_index": 0, + "resource_code": "1002", + "resource_kind": "labor", + "resource_name": "보통인부", + "resource_spec": "", + "work_item_code": "FP-09-13-01" + }, + { + "amount": "0.31", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0262", + "raw_row_index": 0, + "resource_code": "1002", + "resource_kind": "labor", + "resource_name": "보통인부", + "resource_spec": "", + "work_item_code": "FP-09-13-02" + }, + { + "amount": "0.39", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0263", + "raw_row_index": 0, + "resource_code": "1002", + "resource_kind": "labor", + "resource_name": "보통인부", + "resource_spec": "", + "work_item_code": "FP-09-13-03" + }, + { + "amount": "0.345", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0264", + "raw_row_index": 0, + "resource_code": "1002", + "resource_kind": "labor", + "resource_name": "보통인부", + "resource_spec": "", + "work_item_code": "FP-09-13-04" + }, + { + "amount": "0.465", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0265", + "raw_row_index": 0, + "resource_code": "1002", + "resource_kind": "labor", + "resource_name": "보통인부", + "resource_spec": "", + "work_item_code": "FP-09-13-05" + }, + { + "amount": "0.585", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0266", + "raw_row_index": 0, + "resource_code": "1002", + "resource_kind": "labor", + "resource_name": "보통인부", + "resource_spec": "", + "work_item_code": "FP-09-13-06" + }, + { + "amount": "1.6", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0267", + "raw_row_index": 0, + "resource_code": "1017", + "resource_kind": "labor", + "resource_name": "할석공", + "resource_spec": "", + "work_item_code": "FP-09-13-07" + }, { "amount": "0.8", "amount_unit": "", @@ -511,6 +631,18 @@ "resource_spec": "", "work_item_code": "FP-09-13-07" }, + { + "amount": "1.8", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0268", + "raw_row_index": 0, + "resource_code": "1017", + "resource_kind": "labor", + "resource_name": "할석공", + "resource_spec": "", + "work_item_code": "FP-09-13-08" + }, { "amount": "0.9", "amount_unit": "", @@ -523,6 +655,18 @@ "resource_spec": "", "work_item_code": "FP-09-13-08" }, + { + "amount": "2.0", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0269", + "raw_row_index": 0, + "resource_code": "1017", + "resource_kind": "labor", + "resource_name": "할석공", + "resource_spec": "", + "work_item_code": "FP-09-13-09" + }, { "amount": "1.0", "amount_unit": "", @@ -535,6 +679,54 @@ "resource_spec": "", "work_item_code": "FP-09-13-09" }, + { + "amount": "1.2", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0270", + "raw_row_index": 0, + "resource_code": "1002", + "resource_kind": "labor", + "resource_name": "보통인부", + "resource_spec": "", + "work_item_code": "FP-09-13-10" + }, + { + "amount": "1.35", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0271", + "raw_row_index": 0, + "resource_code": "1002", + "resource_kind": "labor", + "resource_name": "보통인부", + "resource_spec": "", + "work_item_code": "FP-09-13-11" + }, + { + "amount": "1.5", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0272", + "raw_row_index": 0, + "resource_code": "1002", + "resource_kind": "labor", + "resource_name": "보통인부", + "resource_spec": "", + "work_item_code": "FP-09-13-12" + }, + { + "amount": "2.8", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0273", + "raw_row_index": 0, + "resource_code": "1017", + "resource_kind": "labor", + "resource_name": "할석공", + "resource_spec": "", + "work_item_code": "FP-09-13-13" + }, { "amount": "1.266", "amount_unit": "", @@ -547,6 +739,18 @@ "resource_spec": "", "work_item_code": "FP-09-13-13" }, + { + "amount": "3.5", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0274", + "raw_row_index": 0, + "resource_code": "1017", + "resource_kind": "labor", + "resource_name": "할석공", + "resource_spec": "", + "work_item_code": "FP-09-13-14" + }, { "amount": "1.566", "amount_unit": "", @@ -559,6 +763,18 @@ "resource_spec": "", "work_item_code": "FP-09-13-14" }, + { + "amount": "4.2", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0275", + "raw_row_index": 0, + "resource_code": "1017", + "resource_kind": "labor", + "resource_name": "할석공", + "resource_spec": "", + "work_item_code": "FP-09-13-15" + }, { "amount": "1.866", "amount_unit": "", @@ -571,6 +787,18 @@ "resource_spec": "", "work_item_code": "FP-09-13-15" }, + { + "amount": "4.20", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0276", + "raw_row_index": 0, + "resource_code": "1017", + "resource_kind": "labor", + "resource_name": "할석공", + "resource_spec": "", + "work_item_code": "FP-09-13-16" + }, { "amount": "1.899", "amount_unit": "", @@ -583,6 +811,18 @@ "resource_spec": "", "work_item_code": "FP-09-13-16" }, + { + "amount": "5.25", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0277", + "raw_row_index": 0, + "resource_code": "1017", + "resource_kind": "labor", + "resource_name": "할석공", + "resource_spec": "", + "work_item_code": "FP-09-13-17" + }, { "amount": "2.349", "amount_unit": "", @@ -595,6 +835,18 @@ "resource_spec": "", "work_item_code": "FP-09-13-17" }, + { + "amount": "6.3", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0278", + "raw_row_index": 0, + "resource_code": "1017", + "resource_kind": "labor", + "resource_name": "할석공", + "resource_spec": "", + "work_item_code": "FP-09-13-18" + }, { "amount": "2.799", "amount_unit": "", @@ -607,6 +859,18 @@ "resource_spec": "", "work_item_code": "FP-09-13-18" }, + { + "amount": "0.10", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0279", + "raw_row_index": 0, + "resource_code": "1002", + "resource_kind": "labor", + "resource_name": "보통인부", + "resource_spec": "", + "work_item_code": "FP-09-14-01" + }, { "amount": "0.019", "amount_unit": "", @@ -1075,6 +1339,18 @@ "resource_spec": "", "work_item_code": "FP-12-23" }, + { + "amount": "0.2", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0372", + "raw_row_index": 1, + "resource_code": "1003", + "resource_kind": "labor", + "resource_name": "특별인부", + "resource_spec": "", + "work_item_code": "FP-12-24-02" + }, { "amount": "4.0", "amount_unit": "", @@ -1099,6 +1375,18 @@ "resource_spec": "", "work_item_code": "FP-12-26" }, + { + "amount": "0.2", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0371", + "raw_row_index": 1, + "resource_code": "1003", + "resource_kind": "labor", + "resource_name": "특별인부", + "resource_spec": "", + "work_item_code": "FP-12-24-01" + }, { "amount": "4.0", "amount_unit": "", @@ -1423,6 +1711,18 @@ "resource_spec": "", "work_item_code": "FP-13-12-02" }, + { + "amount": "0.20", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0441", + "raw_row_index": 0, + "resource_code": "1002", + "resource_kind": "labor", + "resource_name": "보통인부", + "resource_spec": "", + "work_item_code": "FP-13-13-02" + }, { "amount": "0.0027", "amount_unit": "", @@ -1448,12 +1748,12 @@ "sha256": "593653135d5a2871180e7a3921f9238629b275438ef47230412aa21e9bdd80c0" }, "stats": { - "rows": 119, + "rows": 144, "skipped_forms": { "coefficient": 19, "reference": 98, "undetermined": 76 }, - "unmatched": 355 + "unmatched": 330 } } \ No newline at end of file diff --git a/resources/data_cost_resource_axis/unmatched_2026-01-01.json b/resources/data_cost_resource_axis/unmatched_2026-01-01.json index b2414650..119aa796 100644 --- a/resources/data_cost_resource_axis/unmatched_2026-01-01.json +++ b/resources/data_cost_resource_axis/unmatched_2026-01-01.json @@ -1107,25 +1107,13 @@ "work_item_code": "FP-09-10-02" }, { - "cell": "인력(10%)", + "cell": "유압식백호우 (무한궤도,0.7㎥)", "pum_table_id": "F0258", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-12-01" }, { - "cell": "장비(90%)", - "pum_table_id": "F0258", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-12-01" - }, - { - "cell": "인력 (10%)", - "pum_table_id": "F0259", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-12-02" - }, - { - "cell": "장비 (90%)", + "cell": "대형브레이커(㎥/hr)", "pum_table_id": "F0259", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-12-02" @@ -1143,13 +1131,7 @@ "work_item_code": "FP-09-12-02" }, { - "cell": "인력 (10%)", - "pum_table_id": "F0260", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-12-03" - }, - { - "cell": "장비 (90%)", + "cell": "대형브레이커(㎥/hr)", "pum_table_id": "F0260", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-12-03" @@ -1167,61 +1149,19 @@ "work_item_code": "FP-09-12-03" }, { - "cell": "인력 (10%)", + "cell": "유압식백호우 (무한궤도,0.7㎥)", "pum_table_id": "F0261", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-13-01" }, { - "cell": "장비 (90%)", - "pum_table_id": "F0261", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-13-01" - }, - { - "cell": "인력 (10%)", - "pum_table_id": "F0262", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-13-02" - }, - { - "cell": "인력 (10%)", - "pum_table_id": "F0263", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-13-03" - }, - { - "cell": "인력 (10%)", + "cell": "유압식백호우 (무한궤도,0.7㎥)", "pum_table_id": "F0264", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-13-04" }, { - "cell": "장비 (90%)", - "pum_table_id": "F0264", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-13-04" - }, - { - "cell": "인력 (10%)", - "pum_table_id": "F0265", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-13-05" - }, - { - "cell": "인력 (10%)", - "pum_table_id": "F0266", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-13-06" - }, - { - "cell": "인력 (10%)", - "pum_table_id": "F0267", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-13-07" - }, - { - "cell": "장비 (90%)", + "cell": "깨기", "pum_table_id": "F0267", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-13-07" @@ -1239,13 +1179,7 @@ "work_item_code": "FP-09-13-07" }, { - "cell": "인력 (10%)", - "pum_table_id": "F0268", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-13-08" - }, - { - "cell": "장비 (90%)", + "cell": "깨기", "pum_table_id": "F0268", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-13-08" @@ -1257,13 +1191,7 @@ "work_item_code": "FP-09-13-08" }, { - "cell": "인력 (10%)", - "pum_table_id": "F0269", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-13-09" - }, - { - "cell": "장비 (90%)", + "cell": "깨기", "pum_table_id": "F0269", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-13-09" @@ -1275,13 +1203,7 @@ "work_item_code": "FP-09-13-09" }, { - "cell": "인력 (10%)", - "pum_table_id": "F0270", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-13-10" - }, - { - "cell": "장비 (90%)", + "cell": "깨기", "pum_table_id": "F0270", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-13-10" @@ -1299,13 +1221,7 @@ "work_item_code": "FP-09-13-10" }, { - "cell": "인력 (10%)", - "pum_table_id": "F0271", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-13-11" - }, - { - "cell": "장비 (90%)", + "cell": "깨기", "pum_table_id": "F0271", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-13-11" @@ -1317,13 +1233,7 @@ "work_item_code": "FP-09-13-11" }, { - "cell": "인력 (10%)", - "pum_table_id": "F0272", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-13-12" - }, - { - "cell": "장비 (90%)", + "cell": "깨기", "pum_table_id": "F0272", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-13-12" @@ -1335,13 +1245,7 @@ "work_item_code": "FP-09-13-12" }, { - "cell": "인력 (10%)", - "pum_table_id": "F0273", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-13-13" - }, - { - "cell": "장비 (90%)", + "cell": "깨기", "pum_table_id": "F0273", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-13-13" @@ -1359,13 +1263,7 @@ "work_item_code": "FP-09-13-13" }, { - "cell": "인력 (10%)", - "pum_table_id": "F0274", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-13-14" - }, - { - "cell": "장비 (90%)", + "cell": "깨기", "pum_table_id": "F0274", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-13-14" @@ -1377,13 +1275,7 @@ "work_item_code": "FP-09-13-14" }, { - "cell": "인력 (10%)", - "pum_table_id": "F0275", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-13-15" - }, - { - "cell": "장비 (90%)", + "cell": "깨기", "pum_table_id": "F0275", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-13-15" @@ -1395,13 +1287,7 @@ "work_item_code": "FP-09-13-15" }, { - "cell": "인력 (10%)", - "pum_table_id": "F0276", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-13-16" - }, - { - "cell": "장비 (90%)", + "cell": "깨기", "pum_table_id": "F0276", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-13-16" @@ -1419,13 +1305,7 @@ "work_item_code": "FP-09-13-16" }, { - "cell": "인력 (10%)", - "pum_table_id": "F0277", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-13-17" - }, - { - "cell": "장비 (90%)", + "cell": "깨기", "pum_table_id": "F0277", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-13-17" @@ -1437,13 +1317,7 @@ "work_item_code": "FP-09-13-17" }, { - "cell": "인력 (10%)", - "pum_table_id": "F0278", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-13-18" - }, - { - "cell": "장비 (90%)", + "cell": "깨기", "pum_table_id": "F0278", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-13-18" @@ -1455,13 +1329,7 @@ "work_item_code": "FP-09-13-18" }, { - "cell": "인력 (10%)", - "pum_table_id": "F0279", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-14-01" - }, - { - "cell": "장비 (90%)", + "cell": "유압식백호우 (무한궤도,0.7㎥)", "pum_table_id": "F0279", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-14-01" @@ -1809,13 +1677,7 @@ "work_item_code": "FP-12-23" }, { - "cell": "인력 (10%)", - "pum_table_id": "F0372", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-12-24-02" - }, - { - "cell": "장비 (90%)", + "cell": "부설", "pum_table_id": "F0372", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-12-24-02" @@ -1839,13 +1701,7 @@ "work_item_code": "FP-12-26" }, { - "cell": "인력 (10%)", - "pum_table_id": "F0371", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-12-24-01" - }, - { - "cell": "장비 (90%)", + "cell": "부설", "pum_table_id": "F0371", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-12-24-01" @@ -2079,13 +1935,7 @@ "work_item_code": "FP-13-13-01" }, { - "cell": "인력(10%)", - "pum_table_id": "F0441", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-13-13-02" - }, - { - "cell": "장비(90%)", + "cell": "굴착기 (0.2㎥)", "pum_table_id": "F0441", "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-13-13-02"