Files
Aislo/resources/tester/test_795_4_b09_copy.py
T
eomsangdonandClaude Opus 5.5 3e1dc294b2 feat(795-4): B09 사본 규칙 — 만든 때 그대로 · [단가 갱신] 값만 · [재생성] 따로 · 시점 섞임 고침
- 구조물 · 서고 사본 단가 묶음 = M02 일위대가 · SC · SK 로직 사본 줄 먼저(`collect(over)`) · M01 에서 지워진 키 = `사본_남은줄.json`
- 단가 책 새 줄([저장] · 첫 굳힘) = 연결 사본 `단가.json` 자원 줄(구조물 지정 때 값 · `Linked.frozen_rows`) · 없는 자원만 지금 M01 + 책 `now_keys` · 입력 `now` · 알림 한 줄
- POST …/estimation/prices 기본 = 단가 갱신(값만 · 사본 재적용 없음) · job 재생성 = 옛 [업데이트] · 미리 보기 단가 갱신 = 바뀐 자원 · 총괄 · 대가 합 전 → 후
- B09 화면 단추 [업데이트] → [단가 갱신] · 미리 보기 창 [재생성] → 재생성 미리 보기 → [적용] · 안내 글 둘 이름 맞춤
- 옮기기 도구(보기만) — 프로젝트 층 못셈 14 = 「못셈(옛 SP 참조)」 갈래 · B09 구조물 · 서고 사본 대조(그대로 · 재생성 금액)
- 새 시험 test_795_4_b09_copy(8) · 낡은 시험 고침: test_203_1 · test_296 = 단추 이름 「업데이트」 · 호버 글 · 몸 `{ user }` 를 박아 둔 것 → 795-4 이름 · job 덧칸으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 11:54:10 +09:00

190 lines
9.1 KiB
Python

# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""795-4 프로젝트(B09) 사본 규칙 — 만든 때 그대로 · [단가 갱신] 값만 · [재생성] 따로 ·
시점 섞임 고침(임시 storage · M01 은 메모리 덮개 · 정본 안 고침).
① 구조물 사본 단가 묶음 = M02 일위대가 사본 줄(만든 때) · 뒤에 M01 로직 · 노임이 바뀌어도
사본 금액 그대로 · M01 에서 지워진 키 = 사본 줄 그대로 ② 사본 없는 옛 문서 = 옛 길(지금 M01)
③ [저장] 새 단가 줄 = 사본 기준 시점 값 · 없는 자원만 지금 값 + 표지 · 옛 책 줄 그대로
④ 연결 사본 `단가.json` 자원 줄 = 기준 시점 원천 ⑤ [단가 갱신] = 값만(사본 재적용 없음) ·
[재생성] = 따로 누를 때만 ⑥ 미리 보기 꼴.
"""
from __future__ import annotations
import asyncio
import copy
import json
from uuid import uuid4
from B09_Estimation import B09_Estimation_Linked as linked
from B09_Estimation import B09_Estimation_Prices as prices
from B09_Estimation import B09_Estimation_Refresh as refresh
from B09_Estimation import B09_Estimation_Router as router
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Item_Prices as item_prices
from M02_MasterTemplete import M02_Item_Source as source
from M02_MasterTemplete import M02_Sangeun_Copy as skc
from resources.tester.test_145_2_b09_labor_calc import _project, _snap
from resources.tester.test_795_1_sk_copy import _changed, _fake, _live
from resources.tester.test_795_2_sc_copy import LB, PCT, _double, _labor
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
MAN = "LB000002"
def _json(value):
return json.loads(json.dumps(value, ensure_ascii=False, default=str))
def _m01(monkeypatch, rows=(), drop=()):
"""M01 바꿈 흉내 — 색인(`_fake`) + 파일 묶음(단가 묶음이 읽음) 둘 다 · 정본 안 고침."""
files = m01.loaded()[0]
fake = _fake(monkeypatch, rows, drop)
new = {str(r["키"]): r for r in rows}
out = {}
for name, data in files.items():
slot = m01.items_key(data)
got = [new.get(str(r.get("키")), r) for r in data.get(slot) or []]
out[name] = {**data, slot: [r for r in got if str(r.get("키")) not in drop]}
monkeypatch.setattr(m01, "loaded", lambda: (out, fake))
def _amount(labor_rows, bundle):
got = item_prices.calc(labor_rows, bundle, {}, {})
return _json(got["반영합"])
def test_bundle_is_m02_copy_then_frozen(world, monkeypatch):
"""① 사본 묶음 = M02 사본 줄 · M01 로직 · 노임 바뀜 · 지워짐에도 금액 그대로."""
me = world["personal"]
doc = _labor(me, [{"로직": PCT, "수량": {"손": 2}}])
assert skc.has(doc)
rows = source.labor_doc_rows(doc)
was = _amount(rows, source.price_bundle(rows, me)) # 지금 M01 = 사본 만든 때
bundle = source.price_bundle(rows, me)
_m01(monkeypatch, [_changed(_live().logic(PCT)), _double(LB)])
assert _amount(rows, bundle) == was # 굳힌 묶음 = M01 안 읽음
late = source.price_bundle(source.labor_doc_rows(doc), me) # 구조물 지정이 M01 바뀐 뒤
assert _amount(rows, late) == was # M02 사본 = 만든 때
old = {k: v for k, v in doc.items() if k != skc.KEY} # ② 사본 없는 옛 문서 = 지금 M01
old_rows = source.labor_doc_rows(old)
assert _amount(old_rows, source.price_bundle(old_rows, me)) != was
def test_deleted_m01_key_kept_in_copy(world, monkeypatch):
"""① M01 에서 지워진 로직 키 = 사본 줄 그대로(`사본_남은줄.json`) · 같은 금액."""
me = world["personal"]
doc = _labor(me, [{"로직": PCT, "수량": {"손": 1}}])
rows = source.labor_doc_rows(doc)
was = _amount(rows, source.price_bundle(rows, me))
_m01(monkeypatch, drop=[PCT])
late = source.price_bundle(rows, me)
assert [r["키"] for r in late[item_prices.COPY_FILE]["줄"]] == [PCT]
assert _amount(rows, late) == was
def test_copy_rows_order_and_sk_origin():
"""사본 줄 모음 — 앞 문서 먼저 · 굳힌 SK 는 `원본` 사본."""
a = {skc.KEY: {"로직": [{"키": "GC1", "v": 1}], "단가": [{"키": "LB1", "v": 1}]}}
b = {"원본": {skc.KEY: {"로직": [{"키": "GC1", "v": 2}, {"키": "GC2"}], "단가": []}}}
got = source.copy_rows([a, b, {"x": 1}, None])
assert [(r["키"], r.get("v")) for r in got] == [("GC1", 1), ("LB1", 1), ("GC2", None)]
assert item_prices.collect(["GF000219"], over=[]) == item_prices.collect(["GF000219"])
def _row(key, price):
row = copy.deepcopy(prices._row(_live(), key))
row["값"] = price
return item_prices.to_json(row)
def test_book_new_rows_use_copy_basis():
"""③ [저장] 새 줄 = 사본 기준 시점 값 · 없는 자원 = 지금 값 + 표지 · 옛 책 줄 그대로."""
frozen = {LB: _row(LB, 111)}
first = _json(prices.extend_book(None, [LB], frozen))
assert first["rows"][LB]["price"] == 111 and first["now_keys"] == []
both = _json(prices.extend_book(first, [LB, MAN], frozen))
assert both["rows"][LB] == first["rows"][LB]
assert both["rows"][MAN]["price"] == float(prices._row(_live(), MAN)["값"])
assert both["now_keys"] == [MAN]
inputs = prices.build_inputs(None, [LB, MAN], None, frozen)
assert [r["price"] for r in inputs["labor"]] == [111, both["rows"][MAN]["price"]]
assert inputs["now"] == [MAN] and prices.build_inputs(both, [LB, MAN])["now"] == [MAN]
old = _json(prices.refresh_book([LB, MAN])) # 옛 책(사본 규칙 전) = 줄 · 값 그대로
again = _json(prices.extend_book(old, [LB, MAN], frozen))
assert again["rows"] == old["rows"] and again["now_keys"] == []
fresh = _json(prices.refresh_book([LB, MAN], both)) # [단가 갱신] = 지금 값 · 표지 없음
assert fresh["rows"][LB]["price"] != 111 and fresh["now_keys"] == []
def test_frozen_rows_from_linked_copy(tmp_path):
"""④ 연결 사본 `단가.json` 자원 줄 = 기준 시점 원천(자원 표만)."""
root = _project(tmp_path / "proj", [(10.0, "SS000052")])
_snap(root, "pipe@10.0", "SS000052", [{"로직": "GF000219", "수량": {"손": 1}}], {})
got = linked.frozen_rows(root)
assert got and all(k[:2] in skc.RES for k in got)
bundle = json.loads(
(linked.structures_dir(root) / "pipe@10.0" / "단가.json").read_text("utf-8")
)
inside = {r["키"]: r for d in bundle.values() if isinstance(d, dict) for r in d.get("줄") or []}
assert all(inside[k] == row for k, row in got.items())
def _router(monkeypatch, tmp_path, calls):
async def root(_):
return tmp_path
async def demand(*_):
return [LB]
async def payload(*_a, **_k):
return {"user": {}, "notes": [], "sheets": {}}
monkeypatch.setattr(router, "_project_root", root)
monkeypatch.setattr(router, "_demand", demand)
monkeypatch.setattr(router, "payload", payload)
monkeypatch.setattr(router.store, "read_book", lambda _r: None)
monkeypatch.setattr(router.store, "write_book", lambda _r, b: calls.append(("book", b)))
monkeypatch.setattr(
router.refresh,
"reapply_changed",
lambda *_a: calls.append("regen") or {"다시": ["A"], "막힘": []},
)
def test_price_refresh_default_regen_separate(monkeypatch, tmp_path):
"""⑤ [단가 갱신] = 값만(사본 재적용 없음) · [재생성] = job 줄 때만."""
calls = []
_router(monkeypatch, tmp_path, calls)
out = asyncio.run(router.refresh_prices(uuid4(), {"user": {}}, {}))
assert "regen" not in calls and calls[-1][0] == "book"
assert out["notes"][0].startswith("단가 갱신 — ")
out = asyncio.run(router.refresh_prices(uuid4(), {"user": {}, "job": refresh.REGEN}, {}))
assert calls.count("regen") == 1
assert out["notes"][0].startswith("재생성 — 구조물 사본 1 다시 가져옴")
def test_price_preview_shape():
"""⑥ 미리 보기 — 바뀐 자원(전 · 후) · 총괄 전 → 후 · 사본 말 없음."""
old = _json(prices.refresh_book([LB, MAN]))
old["rows"][LB] = {**old["rows"][LB], "price": 1}
new = _json(prices.refresh_book([LB, MAN], old))
sheets = lambda g: {"total": {"subtotals": {"grand": g}}} # noqa: E731
got = refresh.price_preview(old, new, sheets(10), sheets(20), 3)
assert got["할일"] == "단가갱신" and (got["전"], got["후"], got["고친칸"]) == (10, 20, 3)
assert [(r["키"], r["전"]) for r in got["바뀐자원"]] == [(LB, 1)]
assert got["바뀐자원"][0]["후"] == new["rows"][LB]["price"]
def test_migrate_view_b09_copy(tmp_path):
"""옮기기 도구 B09 대조(보기만) — 구조물 사본 = 「그대로」 · 전 금액 · 파일 안 바꿈."""
from M02_MasterTemplete import M02_Copy_Migrate as mig
root = _project(tmp_path / "proj", [(10.0, "SS000052")])
_snap(root, "pipe@10.0", "SS000052", [{"로직": "GF000219", "수량": {"손": 1}}], {})
folder = linked.structures_dir(root) / "pipe@10.0"
before = {p.name: p.read_bytes() for p in folder.iterdir()}
got = mig.b09_check("구조물사본", folder)
assert got["상태"] == "그대로" and got["전"]["기준합"]["계"] > 0
assert {p.name: p.read_bytes() for p in folder.iterdir()} == before