- 잡품을 연료 수량에 접지 않고 주연료비의 % 가산 행으로(STmate 잡품 단가 칸 = 연료 금액) - 시간당 중기사용료(X) 호표 — 줄 금액 0.1원 미만 · 성분 소계 원 미만 절사(명세 7장, 봉화 0.7㎥ 96,843) - 조종원 시간당 노임 자르는 자리를 프로젝트 설정 operator_wage_digits 로(기본 0, 화면은 뒤) - 골든셋 ④ 벌 — 실무 6건 중기사용료 시트 143 호표를 우리 단가표 층으로 되풀어 전수 일치 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
102 lines
4.5 KiB
Python
102 lines
4.5 KiB
Python
"""조종원 시간당 노임 — 식 좌→우 순차 + 원 미만 절사 (2026-09-13, PLAN 6장 · 명세 7장).
|
||
|
||
기준점 — STmate 분석 18번 §2.1: 6건 × 3직종 18/18 이 좌→우일 때만 일치(계수 선계산 14/18).
|
||
건설기계운전사 267,360 → 55,700 · 화물차운전사 226,709 → 47,231 ·
|
||
일반기계운전사 161,142 → 33,571
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from decimal import Decimal
|
||
from functools import lru_cache
|
||
|
||
import pytest
|
||
|
||
from B09_Estimation.B09_Estimation_MachineCost import (
|
||
OPERATOR_ALLOWANCE_FACTOR,
|
||
OPERATOR_WAGE_FORMULA,
|
||
hourly_operator_wage,
|
||
)
|
||
from B09_Estimation.B09_Estimation_UnitPrice import HOURLY_WAGE_SUFFIX, build_unit_prices
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("daily", "hourly"),
|
||
[(267360, 55700), (226709, 47231), (161142, 33571)],
|
||
)
|
||
def test_좌에서_우로_차례로_풀고_원_미만_절사(daily: int, hourly: int) -> None:
|
||
assert OPERATOR_WAGE_FORMULA == "1/8*16/12*25/20"
|
||
assert hourly_operator_wage(Decimal(daily)) == Decimal(hourly)
|
||
|
||
|
||
def test_계수를_미리_접으면_1원_틀린다() -> None:
|
||
"""회귀 막이 — 접은 계수 곱은 55,699.99… 라 절사하면 55,699(STmate 55,700)."""
|
||
folded = (Decimal(267360) / 8 * OPERATOR_ALLOWANCE_FACTOR).to_integral_value(
|
||
rounding="ROUND_FLOOR"
|
||
)
|
||
assert folded == Decimal(55699) # 종전 코드 — 계수 1.666…6 이 끝자리에서 잘려 55,699.99…
|
||
assert hourly_operator_wage(Decimal(267360), digits=None) == Decimal(55700)
|
||
|
||
|
||
@lru_cache(maxsize=1)
|
||
def _build():
|
||
return build_unit_prices()
|
||
|
||
|
||
def test_자르는_자리는_프로젝트_설정이고_기본은_원_미만() -> None:
|
||
"""실무마다 다름(다섯 건 0 · 2025 울진 소광 1) — 규정이 없어 설정 칸(브레인 판정)."""
|
||
from B09_Estimation.B09_Estimation_UnitPrice import parse_operator_wage_digits
|
||
|
||
assert [parse_operator_wage_digits(v) for v in ("", None, "0", "1", "2", "3", "x")] == [
|
||
0, 0, 0, 1, 2, 0, 0,
|
||
] # fmt: skip
|
||
assert hourly_operator_wage(Decimal(273971), digits=1) == Decimal("57077.2")
|
||
one = build_unit_prices(operator_wage_digits=1)
|
||
assert one.operator_wage_digits == 1
|
||
code = next(c for c in one.book.titles if c.endswith(HOURLY_WAGE_SUFFIX))
|
||
daily = _build().book.titles[code].adopted_price() # 기본 벌 — 원 미만
|
||
assert one.book.titles[code].adopted_price() >= daily
|
||
assert daily == daily.to_integral_value()
|
||
|
||
|
||
def test_중기_호표의_운전원은_시간당_노임_제목을_1시간분_부른다() -> None:
|
||
book = _build().book
|
||
hourly = [code for code in book.titles if code.endswith(HOURLY_WAGE_SUFFIX)]
|
||
assert hourly, "조종원 시간당 제목이 한 벌도 안 섬"
|
||
for code in hourly:
|
||
daily = (
|
||
book.titles[code.removesuffix(HOURLY_WAGE_SUFFIX)].adopted_price()
|
||
if code.removesuffix(HOURLY_WAGE_SUFFIX) in book.titles
|
||
else None
|
||
)
|
||
price = book.titles[code].adopted_price()
|
||
assert price == price.to_integral_value() # 원 미만 절사
|
||
if daily is not None:
|
||
assert price == hourly_operator_wage(daily)
|
||
# X 층 운전원 줄은 계수 접은 수량이 아니라 시간당 제목 × 인수.
|
||
operator_rows = [
|
||
detail
|
||
for details in book.details.values()
|
||
for detail in details
|
||
if detail.ref_code.endswith(HOURLY_WAGE_SUFFIX)
|
||
]
|
||
assert operator_rows and all(
|
||
d.quantity == d.quantity.to_integral_value() for d in operator_rows
|
||
)
|
||
|
||
|
||
def test_중기_호표는_연료를_따로_세우고_잡품은_비율_가산_행이며_소계를_자른다() -> None:
|
||
"""PLAN 6장 ④ — 연료 수량에 잡품을 접지 않음 · 줄 0.1원 · 성분 소계 원 미만(명세 7장)."""
|
||
from B09_Estimation.B09_Estimation_MachineOperating import load_operating_records
|
||
|
||
book = _build().book
|
||
record = next(r for r in load_operating_records().records if r.machine_code == "0201-0070")
|
||
details = book.details["X-0201-0070"]
|
||
fuel = next(d for d in details if d.ref_code.startswith("M-FUEL-"))
|
||
misc = next(d for d in details if d.percent_of_material is not None)
|
||
assert fuel.quantity == record.fuel_liters_per_hour # 종전엔 × (1 + 잡품%)
|
||
assert misc.percent_of_material == record.misc_material_percent and misc.note.startswith("잡품")
|
||
money = book.resolve("X-0201-0070")
|
||
for part in (money.material, money.labor, money.expense):
|
||
assert part == part.to_integral_value() # 성분 소계 원 미만 절사
|