Merge remote-tracking branch 'origin/dev' into main_laptop_1

This commit is contained in:
2026-09-14 00:57:14 +09:00
9 changed files with 248 additions and 34 deletions
@@ -370,6 +370,7 @@ from B09_Estimation.B09_Estimation_BillOfQuantities_Rows import ( # noqa: E402
_leaf_row,
_material_row,
_structure_price_row,
bill_line, # noqa: F401 — 내역 줄 성분별 절사(골든셋 시험이 여기서 부름)
)
@@ -9,6 +9,8 @@
from __future__ import annotations
from decimal import Decimal
from B09_Estimation.B09_Estimation_BillOfQuantities import (
SUPPLY_OWNER,
SUPPLY_UNKNOWN,
@@ -20,10 +22,24 @@ from B09_Estimation.B09_Estimation_BillOfQuantities import (
_MasterNode,
_decimal,
)
from B09_Estimation.B09_Estimation_PriceBook import Money3
from B09_Estimation.B09_Estimation_Rounding import OutputPlace, round_at
from B09_Estimation.B09_Estimation_UnitPrice import UnitPriceBuild, find_variant_code
def bill_line(unit: Money3, quantity) -> Money3:
"""내역 줄 금액 — **성분마다** `절사(수량 × 성분 단가)`, 줄 합계는 셋의 합(명세 7장).
근거 STmate 16번 행 금액 1,515건(505줄 × 성분 셋) · 착공내역서 178건 전수 절사.
⚠ 합을 한 번에 자르면 성분 합과 1~2원 갈림 — 원가계산서 직접비 밑수도 이 자른 성분의 합.
"""
return Money3(
material=round_at(unit.material * quantity, OutputPlace.BOQ_ROW),
labor=round_at(unit.labor * quantity, OutputPlace.BOQ_ROW),
expense=round_at(unit.expense * quantity, OutputPlace.BOQ_ROW),
)
def _composite_row(
item_no: str,
item: HandoffWorkItem,
@@ -54,7 +70,8 @@ def _composite_row(
if not code or amount is None or f"B-{code}" not in unit_prices.book.titles:
missing_parts.append(code or str(part.get("name") or "이름 없음"))
continue
scaled = unit_prices.book.resolve(f"B-{code}").scaled(amount)
# 묶음도 호표 한 장 — 조각 줄 0.1원 · 성분 소계 원 미만 절사(아래 `floored`, 명세 7장).
scaled = unit_prices.book.resolve(f"B-{code}").scaled(amount).floored(Decimal("0.1"))
money = scaled if money is None else money + scaled
reasons: list[str] = []
@@ -85,9 +102,10 @@ def _composite_row(
)
return row
line = money.scaled(item.quantity)
money = money.floored(Decimal(1))
line = bill_line(money, item.quantity)
row.unit_price_krw = round_at(money.total, OutputPlace.UNIT_PRICE_ROW)
row.amount_krw = round_at(line.total, OutputPlace.BOQ_ROW)
row.amount_krw = line.total
row.material_krw = line.material
row.labor_krw = line.labor
row.expense_krw = line.expense
@@ -141,9 +159,9 @@ def _structure_price_row(
return row
# 단가 = 호표 계금(구조물도 화면과 같은 값) · 금액 = 호표 성분 소계 × 수량(명세 7장).
line = entry["money"].scaled(item.quantity)
line = bill_line(entry["money"], item.quantity)
row.unit_price_krw = entry["total"]
row.amount_krw = round_at(line.total, OutputPlace.BOQ_ROW)
row.amount_krw = line.total
row.material_krw = line.material
row.labor_krw = line.labor
row.expense_krw = line.expense
@@ -443,11 +461,11 @@ def _leaf_row(
row.price_code = price_code
unit_money = unit_prices.book.resolve(price_code)
line = unit_money.scaled(item.quantity)
line = bill_line(unit_money, 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.amount_krw = line.total
# 3분할도 자른 값 — ⑤ 직접비 밑수가 곧 성분별로 자른 줄 금액의 합(STmate 16번 원 단위 일치).
row.material_krw = line.material
row.labor_krw = line.labor
row.expense_krw = line.expense
+8 -3
View File
@@ -61,8 +61,10 @@ CATALOG_KINDS = frozenset({PriceKind.MATERIAL, PriceKind.LABOR, PriceKind.MACHIN
#: 근거 STmate 17번 — 중기사용료 호표 성분 소계 398/398 절사
#: (굴삭기 0.7㎥ 23,128 + 55,700 + 18,015 = 96,843 · 골든셋 실무 143 호표 전수).
#: D(단가산출)도 같음 — 줄 `중기 성분 ÷ Q` 0.1원 · 머리 성분 원 미만(봉화 2024 제2호표 3,545.5 → 3,545).
#: ⚠ 일위대가(B) 호표도 같은 규칙(345 중 94.2%)이나 **아래층(D)부터 맞춘 뒤** 붙임(PLAN 6장 판정).
TRUNCATED_KINDS = frozenset({PriceKind.MACHINE_HOURLY, PriceKind.PRICE_BASIS})
#: B(일위대가)도 같음 — 골든셋 실무 성분 소계 477/510(93.5%, 17번 원문 94.2%). 안 맞는 것은 반올림으로
#: 뽑은 원본 한 건·적용률 줄 서식 — **새 반올림 규칙을 넣지 말 것**(PLAN 6장 판정).
#: ⚠ 비율 줄(공구손료·제잡비·품 할증)도 줄 금액이라 0.1원 미만 절사.
TRUNCATED_KINDS = frozenset({PriceKind.MACHINE_HOURLY, PriceKind.PRICE_BASIS, PriceKind.UNIT_PRICE})
_TENTH = Decimal("0.1")
_WON = Decimal(1)
@@ -290,6 +292,8 @@ class PriceBook:
# 임도는 산림품셈 문구를 따른다 — 결과값은 어차피 같다.
# ⚠ 「**상한**」이다 — 곱한 값 **이하**로 계상하는 값이라 설계자가 낮출 수 있다.
share = direct_labor * row.percent_of_labor / Decimal(100)
if truncate:
share = share.quantize(_TENTH, rounding=ROUND_FLOOR)
if row.percent_of_labor_target == "labor":
# 품 할인·할증(1-4) — **품이 늘어난 것**이라 노무비로 들어가고, 뒤에 오는
# 제잡비의 밑수에도 든다.
@@ -317,7 +321,8 @@ class PriceBook:
child = self.resolve(row.ref_code, (*_seen, code))
if row.percent_of_parent is not None:
# 비율 행 — 지금까지 쌓인 값의 %로 붙는다(공구손료 등).
total = total + total.scaled(row.percent_of_parent / Decimal(100))
share = total.scaled(row.percent_of_parent / Decimal(100))
total = total + (share.floored(_TENTH) if truncate else share)
continue
scaled = child.scaled(row.quantity)
if truncate:
+2 -1
View File
@@ -1238,7 +1238,8 @@ def direct_cost_from_quantities(
result.missing.append(raw_code)
continue
unit_money = book.resolve(code)
line = unit_money.scaled(Decimal(str(quantity)))
# 내역 줄과 같은 자르기 — 성분마다 1원 미만 절사(`BillOfQuantities_Rows.bill_line`).
line = unit_money.scaled(Decimal(str(quantity))).floored(Decimal(1))
result.material += line.material
result.labor += line.labor
result.expense += line.expense