Files
Aislo/resources/tester/test_795_4_b09_copy.py
T
eomsangdonandClaude Opus 5.5 5d8d9a4fd3 test(841-1): B09 = M02 값 — 산근 · 대가 · 고정 줄 셈 다듬기 · 낡은 시험 정리 — C 걸음
코드
- 고정 줄 단가 = M02_Labor_Fixed.line(사본 묶음 · 이은 노임 · 자재 = 묶음 값) — B09 단가 책으로 다시 안 덮음
- 대가 · 산근 고친 칸 = 품만 실제로(단가 칸 덮기 줄 남던 흠 · 품 고친 뒤 줄 금액 다시 셈 흠 py · ts 짝)
- 산근 글 = 엔진 단가 셈 그대로 · 별도계상 재료 줄 끝 「· 별도계상 = 0.0」 · 계 줄 = 블록 합
- 노무비 · 중기목록 이름에서 규격 겹침 뗌 · TS 바탕 셈 이름 PARTS → MACHINE_PARTS(짝 시험이 TS 를 이어 붙일 때 겹침)

시험(고친 까닭 = 단가 원천이 B09 책 → M02 엔진 · 노임 · 기계 덮개 · 단가 고친 칸 · 중기사용료 걷음)
- 144 바탕 · 뼈대: 운전사 · 중기사용료 · 경비 · 덮개 시험 걷음 → 노무 · 중기 나열 · 사급 표시 · 자재만 책
- 145-1 · 145-2 · 155 · 157-1 · 166-2 · 166-6 · 719-3 · 719-4 · 723-4 · 778-4 · 792-3: 줄 단가 = own(엔진) · 산근 = Solver() · collect 새 부름 꼴
- 702 · 705-1 · 719-2: 단가 칸 덮개 = 안 읽음 · 고정 줄 = M02 셈(노임 새 판 따라가기 시험 걷음)
- 203-1 · 296 · 703-3 · 795-4: 단가 책 = 자재 줄로
- 707 · 707-6 · 707-7: 단가표 불러오기 = 자재만 · 거창 이은 안내판 줄 = M02 사본 셈(엑셀 −50,000)
- 201-1: 환율 · 중기사용료 표 걷음 · b09 저장 속도: 산근 앞 풀이 그대로(단가 표 같음 견줌 걷음)
- 대상 113 파일 764 통과 · 25 실패 = 원래 실패(formatMoney · formatPrice 없음 · 단위 글 ㎜ · sec · 719-1 · 796-2e · 717-4 · 721 · 311 · 243 · 246)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-15 16:57:01 +09:00

197 lines
9.5 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 — 같은 임시 층
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"])
#: 841-1 단가 책 = 자재만 — 원목 · 미송각재
WOOD, BAR = "MT000401", "MT000338"
def _row(key, price):
row = copy.deepcopy(prices._row(_live(), key))
row["물가자료"] = price
return item_prices.to_json(row)
def _price(book_row):
return book_row["prices"]["물가자료"]
def test_book_new_rows_use_copy_basis():
"""③ [저장] 새 줄 = 사본 기준 시점 값 · 없는 자원 = 지금 값 + 표지 · 옛 책 줄 그대로."""
frozen = {WOOD: _row(WOOD, 111)}
first = _json(prices.extend_book(None, [WOOD], frozen))
assert _price(first["rows"][WOOD]) == 111 and first["now_keys"] == []
both = _json(prices.extend_book(first, [WOOD, BAR], frozen))
assert both["rows"][WOOD] == first["rows"][WOOD]
assert _price(both["rows"][BAR]) == float(prices._row(_live(), BAR)["물가자료"])
assert both["now_keys"] == [BAR]
inputs = prices.build_inputs(None, [WOOD, BAR], None, frozen)
assert [_price(r) for r in inputs["materials"]] == [111, _price(both["rows"][BAR])]
assert inputs["now"] == [BAR] and prices.build_inputs(both, [WOOD, BAR])["now"] == [BAR]
old = _json(prices.refresh_book([WOOD, BAR])) # 옛 책(사본 규칙 전) = 줄 · 값 그대로
again = _json(prices.extend_book(old, [WOOD, BAR], frozen))
assert again["rows"] == old["rows"] and again["now_keys"] == []
fresh = _json(prices.refresh_book([WOOD, BAR], both)) # [단가 갱신] = 지금 값 · 표지 없음
assert _price(fresh["rows"][WOOD]) != 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([WOOD, BAR]))
old["rows"][WOOD] = {**old["rows"][WOOD], "prices": {"물가자료": 1}}
new = _json(prices.refresh_book([WOOD, BAR], old))
sheets = lambda g: {"total": {"subtotals": {"grand": g}}} # noqa: E731
copies = [{"키": LB, "이름": "사본 노임", "전": 1, "후": 2}]
got = refresh.price_preview(old, new, sheets(10), sheets(20), 3, copies)
assert got["할일"] == "단가갱신" and (got["전"], got["후"], got["고친칸"]) == (10, 20, 3)
assert [(r["키"], r["전"]) for r in got["바뀐자원"]] == [(LB, 1), (WOOD, {"물가자료": 1})]
assert got["바뀐자원"][1]["후"] == new["rows"][WOOD]["prices"]
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