- 공용 줄 쌓기 화면: 페이지가 저장을 맡는 꼴(`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>
263 lines
12 KiB
Python
263 lines
12 KiB
Python
# 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
|