Files
Aislo/resources/tester/test_702_b09_daega_cover.py
T
eomsangdonandClaude Opus 5.5 969d7f74f3 feat(B09 일위대가): 고친 칸 덮개 셈 — 사용자 값 daega_cover · 블록 cover · 줄 cid · 파이썬·TS 짝 (PLAN 702-2)
- 덮개 자리 = B09 사용자 값(user_values.json · 화면 캐시와 같은 user) {블록 cover: {줄 cid: {qty · 노 · 재 · 경 단가}}} · 원본 층 · 사본 파일 안 건드림(브레인 승인)
- 블록 cover = 조합 S:사본 폴더 · 서고 L:폴더 · 사전조합 C:참조 · 품셈 P:로직+입력 해시 · 줄 cid = 줄 정체(코드 · 산근 로직 · 부른 대가 로직) + n번째 — 내용 해시 밖(호표 id 그대로)
- 셈 짝 Calc_Labor.py · .ts — 고친 칸만 셈 값 대신 · 줄 base(품셈 값) · covered · 짝 잃은 덮개 daega_orphans(지우지 않음) · 데이터 점검 알림
- [저장] = 서버가 덮개 입력값만 받아 다시 셈(Store.clean_cover 수만) · 노임 새 판 · [재적용] 뒤에도 남음
- B08 구조물도 카드용 고친 칸 수 cover_count
- 시험 test_702_b09_daega_cover(거울 · 판 바꿈 · 재적용 꼴 · 다듬기) · test_145_2 _ts 덮개 인자

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:41:22 +09:00

