"""조종원 시간당 노임 — 식 좌→우 순차 + 원 미만 절사 (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() # 성분 소계 원 미만 절사