- 표지 = 수식 문서 머리 · 옛 로직 줄 칸 「부품」 — 기계경비식 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>
261 lines
13 KiB
Python
261 lines
13 KiB
Python
# 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["수식셈"]["까닭"]
|