241 lines
11 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.
"""702-2 · 702-4 B09 일위대가 고친 칸 덮개 — 사용자 값 `daega_cover`
{블록 cover: {줄 cid: {qty · labor · material · expense}}}.
- 덮개 열쇠 = 블록 `cover` · 줄 `cid`(`B09_Estimation_Labor_Source` · 내용 해시 밖)
- 셈 = `B09_Estimation_Calc_Labor` 파이썬 · TS 짝(거울) — 고친 칸만 셈 값 대신 · `base` = 품셈 값
- 노임 새 판(바탕 단가 바뀜) · 사본 다시 만듦([재적용]) 뒤에도 덮개 칸 그대로 · 안 고친 칸만 새 값
- 짝 잃은 덮개 = `daega_orphans`(지우지 않음) · 저장 다듬기 = `B09_Estimation_Store.clean_cover`
견본 = `test_145_2_b09_labor_calc` 엑셀 블록 · 견본 사본.
"""
import copy
import importlib.util
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_Store as store # noqa: E402
from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor, daega_price # noqa: E402
from B09_Estimation.B09_Estimation_Labor_Source import cover_count, daega_inputs # noqa: E402
from M02_MasterTemplete import M02_Item_Snapshot as snapshots # noqa: E402
_spec = importlib.util.spec_from_file_location(
"b145", Path(__file__).with_name("test_145_2_b09_labor_calc.py")
)
b145 = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(b145)
def _covered_excel():
"""엑셀 블록 + cover · cid(사본 길이 붙이는 꼴)."""
daega = copy.deepcopy(b145.EXCEL)
for b in daega["blocks"]:
b["cover"] = f"T:{b['id']}"
for n, ln in enumerate(b["lines"]):
ln["cid"] = f"{ln['kind']}:{ln.get('key') or ln['name']}#{n}"
return daega
#: DG9 찰붙임 석공 품 0.1 → 0.12 · DG2 % 2 → 3 · DG14 산근 SG10 노무 단가 손 · 짝 잃은 둘
COVER = {
"T:DG9": {"labor:L00005#0": {"qty": 0.12}},
"T:DG2": {"pct:공구손료#3": {"qty": 3}},
"T:DG14": {"sangeun:SG10#2": {"labor": 20000}},
"T:없는블록": {"labor:L00017#0": {"qty": 1}},
"T:DG1": {"labor:없는줄#9": {"qty": 1}},
}
def _line(out, bid, n):
return next(b for b in out["daega_blocks"] if b["id"] == bid)["lines"][n]
def test_고친_칸만_셈값_대신():
daega = _covered_excel()
plain = compute_labor(daega, b145.SHEETS, b145.SANGEUN)
out = compute_labor(daega, b145.SHEETS, b145.SANGEUN, COVER)
stone = _line(out, "DG9", 0)
assert stone["qty"] == 0.12 and stone["covered"] == ["qty"]
assert stone["base"] == {"qty": 0.1, "labor": 258935, "material": 0, "expense": 0}
assert stone["amounts"]["labor"] == 31072.2 # ROUNDDOWN(258,935 × 0.12, 1)
assert "covered" not in _line(out, "DG9", 1) # 안 고친 줄 = 덧칸 없음
# 부른 블록이 바뀌면 부르는 블록 금액도(DG14 ← DG9) · 내역 단가도
nine = next(b for b in out["daega"] if b["key"] == "DG9")
assert nine["labor"] == 42556 + 31072 - 25893 # 석공 줄만 바뀜(ROUNDDOWN 0)
assert daega_price(out, "pipe@100.0")["sum"] != daega_price(plain, "pipe@100.0")["sum"]
pct = _line(out, "DG2", 4) # % 줄 = 끝에 셈(차례 4)
assert pct["qty"] == 3 and pct["base"] == {"qty": 2} and pct["covered"] == ["qty"]
sg = _line(out, "DG14", 2)
assert sg["prices"]["labor"] == 20000 and sg["base"]["labor"] == 18084
assert sg["covered"] == ["labor"] and sg["prices"]["material"] == 2498
assert out["daega_orphans"] == [
{"cover": "T:없는블록", "cid": "labor:L00017#0"},
{"cover": "T:DG1", "cid": "labor:없는줄#9"},
]
assert plain["daega_orphans"] == []
def test_덮개가_빈칸을_채움():
daega = _covered_excel()
sheets = {**b145.SHEETS, "labor": b145.SHEETS["labor"][:2]} # 석공 노임 없음
bare = compute_labor(daega, sheets, b145.SANGEUN)
assert _line(bare, "DG9", 0)["amount"] is None
out = compute_labor(daega, sheets, b145.SANGEUN, {"T:DG9": {"labor:L00005#0": {"labor": 1e5}}})
assert _line(out, "DG9", 0)["amount"] == 10000.0
assert not [g for g in out["gaps"] if "찰붙임" in g["name"]]
@pytest.mark.parametrize("case", ["cover", "none"])
def test_거울_파이썬_TS(case):
daega = _covered_excel()
cover = COVER if case == "cover" else {}
py = json.loads(json.dumps(compute_labor(daega, b145.SHEETS, b145.SANGEUN, cover)))
ts = b145._ts(daega, b145.SHEETS, b145.SANGEUN, cover)
assert b145._same(py, ts["out"])
assert b145._same(daega_price(py, "pipe@100.0"), ts["price"])
def test_노임_새_판_덮개_칸_그대로():
"""702-4 — 바탕 단가가 새 판으로 바뀌어도 고친 칸 = 그대로 · 안 고친 칸만 새 값."""
daega = _covered_excel()
cover = {"T:DG9": {"labor:L00005#0": {"qty": 0.12}, "labor:L00017#1": {"labor": 170000}}}
old = compute_labor(daega, b145.SHEETS, b145.SANGEUN, cover)
new_sheets = {
**b145.SHEETS,
"labor": [{**r, "price": round(r["price"] * 1.05)} for r in b145.SHEETS["labor"]],
}
new = compute_labor(daega, new_sheets, b145.SANGEUN, cover)
a, b = _line(old, "DG9", 0), _line(new, "DG9", 0)
assert a["qty"] == b["qty"] == 0.12 # 고친 품 그대로
assert b["prices"]["labor"] == round(258935 * 1.05) != a["prices"]["labor"] # 안 고친 단가 = 새
assert b["base"]["labor"] == b["prices"]["labor"] # 품셈 값 곁 = 새 판 값
c, d = _line(old, "DG9", 1), _line(new, "DG9", 1)
assert c["prices"]["labor"] == d["prices"]["labor"] == 170000 # 고친 단가 그대로
assert d["qty"] == 0.03 and d["base"]["labor"] == round(165545 * 1.05)
assert new["daega_orphans"] == []
# ── 사본 → 덮개 열쇠 ───────────────────────────────────────────────────────
@pytest.fixture()
def project(tmp_path) -> Path:
root = b145._project(
tmp_path / "proj", [(100, "SS000901"), (200, "SS000901"), (400, "SS000902")]
)
b145._snap(root, "pipe@100.0", "SS000901", b145.LINES, b145._values(b145.STONE))
b145._snap(root, "pipe@200.0", "SS000901", b145.LINES, b145._values(b145.STONE))
promoted = [
{"로직": "GF000219", "승격": True, "수량": {"손": 1}},
{"로직": "GF000119", "승격": True, "수량": {"손": 1}},
{"참조": {"층": "system", "주인": None, "키": "SC-T"}, "수량": {"손": 4}},
]
b145._snap(root, "pipe@400.0", "SS000902", promoted, b145._values(b145.STONE), b145.SC)
return root
def test_사본_덮개_열쇠(project):
got = daega_inputs(project)
blocks = {b["id"]: b for b in got["blocks"]}
one = blocks[got["owners"]["pipe@100.0"]]
assert one is blocks[got["owners"]["pipe@200.0"]] # 같은 꼴 = 한 호표 · 처음 본 사본 열쇠
assert one["cover"] == f"S:{snapshots.folder_name('pipe@100.0')}"
assert [ln["cid"] for ln in one["lines"]] == [
"sangeun:SG:GF000219#0",
"sangeun:SG:GF000119#0",
"sangeun:SG:GF000143#0",
]
four = blocks[got["owners"]["pipe@400.0"]]
assert [ln["cid"] for ln in four["lines"]] == [
"daega:GF000219#0",
"daega:GF000119#0",
"daega:참조|system||SC-T#0",
]
stone = blocks[four["lines"][0]["key"]]
assert stone["cover"].startswith("P:") and [ln["cid"] for ln in stone["lines"]] == [
"labor:LB000033#0",
"labor:LB000002#0",
]
assert blocks[four["lines"][2]["key"]]["cover"] == "C:참조|system||SC-T"
def test_재적용_뒤_덮개_남음(project):
"""702-4 — 사본을 다시 만들어(재적용 꼴 · 수량 · 줄 차례 바뀜 · 줄 빠짐) 블록 id 가 바뀌어도
cover · cid 가 같아 고친 칸이 맞는 줄에 붙음 · 사라진 줄 덮개 = 짝 잃음(남김)."""
before = daega_inputs(project)
owner = before["owners"]["pipe@100.0"]
first = next(b for b in before["blocks"] if b["id"] == owner)
cover = {
first["cover"]: {
"sangeun:SG:GF000119#0": {"qty": 0.7},
"sangeun:SG:GF000143#0": {"labor": 1000},
}
}
folder = snapshots.structures_dir(project) / snapshots.folder_name("pipe@100.0")
doc = json.loads((folder / "일위대가.json").read_text(encoding="utf-8"))
lines = doc["줄"]["담은로직"]
lines[0]["수량"] = {"손": 5} # 설계 바뀜(수량)
doc["줄"]["담은로직"] = [lines[1], lines[0]] # 차례 바뀜 · GF000143 빠짐
(folder / "일위대가.json").write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8")
after = daega_inputs(project)
block = next(b for b in after["blocks"] if b["id"] == after["owners"]["pipe@100.0"])
assert block["id"] != owner and block["cover"] == first["cover"]
cids = [ln["cid"] for ln in block["lines"]]
assert cids == ["sangeun:SG:GF000119#0", "sangeun:SG:GF000219#0"]
sheets = {"labor": [], "material": [], "machine": [], "gaps": []}
out = compute_labor(after, sheets, {}, cover)
got = next(b for b in out["daega_blocks"] if b["id"] == block["id"])
assert got["lines"][0]["qty"] == 0.7 and got["lines"][0]["covered"] == ["qty"] # 맞는 줄
assert got["lines"][1]["qty"] == 5 and "covered" not in got["lines"][1]
assert out["daega_orphans"] == [{"cover": first["cover"], "cid": "sangeun:SG:GF000143#0"}]
def test_저장_다듬기():
raw = {
"daega_cover": {
"S:a": {"labor:L1#0": {"qty": 0.5, "labor": "x", "memo": 1}, "b#0": {"qty": None}},
"S:빈": {},
"S:c": "글",
"P:d": {"x#0": {"expense": float("inf"), "material": 3}},
}
}
assert store.clean_user(raw)["daega_cover"] == {
"S:a": {"labor:L1#0": {"qty": 0.5}},
"P:d": {"x#0": {"material": 3.0}},
}
assert "daega_cover" not in store.clean_user({"daega_cover": {"S:a": {}}})
def test_저장_뒤_서버_값(tmp_path):
"""[저장](write_user → read_user) 뒤 서버 셈 = 화면 짝 셈(덮개 입력값만 받고 다시 셈)."""
stored = store.write_user(tmp_path, {"daega_cover": COVER})
assert store.read_user(tmp_path)["daega_cover"] == stored["daega_cover"] == COVER
daega = _covered_excel()
server = compute_labor(daega, b145.SHEETS, b145.SANGEUN, stored["daega_cover"])
screen = b145._ts(daega, b145.SHEETS, b145.SANGEUN, COVER)["out"]
assert b145._same(json.loads(json.dumps(server)), screen)
def test_B08_카드_고친_칸_수(project):
"""702-4 B08 구조물도 카드 — 그 사본 블록 + 부른 블록의 덮개 수(카드 금액 = 사본 셈)."""
got = daega_inputs(project)
four = next(b for b in got["blocks"] if b["id"] == got["owners"]["pipe@400.0"])
stone = next(b for b in got["blocks"] if b["id"] == four["lines"][0]["key"])
store.write_user(
project,
{
"daega_cover": {
stone["cover"]: {"labor:LB000033#0": {"qty": 0.3}},
"S:없음": {"x#0": {"qty": 1}},
}
},
)
assert cover_count(project, snapshots.folder_name("pipe@400.0")) == 1 # 부른 품셈 블록
assert cover_count(project, snapshots.folder_name("pipe@100.0")) == 0