- 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
129 lines
5.7 KiB
Python
129 lines
5.7 KiB
Python
"""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"
|