feat(B09): 조종원 노임에 제수당·상여금·퇴직급여충당금 계수 반영
사용자 확정 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>
This commit is contained in:
@@ -148,12 +148,17 @@ def check_operator_hours_basis(
|
||||
daily_wage: Decimal,
|
||||
person_days: Decimal = Decimal(1),
|
||||
hours_per_day: int = 8,
|
||||
allowance_factor: Decimal = Decimal(1),
|
||||
) -> None:
|
||||
"""㉣ 보조 — 조종원 노임 나눗수가 8시간인가.
|
||||
|
||||
나눗수를 몰래 줄이는 것이 곧 효율을 사용료에 넣는 것과 같다.
|
||||
|
||||
⚠ `allowance_factor`(제수당·상여·퇴직충당 1.667배)는 **나눗수와 다르다** — 곱하는
|
||||
자리를 드러내 놓고 검사에도 같이 넘긴다. 계수를 몰래 나눗수에 녹이면 이 검사가
|
||||
통과해 버리므로, 계수는 **곱셈으로만** 들어와야 한다.
|
||||
"""
|
||||
expected = daily_wage * person_days / Decimal(hours_per_day)
|
||||
expected = daily_wage * person_days / Decimal(hours_per_day) * allowance_factor
|
||||
if abs(labor_per_hour - expected) > _TOLERANCE:
|
||||
raise DoubleCountError(
|
||||
f"조종원 시간당 노무비 {labor_per_hour:,.2f} 가 "
|
||||
|
||||
@@ -30,6 +30,10 @@ 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
|
||||
|
||||
@@ -138,10 +142,10 @@ def base_reference_data(build: UnitPriceBuild | None = None) -> dict[str, Any]:
|
||||
"code": code,
|
||||
"name": getattr(title, "name", "") or code,
|
||||
"day_wage_krw": _money(Decimal(str(wage))),
|
||||
# ⚠ 실무·교본은 「÷8 × 16/12 × 25/20」을 쓴다. 우리는 ÷8 만 쓴다 —
|
||||
# 그 계수의 규정 원문(기재부 예정가격 작성기준)을 아직 못 봤기 때문이다.
|
||||
"hourly_krw": _money(Decimal(str(wage)) / Decimal(8)),
|
||||
"formula": "일당 ÷ 8시간",
|
||||
# 제수당·상여금·퇴직급여충당금 계수를 곱한 값 (2026-09-09 사용자 확정 ③).
|
||||
# 근거·한계는 `MachineCost.OPERATOR_ALLOWANCE_FACTOR` 주석 한 곳에 모아 뒀다.
|
||||
"hourly_krw": _money(Decimal(str(wage)) / Decimal(8) * OPERATOR_ALLOWANCE_FACTOR),
|
||||
"formula": "일당 ÷ 8시간 × 16/12 × 25/20",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -152,11 +156,7 @@ def base_reference_data(build: UnitPriceBuild | None = None) -> dict[str, Any]:
|
||||
},
|
||||
"labor": {
|
||||
"rows": operators,
|
||||
"note": (
|
||||
"실무·임도교본은 여기에 「× 16/12 × 25/20」(약 1.67배)을 더 곱합니다. "
|
||||
"그 계수의 규정 원문을 아직 확인하지 못해 적용하지 않았습니다 — "
|
||||
"확정되면 기계가 든 공종 단가가 약 19~24% 오릅니다."
|
||||
),
|
||||
"note": OPERATOR_ALLOWANCE_NOTICE,
|
||||
},
|
||||
"fuel": {
|
||||
"diesel_krw_per_l": _money(fuel_price),
|
||||
|
||||
@@ -48,6 +48,39 @@ _THOUSAND = Decimal(1000)
|
||||
#: (★법대로 PLAN 8-10). TODO(미결 PLAN 9-6): 발주처가 실가동시간을 요구하는 사례 확인.
|
||||
OPERATOR_HOURS_PER_DAY = 8
|
||||
|
||||
#: 조종원 노임에 붙는 **제수당·상여금·퇴직급여충당금** 계수 (= 16/12 × 25/20 ≒ 1.6667).
|
||||
#:
|
||||
#: 왜 붙나 — **공표 노임은 「기본급여액」일 뿐**이다. 대한건설협회 「임금적용요령」 4-나
|
||||
#: (재경원 회계45101-45, 1995.1.13)가 「공표된 시중노임단가는 …기본급여액임. 따라서
|
||||
#: 근로기준법에서 규정하고 있는 **제수당, 상여금 및 퇴직급여충당금**은 …회계예규인
|
||||
#: 예정가격작성기준의 정한 바에 따라 계상하여야 함」이라고 못 박는다. 품셈 8-1-2 5.6호도
|
||||
#: 운전사 노임을 「예정가격 작성기준(기획재정부 회계예규)에 의거 계상한다」로 넘긴다.
|
||||
#: 그 예규가 정한 범위는 기재부 「(계약예규) 정부 입찰·계약 집행기준」 제76조의3
|
||||
#: (노무비의 계상) — **노임 + 제수당 + 상여금(연 400% 한도) + 퇴직급여충당금**이다.
|
||||
#:
|
||||
#: ⚠ **예규 원문은 저장소에 없다.** 위 둘은 그 예규를 **인용한** 문서다. 그래서 「무엇을
|
||||
#: 더하는가」는 원문으로 확인됐고 **「얼마를 더하는가」의 계수는 실무 관행을 따른다**
|
||||
#: (2026-09-09 사용자 확정 ③ 「없으면 실무 기준으로 가되 노티스를 붙여 줄 것」).
|
||||
#: · 16/12 — 상여금 연 400%(= 기본급 4개월분)를 12개월에 나눠 붙인 것과 **산술이 일치**한다.
|
||||
#: · 25/20 — 월 지급일수 25 대 가동일수 20 으로 읽히나 **원문 확인 못 함**. 짐작을 적지 않는다.
|
||||
#: 실물 셋이 같은 계수를 쓴다 — 실무 두 공사지(영월·봉화) 중기사용료 전수,
|
||||
#: 임도교본 예제(`산림과임업기술(임도)/2. 임도/5. 설계.md:512`), 상용 적산 프로그램의
|
||||
#: 기준계수표(`원가계산/다산소프트/ESTX_값사전_자재노무기계.md:427` `1/8*16/12*25/20`=0.20833).
|
||||
#:
|
||||
#: ⚠ **고용보험·산재보험·퇴직공제부금과 헷갈리지 말 것** — 그 셋은 노임 계수가 아니라
|
||||
#: 원가계산 뒷단의 **요율 항목**으로 따로 선다(산재 3.56% · 고용 1.01~1.57% · 퇴직공제 2.3%,
|
||||
#: `원가_입력변수_사전.md`). 여기에 겹쳐 넣으면 이중계상이 된다.
|
||||
#: ⚠ **기계 감가상각도 여기가 아니다** — 상각비는 손료(경비) 쪽이다(품셈 8-1-5 1호).
|
||||
OPERATOR_ALLOWANCE_FACTOR = (Decimal(16) / Decimal(12)) * (Decimal(25) / Decimal(20))
|
||||
|
||||
#: 화면·표가 그대로 띄우는 노티스 한 줄. 계수를 쓴 자리마다 같은 문구가 서야 한다.
|
||||
OPERATOR_ALLOWANCE_NOTICE = (
|
||||
"조종원 노임에 제수당·상여금·퇴직급여충당금 계수 1.667배(16/12 × 25/20)를 넣었습니다 — "
|
||||
"공표 노임은 기본급여액뿐이라 별도 계상해야 합니다(건협 임금적용요령 4-나, "
|
||||
"기재부 정부 입찰·계약 집행기준 제76조의3). ⚠ 계수 자체의 예규 원문은 아직 못 봐 "
|
||||
"실무 관행(실무 두 공사지·임도교본 예제·상용 적산 프로그램이 같은 계수)을 따랐습니다."
|
||||
)
|
||||
|
||||
|
||||
class MachineCostError(LookupError):
|
||||
"""기계경비를 세울 수 없는 경우. 0 으로 때우지 않고 멈춘다."""
|
||||
@@ -164,6 +197,7 @@ def hourly_machine_cost(
|
||||
fuel_price_per_liter: Decimal | None = None,
|
||||
operator_daily_wage: Decimal | None = None,
|
||||
operator_hours_per_day: int = OPERATOR_HOURS_PER_DAY,
|
||||
operator_allowance_factor: Decimal = OPERATOR_ALLOWANCE_FACTOR,
|
||||
efficiency_factor: Decimal | None = None,
|
||||
) -> HourlyMachineCost:
|
||||
"""시간당 사용료 한 시간분.
|
||||
@@ -192,12 +226,14 @@ def hourly_machine_cost(
|
||||
# TODO(미결 PLAN 9-6): `mach_operator_map` 0건 — 기종별 운전사 직종이 품셈 본문에만 있다.
|
||||
gaps.append("운전사 직종 매핑 미확보 — 노무비 성분 비어 있음")
|
||||
else:
|
||||
labor = operator_daily_wage / Decimal(operator_hours_per_day)
|
||||
# 나눗수는 8시간 그대로 두고 **계수를 곱한다** — 나눗수를 줄이는 것과 다르다.
|
||||
labor = (operator_daily_wage / Decimal(operator_hours_per_day)) * operator_allowance_factor
|
||||
# ㉣ 보조 — 나눗수를 몰래 줄이면 효율을 사용료에 넣은 것이 된다.
|
||||
check_operator_hours_basis(
|
||||
labor_per_hour=labor,
|
||||
daily_wage=operator_daily_wage,
|
||||
hours_per_day=operator_hours_per_day,
|
||||
allowance_factor=operator_allowance_factor,
|
||||
)
|
||||
|
||||
return HourlyMachineCost(
|
||||
|
||||
@@ -179,10 +179,11 @@ export function drawMachineTab(body: HTMLElement, data: BaseDataDto): void {
|
||||
// ⚠ 계산 과정을 감추지 않는다(PLAN 8-13). 조종원 환산이 실무와 다른 것을 여기서 밝힌다.
|
||||
body.append(
|
||||
note(
|
||||
"조종원 노임은 「노임 ÷ 8시간」으로 셉니다. 실무·임도교본은 여기에 " +
|
||||
"「× 16/12 × 25/20」(약 1.67배)을 더 곱합니다 — 그 계수의 규정 원문(기재부 " +
|
||||
"예정가격 작성기준)을 아직 확인하지 못해 적용하지 않았습니다. 확정되면 " +
|
||||
"기계가 든 공종 단가가 약 19~24% 오릅니다.",
|
||||
"조종원 노임은 「노임 ÷ 8시간 × 16/12 × 25/20」(약 1.667배)으로 셉니다. " +
|
||||
"공표 노임은 기본급여액뿐이라 제수당·상여금·퇴직급여충당금을 따로 계상해야 " +
|
||||
"합니다(건협 임금적용요령 4-나 · 기재부 정부 입찰·계약 집행기준 제76조의3). " +
|
||||
"⚠ 계수 자체의 예규 원문은 아직 못 봐 실무 관행을 따랐습니다 — 실무 두 공사지· " +
|
||||
"임도교본 예제·상용 적산 프로그램이 모두 같은 계수를 씁니다.",
|
||||
),
|
||||
);
|
||||
body.append(note("잡재료(주연료의 %)는 연료 소요량에 포함되어 있습니다 — 따로 세지 않습니다."));
|
||||
|
||||
@@ -23,7 +23,10 @@ from decimal import Decimal
|
||||
from functools import lru_cache
|
||||
|
||||
from B09_Estimation.B09_Estimation_Guards import check_column_sums, check_surcharge_once
|
||||
from B09_Estimation.B09_Estimation_MachineCost import load_machine_catalog
|
||||
from B09_Estimation.B09_Estimation_MachineCost import (
|
||||
OPERATOR_ALLOWANCE_FACTOR,
|
||||
load_machine_catalog,
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_MachineProductivity import (
|
||||
CycleFactors,
|
||||
FactorGap,
|
||||
@@ -191,7 +194,9 @@ def _add_machine_layers(book: PriceBook, machine_codes: set[str]) -> list[str]:
|
||||
wage_code = record.operator_occupation_code
|
||||
if wage_code and wage_code in wages and record.operator_person_days is not None:
|
||||
# ㉣ 나눗수는 8시간 — `PriceDetail` 수량이 「1시간분 인」이 된다.
|
||||
per_hour_person = record.operator_person_days / Decimal(8)
|
||||
# 여기에 **제수당·상여금·퇴직급여충당금 계수**(1.667배)를 곱한다. 공표 노임이
|
||||
# 기본급여액뿐이라 별도 계상해야 하는 몫이다(`MachineCost` 상수 주석에 근거).
|
||||
per_hour_person = (record.operator_person_days / Decimal(8)) * OPERATOR_ALLOWANCE_FACTOR
|
||||
if wage_code not in book.titles:
|
||||
book.add_title(
|
||||
PriceTitle(
|
||||
@@ -203,7 +208,12 @@ def _add_machine_layers(book: PriceBook, machine_codes: set[str]) -> list[str]:
|
||||
)
|
||||
)
|
||||
book.add_detail(
|
||||
PriceDetail(hourly_code, wage_code, per_hour_person, note="조종원 (1일 8시간)")
|
||||
PriceDetail(
|
||||
hourly_code,
|
||||
wage_code,
|
||||
per_hour_person,
|
||||
note="조종원 (1일 8시간 × 제수당·상여·퇴직충당 16/12 × 25/20)",
|
||||
)
|
||||
)
|
||||
else:
|
||||
incomplete.append(f"{code} (조종원 없음)")
|
||||
|
||||
Reference in New Issue
Block a user