Files
Aislo/resources/tester/test_160_3_b08_unmatched_total.py
T
eomsangdonandClaude Opus 5.5 c6bb3cc5b6 feat(b08): 자재수량 → 총괄 · 콘크리트소요 · 채집석 공제 엑셀 흐름(160-3 · 4 · 5)
- 160-3 맞춤 없는 사본 자재 = 총괄 끝 줄(이름 · 규격 · 단위 그대로 · 알림에 이름 목록) · 맞춤 = 이름 · 규격 · 단위 · 기계 주연료 · 할증(ℓ) 뺌 · 참조 줄(승격 · 사전조합) 안 재료 셈 · 고른 품목 = MT 코드 · 파이썬 · TS 짝
- 160-4 콘크리트(기계) · 모르터 1:3 열 늘 보임(손 단위수량 → 콘크리트소요) · 엑셀 손값과 다른 배합 · 할증은 표 아래 알림 한 줄
- 160-5 채집석 공제(돌 채집 둘 · 고임돌 · 막자갈 합) = 총괄 줄 · 토공 잔토처리 = ROUND 0(잔토 − 공제) · 공제 줄 보임 · TS 짝
- 시험 test_160_3 · 4 · 5 새 · test_143_3 · 143_4 · 69 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sDZSjFvoiDjdGpgj3muc3
2026-10-09 22:53:18 +09:00

