Files
Aislo/resources/tester/test_b09_labor_surcharge.py
T
eomsangdonandClaude Opus 5 f6f51f33f5 feat(B09): 품의 할인·할증 26계열 칸 — 고른 것만 품에 붙음
산림품셈 1-4 가 26표로 품을 할인·할증하는데 한 계열도 안 붙고 있었음(자재 할증만 있었음).

- 붙는 자리 = 일위대가의 품(인력) 줄. 물량에 곱하면 자재·기계까지 부풀어 이중계상임
  (원문 제목이 「품의 할인·할증」이고 1-6 라 표가 직접노무비를 품셈으로 냄).
- 기본은 「안 고름」 — 한 계열도 안 고르면 금액이 한 원도 안 움직임.
- 각 계열에 [주] 원문을 그대로 띄움 — 26계열의 [주] 가 대개 조림·숲가꾸기·방제를
  지목하고 임도 토공에 붙이라는 지시가 원문에 없어, 켜는 것은 사용자 판단임.
- 여럿 고를 때 합산인지 곱인지 원문에 없음 — 합산으로 두되 한 곳(COMBINE_RULE)에서
  갈아 끼울 수 있게 하고 그 사실을 화면 근거에 적음.
- 품이 늘면 제잡비 밑수도 함께 늘도록 줄 차례를 지킴(할증 줄이 제잡비보다 먼저).
- ⚠ 율 표는 24개임 — 1-4-24(장비품의 -20%)·1-4-26(적용시공량 규칙)은 성격이 달라 뺌.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 22:43:14 +09:00

112 lines
5.0 KiB
Python

"""품의 할인·할증 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)