Files
Aislo/resources/tester/test_802_4a_m02_doc.py
T
eomsangdonandClaude Opus 5.5 58a3e039b3 feat(813-2): 보조 문서 부품 표지 73 · 작업량당 기계경비 6 지움 · 단가산출 목록에서 뺌 · 기계 → 부품 길
- 표지 = 수식 문서 머리 · 옛 로직 줄 칸 「부품」 — 기계경비식 9 · 작업량 틀 60(기종 = 기계표 상세구분 · 규격 열쇠) · 계수식 4 · 변환기가 옮김 · 틀 문서 9 장
- 지움 = 작업량당 기계경비 6(GC001145 · 1148 · 1151 · 1156 · 1159 · 1166 · 부르는 곳 0 · 요구 734-4 · 735-4) — 수식 문서 · 옛 로직 · 키대장 폐기 칸 · 빌더 · 변수표
- 이름 꼬리 4 = 원문 제목(지게운반 · 인력운반(기계설비) · 벽돌운반 · 수중펌프 설치 및 해체) · 키 폐기 0
- 서버 = `M01_MasterData_Parts` — M01 `/logics` · M02 `/logic-lib/logics` 목록에서 부품 뺌(1,440) · 키로 열기 · 부르기 그대로 · `/m01/parts`(기종 계산식 · 틀 · 계수식) · `/m02/logic-lib/machine-parts` · 연결 찾기 과녁에서 부품 뺌(1,444 → 1,435 · 판정 코드 그대로)
- 금액 전후 = 문서 1,513 · 셈 9,626(기본 + 옵션 표본) 모두 같음 · 791_1 지문 남은 1,513 같음 → 지문 다시 뜸 · 802_2 변환 대조 1,513 · 목록 xlsx 다시 씀(요약 수)
- 낡은 시험 = 1,519 · 1,518 기대 → 1,513 · GC001148 견본 → 같은 꼴 GF000089 토사깍기(기계)(블록 Q 39.293 · 합계 2,190) · 지운 6 을 셈하던 줄 걷음 · 목록 수 1,440 · 묶음 328 · 부르는 수 507
- 새 지킴 시험 test_813_2_parts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 20:49:52 +09:00

261 lines
13 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.
# 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["수식셈"]["까닭"]