129 lines
5.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.
"""160-3 B08 자재수량 → 총괄 — 맞춤 없는 사본 자재 = 총괄 끝 줄 · 맞춤은 규격 · 단위까지 ·
기계 주연료 · 할증(ℓ) 뺌 · 참조 줄(승격 · 사전조합) 안 재료 셈 · 고른 품목 = MT 코드 · 거울.
"""
from __future__ import annotations
import asyncio
import json
from decimal import Decimal
from B08_Quantity import B08_Quantity_Router_Material as router
from B08_Quantity.B08_Quantity_Material_Calc import clean_overrides, compute, matched
from M02_MasterTemplete import M02_Item_Copy as copies
from M02_MasterTemplete import M02_Item_Prices as item_prices
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete.M02_Labor_Ref import ref_id
from resources.tester.test_121_b08_material_sheets import _run_ts
REVET = "pipe:revet|돌쌓기(찰)|2.0"
SUMMARY = {
"columns": [{"id": REVET, "group": ["흙막이", "찰쌓기", "H=2.0"], "label": "m"}],
"total": {REVET: 10},
"snapshot": {
"materials": [
{"key": "MT1", "name": "레미콘", "spec": "25-18-80", "unit": "m3"},
{"key": "MT2", "name": "이형철근", "spec": "D13", "unit": "ton"}, # 단위 다름
{"key": "부직포|㎡", "name": "토목용부직포", "spec": "", "unit": "㎡"},
],
"units": {REVET: {"MT1": 0.31, "MT2": 0.002, "부직포|㎡": 2.871}},
},
}
def test_맞춤은_이름_규격_단위():
assert matched("레미콘", "25-18-80", "㎥") == "remicon_25_18_80"
assert matched("레미콘", "25-18-80", "M3") == "remicon_25_18_80" # m3 = ㎥
assert matched("이형철근", "D13", "ton") is None # kg ↔ ton 은 맞춤 아님
assert matched("호안블럭", "", "㎥") is None # 규격 빈 고정 열도 단위는 봄
def test_맞춤_없는_자재는_총괄_끝_줄_그대로():
sheets = compute(SUMMARY, clean_overrides({"surcharge": {"부직포|㎡": 10}}))
assert sheets["quantity"]["unmatched"] == ["MT2", "부직포|㎡"]
rows = sheets["material"]["rows"]
tail = {row["key"]: row for row in rows[-2:]}
steel, cloth = tail["MT2"], tail["부직포|㎡"]
assert (steel["group"], steel["spec"], steel["unit"], steel["qty"]) == (
"이형철근",
"D13",
"ton",
0.02,
)
assert steel["surcharge"] is None and steel["amount"] == 0.02 # 할증 빈칸 · 끝수 없음
assert cloth["qty"] == 28.71 and cloth["amount"] == 31.581 # 손 할증 10 %
total = {row["key"]: row for row in rows}
assert total["remicon_25_18_80"]["qty"] == 3.1 # 맞춤 = 고정 줄로
def test_거울_TS와_파이썬이_같다():
over = clean_overrides({"surcharge": {"부직포|㎡": 10}})
assert _run_ts(SUMMARY, over) == compute(SUMMARY, over)
def test_알림에_맞춤_없는_이름(tmp_path, monkeypatch):
async def summary(_project_id):
return {**SUMMARY, "notes": []}
monkeypatch.setattr(router, "structure_summary", summary)
monkeypatch.setattr(router, "snapshot_units", lambda root, s: SUMMARY["snapshot"])
got = asyncio.run(router._sheets("p", tmp_path, clean_overrides({})))
assert got["notes"] == [
"2공구 열 맞춤 없음 2종(총괄 끝 줄 그대로) — 이형철근 D13 ton · 토목용부직포 ㎡"
]
SC = {"층": "project", "주인": None, "키": "SC-000001"}
SP = {"층": "project", "주인": None, "키": "SP-000001"}
def test_참조_줄_재료_연료_뺌_고른_품목(tmp_path, monkeypatch):
"""사전조합(로직 L1 손 2 · 알약 배 10 + 승격) · 승격(L2 · 배 4) 안 재료 · 연료(ℓ) 뺌."""
fake = {
"줄": [
{"수량": 2, "배": 1, "참조": SC},
{"수량": 3, "배": 1, "참조": SP},
{
"수량": 4,
"배": 2,
"재료": [
{"이름": "주연료 할증", "단위": "ℓ", "수량": 5},
{"이름": "페인트", "단위": "ℓ", "수량": 1},
{"이름": "레미콘", "단위": "㎥", "수량": 0.5, "품목": {"키": "MT9"}},
],
},
],
"자재": [],
}
price = {"묶음": {"줄": [{"키": "MT9", "이름": "레디믹스트 레미콘", "규격": "25-18-80"}]}}
frozen = {
ref_id(SC): {
"담은로직": [{"로직": "L1", "수량": {"손": 2}}, {"참조": SP}],
"알약": [{"로직": ["L1"], "배": 10}],
},
ref_id(SP): {"담은로직": [{"로직": "L2"}], "배": 4},
}
mats = {
"L1": [{"이름": "모래", "단위": "㎥", "수량": Decimal("5")}],
"L2": [{"이름": "돌", "단위": "㎥", "수량": 8}, {"이름": "경유", "단위": "ℓ", "수량": 3}],
}
(tmp_path / "단가.json").write_text(json.dumps(price, ensure_ascii=False), "utf-8")
(tmp_path / "일위대가.json").write_text(
json.dumps({"참조사본": frozen}, ensure_ascii=False), "utf-8"
)
monkeypatch.setattr(copies, "calc", lambda root, folder: fake)
monkeypatch.setattr(copies, "folder_of", lambda root, folder: tmp_path)
monkeypatch.setattr(item_prices, "load_prices", lambda bundle: "whole")
monkeypatch.setattr(labor, "run_line", lambda whole, code, *rest: {"재료": mats[code]})
meta: dict = {}
got = router._folder_units("r", "f", meta)
# 사전조합 1 단위 = 모래 5 ÷ 10 × 2 + 돌 2 · 승격 1 단위 = 돌 8 ÷ 4 · 로직 줄 = × 4 ÷ 2
assert got == {"모래|㎥": 2.0, "돌|㎥": 4.0 + 6.0, "페인트|ℓ": 2.0, "MT9": 1.0}
assert meta["MT9"] == {
"key": "MT9",
"name": "레디믹스트 레미콘",
"spec": "25-18-80",
"unit": "㎥",
}
assert matched("레디믹스트 레미콘", "25-18-80", "㎥") == "remicon_25_18_80"