feat(B09): 원가계산 화면에 공종 수량 연결
- 공종 수량(코드=수량)을 일위대가에 곱해 직접비 3분할 생성, ⑤ 원가계산서 밑수로 투입 — 뭉치지 않고 재료·노무·경비 성분 그대로 전달 - 단가 없는 공종은 0 으로 안 때우고 `missing_unit_prices` 로 화면 노출 - 계산 원천을 화면에 표시 (「수량 원천: 손입력 / 직접 입력」) - ⑤ 표에 찍히는 값은 자원 집계표 규칙(반올림)으로 절단 — 소수점 유출 제거 - 일위대가 요약에 총액 분포(최소·중앙·최대)·의심 저가 목록 추가 검증: pytest 136 통과, 공용 브라우저(5174) 실측 — 수량 입력 후 재계산 시 재료비 10,478,189 · 순공사원가 84,454,045 · 총공사비 113,652,213, 미매칭 공종 1건 화면 표시 확인 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import replace as dataclass_replace
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
@@ -28,10 +29,12 @@ from B09_Estimation.B09_Estimation_Engine_Cost import (
|
||||
from B09_Estimation.B09_Estimation_PriceBook import PriceBookError
|
||||
from B09_Estimation.B09_Estimation_Rates import RateLookupError
|
||||
from B09_Estimation.B09_Estimation_Statutory import STATUTORY_ITEMS
|
||||
from B09_Estimation.B09_Estimation_Rounding import OutputPlace, round_at
|
||||
from B09_Estimation.B09_Estimation_UnitPrice import (
|
||||
build_summary,
|
||||
cached_build,
|
||||
detail_of,
|
||||
direct_cost_from_quantities,
|
||||
list_unit_prices,
|
||||
)
|
||||
from common_util.common_util_workflow_state import complete_stage
|
||||
@@ -70,6 +73,11 @@ class CostRequest(BaseModel):
|
||||
|
||||
rate_file_name: str = "rates_2026.json"
|
||||
|
||||
#: 공종별 수량 `{공종코드: 수량}`. 주면 **직접비 3분할을 여기서 만들어** 쓴다.
|
||||
#: ⚠ 일위대가 합계를 뭉쳐 넣지 않는다 — 밑수가 항목마다 갈린다(PLAN 8-9 규칙 2).
|
||||
#: 지금 원천은 **손입력**이고, B08 인계(9번)가 나오면 **원천만 바꿔 끼운다**.
|
||||
quantities: dict[str, Decimal] | None = None
|
||||
|
||||
#: 목표 도급공사비 — 주면 「필요한 이윤 조정액」을 **보여만 준다**.
|
||||
#: ★ 법대로(PLAN 8-10) — 프로그램이 스스로 이윤을 깎지 않는다.
|
||||
target_contract_amount_krw: Decimal | None = None
|
||||
@@ -125,8 +133,25 @@ def _serialize(result: CostResult) -> dict[str, Any]:
|
||||
@router.post("/{project_id}/estimation/cost")
|
||||
async def compute_cost(project_id: UUID, payload: CostRequest) -> JSONResponse:
|
||||
"""공사원가계산서 한 장을 계산해 돌려준다 (저장 없음)."""
|
||||
direct_source = "manual"
|
||||
missing_unit_prices: list[str] = []
|
||||
try:
|
||||
result = calculate_cost(payload.to_engine_input())
|
||||
data = payload.to_engine_input()
|
||||
if payload.quantities:
|
||||
# 수량이 오면 **일위대가에서 직접비 3분할을 만들어** 갈아 끼운다.
|
||||
breakdown = direct_cost_from_quantities(payload.quantities)
|
||||
# ⑤ 표에 찍히는 자리라 **자원 집계표 규칙(반올림)** 으로 자른다 —
|
||||
# 안 자르면 원가계산서에 소수점이 그대로 흘러나온다.
|
||||
summary = OutputPlace.RESOURCE_SUMMARY
|
||||
data = dataclass_replace(
|
||||
data,
|
||||
direct_material_krw=round_at(breakdown.material, summary),
|
||||
direct_labor_krw=round_at(breakdown.labor, summary),
|
||||
direct_expense_krw=round_at(breakdown.expense, summary),
|
||||
)
|
||||
direct_source = "quantities"
|
||||
missing_unit_prices = breakdown.missing
|
||||
result = calculate_cost(data)
|
||||
except RateLookupError as error:
|
||||
# 요율 구간을 못 고른 경우 — 기본값으로 때우지 않고 그대로 알린다.
|
||||
logger.warning("B09 원가계산 요율 조회 실패: project_id=%s, %s", project_id, error)
|
||||
@@ -139,6 +164,10 @@ async def compute_cost(project_id: UUID, payload: CostRequest) -> JSONResponse:
|
||||
)
|
||||
|
||||
body = _serialize(result)
|
||||
# 어느 값으로 계산했는지 화면이 알아야 한다 — 안 보이면 나중에 못 가른다.
|
||||
body["direct_cost_source"] = direct_source
|
||||
# 수량은 있는데 단가가 없는 공종 — **화면에 반드시 보인다**.
|
||||
body["missing_unit_prices"] = missing_unit_prices
|
||||
if payload.target_contract_amount_krw is not None:
|
||||
# 필요액을 **보여만 준다**. 적용은 설계자가 `profit_adjustment_krw` 로 명시해야 한다.
|
||||
body["suggested_profit_adjustment_krw"] = str(
|
||||
|
||||
Reference in New Issue
Block a user