· 조각마다 보통 줄과 같은 두 검사 — 단가가 일부 몫만 섰거나(붙은 몫 N%) 밑수를 모르는 표면 묶음 금액을 안 세우고 어느 조각이 왜인지 사유 · 700줄 넘던 줄 만들기 파일에서 자재 줄(_material_row)을 자재 모듈로 그대로 옮김(순수 나누기 · 부르는 쪽 한 줄) 프로젝트 내역 그대로 · 잴 시험 둘 빨강→초록(온전한 조각 시험은 처음부터 초록) · 전체 시험 1705 + 367 통과 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
194 lines
8.2 KiB
Python
194 lines
8.2 KiB
Python
"""B09 원가계산 — 사급 자재총괄 줄을 **내역 본체 「자재」 줄**로 (PLAN 1장 Ⓐ-2 · 브레인 판정).
|
|
|
|
셈부터 함(배정 프로젝트 자재총괄 9줄): ㉠ 일위대가를 거쳐 이미 내역에 선 자재 0 · ㉡ 일위대가에 안
|
|
드는 자재 9 · ㉢ ㉠ 의 할증 0 — 품셈 표에 재료 줄이 없는 공종(규준틀·찰쌓기·타설)의 자재라 따로
|
|
계상하는 것이 실무 모양. 본체 줄로 서므로 도급 재료비·계약·기성·집계가 고칠 곳 없이 그 줄을 셈.
|
|
|
|
올리는 줄 사급 · 수동 단가가 선 줄만(미확정 1건) — 관급은 수량표에만(도급 금액 밖)
|
|
⚠ 이중계상 가드 — 같은 구조물 내역 줄의 일위대가 자재(붙은 것·못 붙은 것)와 겹치면 안 올리고
|
|
사유로 멈춤. **코드가 있으면 코드로, 없을 때만 이름으로**(명세 2장). 막는 쪽이라 틀려도 과소
|
|
(줄이 안 서고 사유가 뜸) — 양식이 늘어 ㉠ 이 생기는 날 금액이 조용히 부풀지 않게.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from decimal import Decimal
|
|
from typing import Any, Callable
|
|
|
|
from B09_Estimation.B09_Estimation_BillOfQuantities import (
|
|
SUPPLY_OWNER,
|
|
SUPPLY_UNKNOWN,
|
|
BillResult,
|
|
BillRow,
|
|
HandoffMaterial,
|
|
)
|
|
from B09_Estimation.B09_Estimation_PriceBook import Money3, PriceKind
|
|
from B09_Estimation.B09_Estimation_UnitPrice import FUEL_CODE_PREFIX, UnitPriceBuild
|
|
|
|
MATERIAL_GROUP_NAME = "자재(사급)"
|
|
DOUBLE_COUNT_SUSPECT = "double_count_suspect"
|
|
_ZERO = Decimal(0)
|
|
|
|
|
|
def _material_row(
|
|
material: HandoffMaterial, result: BillResult, manual: dict | None = None
|
|
) -> BillRow:
|
|
"""자재 한 줄. 공급 구분이 안 갈렸으면 **어느 쪽에도 안 넣는다**.
|
|
|
|
`manual` — 「자재 단가」 수동 단가(키 「이름 규격」). 사급 줄에 값이 있으면 빠진 목록에 안 올림
|
|
(금액은 본체 「자재(사급)」 줄이 셈 — `BillOfQuantities_Materials`).
|
|
"""
|
|
row = BillRow(
|
|
item_no="",
|
|
level=1,
|
|
code=None,
|
|
name=material.material_name,
|
|
spec=material.spec,
|
|
unit=material.unit,
|
|
quantity=material.total_amount,
|
|
)
|
|
# 할증 사유는 **수량**에 닿는다 — 할증이 곱해진 뒤의 수량이기 때문이다.
|
|
row.add_note("quantity", material.surcharge_note)
|
|
if material.supply_type == SUPPLY_UNKNOWN:
|
|
row.add_note(
|
|
"", "관급·사급이 안 갈렸습니다 — 관급자재대에도, 도급 재료비에도 넣지 않습니다."
|
|
)
|
|
result.missing.append(
|
|
{
|
|
"name": material.display_name,
|
|
"unit": material.unit,
|
|
"quantity": str(material.total_amount),
|
|
"reason": "공급 구분 미정(unknown)",
|
|
}
|
|
)
|
|
return row
|
|
# ⚠ **관급을 「사급」이라 적으면 안 된다** (2026-09-08 메인 창 실측 — 물구멍·야면석이
|
|
# `owner_supplied` 인데 「사급 자재 단가 미확보」로 뜨고 있었다). 갈래마다 **가는 자리도
|
|
# 원천도 다르다** — 관급은 총원가 밖 관급자재대(나라장터), 사급은 도급 재료비(물가지).
|
|
if material.supply_type == SUPPLY_OWNER:
|
|
row.add_note(
|
|
"unit_price_krw",
|
|
"관급 자재 단가 미확보 — 나라장터 목록에 이 품목이 없습니다. "
|
|
"관급자재대(총원가 밖 별도 표기)로 갑니다.",
|
|
)
|
|
reason = "관급 자재 단가 없음"
|
|
elif f"{material.material_name} {material.spec}".strip() in (manual or {}):
|
|
row.add_note(
|
|
"unit_price_krw", "⚠ 사급 자재 수동 단가(미확정) — 본체 「자재(사급)」 줄로 섬"
|
|
)
|
|
return row
|
|
else:
|
|
row.add_note(
|
|
"unit_price_krw", "사급 자재 단가 미확보 — 「자재 단가」 탭에서 수동 입력 대기."
|
|
)
|
|
reason = "사급 자재 단가 없음(미결 No.18)"
|
|
|
|
result.missing.append(
|
|
{
|
|
"name": material.display_name,
|
|
"unit": material.unit,
|
|
"quantity": str(material.total_amount),
|
|
"reason": reason,
|
|
"supply_type": material.supply_type,
|
|
}
|
|
)
|
|
return row
|
|
|
|
|
|
def _flat(text: Any) -> str:
|
|
return "".join(str(text or "").split())
|
|
|
|
|
|
def unit_price_materials(row: BillRow, unit_prices: UnitPriceBuild) -> list[tuple[str, str]]:
|
|
"""그 내역 줄의 일위대가가 밟는 자재 — 붙은 것 (코드, 이름) · 못 붙은 줄 (「」, 글)."""
|
|
book = unit_prices.book
|
|
stack = [code for code in (row.price_code, *(code for code, _ in row.parts)) if code]
|
|
seen: set[str] = set()
|
|
found: list[tuple[str, str]] = []
|
|
while stack:
|
|
code = stack.pop()
|
|
if code in seen:
|
|
continue
|
|
seen.add(code)
|
|
title = book.titles.get(code)
|
|
if title is not None and title.kind is PriceKind.MATERIAL:
|
|
if not code.startswith(FUEL_CODE_PREFIX): # 기계 연료는 자재총괄 자재가 아님
|
|
found.append((code, title.name))
|
|
if code.startswith("B-"):
|
|
labels = unit_prices.unattached.get(code[2:].split("#", 1)[0]) or []
|
|
found.extend(("", label) for label in labels)
|
|
stack.extend(detail.ref_code for detail in book.details.get(code, []))
|
|
return found
|
|
|
|
|
|
def overlap(name: str, code: str, inside: list[tuple[str, str]]) -> str:
|
|
"""겹친 일위대가 자재 글 — 코드가 있으면 코드로, 없으면 이름(공백 무시 앞머리)으로."""
|
|
if code:
|
|
return next((label for found, label in inside if found == code), "")
|
|
wanted = _flat(name)
|
|
return next((label for _, label in inside if wanted and _flat(label).startswith(wanted)), "")
|
|
|
|
|
|
def raise_material_rows(
|
|
result: BillResult,
|
|
unit_prices: UnitPriceBuild,
|
|
next_number: Callable[[str], str],
|
|
bill_line: Callable[[Money3, Decimal], Money3],
|
|
) -> None:
|
|
"""자재대 표의 사급·수동 단가 줄 → 본체 「자재(사급)」 묶음 줄. 가드에 걸린 줄은 사유만."""
|
|
from B09_Estimation.B09_Estimation_BillOfQuantities_Rows import settle_quantity
|
|
|
|
sheet = result.material_sheet
|
|
picked = []
|
|
for item in getattr(sheet, "contractor_rows", []):
|
|
if item.amount_krw is None or not item.manual:
|
|
continue
|
|
inside = [
|
|
pair
|
|
for row in result.rows
|
|
if not row.is_group and row.name in item.source_structure
|
|
for pair in unit_price_materials(row, unit_prices)
|
|
]
|
|
hit = overlap(item.name, getattr(item, "code", ""), inside)
|
|
if hit:
|
|
reason = f"두 번 셀 수 있어 내역 줄로 안 올림 — 같은 구조물 일위대가에 「{hit}」"
|
|
item.note = f"{item.note} · {reason}"
|
|
result.missing.append(
|
|
{
|
|
"name": item.key,
|
|
"unit": item.unit,
|
|
"quantity": str(item.total_amount),
|
|
"reason": reason,
|
|
"blocked_kind": DOUBLE_COUNT_SUSPECT,
|
|
}
|
|
)
|
|
continue
|
|
picked.append(item)
|
|
if not picked:
|
|
return
|
|
group_no = next_number("")
|
|
result.rows.append(
|
|
BillRow(item_no=group_no, level=1, code=None, name=MATERIAL_GROUP_NAME, is_group=True)
|
|
)
|
|
for index, item in enumerate(picked, start=1):
|
|
price = item.unit_price_krw
|
|
row = BillRow(
|
|
item_no=f"{group_no}-{index}",
|
|
level=2,
|
|
code=None,
|
|
name=item.name,
|
|
spec=item.spec,
|
|
unit=item.unit,
|
|
quantity=item.total_amount,
|
|
)
|
|
line = bill_line(Money3(material=price), settle_quantity(row)) # 수량 확정 뒤 금액
|
|
row.unit_material_krw, row.unit_labor_krw, row.unit_expense_krw = price, _ZERO, _ZERO
|
|
row.unit_price_krw = price
|
|
row.amount_krw = line.total
|
|
row.material_krw = line.material
|
|
row.unconfirmed = 1
|
|
row.add_note("quantity", f"자재총괄 할증 뒤 수량 — {', '.join(item.source_structure)}")
|
|
row.add_note("unit_price_krw", item.note)
|
|
result.unconfirmed.append({"name": row.name, "code": item.key, "count": 1})
|
|
result.rows.append(row)
|