feat(B09): ④ 예산내역서 엔드포인트 추가
`GET /api/projects/{id}/estimation/bill` — B08 인계를 서버 안에서 그대로
받아 계층을 세우고 단가를 붙여 돌려줌
- 수량은 B08 것이 정본, 여기서 다시 세지 않음 (CLAUDE.md 5장)
- 단가 없음·밑수 미확보·공종코드 없음은 0 으로 안 때우고 `missing` 으로 냄
- 이중계상 감시에 걸리면 표를 그리지 않고 409 로 멈춤
실측(프로젝트 5cff3920, 공용 브라우저 5174):
14줄(세부 7·머리 7) · 검산줄 1 · 자재 4 · 본체합계 478,680원
측구터파기 90.51㎥ × 5,288.6 = 478,680, 나머지 6줄은 사유째 미확보로 뜸
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import replace as dataclass_replace
|
||||
from decimal import Decimal
|
||||
@@ -27,6 +28,8 @@ from B09_Estimation.B09_Estimation_Engine_Cost import (
|
||||
proposed_profit_adjustment,
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_PriceBook import PriceBookError
|
||||
from B09_Estimation.B09_Estimation_BillOfQuantities import bill_summary, build_bill
|
||||
from B09_Estimation.B09_Estimation_Guards import DoubleCountError
|
||||
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
|
||||
@@ -246,3 +249,52 @@ async def confirm_estimation(project_id: UUID) -> JSONResponse:
|
||||
content={"status": "error", "message": "원가계산 단계 확정에 실패했습니다."},
|
||||
)
|
||||
return JSONResponse(content={"status": "success", "project_id": str(project_id)})
|
||||
|
||||
|
||||
@router.get("/{project_id}/estimation/bill")
|
||||
async def get_bill(project_id: UUID) -> JSONResponse:
|
||||
"""④ 예산내역서 한 장 — B08 인계를 그대로 받아 계층을 세워 돌려준다.
|
||||
|
||||
⚠ **수량을 다시 세지 않는다.** B08 인계가 정본이고 여기서는 단가를 붙여 금액만
|
||||
만든다(CLAUDE.md 5장 「같은 계산을 두 벌로 짜지 않는다」).
|
||||
|
||||
⚠ 단가가 없거나 밑수를 모르는 줄은 **0 으로 안 때우고** `missing` 으로 드러낸다 —
|
||||
화면이 그 목록을 그대로 보인다.
|
||||
"""
|
||||
from B08_Quantity.B08_Quantity_Router_Material import get_handoff
|
||||
|
||||
try:
|
||||
response = await get_handoff(project_id)
|
||||
payload = json.loads(bytes(response.body).decode("utf-8"))
|
||||
except Exception:
|
||||
logger.exception("B09 내역서 조회 실패(인계): project_id=%s", project_id)
|
||||
return JSONResponse(
|
||||
status_code=502,
|
||||
content={"status": "error", "message": "B08 인계 자료를 받지 못했습니다."},
|
||||
)
|
||||
if "work_items" not in payload:
|
||||
# B08 이 오류 응답을 준 경우 — 그 사유를 그대로 넘긴다(감추지 않는다).
|
||||
return JSONResponse(status_code=502, content={"status": "error", **payload})
|
||||
|
||||
try:
|
||||
result = build_bill(payload)
|
||||
except DoubleCountError as error:
|
||||
# 이중계상 감시에 걸린 경우 — 표를 그리지 않고 멈춘다.
|
||||
logger.warning("B09 내역서 이중계상 감지: project_id=%s, %s", project_id, error)
|
||||
return JSONResponse(status_code=409, content={"status": "error", "message": str(error)})
|
||||
except Exception:
|
||||
logger.exception("B09 내역서 조판 실패: project_id=%s", project_id)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"status": "error", "message": "예산내역서를 세우지 못했습니다."},
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"status": "success",
|
||||
"rows": [row.as_dict() for row in result.rows],
|
||||
"excluded": [row.as_dict() for row in result.excluded],
|
||||
"materials": [row.as_dict() for row in result.material_rows],
|
||||
"summary": bill_summary(result),
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user