diff --git a/B09_Estimation/B09_Estimation_Guards.py b/B09_Estimation/B09_Estimation_Guards.py index 0bf0fdc7..997a726c 100644 --- a/B09_Estimation/B09_Estimation_Guards.py +++ b/B09_Estimation/B09_Estimation_Guards.py @@ -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} 가 " diff --git a/B09_Estimation/B09_Estimation_Lists_Sources.py b/B09_Estimation/B09_Estimation_Lists_Sources.py index 9568d708..514c1b4b 100644 --- a/B09_Estimation/B09_Estimation_Lists_Sources.py +++ b/B09_Estimation/B09_Estimation_Lists_Sources.py @@ -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), diff --git a/B09_Estimation/B09_Estimation_MachineCost.py b/B09_Estimation/B09_Estimation_MachineCost.py index 21fdb3a2..a571234b 100644 --- a/B09_Estimation/B09_Estimation_MachineCost.py +++ b/B09_Estimation/B09_Estimation_MachineCost.py @@ -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( diff --git a/B09_Estimation/B09_Estimation_UI_BaseData.ts b/B09_Estimation/B09_Estimation_UI_BaseData.ts index 03b44474..56f6d507 100644 --- a/B09_Estimation/B09_Estimation_UI_BaseData.ts +++ b/B09_Estimation/B09_Estimation_UI_BaseData.ts @@ -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("잡재료(주연료의 %)는 연료 소요량에 포함되어 있습니다 — 따로 세지 않습니다.")); diff --git a/B09_Estimation/B09_Estimation_UnitPrice.py b/B09_Estimation/B09_Estimation_UnitPrice.py index 46d2e785..cceb5ae2 100644 --- a/B09_Estimation/B09_Estimation_UnitPrice.py +++ b/B09_Estimation/B09_Estimation_UnitPrice.py @@ -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} (조종원 없음)")