수량 자릿수 (품셈 1-2-2의 1) - 화면이 모든 수량을 2자리로 찍고 있었음. 품셈은 종목별로 자리를 따로 정함 — 체적합계·시멘트·철근 정수 / 돌쌓기·옹벽·떼 1자리 / 철강재 3자리. - 수량은 반올림([주]①), 금액은 버림 — 반대라 모듈을 갈라 둠. - 항목이 표를 이김([주]②) → override 받음. 표에 없는 종목은 안 건드림. - 서버가 줄마다 quantity_digits 를 실어 보내고 화면은 그 자리로 찍기만 함. 금액 자리 보강 (품셈 1-2-2의 2) - 설계서 총액 1,000원 미만 버림 · 일위대가 계금 1원 미만 버림 추가. 금액란(0.1원)·소계(1원)와 자리가 넷 다 다름. ⚠ 단위 불일치 가드 - 돌쌓기(찰)이 연장 10m 로 오는데 일위대가는 ㎡당이라 52,938.9×10 = 529,389원이 조용히 서 있었음. 면적으로 세면 1,381,753원 — 2.6배 적은 금액이었음. - 단위가 다르면 곱하지 않고 사유를 남김. 표기 차이(㎥/m3·개소/개)는 흡수하되 환산은 안 함. 내역 합계 6,252,172 → 5,048,775 (틀린 두 줄이 빠짐). 제잡비 밑수 - 닫힌 물음임을 원문 인용으로 못 박음 — 산림품셈 [주]③ 여섯 자리가 「노무비의 합계액」이고, 일위대가 안에는 간접노무비가 없으므로 그 표의 노무비 합 = 직접노무비. 하루에 두 번 뒤집힌 값이라 세 번째가 없게 근거를 남김. 시험 10건 추가, 238건 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
87 lines
4.0 KiB
Python
87 lines
4.0 KiB
Python
"""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
|