Files
Aislo/resources/tester/test_804_4_line_stack_take.py
T
eomsangdonandClaude Opus 5.5 eb64bf64ac feat(804-8): M02 단가산출 수식 탭 = M01 과 같은 단추 · 저장 한 번 · 머리 단추 줄바꿈 막음(요구 689)
- 공용 줄 쌓기 화면: 페이지가 저장을 맡는 꼴(`page`)을 M02 단가산출 화면에도 — 탭 위 [고침 버림] · [수식 저장] 걷음 · M02 사본 [단가 갱신] · [재생성] · 「M01 새 판」 배지는 그대로 · 「줄 쌓기에서 못 고치는 칸」 안내 줄 걷음(금액 줄 기호 = 율 줄이 앞 줄 금액을 부르는 속 이름 · 화면에서 고칠 값 아님 · 보존 그대로)
- M02 머리 단추 = [초기화] · [삭제] · [되돌림] · [저장] — 단가산출 상세는 [새로고침] 숨김 · [되돌림] · [저장] = 고침 있을 때만(기본정보 + 수식) · [저장] 한 번 = 수식 먼저 저장(새 판 · 수식 칸 받음) → 단가산출 문서 저장 · [되돌림] · 버리고 떠남 = 수식 캐시도 비움
- 보기 고르기(수식 · 단가산출 상세) = M01 처럼 머리 줄 · [항목 추가] · [율 줄 추가] 걷음(줄 더하기 = 수식 탭 [+] 단가산출 · 일위대가 복사) · 일위대가 줄 모달 안 상세는 그대로
- 머리 단추 줄 맞춤(부품 상자 `fitButtonRow`): 단추가 아래로 떨어지거나 제목이 여러 줄이면 아이콘 단추는 아이콘만(글 = 풍선 도움말) · M01 단추 다섯 · M02 단추 넷 아이콘 · 단가산출 상세 긴 이름은 제목 안에서 줄바꿈
- 사본 할일 막힘 알림 글 = [저장] 또는 [되돌림] 먼저
- 시험: test_804_7 에 M02 단추 · 저장 한 번 · 줄 맞춤 지킴 더함 · test_804_4 권한 시험에 수식 저장 판 = 페이지 저장 판 이어 씀 확인 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 14:56:31 +09:00

