# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀 """802-4a M02 단가산출 · 일위대가 · B09 사본 = 수식 문서 — 임시 storage · M01 은 메모리 덮개. ① 찍기 = M01 문서 사본(금액 = M01 문서 셈) ② 고침 = 사본 문서(M01 안 바뀜 · 옛 칸 고침 409) ③ [단가 갱신] = 자원 단가만(문서 그대로) ④ [재생성] = 지금 M01 문서 · 고친 것 버림 ⑤ 단가산출 연결 = [재생성] 때 따라감 · 끊기 = 사본 안에 굳힘 ⑥ SC · 일위대가 = 문서 셈(옛 금액 같음 · 줄 규칙 켜쌓기율 · 고른 자재 · 별도계상) ⑦ B09 사본 `수식.json` ⑧ 권한 ⑨ 지움 견딤 · 빈 SK 「—」. """ from __future__ import annotations import copy import pytest from fastapi import HTTPException from M02_MasterTemplete import M02_Doc_B09 as doc_b09 from M02_MasterTemplete import M02_Doc_Copy as dc from M02_MasterTemplete import M02_Doc_Router as router from M02_MasterTemplete import M02_Doc_Sk as dsk from M02_MasterTemplete import M02_Doc_Sk_Path as path from M02_MasterTemplete import M02_Item_Snapshot as snap from M02_MasterTemplete import M02_Item_Source as source from M02_MasterTemplete import M02_Labor_Doc as labor from M02_MasterTemplete import M02_MasterTemplete_Store as store from resources.tester.test_795_1_sk_copy import _calc, _changed, _doc, _fake, _live, _save, _sk from resources.tester.test_795_2_sc_copy import LB, PCT, _double, _labor, _sc from resources.tester.test_795_2_sc_copy import _calc as _sc_calc from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층 FORM = "GC000211" # 합판거푸집(옵션 많음) BASIC = "GF000089" # 토사깍기(기계) — 굴착기 시간당 × 1 / 작업량(813-2 GC001148 지움 뒤) ADD = {"종류": "로직", "로직": BASIC, "수량": 2, "입력값": {}, "이름": "sub2임시 굴착기"} def _sum(folder, key): return _calc(folder, _doc(folder, key))["기준"]["계"] def _at(folder, fn, *args): with store.at(folder): return fn(*args) def _twice(doc, at=0): """금액 줄 하나 수량 × 2 — 사용자 고침 흉내.""" doc = copy.deepcopy(doc) line = doc["금액"][at] line["수량"] = f"({line.get('수량') or 1}) × 2" return doc def test_stamp_is_m01_doc_copy(world): """① 만들 때 M01 문서를 찍음 — 키 = SK · 옵션 값 · 금액 = M01 문서 셈 = 옛 금액.""" me = world["personal"] key = _sk(me, 바탕={"로직": FORM}, 입력값={"유형": "복잡", "사용횟수": "3회"}) got = _doc(me, key)[dc.KEY] root, m01 = got["문서"], dc.folder()[FORM] assert root["키"] == key and [v["기호"] for v in root["변수"]] == [ v["기호"] for v in m01["변수"] ] assert next(v for v in root["변수"] if v["열쇠"] == "유형")["연결"]["값"] == "복잡" assert not got.get("따로") and got["로직"] and got["단가"] m01_calc = dc.mc.calc(root, dc.ml.Links(_live(), dc.folder())) assert _sum(me, key) == m01_calc["계"] token = dsk.OLD.set(True) try: assert _sum(me, key) == m01_calc["계"] # 옛 엔진 금액과 같음 finally: dsk.OLD.reset(token) read = _at(me, path.read, key) assert read["셈"]["계"] == m01_calc["계"] and read["사본"]["따로"] is False def test_edit_goes_to_copy_doc(world): """② 고침 = 그 사본 문서(따로) · M01 문서 그대로 · 옛 칸 고침 저장 409 · 다시 열어 같음.""" me = world["personal"] key = _sk(me, 바탕={"로직": PCT}) was, m01_before = _sum(me, key), dc.fp(dc.folder()[PCT]) read = _at(me, path.read, key) pre = _at(me, path.preview, key, {"문서": _twice(read["문서"])}) assert pre["전"]["계"] == was and pre["후"]["계"] > was saved = _at(me, path.save, key, {"문서": _twice(read["문서"]), "판": read["판"]}) assert saved["사본"]["따로"] is True and saved["셈"]["계"] == pre["후"]["계"] assert _sum(me, key) == pre["후"]["계"] # SK 금액(일위대가가 쓰는 값) = 고친 문서 assert dc.fp(dc.folder()[PCT]) == m01_before assert _at(me, path.read, key)["문서"] == saved["문서"] doc = _doc(me, key) with pytest.raises(store.StoreError) as e: _save(me, key, {**doc, "입력값": {"x": 1}, "덮개": {"labor:LB000036#0": {"qty": 3}}}) assert e.value.status == 409 _save(me, key, {**doc, "이름": "sub2임시 이름만"}) # 머리 칸 = 그대로 받음 assert _sum(me, key) == pre["후"]["계"] with pytest.raises(store.StoreError) as e: # 판 다름 _at(me, path.save, key, {"문서": read["문서"], "판": read["판"]}) assert e.value.status == 409 def test_price_refresh_values_only(world, monkeypatch): """③ [단가 갱신] = 자원 단가만 — M01 문서 바뀜은 안 따라감 · 바뀐자원 · 누르기 전 그대로.""" me = world["personal"] key = _sk(me, 바탕={"로직": PCT}) was = _sum(me, key) root = _at(me, path.read, key)["문서"] _fake(monkeypatch, [_changed(_live().logic(PCT)), _double(LB)]) assert _sum(me, key) == was pre = _at(me, path.preview, key, {"할일": "단가갱신"}) assert [x["키"] for x in pre["바뀐자원"]] == [LB] and pre["문서"] == root fresh = _sum(me, _sk(me, 바탕={"로직": PCT})) # 새 문서 + 새 단가 assert pre["후"]["계"] != was and pre["후"]["계"] != fresh read = _at(me, path.read, key) got = _at(me, path.save, key, {"할일": "단가갱신", "판": read["판"]}) assert got["셈"]["계"] == pre["후"]["계"] == _sum(me, key) and got["문서"] == root assert got["사본"]["단가새값"] is False and PCT in got["사본"]["문서새판"] def test_regen_takes_m01_doc_drops_edits(world, monkeypatch): """④ [재생성] = 지금 M01 문서로 · 고친 문서 버림(알림) · 파생으로 돌아감.""" me = world["personal"] key = _sk(me, 바탕={"로직": PCT}) read = _at(me, path.read, key) _at(me, path.save, key, {"문서": _twice(read["문서"], 1), "판": read["판"]}) _fake(monkeypatch, [_changed(_live().logic(PCT))]) fresh = _sum(me, _sk(me, 바탕={"로직": PCT})) pre = _at(me, path.preview, key, {"할일": "재생성", "문서": {"아무": "버려짐"}}) assert pre["버림"]["문서고침"] is True and pre["후"]["계"] == fresh read = _at(me, path.read, key) got = _at(me, path.save, key, {"할일": "재생성", "판": read["판"]}) assert got["사본"]["따로"] is False and got["셈"]["계"] == fresh == _sum(me, key) assert got["사본"]["M01새판"] is False and got["사본"]["문서새판"] == [] def test_link_follows_on_regen_unlink_freezes(world, monkeypatch): """⑤ 단가산출 연결 = 부름 사본(M01 바뀌어도 그대로 · [재생성] 때 따라감) · 끊기 = 사본 안 고침.""" me = world["personal"] key = _sk(me, 바탕={"로직": FORM}, 더한줄=[copy.deepcopy(ADD)]) was = _sum(me, key) read = _at(me, path.read, key) assert BASIC in read["부름"] cut = {BASIC: _twice(read["부름"][BASIC])} pre = _at(me, path.preview, key, {"끊음": cut}) assert pre["후"]["계"] > was saved = _at(me, path.save, key, {"끊음": cut, "판": read["판"]}) assert saved["끊음"][BASIC] == {**cut[BASIC], "키": BASIC} and _sum(me, key) == pre["후"]["계"] _fake(monkeypatch, [_changed(_live().logic(BASIC))]) # 원본 바뀜 assert _sum(me, key) == pre["후"]["계"] # 끊은 것 · 연결 모두 그대로(누르기 전) with pytest.raises(store.StoreError) as e: # 부르지 않는 키 끊기 _at(me, path.preview, key, {"끊음": {"GC999999": read["부름"][BASIC]}}) assert e.value.status == 400 read = _at(me, path.read, key) pre = _at(me, path.preview, key, {"할일": "재생성"}) assert pre["버림"]["끊음"] == [BASIC] got = _at(me, path.save, key, {"할일": "재생성", "판": read["판"]}) assert got["끊음"] == {} and got["셈"]["계"] > was # 연결 = 새 원본 따라감 def test_sc_and_labor_use_doc(world, monkeypatch): """⑥ SC · 일위대가 로직 줄 = 문서 셈(부름 사본) · 옛 엔진 금액과 같음 · M01 바뀌어도 그대로.""" me = world["personal"] sc = _sc(me, [{"로직": PCT, "수량": {"손": 1}}]) _labor(me, [{"로직": FORM, "수량": {"손": 2}, "입력값": {"유형": "보통"}}]) stored = _at(me, store.read, "precombo", sc)["문서"] assert dc.has(stored) and PCT in stored[dc.KEY]["부름"] runs = [] real = dc.run monkeypatch.setattr(dc, "run", lambda *a, **k: runs.append(1) or real(*a, **k)) new = (_sc_calc(me, sc)["기준합"], _sc_calc(me, "sub2임시")["반영합"]) assert runs token = dsk.OLD.set(True) try: assert (_sc_calc(me, sc)["기준합"], _sc_calc(me, "sub2임시")["반영합"]) == new finally: dsk.OLD.reset(token) _fake(monkeypatch, [_changed(_live().logic(PCT))]) assert _sc_calc(me, sc)["기준합"] == new[0] @pytest.mark.parametrize( "code, rate, picks, marks", [ ("GC000261", 90, None, None), # 켜쌓기율 = 변수 r · 수량 × r ÷ 100 ("GF000156", None, {"3": "MT004176", "4": "MT003834"}, None), # 고른 자재 = 요소 키 ("GC000665", None, None, None), # 별도계상(기본) = 합밖 · 「재료비의 %」 밑엔 듦 ], ) def test_line_rules_in_doc(code, rate, picks, marks): """⑥ 일위대가 줄 옵션 = 문서 규칙(브레인 ④) — 옛 `run_line` · `drop_apart` 와 같은 금액.""" whole = _live() old = labor.drop_apart(labor.run_line(whole, code, {}, rate, picks), marks) assert old["기준"] apart = dc.apart_of(marks, old.get("재료")) doc = dc.line_doc(dc.folder()[code], rate, picks, apart) if rate: assert dc.RATE_ID in str(doc["금액"][0]["수량"]) if apart: assert all(doc["금액"][n].get("합밖") for n in apart) got = dc.mc.calc(doc, dc.ml.Links(whole, dc.folder()), dc.given_of(whole, code, {})) assert {k: got[k] for k in dc.COSTS} == {k: old["기준"][k] for k in dc.COSTS} assert dc.line_doc(dc.folder()[code]) is dc.folder()[code] # 옵션 없음 = 그 문서 def test_b09_copy_has_doc_file(world, monkeypatch): """⑦ B09 사본 = 단가 묶음 옆 `수식.json`(만든 때 부름) · 원본판엔 안 듦 · M01 바뀌어도 그대로.""" me = world["personal"] doc = _labor(me, [{"로직": PCT, "수량": {"손": 2}}]) rows = source.labor_doc_rows(doc) files = doc_b09.attach({"일위대가.json": rows, "단가.json": source.price_bundle(rows, me)}) made = files[doc_b09.FILE] assert made["그룹"] == "수식문서" and PCT in made["문서"] assert doc_b09.FILE not in snap.source_files(files) assert doc_b09.attach(files) is files # 이미 있음 = 그대로 _fake(monkeypatch, [_changed(_live().logic(PCT))]) again = doc_b09.build(source.price_bundle(rows, me), rows) assert again["문서"][PCT] == made["문서"][PCT] # 일위대가 수식 사본 = 만든 때 def _session(role="USER"): return {"role": role, "company_id": 7, "user_id": 42} def test_permission_layer(world): """⑧ 읽기 = 층 읽기 · 미리 셈 · 저장 = 층 쓰기(마스터 = 시스템 관리자 · 회사 = 회사 관리자).""" me = world["personal"] key = _sk(me, 바탕={"로직": PCT}) body = router.DocBody(문서=_at(me, path.read, key)["문서"]) for layer in ("system", "company"): for fn in (router.preview_doc, router.save_doc): with pytest.raises(HTTPException) as e: fn(layer, key, body, None, _session()) assert e.value.status_code == 403 assert router.read_doc("personal", key, None, _session())["셈"]["셈됨"] got = router.preview_doc("personal", key, body, None, _session()) assert got["후"] == got["전"] def test_delete_tolerant_and_blank(world): """⑨ 무엇을 지워도 오류 없음 — 변수 지움 = 정의 필요 · 금액 「—」 · 저장됨 · 빈 SK = 「—」.""" me = world["personal"] key = _sk(me, 바탕={"로직": PCT}) read = _at(me, path.read, key) gone = copy.deepcopy(read["문서"]) gone["변수"] = [v for v in gone["변수"] if v["열쇠"] != "w_지붕잇기공"] pre = _at(me, path.preview, key, {"문서": gone}) assert pre["셈"]["셈됨"] is False and pre["후"] is None assert any(m["갈래"] == "정의 필요" for m in pre["셈"]["표지"]) _at(me, path.save, key, {"문서": gone, "판": read["판"]}) got = _calc(me, _doc(me, key)) assert got["기준"] is None and got["수식셈"]["셈됨"] is False lost = copy.deepcopy(read["문서"]) lost["변수"].append({"열쇠": "b", "기호": "b", "연결": {"갈래": "단가산출", "키": "GC999999"}}) lost["금액"].append({"이름": "없는 기초", "비목": "묶음", "단가": "b"}) pre = _at(me, path.preview, key, {"문서": lost}) assert any(m["갈래"] == "연결 끊김" for m in pre["셈"]["표지"]) for bad in ({"문서": "글"}, {"문서": {"금액": "x"}}, {"끊음": []}, {"할일": "몰라"}): with pytest.raises(store.StoreError) as e: _at(me, path.preview, key, bad) assert e.value.status == 400 empty = _sk(me) got = _calc(me, _doc(me, empty)) assert got["기준"] is None and "빈 문서" in got["수식셈"]["까닭"]