"""품의 할인·할증 26계열 (산림품셈 1-4) — 2026-09-09 밤. 한 계열도 안 붙고 있었다. **붙일 자리를 잘못 고르면 그것이 곧 이중계상**이라 자리부터 가렸다. 붙는 자리 = **일위대가의 품(인력) 줄** ⚠ B08 물량에 곱하면 자재·기계까지 함께 부푼다. 근거 — 원문 제목이 「1-4. **품**의 할인·할증」 · 1-6 라 표가 직접노무비를 품셈으로 냄. ⚠ 겨누는 것 여섯 ① **안 고르면 한 원도 안 움직인다** ② 고르면 **노무비만** 커진다 — 재료비·경비는 그대로 ③ 밑수는 **사람 품만** — 기계 줄 안의 조종원 노임은 안 센다(제잡비와 같은 규칙) ④ 제잡비가 **할증 뒤 노무비**를 밑수로 삼는다(줄 차례가 곧 셈 차례) ⑤ 원문에 없는 선택지는 안 받는다 ⑥ 합산·곱 셈법이 **한 곳에서** 갈린다(원문이 안 정한 자리) """ from __future__ import annotations import sys from decimal import Decimal from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from B09_Estimation import B09_Estimation_LaborSurcharge as LS # noqa: E402 from B09_Estimation.B09_Estimation_PriceBook import ( # noqa: E402 PriceBook, PriceDetail, PriceKind, PriceTitle, ) from B09_Estimation.B09_Estimation_UnitPrice import build_unit_prices # noqa: E402 _CODE = "B-FP-09-13-09" # 인력 품이 있는 공종(구조물터파기 암절취 2~3m) def test_원문_행을_그대로_선택지로_낸다() -> None: series = LS.load_series() assert len(series) == 24, "율 표는 24개다(1-4-24·1-4-26 은 성격이 다름)" first = next(item for item in series if item["key"] == "1-4-1") assert [str(option["percent"]) for option in first["options"]] == ["10", "5", "0"] # [주] 원문이 함께 실려야 「어느 작업에 쓰는 표인지」가 화면에 보인다. assert "어린나무가꾸기" in first["source_note"] def test_원문에_없는_선택지는_안_받는다() -> None: assert LS.parse_choices({"1-4-1": "1-4-1:0"}) == {"1-4-1": "1-4-1:0"} assert LS.parse_choices({"1-4-1": "없는값"}) == {} # 계열과 선택지가 어긋나도 안 받는다. assert LS.parse_choices({"1-4-2": "1-4-1:0"}) == {} def test_셈법은_한_곳에서_갈린다(monkeypatch) -> None: picks = LS.parse_choices({"1-4-1": "1-4-1:0", "1-4-5": "1-4-5:1"}) # 10% · 5% assert LS.total_percent(picks)[0] == Decimal(15) monkeypatch.setattr(LS, "COMBINE_RULE", "product") assert LS.total_percent(picks)[0] == Decimal("15.5") def test_안_고르면_한_원도_안_움직인다() -> None: base = build_unit_prices().book.resolve(_CODE) same = build_unit_prices(labor_surcharge_choices={}).book.resolve(_CODE) assert base.total == same.total def test_고르면_노무비만_커진다() -> None: before = build_unit_prices().book.resolve(_CODE) after = build_unit_prices( labor_surcharge_choices={"1-4-1": "1-4-1:0"} # 10% ).book.resolve(_CODE) assert after.material == before.material assert after.expense == before.expense assert after.labor > before.labor def test_밑수는_사람_품만이다() -> None: """③ 기계 줄 안의 조종원 노임까지 세면 할증이 부풀어 붙는다.""" book = PriceBook() book.add_title(PriceTitle("L-1", PriceKind.LABOR, "보통인부", slots=[Decimal(100_000)] * 6)) book.add_title(PriceTitle("S-1", PriceKind.MACHINE_BASE, "기계", slots=[Decimal(1)] * 6)) book.add_title(PriceTitle("X-1", PriceKind.MACHINE_HOURLY, "기계 사용료")) book.add_detail(PriceDetail("X-1", "L-1", Decimal(1))) # 기계 층 안의 조종원 book.add_title(PriceTitle("B-1", PriceKind.UNIT_PRICE, "시험공종")) book.add_detail(PriceDetail("B-1", "L-1", Decimal(1))) book.add_detail(PriceDetail("B-1", "X-1", Decimal(1))) book.add_detail( PriceDetail( "B-1", "B-1", Decimal(0), percent_of_labor=Decimal(10), percent_of_labor_target="labor" ) ) # 사람 품 100,000 의 10% 만 붙어야 한다(기계 층 안의 100,000 은 안 셈). assert book.resolve("B-1").labor == Decimal(210_000) def test_제잡비는_할증_뒤_노무비를_본다() -> None: """④ 품이 늘면 그 품에 비례하는 제잡비도 함께 는다.""" book = PriceBook() book.add_title(PriceTitle("L-1", PriceKind.LABOR, "보통인부", slots=[Decimal(100_000)] * 6)) book.add_title(PriceTitle("B-1", PriceKind.UNIT_PRICE, "시험공종")) book.add_detail(PriceDetail("B-1", "L-1", Decimal(1))) book.add_detail( PriceDetail( "B-1", "B-1", Decimal(0), percent_of_labor=Decimal(10), percent_of_labor_target="labor" ) ) book.add_detail(PriceDetail("B-1", "B-1", Decimal(0), percent_of_labor=Decimal(5))) money = book.resolve("B-1") assert money.labor == Decimal(110_000) assert money.expense == Decimal(110_000) * Decimal(5) / Decimal(100)