263 lines
12 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 에서 받아 씀
"""PLAN 804-4 — 줄 쌓기 다른 단가산출 · 일위대가 복사해 펼침(요구 680-1 · 681) — 임시 storage.
① M01 줄 쌓기 = M01 전체만(찾기 창 탭 · 복사 길 없음) · M01 문서 길은 M02 키를 모름
② M01 단가산출 복사 = 전체 항목 · 줄 · 옵션 굳힘(옵션 0) · 금액 = 원본 · 원본 연결 없음
③ M02 SK 복사 = 그 SK 수식 문서 금액 · 항목 그대로
④ 일위대가(SC) 복사 = 항목 여러 개 · 금액 = 일위대가 계 · 고정(노임 · 손 단가) · 율 · 안 SK(× 수량 ÷ 배)
⑤ 앞 줄 있는 문서에 넣음 = 앞 금액 + 원본(율 밑 = 그 조각만 · 끝수 1 원)
⑥ 권한 — 위 층만(남의 개인 층 403) · 저장 = 서버 정본 셈 같음 · M01 노임 바뀜 → [단가 갱신] 뒤 따라감
"""
from __future__ import annotations
import json
from decimal import Decimal
from pathlib import Path
import pytest
from M02_MasterTemplete import M02_Doc_Copy as dc
from M02_MasterTemplete import M02_Doc_Sk_Path as path
from M02_MasterTemplete import M02_Doc_Take as tk
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from resources.tester.test_795_1_sk_copy import _fake, _live, _sk
from resources.tester.test_795_2_sc_copy import _sc
from resources.tester.test_804_2_line_stack import _ts
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
ROOT = Path(__file__).resolve().parents[2]
FORM = "GC000211" # 합판거푸집(옵션 많음)
DIG = "GF000099" # 측구터파기(굴착기 시간당 경비 부름)
STONE = "GF000219" # 원/㎡
LB = "LB000002" # 보통인부
ME = {"층": "personal", "주인": "7/42"}
def _norm(v):
return json.loads(json.dumps(dc.ml._norm(v), ensure_ascii=False, default=str))
def _take(folder, src):
with store.at(folder):
return _norm(tk.take(src))
def _into(doc, *taken):
return _ts("복사", [{"문서": doc, "받음": list(taken)}])[0]
def _money(doc):
d = json.loads(
json.dumps(dc.ml._norm(doc), default=str), parse_float=Decimal, parse_int=Decimal
)
return dc.mc.calc(d, dc.ml.Links(_live(), dc.folder()))
def _rid(key, at=ME):
return refs.ref_id({**at, "키": key})
def _src(rel):
return (ROOT / rel).read_text(encoding="utf-8")
def test_M01_은_M01전체만():
pick = _src("A00_Common/line_stack_pick.ts")
assert 'kinds: copy ? ["logic", "sk", "pre"] : [want], // M01 = M01 전체만(680-1)' in pick
assert 'const copy = kind === "단가산출" && m02 ? m02 : null;' in pick
lab = _src("M01_MasterData/M01_MasterData_UI_LogicLab.ts")
assert (
'base: "/m01/logic/doc",' in lab and 'cache: "m01_doc_drafts",' in lab
) # 804-7 page = 페이지 [저장]
tabs = _src("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts")
assert "scope: opt.scope, // 804-4" in tabs
screen = _src("A00_Common/line_stack_screen.ts")
assert "cp && opt.scope && key" in screen # 복사 길 = M02 저장한 SK 만
# M01 문서 길 = M02 키 모름(부르면 「없는 단가산출」)
doc = dc.md.blank("X", "x")
doc["변수"] = [
{"기호": "B", "아래": "", "위": "", "연결": {"갈래": "단가산출", "키": _rid("SK-000001")}}
]
doc["금액"] = [{"이름": "남", "비목": "묶음", "수량": "1", "단가": "B"}]
got = dc.mc.calc(doc, dc.ml.Links(_live(), dc.folder()))
assert not got["셈됨"] and "없는 단가산출" in json.dumps(got["표지"], ensure_ascii=False)
def test_M01단가산출_복사_전체_옵션굳힘(world):
me = world["personal"]
t = _take(me, FORM)
assert t["출처"] == f"M01 {FORM}" and [p["갈래"] for p in t["조각"]] == ["문서"]
g = _into(dc.md.blank("SK-000009", "빈"), t)
assert g["옵션"] == [] and g["왕복"] and "번지" not in g["못"]
assert g["항목"][0]["줄"][0] == f"글:M01 {FORM} 「{t['이름']}」에서 복사"
lines = [x for it in g["항목"] for x in it["줄"]]
assert len(lines) > 5 and any(x.startswith("공식:") for x in lines) # 공식 묶음까지 전체
assert _money(g["문서"])["계"] == _money(dc.folder()[FORM])["계"]
# 원본 연결 없음 — 복사본에 원본 키 단가산출 연결 없음 · 기호 모두 이 문서
keys = {(v.get("연결") or {}).get("키") for v in g["문서"]["변수"]}
assert FORM not in keys
def test_SK_복사_그_SK_금액(world):
me = world["personal"]
key = _sk(me, 바탕={"로직": FORM}, 입력값={"유형": "복잡", "사용횟수": "3회"})
with store.at(me):
want = _norm(path.read(key))["셈"]["계"]
t = _take(me, _rid(key))
assert t["출처"] == f"개인 {key}" and t["조각"][0]["갈래"] == "문서"
g = _into(dc.md.blank("SK-000009", "빈"), t)
assert g["옵션"] == [] and _money(g["문서"])["계"] == want
# 옵션 「복잡」 그대로 굳힘
assert any(x.get("식") == "'복잡'" for x in g["문서"]["식"])
def _combo(world):
me = world["personal"]
sk = _sk(me, 바탕={"로직": STONE})
fixed = {"이름": "보통인부", "단위": "인", "단가": {"노무비": 0}, "이음": {"노임": LB}}
hand = {"이름": "sub3임시 잡자재", "단위": "식", "단가": {"재료비": 1234}, "이음": {}}
rate = {"이름": "공구손료", "단위": "%", "단가": {}, "비율": {"기준": "노무비", "비목": "경비"}}
lines = [
{"로직": DIG, "수량": {"손": 2}},
{"로직": "", "고정": fixed, "수량": {"손": 0.3}},
{"로직": "", "고정": hand, "수량": {"손": 2}},
{"참조": {**ME, "키": sk}, "수량": {"손": 3}},
{"로직": "", "고정": rate, "수량": {"손": 3}},
]
return me, sk, _sc(me, lines)
def test_일위대가_복사_여러항목_금액같음(world):
me, sk, sc = _combo(world)
t = _take(me, _rid(sc))
kinds = [p["갈래"] for p in t["조각"]]
assert kinds == ["문서", "인력", "손", "문서", "율"], kinds
assert t["조각"][0]["곱"] == "2" and t["조각"][3]["곱"] == "3"
assert t["조각"][4]["줄"]["단가"] == "노무비 × 3 %"
g = _into(dc.md.blank("SK-000009", "빈"), t)
assert g["옵션"] == [] and g["왕복"] and "번지" not in g["못"]
titles = [it["제목"] for it in g["항목"]]
assert titles[0] == t["이름"] and len(g["항목"]) >= 4 # 머리 + 문서 둘 + 줄 조각 항목
got = float(_money(g["문서"])["계"])
# 일위대가 계 — 끝수 차례만 다름(원본 = 문서 계를 버린 뒤 × 수량 · 복사 = 줄마다 × 수량 뒤 버림)
assert got == pytest.approx(float(t["계"]), abs=10)
# 율 밑 = 그 일위대가 번지 합(앞 줄 섞임 없음)
rate = g["문서"]["금액"][-1]
assert rate["이름"] == "공구손료" and "번지.복사" in rate["단가"]
def test_앞줄있는문서_넣음_율밑안섞임(world):
me = world["personal"]
gc = dc.folder()[FORM]
one = _take(me, DIG)
g = _into(gc, one)
diff = float(_money(g["문서"])["계"]) - float(_money(gc)["계"])
assert diff == pytest.approx(float(_money(dc.folder()[DIG])["계"]), abs=1)
# 일위대가(율 줄 있음) 둘을 잇달아 = 각자 계 합
_, _, sc = _combo(world)
t = _take(me, _rid(sc))
two = _into(dc.md.blank("SK-000009", "빈"), t, t)
one_sc = float(_money(_into(dc.md.blank("SK-000009", "빈"), t)["문서"])["계"])
assert float(_money(two["문서"])["계"]) == pytest.approx(2 * one_sc, abs=2)
def test_권한_저장_단가갱신(world, monkeypatch):
me, sk, sc = _combo(world)
with pytest.raises(store.StoreError) as e: # 회사 층에서 개인 층 = 위 층 아님
_take(world["company"], _rid(sc))
assert e.value.status == 403
with pytest.raises(store.StoreError):
_take(me, "참조|personal|7/43|SC-000001") # 남의 개인 층
# 저장 = 서버 정본 셈 · 다시 읽어 같음
target = _sk(me) # 빈 SK(바탕 · 줄 없음) — 줄 쌓기로 처음부터 복사해 채움
with store.at(me):
read = _norm(path.read(target))
g = _into(read["문서"], _take(me, _rid(sc)))
with store.at(me):
saved = _norm(path.save(target, {"문서": g["문서"], "판": read["판"]}))
again = _norm(path.read(target))
assert saved["셈"]["계"] == again["셈"]["계"] == _money(g["문서"])["계"]
# 페이지 [저장](옛 칸 · 이름) 뒤에도 고친 문서 그대로(빈 SK 씨로 덮지 않음)
with store.at(me):
file = store.read("sangeun", target)
assert (
file["판"] == saved["판"]
) # 804-8 페이지 [저장] 한 번 = 수식 저장 판 그대로 이어 씀(409 없음)
store.write("sangeun", target, file["판"], {**file["문서"], "이름": "sub3임시 이름바꿈"})
again = _norm(path.read(target))
assert again["셈"]["계"] == saved["셈"]["계"] and again["사본"]["따로"]
# M01 노임 바뀜 → [단가 갱신] 미리 셈 = 그 자원 단가 따라감(구조 · 품 그대로) — 값 줄 있는 SK(바탕 = 보통인부 든 로직)
other = _sk(me, 바탕={"로직": STONE})
with store.at(me):
read = _norm(path.read(other))
g = _into(read["문서"], _take(me, _rid(sc)))
with store.at(me):
again = _norm(path.save(other, {"문서": g["문서"], "판": read["판"]}))
row = dict(_live().get(LB))
row["값"] = row["값"] + 1000
_fake(monkeypatch, [row])
with store.at(me):
got = _norm(path.preview(other, {"할일": "단가갱신"}))
assert any(x["키"] == LB for x in got["바뀐자원"])
assert (
got["후"]["계"] > again["셈"]["계"] + 1000 * 0.3
) # 보통인부 줄들(복사한 0.3 인 포함) 따라감
assert got["문서"]["금액"] == again["문서"]["금액"]
def test_끌어옮김_번지이름(world):
"""804-5 — 줄 끌기(항목 안 · 사이) · 항목 끌기 = 되돌려 끌면 같은 문서 · 율 줄 번지 = 읽는 이름."""
gc = dc.folder()[DIG] # 항목 둘(인력 · 굴착기)
one = _ts("옮김", [{"문서": gc, "옮김": []}])[0]
first = one["항목"][0]["줄"][0]
gone = _ts("옮김", [{"문서": gc, "옮김": [["0.0", "1.0"]]}])[0]
assert (
gone["항목"][1]["줄"][0] == first
and len(gone["항목"][0]["줄"]) == len(one["항목"][0]["줄"]) - 1
)
back = _ts("옮김", [{"문서": gc, "옮김": [["0.0", "1.0"], ["1.0", "0.0"]]}])[0]
assert back["문서"] == one["문서"]
# 같은 항목 안 아래로(빼기 전 차례) — 첫 줄을 끝 자리 = 둘째로
inner = _ts("옮김", [{"문서": gc, "옮김": [["0.0", "0.2"]]}])[0]
assert inner["항목"][0]["줄"] == one["항목"][0]["줄"][::-1]
n = len(one["항목"])
rot = _ts("옮김", [{"문서": gc, "옮김": [["0", str(n)]]}])[0]
assert rot["항목"][-1] == one["항목"][0] and rot["항목"][0] == one["항목"][1]
assert (
_money(rot["문서"])["계"] == _money(gc)["계"]
) # 항목만 옮김 = 금액 같음(율 줄은 그 항목 안)
# 율 줄 번지 — 일위대가 복사 = 번지마다 읽는 이름(「복사N」 아님)
me, sk, sc = _combo(world)
g = _into(dc.md.blank("SK-000009", "빈"), _take(me, _rid(sc)))
names = _ts("번지", [{"문서": g["문서"]}])[0]
places = {m["번지"] for m in g["문서"]["금액"] if m.get("번지")}
assert places and set(names) == places
assert all(v and not v.startswith("복사") for v in names.values()), names
def test_표값_화면이름_전수():
"""요구 683 — 표 값 기호(「Q_수량2^표」) = 화면 이름(「Q_작업반장」) · 겹침 없음 · 식 글 왕복 = 같은 식(1,518 전수)."""
docs = dc.folder()
keys = sorted(docs)
got = _ts("별칭", [{"문서": docs[k]} for k in keys])
named = 0
for k, g in zip(keys, got):
assert not g["틀림"], (k, g["틀림"][:3])
alias = g["별칭"]
assert len(set(alias.values())) == len(alias), k # 겹치면 2 · 3
assert all("^표" not in v and k.endswith("^표") for k, v in alias.items()), k
named += len(alias)
assert named > 500
gc = dict(zip(keys, got))[FORM]["별칭"]
assert gc and "Q_보통인부22" not in gc.values() # 번호 꼬리 뗀 뒤 2 · 3
# 문서 기호는 그대로(화면만) — 원본 문서 변수에 별칭 이름 없음
names = {
v["기호"]
+ (f"_{v['아래']}" if v.get("아래") else "")
+ (f"^{v['위']}" if v.get("위") else "")
for v in docs[FORM]["변수"]
}
assert not set(gc.values()) & names