feat(B09): ⑤↔③ 연결 — 일위대가 3분할을 직접비로 접어 넣음 (뭉치지 않음)
**⚠ 일위대가 합계를 순공사비로 뭉쳐 넣으면 ⑤ 의 밑수가 전부 틀림**(8-9 규칙 2) —
산재·고용은 노무비, 건강·연금은 직접노무비, 기타경비는 재료비+노무비를 봄.
일위대가가 3분할을 이미 들고 있으므로 **성분별로 접어 넣음**.
- `direct_cost_from_quantities({공종코드: 수량})` → **직접비 3분할**.
단가가 없는 공종은 **0 으로 안 때우고** `missing` 에 남김 — 수량이 있는데 단가가
없으면 그 공종이 총액에서 조용히 빠짐.
- `cost_input_from_quantities()` → 성분이 그대로 `direct_material/labor/expense_krw` 로 감.
- **뭉치면 틀린다는 것을 수치로 보이는 시험**을 둠 — 같은 총액을 노무비 한 덩어리로
넣으면 건강보험료가 커짐(`test_lumping_would_break_the_bases`).
- ⇒ ④예산내역서 없이도 **수량만 있으면 ⑤ 가 실물로 도는 경로**가 생김.
**자재 잠정(㉡) 화면 표시** — 「사급 자재 단가는 설계자가 직접 넣습니다(6번 슬롯
「적용 단가」) — 유료 물가지 미구독 … (잠정 — 구독하면 1~5번 슬롯에 꽂습니다)」.
화면 실측으로 3줄 다 뜨는 것 확인(일위대가 67건).
- 곁가지 — 화면은 평문이라 `**강조**` 별표가 그대로 보였음. 내보내기 직전에 벗김.
자체검증 — 신규 4건 포함 `pytest tmp/tests/ -q` **133 passed** · ruff 통과 ·
백엔드 재시작 후 화면 클릭 검증.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -151,7 +151,7 @@ def catalog_summary(catalog: MaterialCatalog) -> dict[str, Any]:
|
||||
"owner_supplied_install_unspecified": len(unspecified),
|
||||
"gaps": list(catalog.gaps),
|
||||
"notes": [
|
||||
"자재 단가는 **할증 전·부가세 제외** 값입니다 — 할증은 자재총괄에서 한 번만 붙습니다.",
|
||||
"관급 자재의 **설치 주체가 미지정**이라 안전관리비 대상액에 자동으로 넣지 않습니다.",
|
||||
"자재 단가는 할증 전·부가세 제외 값입니다 — 할증은 자재총괄에서 한 번만 붙습니다.",
|
||||
"관급 자재의 설치 주체가 미지정이라 안전관리비 대상액에 자동으로 넣지 않습니다.",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from decimal import Decimal
|
||||
from functools import lru_cache
|
||||
@@ -43,6 +44,7 @@ from B09_Estimation.B09_Estimation_ResourceAxis import (
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_Rounding import OutputPlace, round_at
|
||||
|
||||
_RE_EMPHASIS = re.compile(r"\*\*(.+?)\*\*")
|
||||
_ZERO = Decimal(0)
|
||||
#: 연료는 자재 카탈로그에 없어 합성 코드로 세운다 — 코드가 있어야 조인이 성립한다.
|
||||
FUEL_CODE_PREFIX = "M-FUEL-"
|
||||
@@ -278,6 +280,11 @@ def cached_build() -> UnitPriceBuild:
|
||||
return build_unit_prices()
|
||||
|
||||
|
||||
def _plain(text: str) -> str:
|
||||
"""화면용 평문 — 마크다운 강조 표시를 벗긴다."""
|
||||
return _RE_EMPHASIS.sub(lambda match: match.group(1), text)
|
||||
|
||||
|
||||
def _status_notes() -> list[str]:
|
||||
"""화면에 낼 「지금 무엇이 안 선 상태인가」.
|
||||
|
||||
@@ -290,17 +297,22 @@ def _status_notes() -> list[str]:
|
||||
)
|
||||
|
||||
summary = catalog_summary(load_material_catalog())
|
||||
# 화면은 평문이라 마크다운 강조가 그대로 보인다 — 내보내기 직전에 벗긴다.
|
||||
return [
|
||||
f"관급 자재 {summary['items']:,}건을 붙였으나 **임도 자재는 거의 없습니다** — "
|
||||
"나라장터 목록이 건축·설비 자재 중심이고, 시멘트·모래·자갈은 애초에 사급이며 "
|
||||
"철근·레미콘·아스콘은 원천에서 빠져 있습니다.",
|
||||
"**사급 자재 단가가 미결**이라 구조물 계열 일위대가가 아직 서지 않습니다 — "
|
||||
"값을 지어내지 않고 6번 슬롯(적용 단가) 수동 입력으로 채웁니다.",
|
||||
(
|
||||
f"관급 자재 **설치 주체가 미지정**"
|
||||
f"({summary['owner_supplied_install_unspecified']:,}건)이라 "
|
||||
"안전관리비 대상액에 자동으로 넣지 않습니다."
|
||||
),
|
||||
_plain(note)
|
||||
for note in [
|
||||
f"관급 자재 {summary['items']:,}건을 붙였으나 **임도 자재는 거의 없습니다** — "
|
||||
"나라장터 목록이 건축·설비 자재 중심이고, 시멘트·모래·자갈은 애초에 사급이며 "
|
||||
"철근·레미콘·아스콘은 원천에서 빠져 있습니다.",
|
||||
"**사급 자재 단가는 설계자가 직접 넣습니다**(6번 슬롯 「적용 단가」) — "
|
||||
"유료 물가지 미구독. 값을 지어내지 않으므로, 넣기 전까지 구조물 계열 "
|
||||
"일위대가는 서지 않습니다. (잠정 — 물가지를 구독하면 1~5번 슬롯에 꽂습니다.)",
|
||||
(
|
||||
f"관급 자재 **설치 주체가 미지정**"
|
||||
f"({summary['owner_supplied_install_unspecified']:,}건)이라 "
|
||||
"안전관리비 대상액에 자동으로 넣지 않습니다."
|
||||
),
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
@@ -322,7 +334,7 @@ def build_summary(build: UnitPriceBuild) -> dict:
|
||||
|
||||
|
||||
def _money_text(value: Decimal) -> str:
|
||||
"""화면에 낼 금액 — **일위대가 금액란은 0.1원 미만 버림**(품셈 1-2-2).
|
||||
"""화면에 낼 금액 — 일위대가 금액란은 0.1원 미만 버림(품셈 1-2-2).
|
||||
|
||||
계산은 전정밀로 두고 **표를 그리는 자리에서만** 자른다
|
||||
(`B09_Estimation_Rounding` — 단수는 출력 위치에 붙는다).
|
||||
@@ -413,3 +425,76 @@ def detail_of(build: UnitPriceBuild, code: str) -> dict:
|
||||
"precise_total": _money_text(money.total),
|
||||
"rows": rows,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class DirectCostBreakdown:
|
||||
"""⑤ 공사원가계산서가 받는 **직접비 3분할**.
|
||||
|
||||
⚠ **일위대가 합계를 순공사비로 뭉쳐 넣으면 안 된다.** ⑤ 의 밑수는 항목마다 갈리고
|
||||
(산재·고용 = 노무비 / 건강·연금 = 직접노무비 / 기타경비 = 재료비+노무비 …),
|
||||
뭉쳐 넣으면 그 밑수가 전부 틀린다(PLAN 8-9 규칙 2). 일위대가는 3분할을 이미
|
||||
들고 있으니 **성분별로 접어 넣는다.**
|
||||
"""
|
||||
|
||||
material: Decimal = _ZERO
|
||||
labor: Decimal = _ZERO
|
||||
expense: Decimal = _ZERO
|
||||
#: 값을 못 세운 공종 — 수량이 있는데 단가가 없으면 여기 남는다(0 으로 안 때운다).
|
||||
missing: list[str] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def total(self) -> Decimal:
|
||||
return self.material + self.labor + self.expense
|
||||
|
||||
|
||||
def direct_cost_from_quantities(
|
||||
quantities: dict[str, Decimal],
|
||||
build: UnitPriceBuild | None = None,
|
||||
) -> DirectCostBreakdown:
|
||||
"""공종별 수량을 일위대가에 곱해 **직접비 3분할**을 만든다.
|
||||
|
||||
`quantities` = `{공종코드: 수량}`. 공종코드는 `FP-09-21` 처럼 마스터 코드를 쓰거나
|
||||
`B-FP-09-21` 처럼 일위대가 코드를 그대로 써도 된다.
|
||||
|
||||
단가가 없는 공종은 **0 으로 안 때우고** `missing` 에 남긴다 — 수량이 있는데 단가가
|
||||
없으면 그 공종이 총액에서 조용히 빠진다.
|
||||
"""
|
||||
book = (build or cached_build()).book
|
||||
result = DirectCostBreakdown()
|
||||
|
||||
for raw_code, quantity in quantities.items():
|
||||
code = raw_code if raw_code.startswith("B-") else f"B-{raw_code}"
|
||||
if code not in book.titles:
|
||||
result.missing.append(raw_code)
|
||||
continue
|
||||
unit_money = book.resolve(code)
|
||||
line = unit_money.scaled(Decimal(str(quantity)))
|
||||
result.material += line.material
|
||||
result.labor += line.labor
|
||||
result.expense += line.expense
|
||||
return result
|
||||
|
||||
|
||||
def cost_input_from_quantities(
|
||||
quantities: dict[str, Decimal],
|
||||
build: UnitPriceBuild | None = None,
|
||||
**cost_input_kwargs,
|
||||
):
|
||||
"""직접비 3분할을 ⑤ 엔진 입력으로 접어 넣는다.
|
||||
|
||||
성분이 그대로 `direct_material_krw`·`direct_labor_krw`·`direct_expense_krw` 로 간다 —
|
||||
**뭉치지 않는다.**
|
||||
"""
|
||||
from B09_Estimation.B09_Estimation_Engine_Cost import CostInput
|
||||
|
||||
breakdown = direct_cost_from_quantities(quantities, build)
|
||||
return (
|
||||
CostInput(
|
||||
direct_material_krw=breakdown.material,
|
||||
direct_labor_krw=breakdown.labor,
|
||||
direct_expense_krw=breakdown.expense,
|
||||
**cost_input_kwargs,
|
||||
),
|
||||
breakdown,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user