Files
Aislo/resources/tester/test_b09_material_prices.py
T

165 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""자재 수동 단가(PLAN 1장 Ⓐ · 2026-09-14 브레인 판정).
지키는 것
① 키 = 코드(`AR-M-…`) · 코드 없는 자재만 「이름 규격」 — 조립엔 코드 키만 얹음
② 0 이하·수 아님은 안 받음 · 비우면 지움 · 값·출처가 같으면 넣은 날 그대로
③ 넣으면 자재 제목(6번 슬롯)이 서고 자원 축 자재 줄이 붙어 재료비가 섬 — 안 넣으면 종전 벌 그대로
④ 수동 단가가 닿은 내역 줄 = 금액은 서되 「미확정 N건」
"""
from __future__ import annotations
import sys
from decimal import Decimal
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from B09_Estimation.B09_Estimation_MaterialPrices import ( # noqa: E402
MaterialPriceError,
build_key,
listing,
merge,
)
from B09_Estimation.B09_Estimation_UnitPrice import cached_build # noqa: E402
STAKE = "AR-M-dce99e46" # 말뚝 직경4~6㎝ — FP-05-15 에 2개/단위
def test_키와_넣은_날() -> None:
first = merge({}, [{"key": STAKE, "price_krw": "1,500", "source": "견적 A"}], "2026-09-14")
assert first[STAKE]["price_krw"] == "1500" and first[STAKE]["entered_at"] == "2026-09-14"
same = merge(first, [{"key": STAKE, "price_krw": "1500", "source": "견적 A"}], "2026-09-20")
assert same[STAKE]["entered_at"] == "2026-09-14" # 다시 저장해도 안 바뀜
moved = merge(first, [{"key": STAKE, "price_krw": "1600", "source": "견적 A"}], "2026-09-20")
assert moved[STAKE]["entered_at"] == "2026-09-20"
assert merge(first, [{"key": STAKE, "price_krw": ""}], "2026-09-20") == {}
for bad in ("0", "-3", "abc"):
with pytest.raises(MaterialPriceError):
merge({}, [{"key": STAKE, "price_krw": bad}], "2026-09-14")
named = merge(first, [{"key": "각재 50×50", "price_krw": "900000"}], "2026-09-14")
assert build_key(named) == ((STAKE, "1500", "견적 A"),) # 이름 키는 조립에 안 얹음
def test_넣으면_재료비가_서고_안_넣으면_종전_그대로() -> None:
base = cached_build()
assert base.book.resolve("B-FP-05-15").material == 0
assert STAKE not in base.book.titles and not base.manual_materials
built = cached_build(material_prices=((STAKE, "1500", "견적 A"),))
title = built.book.titles[STAKE]
assert (title.slots[5], title.slot_pages[5], title.unit) == (Decimal("1500"), "견적 A", "개")
assert built.book.resolve("B-FP-05-15").material == Decimal("3000") # 2개 × 1,500
assert built.book.resolve("B-FP-05-15").labor == base.book.resolve("B-FP-05-15").labor
assert "FP-05-15" in built.material_uses[STAKE]
def test_내역_줄은_미확정으로_선다() -> None:
from B09_Estimation.B09_Estimation_BillOfQuantities import build_bill
built = cached_build(material_prices=((STAKE, "1500", "견적 A"),))
payload = {
"work_items": [
{
"work_item_code": "FP-05-15",
"name": "말뚝박기",
"unit": built.book.titles["B-FP-05-15"].unit,
"quantity": 10,
"in_bill": True,
}
],
"materials": [],
}
row = next(r for r in build_bill(payload, build=built).rows if r.name == "말뚝박기")
assert row.material_krw == Decimal("30000") and row.unconfirmed == 1
result = build_bill(payload, build=built)
assert result.unconfirmed == [{"name": "말뚝박기", "code": "B-FP-05-15", "count": 1}]
plain = build_bill(payload, build=cached_build())
assert not plain.unconfirmed
def _자재(name: str, spec: str, total: str, supply: str, source: list[str]) -> dict:
return {
"material_name": name,
"spec": spec,
"unit": "㎥",
"net_amount": total,
"total_amount": total,
"supply_type": supply,
"source_structure": source,
}
def test_사급_자재총괄_줄은_본체_자재_줄로_서고_관급은_수량만() -> None:
"""Ⓐ-2 — 셈(㉠ 0 · ㉡ 9 · ㉢ 0) 뒤 브레인 판정. 도급 재료비에 들고 미확정으로 셈."""
from B09_Estimation.B09_Estimation_BillOfQuantities import build_bill
payload = {
"work_items": [],
"materials": [
_자재("각재", "50×50", "0.2684", "contractor_supplied", ["비탈 규준틀"]),
_자재("판재", "T12", "0.1769", "contractor_supplied", ["비탈 규준틀"]),
_자재("채움콘크리트", "180", "3.4", "owner_supplied", ["돌쌓기(찰)"]),
],
}
prices = {"각재 50×50": {"price_krw": "650000"}, "채움콘크리트 180": {"price_krw": "90000"}}
result = build_bill(payload, build=cached_build(), material_prices=prices)
group = next(r for r in result.rows if r.name == "자재(사급)")
rows = [r for r in result.rows if r.item_no.startswith(f"{group.item_no}-")]
assert [(r.name, r.spec) for r in rows] == [("각재", "50×50")] # 판재 단가 없음 · 관급 안 올림
assert rows[0].material_krw == Decimal("174460") # 0.2684 × 650,000
assert rows[0].unconfirmed == 1 and result.direct_material_krw == Decimal("174460")
assert any(m["name"] == "판재 T12" and "단가 없음" in m["reason"] for m in result.missing)
sheet = result.material_sheet
assert (
sheet.contractor_rows[0].manual and not sheet.owner_rows[0].manual
) # 관급엔 수동 단가 안 씀
plain = build_bill(payload, build=cached_build())
assert not any(r.name == "자재(사급)" for r in plain.rows) and plain.direct_material_krw == 0
def test_같은_구조물_일위대가_자재와_겹치면_안_올리고_사유() -> None:
"""이중계상 가드 — 코드가 있으면 코드로, 없을 때만 이름으로(막는 쪽이라 틀려도 과소)."""
from B09_Estimation.B09_Estimation_BillOfQuantities import build_bill
from B09_Estimation.B09_Estimation_BillOfQuantities_Materials import overlap
built = cached_build(material_prices=((STAKE, "1500", ""),))
unit = built.book.titles["B-FP-05-15"].unit
payload = {
"work_items": [
{"work_item_code": "FP-05-15", "name": "말뚝박기", "unit": unit, "quantity": 10}
],
"materials": [_자재("말뚝", "", "20", "contractor_supplied", ["말뚝박기"])],
}
result = build_bill(payload, build=built, material_prices={"말뚝": {"price_krw": "1500"}})
assert not any(r.name == "자재(사급)" for r in result.rows)
hit = next(m for m in result.missing if m.get("blocked_kind") == "double_count_suspect")
assert "말뚝" in hit["reason"]
inside = [(STAKE, "말뚝"), ("", "시멘트 510 kg — 자재 단가 층 없음")]
assert overlap("시멘트", "", inside).startswith("시멘트")
assert overlap("말뚝", "AR-M-00000000", inside) == "" # 코드가 있으면 코드로만
def test_목록은_자원_축_자재와_사라진_저장_줄() -> None:
built = cached_build()
stored = {
STAKE: {"price_krw": "1500"},
"AR-M-00000000": {"price_krw": "7", "name": "옛 자재"},
}
materials = [_자재("각재", "50×50", "0.2684", "contractor_supplied", ["비탈 규준틀"])]
rows = {
row["key"]: row for row in listing(built, {**stored, "못": {"price_krw": "3"}}, materials)
}
assert rows[STAKE]["name"] == "말뚝" and rows[STAKE]["price_krw"] == "1500"
assert rows["AR-M-00000000"]["missing"] is True # 조용히 안 버림
assert rows["AR-M-b0853497"]["price_krw"] is None
sheet_row = rows["각재 50×50"]
assert (sheet_row["name"], sheet_row["spec"], sheet_row["origin"]) == (
"각재",
"50×50",
"material_sheet",
)
assert sheet_row["supply_type"] == "contractor_supplied" and rows["못"]["missing"] is True