산림품셈 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>
112 lines
5.0 KiB
Python
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)
|