"""B09 원가계산 — 단수 처리는 **출력 위치**에 붙는다. `resources/knowledge/technical_info/01_임도/05_원가정보/단수처리_규칙.md` §1 관측: 같은 「수량 × 단가」라도 **내역서 본체는 절사(ROUNDDOWN), 집계표는 반올림(ROUND)** 이다. 즉 단수 함수는 **항목이 아니라 표 종류에 바인딩**된다. 그래서 이 모듈이 있는 자리 — - **계산 함수 안에서 자르지 않는다.** 계산은 전정밀 `Decimal` 로 내고, **표를 그리는 자리에서** 이 모듈의 함수로 자른다. - ⚠ **집계표(반올림)와 본체(절사)를 더하면 합이 1원 단위로 어긋나는 것이 정상**이다. 나중에 「합계가 안 맞는다」는 지적이 반드시 나오는데, **그때 계산을 고치면 안 된다.** 어긋남 자체가 규칙이다. 값은 원문에서 복사하지 않는다 — 자릿수·함수만 여기 두고 근거는 위 문서를 가리킨다. """ from __future__ import annotations from decimal import ROUND_CEILING, ROUND_FLOOR, ROUND_HALF_UP, Decimal from enum import Enum _ONE = Decimal(1) _THOUSAND = Decimal(1000) class OutputPlace(str, Enum): """숫자가 찍히는 자리. 자리마다 단수 함수가 다르다.""" #: 설계내역서 행 금액(수량×단가) — 절사 BOQ_ROW = "boq_row" #: 제경비 각 항목(밑수×율) — 행별 절사 OVERHEAD_ROW = "overhead_row" #: 조달수수료(관급×율) — 절사 PROCUREMENT_FEE = "procurement_fee" #: 자원 집계표(재료·노무·경비·중기) — **반올림** RESOURCE_SUMMARY = "resource_summary" #: 관급자재대 총액 — **천원 올림** OWNER_MATERIAL_TOTAL = "owner_material_total" #: 일위대가표 금액란 — **0.1원 미만 버림** (품셈 1-2-2 「일위대가 금액란 0.1원 미만 버림」) UNIT_PRICE_ROW = "unit_price_row" #: 일위대가표의 **계금** — 1원 미만 버림. ⚠ 금액란(0.1원)과 **자리가 다르다** #: (품셈 1-2-2의 2 표에 두 줄이 따로 있다). UNIT_PRICE_TOTAL = "unit_price_total" #: **설계서의 총액** — 1,000원 미만 버림 (품셈 1-2-2의 2 「설계서의 총액 … 1,000 미만버림」). #: ⚠ 소계·금액란(1원)과 다르다. 맨 마지막 한 자리에서만 쓴다. GRAND_TOTAL = "grand_total" def round_at(value: Decimal, place: OutputPlace) -> Decimal: """그 자리의 규칙대로 자른다. 자리를 안 대고 부르는 길을 두지 않는다 — 기본값을 두면 어느 자리인지 모른 채 아무 함수나 쓰게 된다. """ if place is OutputPlace.GRAND_TOTAL: return (value / _THOUSAND).quantize(_ONE, rounding=ROUND_FLOOR) * _THOUSAND if place in ( OutputPlace.BOQ_ROW, OutputPlace.OVERHEAD_ROW, OutputPlace.PROCUREMENT_FEE, OutputPlace.UNIT_PRICE_TOTAL, ): return value.quantize(_ONE, rounding=ROUND_FLOOR) if place is OutputPlace.RESOURCE_SUMMARY: return value.quantize(_ONE, rounding=ROUND_HALF_UP) if place is OutputPlace.UNIT_PRICE_ROW: return value.quantize(Decimal("0.1"), rounding=ROUND_FLOOR) if place is OutputPlace.OWNER_MATERIAL_TOTAL: return (value / _THOUSAND).quantize(_ONE, rounding=ROUND_CEILING) * _THOUSAND raise ValueError(f"단수 처리 자리를 모릅니다: {place}") #: 집계표와 본체를 나란히 보일 때 화면 비고에 다는 문구. #: 「합이 1원 안 맞는다」는 지적에 계산을 고치지 않게 하려는 것이다. SUMMARY_MISMATCH_NOTE = ( "집계표는 반올림, 내역서 본체는 절사 — 두 표의 합이 원 단위로 어긋나는 것은 정상입니다." ) def summary_vs_body_gap(summary_total: Decimal, body_total: Decimal) -> Decimal: """집계표 합계와 내역서 본체 합계의 차이. **0 이 아닌 것이 정상**이다. 화면에 그 차이를 숨기지 않고 보여, 설계자가 「어긋남이 규칙임」을 알고 넘어가게 한다. """ return summary_total - body_total