사용자 확정 2차 ③ — 기술문서 DB 를 먼저 뒤지고, 계수 원문이 없으면 실무 기준 + 노티스. 근거 (원문 확인분) - 대한건설협회 임금적용요령 4-나 (재경원 회계45101-45, 1995.1.13): 공표 노임은 **기본급여액**이며 제수당·상여금·퇴직급여충당금은 예정가격작성기준에 따라 별도 계상 - 기재부 「(계약예규) 정부 입찰·계약 집행기준」 제76조의3(노무비의 계상): 노임 + 제수당 + 상여금(연 400% 한도) + 퇴직급여충당금 - 품셈 8-1-2 5.6호가 운전사 노임을 그 예규로 넘김 미확인 (노티스로 드러냄) - 예규 원문 자체는 저장소에 없음 — 위 둘은 인용 문서 - 16/12 는 상여 연 400% 와 산술이 일치하나 25/20 은 원문 근거 못 찾음 - 실물 셋이 같은 계수 사용: 실무 두 공사지·임도교본 예제·다산 ESTX 기준계수 0.20833 구현 - `OPERATOR_ALLOWANCE_FACTOR` = 16/12 × 25/20, 근거·한계 주석을 이 한 곳에 모음 - 나눗수 8시간은 그대로 두고 **곱셈으로만** 적용 — 나눗수를 줄이면 작업효율을 시간당 사용료에 넣은 것이 되어 ㉣ 위반. 검사(`check_operator_hours_basis`)에도 계수를 넘겨 곱셈 자리를 드러냄 - 일위대가 조종원 줄·A10 인건비·중기 탭 노티스를 같은 문구로 맞춤 검증 (79da822d 기준) - 굴착기 0.7 86,096.4 → 109,704.5 (+27.4%) · 불도저 19t 124,842.3 → 148,450.4 (+18.9%) - 조종원 수량 0.20833 = 다산 ESTX 기준계수와 일치 - 실무 시트 재현: 267,360 ÷ 8 × 계수 = 55,700 (시트 값과 일원까지 일치) - 본체 11,356,910 → 12,141,903 (+784,993, +6.9%) - pytest 267 통과 · tsc 통과 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
173 lines
7.8 KiB
Python
173 lines
7.8 KiB
Python
"""B09 원가계산 — **자재단가대비표(A9) · 환율및기초자료(A10)**.
|
||
|
||
사용자 확정 12번(내야 할 표 16개) 중 남은 둘. ③·⑮ 와 한 벌이다.
|
||
|
||
A9 자재단가대비표 코드·명칭·규격·단위 · **원천 5칸(단가+쪽수)** · 적용(단가+출처) · 비고
|
||
A10 환율및기초자료 ① 환율 ② 인건비(운전사 3종) ③ 단가 및 재료비
|
||
|
||
**서식은 지어내지 않았다** — 실무 내역서(영월 기번6) 같은 이름 시트를 그대로 옮겼다.
|
||
실측한 머리글:
|
||
|
||
코드번호 | 명칭 | 규격 | 단위 | 물가자료 | 물가정보 | 유통물가 | 거래가격등 | 기타단가 | 적용 | 비고
|
||
(각 칸이 「단가 · 페이지」 두 칸)
|
||
적용 칸의 「페이지」 자리에는 **출처 약호**가 온다 — 「기.가」·「유.물」·「견적」.
|
||
|
||
⚠ **원천 이름은 사무소마다 다르다**(PLAN 9-4 미결). 그래서 이름을 코드에 박지 않고
|
||
`PriceBook.slot_names` 를 그대로 쓴다 — 프로젝트가 고르면 표 머리도 따라 바뀐다.
|
||
|
||
⚠ **사용자 확정 ③ — 자재값 출처는 「둘 다」**(업체 견적·지역 상차가 **와** 물가지).
|
||
그래서 슬롯마다 **쪽수/업체명·날짜**를 함께 들 자리를 낸다. 값이 없으면 **빈칸으로 두고
|
||
지어내지 않는다** — 나중에 「왜 이 값인지」를 되짚어야 하기 때문이다.
|
||
|
||
⚠ **사용자 확정 ⑮ — 유가는 전국 공시가/지역 공시가를 고를 수 있어야 한다.**
|
||
지금 자료(`oil_*.json`)에는 **전국평균 하나뿐**이라(`scope: national_average`),
|
||
**고르는 칸은 내되 지역값은 지어내지 않는다.** 고를 수 있는 것만 보이고, 지역을 고르면
|
||
「아직 자료가 없다」고 밝힌다.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from decimal import Decimal
|
||
from typing import Any
|
||
|
||
from B09_Estimation.B09_Estimation_MachineCost import (
|
||
OPERATOR_ALLOWANCE_FACTOR,
|
||
OPERATOR_ALLOWANCE_NOTICE,
|
||
)
|
||
from B09_Estimation.B09_Estimation_PriceBook import PRICE_SLOT_COUNT, PriceKind
|
||
from B09_Estimation.B09_Estimation_UnitPrice import UnitPriceBuild, cached_build
|
||
|
||
#: 유가 적용 범위 — 사용자 확정 ⑮. 값이 있는 것만 고를 수 있다.
|
||
FUEL_SCOPES: tuple[dict[str, Any], ...] = (
|
||
{"key": "national_average", "label": "전국 공시가", "available": True},
|
||
{
|
||
"key": "regional",
|
||
"label": "지역 공시가",
|
||
"available": False,
|
||
"why": (
|
||
"오피넷 지역별 값을 아직 안 받아 왔습니다 — 품셈 8-1-7 5호가 「유류가격은 "
|
||
"해당 지역의 가격」이라 규정하므로 자료를 받으면 고를 수 있게 됩니다."
|
||
),
|
||
},
|
||
)
|
||
|
||
|
||
def _money(value: Decimal | None) -> str | None:
|
||
return None if value is None else str(value)
|
||
|
||
|
||
def material_price_comparison(build: UnitPriceBuild | None = None) -> dict[str, Any]:
|
||
"""자재단가대비표 — **원천을 나란히 두고 채택한 것을 표시**한다 (실무 서식 그대로).
|
||
|
||
⚠ 값이 없는 슬롯은 **0 이 아니라 빈칸**이다. 0 을 넣으면 「0원짜리 견적」으로 읽힌다.
|
||
"""
|
||
prices = build or cached_build()
|
||
book = prices.book
|
||
rows: list[dict[str, Any]] = []
|
||
for code in sorted(book.titles):
|
||
title = book.titles[code]
|
||
if title.kind is not PriceKind.MATERIAL:
|
||
continue
|
||
slots: list[dict[str, Any]] = []
|
||
for index in range(PRICE_SLOT_COUNT):
|
||
value = title.slots[index] if index < len(title.slots) else None
|
||
page = title.slot_pages[index] if index < len(title.slot_pages) else None
|
||
slots.append(
|
||
{
|
||
"name": (
|
||
book.slot_names[index]
|
||
if index < len(book.slot_names)
|
||
else f"슬롯{index + 1}"
|
||
),
|
||
"price_krw": _money(value),
|
||
# 「페이지」 자리 — 물가지는 쪽수, 견적은 업체명·날짜가 온다(확정 ③).
|
||
"source_note": page or "",
|
||
"adopted": (index + 1) == title.adopted_slot,
|
||
}
|
||
)
|
||
try:
|
||
adopted: Decimal | None = title.adopted_price()
|
||
note = ""
|
||
except Exception as error: # 채택 슬롯이 비었다 — 지어내지 않는다
|
||
adopted, note = None, str(error)
|
||
rows.append(
|
||
{
|
||
"code": code,
|
||
"name": title.name,
|
||
"spec": title.spec,
|
||
"unit": title.unit,
|
||
"slots": slots,
|
||
"adopted_slot": title.adopted_slot,
|
||
"adopted_price_krw": _money(adopted),
|
||
"note": note,
|
||
}
|
||
)
|
||
return {
|
||
"slot_names": list(book.slot_names),
|
||
"rows": rows,
|
||
"notes": [
|
||
"원천을 나란히 두고 하나를 채택합니다 — 실무 자재단가대비표와 같은 서식입니다.",
|
||
"값이 없는 원천은 빈칸입니다. 0 으로 채우지 않습니다.",
|
||
"견적을 넣을 때는 업체명·견적일을 함께 남기십시오 — 국가계약법 시행령 §9 의 "
|
||
"4순위(앞의 셋으로 정할 수 없을 때)라 근거가 남아야 합니다.",
|
||
],
|
||
}
|
||
|
||
|
||
def base_reference_data(build: UnitPriceBuild | None = None) -> dict[str, Any]:
|
||
"""환율및기초자료 — 실무 시트 세 구획을 그대로 낸다.
|
||
|
||
① 환율 ② 인건비(운전사 3종) ③ 단가 및 재료비(유류 등)
|
||
"""
|
||
from B09_Estimation.B09_Estimation_MachineOperating import (
|
||
load_fuel_price,
|
||
load_operator_wages,
|
||
)
|
||
|
||
prices = build or cached_build()
|
||
fuel_price, fuel_meta = load_fuel_price()
|
||
wages = load_operator_wages()
|
||
|
||
# ⚠ **운전사 세 직종만** 싣는다. 실무 「환율및기초자료」 시트가 그 셋뿐이고
|
||
# (건설기계운전사·화물차운전사·일반기계운전사, 건설품셈 8-1-3 5~6호의 구분),
|
||
# 직종 전체를 실으면 **노무비목록표와 같은 표가 두 벌**이 된다.
|
||
operator_names = ("건설기계운전사", "화물차운전사", "일반기계운전사")
|
||
operators: list[dict[str, Any]] = []
|
||
for code, wage in sorted(wages.items()):
|
||
title = prices.book.titles.get(code)
|
||
if getattr(title, "name", "") not in operator_names:
|
||
continue
|
||
operators.append(
|
||
{
|
||
"code": code,
|
||
"name": getattr(title, "name", "") or code,
|
||
"day_wage_krw": _money(Decimal(str(wage))),
|
||
# 제수당·상여금·퇴직급여충당금 계수를 곱한 값 (2026-09-09 사용자 확정 ③).
|
||
# 근거·한계는 `MachineCost.OPERATOR_ALLOWANCE_FACTOR` 주석 한 곳에 모아 뒀다.
|
||
"hourly_krw": _money(Decimal(str(wage)) / Decimal(8) * OPERATOR_ALLOWANCE_FACTOR),
|
||
"formula": "일당 ÷ 8시간 × 16/12 × 25/20",
|
||
}
|
||
)
|
||
|
||
return {
|
||
"exchange": {
|
||
"rows": [],
|
||
"note": "수입 기계가 아직 없어 환율을 쓰는 자리가 없습니다 — 생기면 여기에 섭니다.",
|
||
},
|
||
"labor": {
|
||
"rows": operators,
|
||
"note": OPERATOR_ALLOWANCE_NOTICE,
|
||
},
|
||
"fuel": {
|
||
"diesel_krw_per_l": _money(fuel_price),
|
||
"scope": fuel_meta.get("scope", ""),
|
||
"effective_date": fuel_meta.get("effective_date", ""),
|
||
"dataset_id": fuel_meta.get("dataset_id", ""),
|
||
"scopes": list(FUEL_SCOPES),
|
||
"note": (
|
||
"품셈 8-1-7 5호 「유류가격은 해당 지역의 가격」 — 지역 공시가를 고를 수 있게 "
|
||
"칸을 두었으나 아직 전국 공시가만 받아 와 있습니다."
|
||
),
|
||
},
|
||
}
|