B08 자재 길 답 = {source, sheets: {material}} 인데 B09 가 한 층 위를 읽어 총괄 줄 · 운반 수량이 늘 빈칸 · 시험 가짜 답도 같은 꼴로 바로잡음
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sDZSjFvoiDjdGpgj3muc3
182 lines
8.0 KiB
Python
182 lines
8.0 KiB
Python
"""145-5 B09 집계표 넷 — 역전개(겹 호표) · 엑셀 끝수 · 검산 대조(서버 단독 · 거울 없음).
|
||
|
||
견본 = 기번3 공통 엑셀 집계표:
|
||
중기시간금액집계표 4줄 불도우저19ton 42.95 시간 → 2,392,315 · 1,584,340 · 1,435,045 = 5,411,700
|
||
노무비수량금액집계표 4줄 건축목공 8.29 인 × 268,058 = 2,222,201
|
||
재료비수량금액집계표 각재 0.56 × 500,661 = 280,370 · 못 3.54 × 1,503 = 5,321
|
||
경비수량금액집계표 구역화물자동차 11.03 회 × 61,500 = 678,345
|
||
"""
|
||
|
||
import asyncio
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
if str(ROOT) not in sys.path:
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
from B09_Estimation import B09_Estimation_Router as router_mod # noqa: E402
|
||
from B09_Estimation.B09_Estimation_Summary import compute_summary # noqa: E402
|
||
|
||
SHEETS = {
|
||
"labor": [{"key": "L1", "name": "건축목공", "spec": "", "unit": "인", "price": 268058}],
|
||
"material": [
|
||
{"key": "M3", "name": "각재", "spec": "외송", "unit": "M3", "price": 500661},
|
||
{"key": "M4", "name": "못", "spec": "N 50", "unit": "KG", "price": 1503},
|
||
],
|
||
"expense": [
|
||
{"key": "E16", "name": "구역화물자동차", "spec": "5톤", "unit": "회", "price": 61500}
|
||
],
|
||
"machine": [
|
||
{
|
||
"key": "X1",
|
||
"name": "불도우저19ton",
|
||
"spec": "(토사)",
|
||
"unit": "시간",
|
||
"sum": 126000,
|
||
"labor": 55700,
|
||
"material": 36888,
|
||
"expense": 33412,
|
||
},
|
||
], # fmt: skip
|
||
"bill": {
|
||
"rows": [
|
||
{"kind": "title", "key": "sec:1", "qty": None},
|
||
{"kind": "item", "key": "a", "qty": 10, "ref": {"tab": "sg_list", "key": "A"}},
|
||
{"kind": "item", "key": "m", "qty": 0.56, "ref": {"tab": "material", "key": "M3"}},
|
||
{"kind": "item", "key": "none", "qty": None, "ref": {"tab": "sg_list", "key": "A"}},
|
||
{"kind": "item", "key": "loop", "qty": 1, "ref": {"tab": "dg_list", "key": "C"}},
|
||
]
|
||
},
|
||
}
|
||
LINES = {
|
||
"A": {"lines": [
|
||
{"kind": "machine", "key": "X1", "qty": 4.295},
|
||
{"kind": "hopyo", "key": "B", "qty": 0.5},
|
||
{"kind": "material", "key": "M4", "qty": 0.354},
|
||
{"kind": "expense", "key": "E16", "qty": 1.103}, # 합성 줄 — 산근 · 대가는 경비 줄을 안 냄(166-5)
|
||
]},
|
||
"B": {"lines": [{"kind": "labor", "key": "L1", "qty": 1.658},
|
||
{"kind": "labor", "key": "L9", "qty": 1}]},
|
||
"C": {"lines": [{"kind": "hopyo", "key": "C", "qty": 1}]}, # 스스로 부름 — 깊이 한도에서 멈춤
|
||
} # fmt: skip
|
||
|
||
|
||
def test_역전개_엑셀_금액():
|
||
s = compute_summary(SHEETS, LINES)
|
||
machine = s["sum_machine"][0]
|
||
assert (machine["qty"], machine["labor"], machine["material"], machine["expense"]) == (
|
||
42.95, 2392315, 1584340, 1435045,
|
||
) # fmt: skip
|
||
assert machine["sum"] == 5411700
|
||
labor = {r["key"]: r for r in s["sum_labor"]}
|
||
assert (labor["L1"]["qty"], labor["L1"]["amount"]) == (
|
||
8.29,
|
||
2222201,
|
||
) # 겹 호표 10 × 0.5 × 1.658
|
||
assert labor["L9"]["amount"] is None and labor["L9"]["note"] == "목록표에 없음"
|
||
material = {r["key"]: (r["qty"], r["amount"]) for r in s["sum_material"]}
|
||
assert material == {"M4": (3.54, 5321), "M3": (0.56, 280370)} # 내역 자재 줄 그대로
|
||
assert [(r["qty"], r["amount"]) for r in s["sum_expense"]] == [(11.03, 678345)]
|
||
|
||
|
||
def test_호표_구성_없으면_자재_줄만():
|
||
s = compute_summary(SHEETS, {})
|
||
assert s["sum_labor"] == [] and s["sum_machine"] == [] and s["sum_expense"] == []
|
||
assert [r["key"] for r in s["sum_material"]] == ["M3"]
|
||
|
||
|
||
def test_GET_응답에_집계표(tmp_path, monkeypatch):
|
||
async def fake_materials(_project_id):
|
||
return {"sheets": {"material": {"rows": [{"key": "sand", "group": "골재", "spec": "모래",
|
||
"unit": "㎥", "amount": 16}]}}} # fmt: skip
|
||
|
||
monkeypatch.setattr(router_mod, "material_sheets", fake_materials)
|
||
body = asyncio.run(router_mod.payload("p", tmp_path))
|
||
assert set(body["summary"]) == {
|
||
"sum_material", "sum_labor", "sum_expense", "sum_machine", "unlinked",
|
||
} # fmt: skip
|
||
assert body["summary"]["sum_material"] == [] # 「별도」 · 「관급」 표시 전엔 내역 자재 줄 없음
|
||
|
||
|
||
# ── 산근 블록(145-1 `sheets['sg']` 꼴)으로 역전개 — 산근 → 하위 산근 → 중기 · 엑셀 반올림 줄 ──
|
||
from B09_Estimation.B09_Estimation_Summary import hopyo_lines # noqa: E402
|
||
|
||
|
||
def _ln(kind, tab, key, qty):
|
||
return {"kind": kind, "ref": {"tab": tab, "key": key}, "qty": qty, "name": key}
|
||
|
||
|
||
SG = [
|
||
{"key": "S1", "lines": [
|
||
_ln("labor", "labor", "L3", 1.3701),
|
||
_ln("machine", "machine", "X7", 14.3614),
|
||
_ln("sangeun", "sg", "S2", 0.5),
|
||
{"kind": "extra", "ref": None, "qty": 1.0, "name": "잡재료 2%"}, # % 줄 = 자원 아님
|
||
{"kind": "blank", "ref": None, "qty": None, "name": "빈칸"},
|
||
]},
|
||
{"key": "S2", "lines": [
|
||
_ln("labor", "labor", "L6", 4.4008),
|
||
_ln("material", "compare", "M7", 80.5194),
|
||
]},
|
||
] # fmt: skip
|
||
SG_SHEETS = {
|
||
"labor": [
|
||
{"key": "L3", "name": "석공", "price": 258935},
|
||
{"key": "L6", "name": "보통인부", "price": 165545},
|
||
],
|
||
"material": [{"key": "M7", "name": "폭약", "spec": "28m/m", "unit": "KG", "price": 12712}],
|
||
"expense": [],
|
||
"machine": [{"key": "X7", "name": "굴삭기+브레카(0.7m3)", "labor": 55700, "material": None,
|
||
"expense": None}],
|
||
"sg": SG,
|
||
"bill": {"rows": [
|
||
{"kind": "item", "key": "rock", "qty": 100, "ref": {"tab": "sg_list", "key": "S1"}},
|
||
{"kind": "item", "key": "earth", "qty": 5, "ref": None}, # 조합 밖 토공
|
||
{"kind": "item", "key": "st", "qty": 2, "ref": {"tab": "dg_list", "key": "st:x"}},
|
||
{"kind": "item", "key": "none", "qty": None, "ref": {"tab": "sg_list", "key": "S1"}},
|
||
]},
|
||
} # fmt: skip
|
||
|
||
|
||
def test_산근_블록_역전개_엑셀_반올림():
|
||
lines = hopyo_lines(SG_SHEETS)
|
||
assert lines["S1"]["lines"] == [
|
||
{"kind": "labor", "key": "L3", "qty": 1.3701},
|
||
{"kind": "machine", "key": "X7", "qty": 14.3614},
|
||
{"kind": "hopyo", "key": "S2", "qty": 0.5},
|
||
]
|
||
s = compute_summary(SG_SHEETS, lines)
|
||
labor = {r["name"]: (r["qty"], r["amount"]) for r in s["sum_labor"]}
|
||
# 엑셀 노무비수량금액집계표 6 · 9 줄(ROUND — 220.04 × 165,545 = 36,426,521.8 → …522)
|
||
assert labor == {"석공": (137.01, 35476684), "보통인부": (220.04, 36426522)}
|
||
# 재료비수량금액집계표 8 줄 폭약 4,025.97 × 12,712 = 51,178,130.6 → 51,178,131
|
||
assert [(r["qty"], r["amount"]) for r in s["sum_material"]] == [(4025.97, 51178131)]
|
||
machine = s["sum_machine"][0] # 중기시간금액집계표 10 줄 노무비
|
||
assert (machine["qty"], machine["labor"], machine["sum"]) == (1436.14, 79992998, None)
|
||
assert s["unlinked"] == 3 # 토공 · 대가 전 구조물 · 수량 빈칸 = 전개 안 함
|
||
|
||
|
||
# ── 166-5 경비 집계표 — 실제 블록 꼴은 경비 자원 0 → 표 위 안내 ──
|
||
from test_155_b09_tabs import _node # noqa: E402
|
||
|
||
EXPENSE_NONE = "경비 갈래 자원 없음 — 중기 · 자재로 셈 · 경비 로직이 들어오면 채워짐"
|
||
|
||
|
||
def _expense_tab(summary):
|
||
return _node(
|
||
f"\nconst t = buildSumExpense({{ payload: {{ summary: {json.dumps(summary)} }} }});"
|
||
"\nconsole.log(JSON.stringify({ n: t.rows.length, hint: t.hint }));",
|
||
"UI_Tabs_Summary",
|
||
)
|
||
|
||
|
||
def test_166_5_실자료_경비_0_이면_안내():
|
||
s = compute_summary(SG_SHEETS, hopyo_lines(SG_SHEETS))
|
||
assert s["sum_expense"] == [] # 산근 블록 꼴 = 경비 갈래 줄 없음
|
||
out = _expense_tab(s)
|
||
assert out["n"] == 0 and out["hint"].startswith(EXPENSE_NONE)
|
||
filled = _expense_tab(compute_summary(SHEETS, LINES)) # 합성 경비 줄이 있으면 안내 없음
|
||
assert filled["n"] == 1 and EXPENSE_NONE not in filled["hint"]
|