Files
Aislo/resources/tester/test_145_1_b09_sangeun.py
T
eomsangdonandClaude Opus 5.5 021f6873a0 Merge remote-tracking branch 'origin/sub_desktop_1' into sub_laptop_4
145-1 · 145-3 · 145-4 · 145-5 받음 · 충돌 = B09 이음 자리 넷 — Router · Shell 짝 셈 한 벌(바탕 → 산근 → 대가 → 내역 → 총괄) · used_keys 는 사본 셈 길(참조사본 포함) · 144 · 145-1 시험 단언 이음에 맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt
2026-10-09 15:31:31 +09:00

180 lines
9.0 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.
"""145-1 B09 단가산출근거 · 목록표 — 승격 줄 → 산근 블록 · 엑셀 검산 · 같은 산근 하나 · 빈 표.
- 산근 = 연결된 사본 일위대가 로직 줄 중 `승격: true` + 옛 SP 참조 줄(134-2 ③) · 승격 아닌 줄은 안 담음
- 줄 금액 = ROUNDDOWN(B09 굳힌 단가 × 1 단위 수량, 1) · 총계 = 비목마다 ROUNDDOWN 0 — 엑셀
단가산출근거 제 6호표(뒷채움) 줄 값 · 총계 8,328(6,878 · 538 · 912)과 같음
- 같은 로직 + 같은 입력값 = 산근 하나(쓰는 구조물이 늘어남) · 입력이 다르면 둘 · 번호 = 첫 등장
- 사본 없으면 빈 표 + 「연결 없음」 · 조합 밖 토공 · 운반 산근 안내 한 줄 · 작업량 Q = ROUND 2
"""
import asyncio
import json
import sys
from pathlib import Path
import pytest
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_Prices as prices # noqa: E402
from B09_Estimation import B09_Estimation_Router as router_mod # noqa: E402
from B09_Estimation import B09_Estimation_Sangeun as sg # noqa: E402
from B09_Estimation.B09_Estimation_Calc_Base import compute_base # noqa: E402
from B09_Estimation.B09_Estimation_Linked import used_keys # noqa: E402
from M02_MasterTemplete import M02_Item_Prices as item_prices # noqa: E402
STONE = "GF000219" # 돌쌓기 메쌓기(인력) — 석공 0.195 · 보통인부 0.156 / ㎡
NET = "GC000337" # 낙석방지망 — 노무 · 크레인(중기) · 자재 셋
NET_IN = {"구분": "기계식", "방지망": "포켓식", "지역": "전국평균", "크레인": "2104-0010"}
SP_REF = {"층": "system", "주인": None, "키": "SP-000001"}
def _snapshot(folder: Path, rows, values=None, copies=None) -> Path:
folder.mkdir(parents=True, exist_ok=True)
labor = {
"줄": {"종류": "일위대가", "담은로직": rows},
**({"참조사본": copies} if copies else {}),
}
files = {
"단가.json": item_prices.to_json(item_prices.collect([STONE, NET])),
"일위대가.json": labor,
"입력값.json": {k: {n: {"값": v} for n, v in d.items()} for k, d in (values or {}).items()},
}
for name, data in files.items():
(folder / name).write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
return folder
def _sheets(*folders: Path) -> dict:
keys = [k for f in folders for k in used_keys(f)]
return compute_base(prices.build_inputs(None, list(dict.fromkeys(keys))), {})
def test_승격_줄만_산근_블록_단가는_B09_표(tmp_path):
a = _snapshot(tmp_path / "a", [{"로직": STONE, "승격": True}, {"로직": NET}], {NET: NET_IN})
sheets = _sheets(a)
got = sg.collect([("s1", a)], sheets)
(block,) = got["sg"]
assert (block["no"], block["logic"], block["unit"]) == (1, STONE, "㎡")
labor = {r["key"]: r["price"] for r in sheets["labor"]}
lines = block["lines"]
assert [(ln["kind"], ln["ref"]["key"], ln["qty"]) for ln in lines] == [
("labor", "LB000033", 0.195),
("labor", "LB000002", 0.156),
]
want = [sg.rounddown(labor[ln["ref"]["key"]] * ln["qty"], 1) for ln in lines]
assert [ln["labor"] for ln in lines] == want
assert block["labor"] == sg.rounddown(sum(want), 0) and block["sum"] == block["labor"]
assert block["material"] == 0 and block["expense"] == 0 and block["why"] is None
assert got["sg_list"][0]["key"] == block["key"] and got["sg_gaps"] == []
assert sg.price_of(got, block["key"]) == {
"no": 1, "labor": block["labor"], "material": 0.0, "expense": 0.0, "sum": block["sum"],
} # fmt: skip
def test_중기는_중기목록표_노_재_경_자재는_적용_단가(tmp_path):
a = _snapshot(tmp_path / "a", [{"로직": NET, "승격": True}], {NET: NET_IN})
sheets = _sheets(a)
(block,) = sg.collect([("s1", a)], sheets)["sg"]
kinds = [ln["kind"] for ln in block["lines"]]
assert kinds == ["labor", "labor", "machine", "material", "material", "material"]
crane = block["lines"][2]
row = next(r for r in sheets["machine"] if r["key"] == crane["ref"]["key"])
for part in sg.PARTS:
assert crane[part] == sg.rounddown(row[part] * crane["qty"], 1)
net = block["lines"][3]
applied = next(r["applied"] for r in sheets["compare"] if r["key"] == net["ref"]["key"])
assert net["material"] == sg.rounddown(applied * net["qty"], 1)
assert block["sum"] == block["labor"] + block["material"] + block["expense"]
def test_엑셀_6호표_뒷채움_검산():
"""단가산출근거 C285 = 8,328(6,878 · 538 · 912) — 보통인부 0.018 × 165,545 · 굴삭기(0.2m3) 0.07 ×
중기목록표 55,700 · 7,695 · 13,041(줄 2,979.8 · 3,899.0 · 538.6 · 912.8 · 계 8,330.2 → 비목마다 버림)."""
zero = dict.fromkeys(sg.PARTS, 0.0)
labor = {**zero, "labor": sg._money(165545, 0.018)}
machine = {p: sg._money(v, 0.07) for p, v in zip(sg.PARTS, (55700, 7695, 13041))}
assert labor["labor"] == 2979.8
assert (machine["labor"], machine["material"], machine["expense"]) == (3899.0, 538.6, 912.8)
assert sg.block_totals([labor, machine]) == {
"labor": 6878, "material": 538, "expense": 912, "sum": 8328,
} # fmt: skip
assert sg.block_totals([labor, {**machine, "material": None}])["sum"] is None # 빈칸 = 빈칸
def test_같은_로직_같은_입력은_하나_다르면_둘_옛_SP_도_같은_산근(tmp_path):
a = _snapshot(tmp_path / "a", [{"로직": NET, "승격": True}], {NET: NET_IN})
b = _snapshot(tmp_path / "b", [{"로직": NET, "승격": True}], {NET: {**NET_IN, "": ""}})
other = {**NET_IN, "방지망": "비포켓식"}
c = _snapshot(tmp_path / "c", [{"로직": NET, "승격": True}], {NET: other})
sp = {"담은로직": [{"로직": NET, "입력값": NET_IN}], "배": 1}
d = _snapshot(
tmp_path / "d",
[{"로직": "", "참조": SP_REF}, {"로직": STONE, "승격": True}],
copies={"참조|system||SP-000001": sp},
)
got = sg.collect([("s1", a), ("s2", b), ("s3", c), ("s4", d)], _sheets(a, c, d))
blocks = got["sg"]
assert [(x["no"], x["logic"], x["uses"]) for x in blocks] == [
(1, NET, ["s1", "s2", "s4"]), # 빈 입력 칸은 무시 · 옛 SP(같은 로직 · 입력) = 같은 산근
(2, NET, ["s3"]),
(3, STONE, ["s4"]),
]
assert blocks[0]["key"] == sg.sangeun_key(NET, {**NET_IN, "x": None}, 100, {})
assert blocks[0]["key"] != sg.sangeun_key(NET, NET_IN, 90) # 켜쌓기율이 다르면 다른 산근
def test_사본_없으면_빈_표_연결_없음(tmp_path, monkeypatch):
got = sg.collect([("s1", tmp_path / "없음")], {})
assert got["sg"] == [] and got["sg_list"] == [] and got["sg_gaps"] == []
assert got["sg_notes"] == [sg.NO_SOURCE, sg.EMPTY]
plain = _snapshot(tmp_path / "p", [{"로직": STONE}]) # 승격 없는 사본도 빈 표
assert sg.collect([("s1", plain)], {})["sg"] == []
async def fake_materials(_project_id):
return {}
monkeypatch.setattr(router_mod, "material_sheets", fake_materials)
sheets = asyncio.run(router_mod.payload("p", tmp_path))["sheets"]
assert sheets["sg"] == [] and sheets["sg_list"] == []
assert sg.EMPTY in sheets["sg_notes"] and sg.NO_SOURCE in sheets["sg_notes"]
def test_단가_없으면_빈칸과_연결_없음(tmp_path):
a = _snapshot(tmp_path / "a", [{"로직": STONE, "승격": True}])
got = sg.collect([("s1", a)], {"labor": [], "machine": [], "compare": []})
(block,) = got["sg"]
assert block["labor"] is None and block["sum"] is None
assert "노무비목록표에 없음" in block["why"]
assert [(g["tab"], g["key"]) for g in got["sg_gaps"]] == [("sg", block["key"])]
def test_작업량_Q는_소수_둘():
import M01_MasterData.M01_MasterData_Store as m01
whole = m01.loaded()[1]
for code, row in sorted((whole.index.get("GF") or {}).items()):
if not any(sg._Q.fullmatch(s["이름"]) for s in row.get("중간", [])):
continue
try:
env = sg._env(whole, row, {})
except Exception: # noqa: BLE001 — 입력이 필요한 로직은 건너뜀
continue
q = next(env[s["이름"]] for s in row["중간"] if sg._Q.fullmatch(s["이름"]))
if hasattr(q, "as_tuple"):
assert q == round(q, 2), code
return
pytest.skip("자동값으로 도는 작업량 Q 로직 없음")
def test_화면_탭_둘_서버_산근_그대로():
shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
assert "sg_list: buildSgList," in shell and "sg: buildSg," in shell
assert 'const SERVER_ONLY = ["sg", "sg_list", "sg_gaps", "sg_notes"] as const;' in shell
assert "gaps: [...base.gaps, ...(server.sg_gaps || [])" in shell # 뒤에 대가(145-2) gaps 이음
tabs = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Sangeun.ts").read_text("utf-8")
assert 'link: (i) => (rows[i] ? { tab: "sg", key: String(rows[i].key) } : null),' in tabs
assert "PARTS.filter((p) => ln.price?.[p] !== 0)" in tabs # 중기 = 노 · 재 · 경 세 줄