Merge remote-tracking branch 'origin/dev' into sub_laptop_8

- 충돌 시험 셋(717-4 · 719-2 · hopyo build) = 740-1 펼침 단언 + 742-1 저장 수량 단언 합침
- 740-2 일위대가표 풍선 줄의 body(742-1 이 걷은 휘발 셈 몸) → cur

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 21:47:51 +09:00
co-authored by Claude Opus 5.5
66 changed files with 2663 additions and 244 deletions
+2 -2
View File
@@ -106,8 +106,8 @@ def test_같은층_위층_찾기_참조_지문(client):
{"층": "system", "주인": None, "키": "SK-000006"}
]
assert same.find_same(me, "sangeun", {**SK, "바탕": None, "더한줄": []}) == []
# 참조 = 가리킨 문서 지문 — 층 넘어 같은 내용 사본을 가리키면 같음
# 참조 = 펼친 꼴(740-1) 지문 — 층 넘어 같은 내용 사본을 가리키면 같음 · 다른 내용이면 다름
a = {**SK, "바탕": {"참조": {"층": "company", "주인": "7", "키": "SK-000005"}}, "덮개": {}}
b = {**a, "바탕": {"참조": {"층": "system", "주인": None, "키": "SK-000006"}}}
c = {**a, "바탕": {"참조": {"층": "personal", "주인": "7/42", "키": "SK-000003"}}}
c = {**a, "바탕": {"참조": {"층": "personal", "주인": "7/42", "키": "SK-000004"}}}
assert same.finger("sangeun", a) == same.finger("sangeun", b) != same.finger("sangeun", c)
@@ -96,6 +96,7 @@ CALC = {"줄": [{"로직": "GF1", "분모": "㎡", "배": 10, "수량": 2.09,
def test_M02_문서_calc_를_격자_줄로():
files = [GRID, "M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts",
"M02_MasterTemplete/M02_MasterTemplete_Labor_Spec.ts",
"M02_MasterTemplete/logic/M02_Born_Tag.ts",
"M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts"] # fmt: skip
logics = [
["GF1", {"이름": "돌쌓기 찰쌓기", "입력": [], "결과단위": "㎡"}],
@@ -146,6 +147,7 @@ def test_세_곳_같은_부품():
def test_번호는_글에_안_냄_사람_글은_그대로():
files = [GRID, "M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts",
"M02_MasterTemplete/M02_MasterTemplete_Labor_Spec.ts",
"M02_MasterTemplete/logic/M02_Born_Tag.ts",
"M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts"] # fmt: skip
ref = {"로직": "", "별칭": None, "입력값": {}, "수량": {"손": 1},
"참조": {"층": "personal", "주인": "1/3", "키": "SC-000002"}} # fmt: skip
+1
View File
@@ -176,6 +176,7 @@ def test_격자_규격_열_수량_칸_열마다():
files = [g.GRID, "M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts",
"M02_MasterTemplete/M02_MasterTemplete_Labor_Spec.ts",
"M02_MasterTemplete/logic/M02_Born_Tag.ts",
"M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts"] # fmt: skip
doc = {**g.DOC, "규격": {"이름": "H", "열": ["1.5", "2.0"], "기본": "2.0",
"수량": {"GF1": {"1.5": 1.5, "2.0": 2.09}, "MT1": {"1.5": 4}}}} # fmt: skip
+2 -3
View File
@@ -94,10 +94,9 @@ def test_실무_xlsx_넷_붙여넣기_만듦_합계_같음(client, tag):
assert got["같음"] == FILES[tag][1] and all(abs(x["차이"]) <= 1 for x in got["다름"])
assert len(got["만듦"]) == got["호표"] and got["고정"] + got["호표참조"] == got["줄"]
assert master_hash() == before
# 718-1 — 붙여넣기 실무자료 = 지문 없음 → 불러오기 목록엔 숨김 · 파일은 남음
# 743-1 — 붙여넣기 실무자료 = 만든 뒤 지움(엑셀 보관 안 함)
assert client.get("/api/m02/layers/personal/hopyo").json()["줄"] == []
doc = client.get(f"/api/m02/layers/personal/hopyo/{got['키']}").json()
assert doc["파일"].startswith("붙여넣기") and doc["시트"] == "붙여넣기"
assert client.get(f"/api/m02/layers/personal/hopyo/{got['키']}").status_code == 404
def _block(rows, no):
+2 -2
View File
@@ -181,8 +181,8 @@ def test_내_자료집에_저장_하위_호표_같이_한_번(world, monkeypatch
saved = {p.stem: json.loads(p.read_text("utf-8")) for p in (folder / "precombo").glob("*.json")}
for key in keys:
for one in saved[key]["담은로직"]:
if "참조" in one:
assert one["참조"]["층"] == "personal" and one["참조"]["키"] in saved
if "참조" in one: # 740-1 하위 호표 = 사본 블록(본뜬 = 같이 만든 개인 SC)
assert one["참조"]["층"] == "사본" and one["본뜬"]["키"] in saved
copied = [d["본뜬"]["복사원본"] for d in saved.values() if "복사원본" in (d.get("본뜬") or {})]
assert len(copied) == len({json.dumps(x, sort_keys=True) for x in copied}) # 같은 하위 한 번
for p in (folder / "precombo").glob("*.json"): # 다음 시험에 층 쓰기 안 남김
+16 -20
View File
@@ -2,7 +2,7 @@
"""M02 단가산출(SK) 서고 · 셈(PLAN 715-1b · 715-2) — 임시 storage.
고친 줄 값 = 손셈 · 덧줄 % 엔진이 다시 셈 · 뺀줄 뒤 고름 제자리 · 짝 잃음 남김 · 원값 서버 ·
순환 422 · 쓰는 곳 지우기 409 · 사전조합이 SK 를 담음 · 층 복사 왕복 같은 값.
바탕 · 사전조합에 SK 넣기 = 펼쳐 담음(740-1 · 원본 지워도 그대로) · 층 복사 왕복 같은 값.
"""
from decimal import Decimal
@@ -106,32 +106,28 @@ def test_lost_cover_kept_and_listed(world):
assert got["기준"]["계"] == pytest.approx(float(_bare(PCT)["기준"]["계"]))
def test_chain_loop_422_and_delete_409(world):
"""바탕 사슬 A ← B · B 를 A 의 바탕으로 = 422 · 쓰는 곳 있는 A 지우기 = 409(이름)."""
def test_chain_base_flattened_delete_free(world):
"""바탕 SK 넣기(740-1) = 펼쳐 담음 — 바탕 = M01 기초 · 위 덮개 겹침 · 값 같음 ·
원본 지워도 그대로."""
a = _sk(world["personal"], 바탕={"로직": PCT})
b = _sk(
world["personal"], 바탕={"참조": {**MINE, "키": a}}, 덮개={"labor:LB000002#0": {"qty": 0}}
)
assert _doc(world["personal"], b)["바탕"] == {"로직": PCT}
got = _calc(world["personal"], b)
bare = _bare(PCT)
assert got["기준"]["노무비"] == pytest.approx(float(bare["줄"][0]["금액"]))
with store.at(world["personal"]):
doc = store.read("sangeun", a)
with pytest.raises(store.StoreError) as e:
store.write(
"sangeun", a, doc["판"], {**doc["문서"], "바탕": {"참조": {**MINE, "키": b}}}
)
assert e.value.status == 422
with pytest.raises(store.StoreError) as e:
store.delete("sangeun", a)
assert e.value.status == 409 and "단가산출 개인 7/42" in str(e.value.detail)
store.delete("sangeun", a) # 쓰는 곳 없음(서로 독립)
assert _calc(world["personal"], b)["기준"] == got["기준"]
def test_precombo_holds_sangeun(world):
"""사전조합 참조 줄이 SK 를 담음 — 1 단위 값 × 수량 · SK 지우기 막음."""
"""사전조합에 SK 넣기(740-1) = 로직 줄 + 패치(SK 몸) · 1 단위 값 × 수량 · SK 지워도 그대로."""
from M02_MasterTemplete import M02_Labor_Doc as labor
sk = _sk(world["personal"], 바탕={"로직": PCT}, 덮개={"labor:LB000036#0": {"qty": 0.5}})
one = _calc(world["personal"], sk)["1단위"]["계"]
with store.at(world["personal"]):
sc = refs.create(
"precombo",
@@ -141,17 +137,17 @@ def test_precombo_holds_sangeun(world):
"담은로직": [{"참조": {**MINE, "키": sk}, "수량": {"손": 2}}],
},
)["이름"]
line = store.read("precombo", sc)["문서"]["담은로직"][0]
assert line["로직"] == PCT and line["패치"]["덮개"] and line["본뜬"]["키"] == sk
store.delete("sangeun", sk)
pill = {f"{refs.ref_id({**MINE, '키': sc})}#0": 2} # 723-1 안 줄 수량 = 쓰는 쪽 알약
got = labor.calc({"담은로직": [{"참조": {**MINE, "키": sc}, "수량": {"손": 1}}]}, pill)
with pytest.raises(store.StoreError) as e:
store.delete("sangeun", sk)
one = _calc(world["personal"], sk)["1단위"]["계"]
assert got["반영합"]["계"] == pytest.approx(one * 2, abs=1)
assert e.value.status == 409 and "일위대가 개인 7/42" in str(e.value.detail)
def test_layer_copy_round_trip(world):
"""개인 B(바탕 = 개인 A) → 회사로 복사 = A 도 같이(새 키 · 본뜬) → 개인으로 다시 = 같은 값."""
"""개인 B(바탕 = 개인 A) → 회사로 복사 = 펼친 몸(바탕 = 기초 · 본뜬) → 개인으로 다시 =
같은 값."""
a = _sk(world["personal"], 바탕={"로직": PCT}, 덮개={"labor:LB000036#0": {"qty": 0.5}})
b = _sk(world["personal"], 바탕={"참조": {**MINE, "키": a}}, 뺀줄=["labor:LB000002#0"])
want = _calc(world["personal"], b)["기준"]
@@ -160,15 +156,15 @@ def test_layer_copy_round_trip(world):
made = refs.create(
"sangeun", refs.carry(refs.origin_mark(src, {**MINE, "키": b}), ("personal", "7/42"))
)
assert not list((world["company"] / "sangeun").glob("*.json"))[1:] # A 따로 복사 안 함
copy = _doc(world["company"], made["이름"])
assert copy["바탕"]["참조"]["층"] == "company" and copy["본뜬"]["키"] == b
assert copy["바탕"] == {"로직": PCT} and copy["본뜬"]["키"] == b
assert _calc(world["company"], made["이름"])["기준"] == want
with store.at(world["personal"]):
back = refs.create(
"sangeun",
refs.carry(refs.origin_mark(copy, {**COMPANY, "키": made["이름"]}), ("company", "7")),
)
assert _doc(world["personal"], back["이름"])["바탕"]["참조"]["층"] == "company"
assert _calc(world["personal"], back["이름"])["기준"] == want
+3 -3
View File
@@ -32,15 +32,15 @@ def test_sangeun_row_base_edits_users(world):
assert row["바탕"] == f"{PCT} {name}".strip()
assert row["고친줄수"] == 2 and row["쓰는곳수"] == 0
assert row["제목"] == "목록 시험" and row["단위"] == "㎡"
# 바탕 참조 SK · 더한줄 하나 → 그 SK 쓰는 곳 1
# 바탕 참조 SK(740-1) = 펼쳐 담음 → 바탕 = 기초 · 고친 줄 = 겹친 덮개 · 뺀줄 + 더한줄 · 쓰는 곳 0
b = _sk(
mine,
바탕={"참조": {**MINE, "키": a}},
더한줄=[{"종류": "인력", "요소": "LB000036", "이름": "보통인부", "단위": "인", "수량": 1}],
)
row_b = _row(mine, "sangeun", b)
assert row_b["바탕"] == a and row_b["고친줄수"] == 1
assert _row(mine, "sangeun", a)["쓰는곳수"] == 1
assert row_b["바탕"] == f"{PCT} {name}".strip() and row_b["고친줄수"] == 3
assert _row(mine, "sangeun", a)["쓰는곳수"] == 0
# 바탕 없음
c = _sk(mine)
row_c = _row(mine, "sangeun", c)
@@ -9,6 +9,7 @@
"""
import json
import re
import shutil
import subprocess
from decimal import Decimal
@@ -32,7 +33,9 @@ def _text(p: Path) -> str:
def _node(body: str) -> object:
src = _text(MODEL) + "\n" + body
born = _text(M02 / "logic" / "M02_Born_Tag.ts") # 740-2 — 모델이 쓰는 꼬리표 글(import 줄은 뺌)
model = re.sub(r"^import .*$", "", _text(MODEL), flags=re.M)
src = born + "\n" + model + "\n" + body
out = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-e", src],
+4 -2
View File
@@ -72,12 +72,14 @@ def test_list_amount_and_labor_line_holds_upper_sk(world): # noqa: F811
},
)["이름"]
line = store.read("precombo", sc)["문서"]["담은로직"][0]
saved, inner = line["참조"], [line["수량"]]
saved, inner = line["본뜬"], [line["수량"]]
outer = {"담은로직": [{"참조": {**MINE, "키": sc}, "수량": {"손": 1}}]}
got = labor.calc(outer, {})
fed = labor.calc(outer, {f"{refs.ref_id({**MINE, '키': sc})}#0": 3})
direct = labor.calc({"담은로직": [{"참조": {**COMPANY, "키": sk}, "수량": {"손": 3}}]}, {})
assert saved == {**COMPANY, "키": sk} # 참조 = 회사 SK 그대로
# 740-1 — 회사 SK = 로직 줄 + 패치로 펼쳐 담음 · 출처 = 본뜬
assert {k: saved[k] for k in ("층", "주인", "키")} == {**COMPANY, "키": sk}
assert line["로직"] and "참조" not in line
# 742-1 — 일위대가 안 줄 수량 = 저장(사용자 입력) · 쓰는 자리 알약이 이어지면 그 값
assert inner == [{"손": 3}]
assert got["반영합"]["계"] == pytest.approx(one * 3, abs=1) # 저장 수량 3
+7 -7
View File
@@ -1,7 +1,7 @@
"""일위대가 불러오기 한시(PLAN 718-1) — 올림 지문 · [끝내기] 지움 · 겹침 막음.
"""일위대가 불러오기 한시(PLAN 718-1 · 743-1) — 올림 지문 · [끝내기] 지움.
같은 엑셀 작업 중 다시 올림 = 그 파일 이어서(새 키 없음) · [끝내기] = HP · 격자 · 맞춤셈 캐시 0 ·
만든 일위대가 · 별칭 장부 그대로 · 끝낸 같은 엑셀 다시 올림 409 · 만든 것 없이 지움 = 다시 올림 됨 ·
만든 일위대가 · 별칭 장부 그대로 · 끝낸 같은 엑셀 다시 올림 = 새 키(743-1 · 409 걷음) ·
옛 HP(지문 없음) = 파일 그대로 목록에서만 숨김 · 키 번 = 지운 뒤에도 다시 안 씀. 층 = 임시 폴더.
"""
@@ -44,9 +44,9 @@ def test_이어서_끝내기_지움_겹침_막음(client):
assert hs.memory(folder) == {"장부 줄": {"갈래": "안 씀"}}
assert sorted((folder / "precombo").glob("SC-*.json")) == sc_before # 만든 일위대가 그대로
block = _post(client) # 끝낸 같은 엑셀 = 막음 · 안내
assert block.status_code == 409 and "이미 불러온 엑셀" in block.json()["detail"]
assert f"단가산출 {n}" in block.json()["detail"] and _keys(client) == []
again = _post(client) # 743-1 — 끝낸 같은 엑셀 = 다시 올림 됨(새 키)
assert again.status_code == 200 and again.json()["키"] == "HP-000002"
assert _keys(client) == ["HP-000002"]
assert sorted((folder / "precombo").glob("SC-*.json")) == sc_before
@@ -88,6 +88,6 @@ def test_결과_단가산출_산근_블록_기초_SK_고름대기_겹침_없음(
again = client.post(url).json() # 다시 = 그대로(겹침 없음)
assert again["만듦"] == [] and again["그대로"] == len(got["만듦"])
res = client.delete(URL.format("personal") + "/HP-000001").json()
assert res["만든"] == len(got["만듦"]) # 단가산출만 만들어도 지문 기록(블록마다)
assert res["만든"] == len(docs) # 같은 내용 SK 한 벌 — 사본 걷은 문서 수
assert len(list((folder / "sangeun").glob("SK-*.json"))) == len(docs) # 남음
assert _post(client).status_code == 409
assert _post(client).status_code == 200 # 743-1 — 다시 올림 됨
+2 -2
View File
@@ -119,10 +119,10 @@ def test_B09_서고_사본_값_같음_원본판(world, tmp_path):
assert library.source_version(built) == mark # 다시 모아도 같은 판
again = library.import_copy(root, "precombo", "personal", "7/42", sc)
assert again == {"폴더": made["폴더"], "만듦": False}
with store.at(world["personal"]): # SK 고침 → 원본판 다름
with store.at(world["personal"]): # 740-1 — SK 고쳐도 SC 는 펼친 사본이라 원본판 그대로
now = store.read("sangeun", ref["ref"]["키"])
store.write("sangeun", ref["ref"]["키"], now["판"], {**now["문서"], "덮개": {}})
assert library.source_version(library._build("precombo", "personal", "7/42", sc)[0]) != mark
assert library.source_version(library._build("precombo", "personal", "7/42", sc)[0]) == mark
def test_SK_없는_사본_그대로():
+6 -29
View File
@@ -122,23 +122,18 @@ def test_SK_단가_덮개_속_일위대가_펼침(world, tmp_path):
if not _engine_ready(world):
pytest.skip("715-1b 엔진 입력 덧칸(단가 · 값묶음) 뒤 — 지금은 M02 SK 셈도 못 함")
key = _mine(world, full=True)
# 742-1 속 일위대가 안 줄 저장 수량(손 1) → M02 SK · B09 모두 빈 줄 없음
assert _calc(world["personal"], key)["빠진줄"] == []
# 740-1 — 속 일위대가 = 넣을 때 펼쳐 담음(SC 로직 줄 = 로직 더한줄 · 본뜬) → 빠진 줄 없음
live = _calc(world["personal"], key)
added = _added(world, key)
assert live["빠진줄"] == [] and not any("참조" in x for x in added)
assert added[-1]["로직"] == PCT and added[-1]["본뜬"]["키"].startswith("SC-")
root, folder = _lib(world, tmp_path, key)
daega, done, _ = _b09(root)
block = done[daega["libs"][folder]]
lines = block["lines"]
assert lines[1]["kind"] == "fixed" and lines[1]["prices"]["labor"] == pytest.approx(200000)
sc_line = next(ln for ln in lines if ln["kind"] == "daega")
sc = done[sc_line["key"]]
assert sc["kind"] == "사전조합" and sc_line["qty"] == 2
assert sc["blank"] == 0 and sc["lines"][0]["qty"] == 1 and block["blank"] == 0
# 이 공사에서 입력(702 덮개 qty 1) = 저장 1 과 같은 값 → M02 셈(알약 값 1) 과 같은 금액
cover = {sc["cover"]: {sc["lines"][0]["cid"]: {"qty": 1}}}
block = _b09(root, cover)[1][daega["libs"][folder]]
assert block["blank"] == 0
sc_ref = next(x for x in _added(world, key) if "참조" in x)["참조"]
_same_money(block, _calc_given(world["personal"], key, {f"{refs.ref_id(sc_ref)}#0": 1}))
_same_money(block, live)
def _added(world, key):
@@ -146,24 +141,6 @@ def _added(world, key):
return store.read("sangeun", key)["문서"]["더한줄"]
def _calc_given(folder, key, pills):
"""M02 SK 셈 — 속 일위대가 안 줄 수량 = 알약 값(723-1 `Expand` given)."""
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Sangeun_Calc as calc
from M02_MasterTemplete import M02_Sangeun_Doc as sd
from M02_MasterTemplete.M02_Labor_Expand import Expand
whole = m01.loaded()[1]
with store.at(folder):
clean = sd.clean(store.read("sangeun", key)["문서"], None, whole)
me = refs.ref_id({"층": refs.here()[0], "주인": refs.here()[1], "키": clean["키"]})
got = calc.run(
clean, whole, None, refs.read_ref, Expand(whole, refs.read_ref, pills), (me,)
)
assert not got.get("까닭")
return got
def test_구조물_사본_SK_줄_업데이트_덮개_입력값_고른자재_굳힌키_그대로(
world, tmp_path, monkeypatch
):
@@ -76,8 +76,8 @@ def test_line_without_input_only_that_line_drops(world, monkeypatch): # noqa: F
assert up["기준"] is None and "입력 「지역」 없음" in up["까닭"]
def test_precombo_without_qty_only_that_line_drops(world): # noqa: F811
"""수량 없는 일위대가 줄(723-1) = 그 줄만 금액 — · 까닭 · 나머지 계 같음."""
def test_precombo_in_sk_flattened_no_gap(world): # noqa: F811
"""740-1 — SK 에 넣은 일위대가 = 줄마다 더한줄로 펼쳐 담음(수량 = 넣은 수 × 1) · 빠진 줄 없음."""
with store.at(world["personal"]):
sc = refs.create(
"precombo", {"이름": "sub7임시SC", "단위": "㎥", "담은로직": [{"로직": PCT}]}
@@ -86,10 +86,9 @@ def test_precombo_without_qty_only_that_line_drops(world): # noqa: F811
doc = _sk(world["personal"], 더한줄=[line, PCT_LINE])
rest = _sk(world["personal"], 더한줄=[PCT_LINE])
got, want = _run(world["personal"], doc), _run(world["personal"], rest)
gap = _line(got, "add:#0")
assert gap["금액"] is None and gap["까닭"] == calc.NO_QTY_WHY and gap["이름"] == "sub7임시SC"
assert [x["cid"] for x in got["빠진줄"]] == ["add:#0"]
assert got["기준"]["계"] == pytest.approx(float(want["기준"]["계"]), rel=1e-9)
assert doc["더한줄"][0]["로직"] == PCT and doc["더한줄"][0]["본뜬"]["키"] == sc["이름"]
assert got["빠진줄"] == []
assert got["기준"]["계"] == pytest.approx(3 * float(want["기준"]["계"]), rel=1e-9)
def test_screen_shows_dropped_lines():
+7 -1
View File
@@ -206,7 +206,13 @@ def _model(ans, doc, script):
f"\nconst ans = {json.dumps(ans, ensure_ascii=False)};"
f"\nconst doc = {json.dumps(doc, ensure_ascii=False)};\n{script}"
)
return _node(["M02_MasterTemplete/logic/M02_Sangeun_Cover_Model.ts"], body)
return _node(
[
"M02_MasterTemplete/logic/M02_Born_Tag.ts",
"M02_MasterTemplete/logic/M02_Sangeun_Cover_Model.ts",
],
body,
)
def test_727_2_화면_꼴_세부_줄_고침(world): # noqa: F811
+4 -3
View File
@@ -126,8 +126,8 @@ def test_실무_단가산출_기초_SK_고름대기_일위대가_막음(client):
ok = client.post(BUILD, json={**body, "줄": picks})
assert ok.status_code == 200, ok.text
doc = json.loads((folder / "precombo" / f"{ok.json()['참조']['키']}.json").read_bytes())
refs_sk = [x for x in doc["담은로직"] if str((x.get("참조") or {}).get("키")).startswith("SK")]
assert refs_sk # 단가산출 줄 = SK 참조(고정 줄 대신)
refs_sk = [x for x in doc["담은로직"] if str((x.get("본뜬") or {}).get("키")).startswith("SK")]
assert refs_sk # 단가산출 줄 = SK 펼쳐 담음(740-1 · 로직 줄 + 패치 · 본뜬 = 그 SK)
assert client.post(LEFT, json={**body, "줄": picks}).json()["수"] == 0
assert master_hash() == before
@@ -223,7 +223,8 @@ def test_별칭_체크_줄_못옮김_SK_남은줄_그대로_SK_남은못옮김(c
ok = client.post(BUILD, json={**body, "줄": [check, *picks]})
assert ok.status_code == 200, ok.text
doc = json.loads((folder / "precombo" / f"{ok.json()['참조']['키']}.json").read_bytes())
assert any((x.get("참조") or {}).get("키") == sk["키"] for x in doc["담은로직"]) # 차이 SK 참조
# 차이 SK = 펼쳐 담음(740-1 · 본뜬 = 그 SK)
assert any((x.get("본뜬") or {}).get("키") == sk["키"] for x in doc["담은로직"])
assert master_hash() == before
+10 -10
View File
@@ -93,23 +93,23 @@ def test_공유_승인_회사_같은_내용_409_씀_새로(world):
def test_복사_딸림_같은_원본_재사용(world):
"""구조물 조합 복사 딸림(733-2) = 같은 원본 사본 재사용 · 740-1 — 일위대가 안 일위대가는
넣을 때 사본 블록(딸림 없음)."""
client = _client(world)
_as(world)
mine = layers.personal_dir(7, 42)
url = LIB.format("personal", "precombo")
inner = client.post(url, json=_doc("안")).json()["이름"]
ref = {"층": "personal", "주인": "7/42", "키": inner}
outer = {"문서": {"이름": "밖", "단위": "㎡", "담은로직": [{"참조": ref}]}}
top = client.post(url, json=outer).json()["이름"]
with store.at(mine):
doc = store.read("precombo", top)["문서"]
src = {"층": "personal", "주인": "7/42", "키": top}
with store.at(layers.personal_dir(7, 42)):
assert store.read("precombo", top)["문서"]["담은로직"][0]["참조"]["층"] == "사본"
company = layers.company_dir(7)
combo = {"종류": "일위대가", "열": "rv", "담은로직": [{"참조": ref}], "자재": []}
with store.at(company):
one = refs.create("precombo", refs.carry(refs.origin_mark(doc, src), ("personal", "7/42")))
assert _count(company) == 2 # 밖 + 딸린 안
two = refs.create("precombo", refs.carry(refs.origin_mark(doc, src), ("personal", "7/42")))
assert _count(company) == 3 # 다시 복사 = 밖만 새로 · 딸린 안 = 앞 사본 재사용
with store.at(company):
got = [store.read("precombo", m["이름"])["문서"]["담은로직"][0]["참조"] for m in (one, two)]
one = refs.carry(combo, ("personal", "7/42"))
assert _count(company) == 1 # 딸린 안 = 회사 새 키
two = refs.carry(combo, ("personal", "7/42"))
assert _count(company) == 1 # 다시 복사 = 앞 사본 재사용
got = [x["담은로직"][0]["참조"] for x in (one, two)]
assert got[0] == got[1] and got[0]["층"] == "company"
@@ -3,6 +3,7 @@
묶음 = 이 층 + 위 층 같은 지문 · 합치기 = 쓰는 곳 참조 → 남김(금액 같음) · 지운 문서 `_합친것/` ·
되돌림 = 문서 · 참조 되살림 · 지문 · 판 다르면 409 · 권한 밖 · 같은 참조 두 번 = 그 문서 남김 ·
쓰는 곳 = 구조물 조합(740-1 — 단가산출 · 일위대가끼리는 펼쳐 담겨 참조 없음) ·
위 층과 같은 아래 층 문서 = 남김 = 위 층 키 · 일반 사용자 회사 층 = 403.
"""
@@ -55,6 +56,27 @@ def _top(ref_keys, at=MINE):
]
def _user(folder, ref_keys, at=MINE, col="x1"):
"""쓰는 곳 = 구조물 조합(740-1 — 단가산출 · 일위대가끼리는 펼쳐 담겨 참조 없음)."""
lines = [{**x, "수량": {"손": x["수량"]}} for x in _top(ref_keys, at)]
with store.at(folder):
store.write("labor", col, "", {"열": col, "담은로직": lines, "자재": []})
return col
def _lines(folder, col):
with store.at(folder):
return store.read("labor", col)["문서"]["담은로직"]
def _sum(folder, col):
from M02_MasterTemplete import M02_Labor_Doc as labor
with store.at(folder):
doc = labor.clean({**store.read("labor", col)["문서"], "열": col})
return float(labor.calc(doc, {})["반영합"]["계"])
def _merge(api, keep, gone, 판, layer="personal"):
body = {"남김": keep, "지움": gone, "판": 판}
return api.post(URL.format(layer) + "/merge", json=body)
@@ -64,14 +86,14 @@ def test_권한_밖이면_안_지움(api, world, monkeypatch):
me = world["personal"]
a = _make(me, 이름="가", 바탕={"로직": PCT})
c = _make(me, 이름="다", 바탕={"로직": PCT})
top = _make(me, 이름="위", 단위="식", 바탕=None, 더한줄=_top([c]))
top = _user(me, [c])
monkeypatch.setattr(same_router, "_can_write", lambda session, at: False)
판 = api.get(URL.format("personal")).json()["판"]
res = _merge(api, {**MINE, "키": a}, [c], 판)
left = res.json()["남김"]
assert res.status_code == 200 and left[0]["키"] == c and "권한 밖" in left[0]["쓰는곳"][0]
assert res.json()["합침"] == [] and (me / "sangeun" / f"{c}.json").is_file()
assert _read(me, top)["더한줄"][0]["참조"]["키"] == c
assert _lines(me, top)[0]["참조"]["키"] == c
def test_합치기_참조_옮김_금액_같음_되돌림(api, world):
@@ -79,8 +101,8 @@ def test_합치기_참조_옮김_금액_같음_되돌림(api, world):
a = _make(me, 이름="밑", 바탕={"로직": PCT})
b = _make(me, 이름="다른 이름 같은 내용", 바탕={"로직": PCT})
_make(me, 이름="다른 내용", 바탕={"로직": PCT}, 더한줄=_top([])[-1:])
top = _make(me, 이름="위", 단위="식", 바탕=None, 더한줄=_top([b]))
before = _total(me, top)
top = _user(me, [b])
before = _sum(me, top)
got = api.get(URL.format("personal"))
assert got.status_code == 200, got.text
view = got.json()
@@ -96,16 +118,16 @@ def test_합치기_참조_옮김_금액_같음_되돌림(api, world):
assert out["묶음"] == [] and [g["키"] for g in out["합친것"]] == [b]
assert not (me / "sangeun" / f"{b}.json").exists()
assert (me / same_router.GONE / f"{b}.json").is_file()
assert _read(me, top)["더한줄"][0]["참조"]["키"] == a
assert _total(me, top) == pytest.approx(before)
assert _lines(me, top)[0]["참조"]["키"] == a
assert _sum(me, top) == pytest.approx(before)
back = api.post(URL.format("personal") + "/undo", json={"키": b})
assert back.status_code == 200, back.text
assert back.json()["되돌린곳수"] == 1 and back.json()["합친것"] == []
assert (me / "sangeun" / f"{b}.json").is_file()
assert not (me / same_router.GONE / f"{b}.json").exists()
assert _read(me, top)["더한줄"][0]["참조"]["키"] == b
assert _total(me, top) == pytest.approx(before)
assert _lines(me, top)[0]["참조"]["키"] == b
assert _sum(me, top) == pytest.approx(before)
assert api.post(URL.format("personal") + "/undo", json={"키": b}).status_code == 404
@@ -130,24 +152,25 @@ def test_권한_밖_남김_둘다_담은_문서(api, world, monkeypatch):
a = _make(me, 이름="가", 바탕={"로직": PCT})
b = _make(me, 이름="나", 바탕={"로직": PCT})
c = _make(me, 이름="다", 바탕={"로직": PCT})
both = _make(me, 이름="둘 다", 단위="식", 바탕=None, 더한줄=_top([a, b]))
before = _total(me, both)
both = _user(me, [a, b])
before = _sum(me, both)
판 = api.get(URL.format("personal")).json()["판"]
res = _merge(api, {**MINE, "키": a}, [b, c], 판) # 둘 다 담은 문서 = 두 줄 모두 a · 금액 같음
# 둘 다 담은 구조물 조합 = 같은 참조 두 번이 되어 못 옮김 → b 남김 · c 만 합침 · 금액 같음
res = _merge(api, {**MINE, "키": a}, [b, c], 판)
assert res.status_code == 200, res.text
assert [r["키"] for r in res.json()["합침"]] == [b, c]
assert [x["참조"]["키"] for x in _read(me, both)["더한줄"][:2]] == [a, a]
assert _total(me, both) == pytest.approx(before)
for key in (b, c):
assert api.post(URL.format("personal") + "/undo", json={"키": key}).status_code == 200
assert [r["키"] for r in res.json()["합침"]] == [c]
assert [r["키"] for r in res.json()["남김"]] == [b]
assert [x["참조"]["키"] for x in _lines(me, both)[:2]] == [a, b]
assert _sum(me, both) == pytest.approx(before)
assert api.post(URL.format("personal") + "/undo", json={"키": c}).status_code == 200
def test_위층과_같으면_남김_위층_키(api, world):
me, firm = world["personal"], world["company"]
up = _make(firm, 이름="회사 것", 바탕={"로직": PCT})
mine = _make(me, 이름="내 것", 바탕={"로직": PCT})
top = _make(me, 이름="위", 단위="식", 바탕=None, 더한줄=_top([mine]))
before = _total(me, top)
top = _user(me, [mine])
before = _sum(me, top)
view = api.get(URL.format("personal")).json()
rows = view["묶음"][0]["문서"]
assert [(r["층"], r["키"], r["위층"]) for r in rows] == [
@@ -156,8 +179,8 @@ def test_위층과_같으면_남김_위층_키(api, world):
]
res = _merge(api, {**COMPANY, "키": up}, [mine], view["판"])
assert res.status_code == 200, res.text
assert _read(me, top)["더한줄"][0]["참조"] == {**COMPANY, "키": up}
assert _total(me, top) == pytest.approx(before)
assert _lines(me, top)[0]["참조"] == {**COMPANY, "키": up}
assert _sum(me, top) == pytest.approx(before)
assert (firm / "sangeun" / f"{up}.json").is_file()
# 위 층 문서는 지울 수 없음 · 일반 사용자는 회사 층 합치기 403
assert _merge(api, {**MINE, "키": mine}, [up], view["판"]).status_code in (403, 404)
@@ -136,7 +136,9 @@ def test_폴더_통째_한_기록_되돌림_자리까지_그새_고친_문서는
a = _sc(me, "가", [FIX])
b = _sc(me, "나", [FIX])
_labor(me, [{"참조": {**MINE, "키": a}, "수량": {"손": 1}}, FIX])
other = _sc(me, "다른", [{"참조": {**MINE, "키": b}}, FIX])
with store.at(me): # 쓰는 곳 = 구조물 조합(740-1 — 일위대가 안 일위대가는 사본 블록)
other = [{"참조": {**MINE, "키": b}, "수량": {"손": 1}}, FIX]
store.write("labor", "x2", "", {"열": "x2", "담은로직": other, "자재": []})
assert _tree(api, op="add-folder", 경로=[], 이름="묶음").status_code == 200
for key in (a, b):
assert _tree(api, op="move", 키=key, 새경로=["묶음"]).status_code == 200
@@ -147,9 +149,10 @@ def test_폴더_통째_한_기록_되돌림_자리까지_그새_고친_문서는
assert res.status_code == 200, res.text
name = res.json()["알림"]["기록"]
assert [g["키"] for g in api.get(SAME).json()["합친것"]] == [name]
with store.at(me): # 그새 「다른」 을 고침 → 그 문서는 앞판으로 안 돌림
have = store.read("precombo", other)
store.write("precombo", other, have["판"], {**have["문서"], "이름": "다른 고침"})
with store.at(me): # 그새 x2 를 고침 → 그 문서는 앞판으로 안 돌림
have = store.read("labor", "x2")
lines = [{**x, "메모": "고침"} for x in have["문서"]["담은로직"]]
store.write("labor", "x2", have["판"], {**have["문서"], "담은로직": lines})
back = api.post(SAME + "/undo", json={"키": name}).json()
assert back["되돌린곳수"] == 1 and "그새 고침" in back["못한곳"][0]
assert (me / "precombo" / f"{a}.json").is_file() and (me / "precombo" / f"{b}.json").is_file()
+374
View File
@@ -0,0 +1,374 @@
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""740-1 단가산출(SK) · 일위대가(SC) 서로 독립 · 연결 = M01 키 · 초기화(임시 storage · M01 읽기만).
- 넣기 = 펼침(SK 에 SK = 몸 통째 한 줄 · SC 에 SK = 로직 줄 + 패치 · SK 에 SC = 줄마다
더한줄 · SC 에 SC = 넣기 길만 안 줄 펴 넣음 · 저장 · 읽기는 하위 호표 참조 그대로) · 줄 `본뜬` ·
펼치기 전후 금액 같음 · 원본 고쳐도 · 지워도 그대로
- 분석 579-2 실측 E1(129,073 → 481,406) = 이제 안 일어남
- 바탕 SK 참조 · 패치 SK 참조 = 덮개 겹침 · 못 펼침(겹침 · 바탕 없음 · 환산) = 넣기 400 ·
저장은 옛 참조
- 초기화 = 덮개 · 뺀줄 · 더한줄 단가 걷음 → M01 값 · 옵션 · 항목 · 수량 남음 · 길(미리 보기 · 씀)
- 옮기기 함수 = 옛 참조 문서 펼쳐 씀(전후 같음 · 겹침 남김 · 두 번째 = 할 것 없음) · 733 지문 같음
- B09 = SC 덮개 줄 굳힘 → 대가 블록 금액 = M02(옛 산근 길은 덮개를 안 읽었음)
"""
import json
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from common_util.common_util_auth import verify_session
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Library_Flat as flat
from M02_MasterTemplete import M02_Library_Same as same
from M02_MasterTemplete import M02_Library_Same_Router as same_router
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Sangeun_Calc as calc
from M02_MasterTemplete import M02_Sangeun_Doc as sd
from resources.tester.test_m02_labor_ref import MINE, world # noqa: F401
GF = "GF000099" # 측구터파기 토사 · 원/㎥
PCT = "GC001109" # 원/㎡ · 지붕잇기공 · 보통인부 · 「노무비 * 3 / 100」
ADD = {
"종류": "인력",
"요소": "LB000002",
"비목": "노무비",
"이름": "보통인부",
"단위": "인",
"수량": 0.5,
}
USER = {"user_id": 42, "company_id": 7, "role": "USER", "is_master": False, "name": "갑"}
def _whole():
return m01.loaded()[1]
def _raw(folder, kind, **doc):
"""옛 꼴 문서(참조 줄 그대로) — 저장 길을 거치지 않고 파일로."""
(folder / kind).mkdir(parents=True, exist_ok=True)
key = f"{refs.PREFIX[kind]}-{len(list((folder / kind).glob('*.json'))) + 1:06d}"
head = {"종류": refs.TITLE[kind], "키": key, "단위": "㎡"}
body = {"입력값": {}, "덮개": {}, "뺀줄": [], "고름": {}, "자재표시": {}, "더한줄": []}
full = {**head, **(body if kind == "sangeun" else {"자재": []}), **doc}
(folder / kind / f"{key}.json").write_text(json.dumps(full, ensure_ascii=False), "utf-8")
return key
def _make(folder, kind, **doc):
with store.at(folder):
extra = {"더한줄": []} if kind == "sangeun" else {}
return refs.create(kind, {"단위": "㎡", **extra, **doc})["이름"]
def _read(folder, kind, key):
with store.at(folder):
return store.read(kind, key)["문서"]
def _sk_total(folder, doc):
with store.at(folder):
return float(calc.run(sd.clean(doc, None, _whole()), _whole())["1단위"]["계"])
def _sc_value(folder, key, read=None):
with store.at(folder):
got = flat._value({**MINE, "키": key}, read or refs.read_ref, _whole())
assert got is not None
return got["계"]
def _y(folder):
"""원본 SK — 바탕 PCT(% 덧줄) · 품 덮개 · 인력 더한줄(덧줄 밑에 듦)."""
return _make(
folder,
"sangeun",
이름="원본",
바탕={"로직": PCT},
덮개={"labor:LB000036#0": {"qty": 0.036}},
더한줄=[ADD],
)
def test_SK_에_SK_몸_통째_금액_같음_원본_고쳐도_지워도_그대로(world):
me = world["personal"]
y = _y(me)
legacy = {
"이름": "밖",
"단위": "㎥",
"바탕": {"로직": GF},
"더한줄": [{"참조": {**MINE, "키": y}, "수량": 2}],
}
before = _sk_total(me, {"키": "SK-000099", **legacy})
x = _make(me, "sangeun", **legacy)
doc = _read(me, "sangeun", x)
entry = doc["더한줄"][0]
assert not refs.refs_in(doc)
assert entry["로직"] == PCT and entry["덮개"] and entry["더한줄"][0]["요소"] == "LB000002"
assert entry["본뜬"]["키"] == y and entry["본뜬"]["이름"] == "원본" and entry["본뜬"]["때"]
assert _sk_total(me, doc) == pytest.approx(before, rel=1e-9)
with store.at(me): # 원본 고침 · 지움 → 넣은 쪽 그대로
now = store.read("sangeun", y)
store.write(
"sangeun", y, now["판"], {**now["문서"], "덮개": {"labor:LB000002#0": {"qty": 2}}}
)
assert refs.users({**MINE, "키": y}) == []
store.delete("sangeun", y)
assert _sk_total(me, _read(me, "sangeun", x)) == pytest.approx(before, rel=1e-9)
with store.at(me): # 화면 셈 길 = 넣은 줄 세부 펼침(안 더한줄 = add:#0/add:#0)
got = calc.calc_doc(_read(me, "sangeun", x))
sub = next(r for r in got["줄"] if r["cid"] == "add:#0")["펼침"]
assert "add:#0/add:#0" in [r["cid"] for r in sub]
def test_SC_에_SK_패치_겹침_E1_원본_고쳐도_그대로(world):
"""분석 579-2 E1 — 밖 SC(SK 참조 + 줄 패치 · 안 SC) 의 원본 SK 다른 줄을 고쳐도 밖 금액
그대로."""
me = world["personal"]
sk = _make(me, "sangeun", 이름="밑", 바탕={"로직": PCT}, 더한줄=[])
inner = _make(me, "precombo", 이름="안", 담은로직=[{"로직": "GC001111"}])
lines = [
{"참조": {**MINE, "키": sk}, "패치": {"덮개": {"labor:LB000036#0": {"qty": 0.5}}}},
{"참조": {**MINE, "키": inner}},
]
old = _raw(me, "precombo", 이름="옛 밖", 담은로직=lines)
before = _sc_value(me, old)
host = _make(me, "precombo", 이름="밖", 담은로직=lines)
doc = _read(me, "precombo", host)
assert doc["담은로직"][0]["로직"] == PCT and doc["담은로직"][0]["본뜬"]["키"] == sk
assert doc["담은로직"][0]["패치"]["덮개"] == {"labor:LB000036#0": {"qty": 0.5}}
assert doc["담은로직"][1]["참조"]["층"] == "사본" # 하위 호표(SC 안 SC) = 사본 블록
assert doc["담은로직"][1]["본뜬"]["키"] == inner
assert _sc_value(me, host) == pytest.approx(before, rel=1e-9)
with store.at(me): # E1 — 원본 SK 의 덮지 않은 줄 품 2
now = store.read("sangeun", sk)
store.write(
"sangeun", sk, now["판"], {**now["문서"], "덮개": {"labor:LB000002#0": {"qty": 2}}}
)
assert _sc_value(me, old) > before * 3 # 옛 참조 = 따라감(129,073 → 481,406 꼴)
assert _sc_value(me, host) == pytest.approx(before, rel=1e-9) # 펼친 것 = 그대로
def test_SC_에_SC_사본_블록_실무수량_겹침_원본_지워도(world):
"""하위 호표(SC 안 SC) = 사본 블록 — 원본 연결 없음 · 금액 같음 · 같은 기초 겹침도 블록 안."""
me = world["personal"]
hands = {"층": "실무자료", "실무수량": {"줄": {"0": 3}, "자재": {}}}
inner = _make(me, "precombo", 이름="안", 담은로직=[{"로직": PCT}], 본뜬=hands)
lines = [{"로직": PCT}, {"참조": {**MINE, "키": inner}, "별칭": "안쪽"}] # 같은 기초 겹침
old = _raw(me, "precombo", 이름="옛", 담은로직=lines)
before = _sc_value(me, old)
host = _make(me, "precombo", 이름="밖", 담은로직=lines)
line = _read(me, "precombo", host)["담은로직"][1]
block = line["참조"]
assert block["층"] == "사본" and block["키"] == inner and line["본뜬"]["키"] == inner
assert block["몸"]["담은로직"][0]["로직"] == PCT and line["별칭"] == "안쪽"
assert block["몸"]["본뜬"]["실무수량"]["줄"] == {"0": 3} # 엑셀 대조 · 하위 호표 끝수
assert _sc_value(me, host) == pytest.approx(before, rel=1e-9)
with store.at(me):
got = flat.flat_lines("precombo", {**MINE, "키": old}, None, {"담은로직": [{"로직": GF}]})
(me / "precombo" / f"{old}.json").unlink() # 옛 꼴 문서(참조) 치움
assert refs.users({**MINE, "키": inner}) == []
store.delete("precombo", inner)
assert _sc_value(me, host) == pytest.approx(before, rel=1e-9) # 원본 지워도 그대로
assert got["담은로직"][0]["참조"]["층"] == "사본" # 넣기 길도 블록(겹침 없음)
def test_SK_에_SC_줄마다_더한줄(world):
me = world["personal"]
sc = _make(me, "precombo", 이름="조합", 담은로직=[{"로직": GF}])
with store.at(me):
got = flat.flat_lines("sangeun", {**MINE, "키": sc}, 2)["더한줄"]
assert len(got) == 1 and got[0]["로직"] == GF and got[0]["수량"] == 2 # 실무수량 없음 = 1 × 2
assert got[0]["본뜬"]["키"] == sc
# 742-1 저장 수량(수량.손)이 있으면 그 수 · 안 SC 펴기 = 바깥 × 안
from decimal import Decimal
hand = flat.sc_adds({"담은로직": [{"로직": GF, "수량": {"손": 3}}]}, Decimal(2), _whole(), {})
assert hand[0]["수량"] == 6
block = {"참조": refs.copy_ref({"담은로직": [{"로직": GF, "수량": {"손": 3}}]}, "SC-000001")}
deep = flat.sc_adds({"담은로직": [{**block, "수량": {"손": 2}}]}, Decimal(1), _whole(), {})
assert deep[0]["로직"] == GF and deep[0]["수량"] == 6 # 사본 블록 = 바깥 × 안
doc = {"키": "SK-000099", "이름": "x", "단위": "㎥", "바탕": None, "더한줄": got}
one = float(labor.run_line(_whole(), GF, {})["기준"]["계"])
assert _sk_total(me, doc) == pytest.approx(2 * one, rel=1e-9)
def test_바탕_SK_겹침_못_펼침_까닭(world):
me = world["personal"]
under = _make(
me, "sangeun", 이름="밑", 바탕={"로직": PCT}, 덮개={"labor:LB000036#0": {"qty": 0.1}}
)
over = {"이름": "위", "단위": "㎡", "바탕": {"참조": {**MINE, "키": under}}, "더한줄": [ADD]}
over |= {"덮개": {"labor:LB000036#0": {"배율": 2}}, "뺀줄": ["labor:LB000002#0"]}
before = _sk_total(me, {"키": "SK-000099", **over})
got = flat.flat_doc("sangeun", {"키": "SK-000099", **over})
assert got["바탕"] == {"로직": PCT} and got["덮개"]["labor:LB000036#0"]["qty"] == 0.2
assert got["뺀줄"] == ["labor:LB000002#0"]
assert _sk_total(me, got) == pytest.approx(before, rel=1e-9)
# SC 에 바탕 없는 SK · 단위 환산 SK = 못 넣음
bare = _make(me, "sangeun", 이름="바탕 없음", 바탕=None, 더한줄=[ADD])
with store.at(me):
for key, why in ((bare, "바탕 기초 없는"),):
with pytest.raises(store.StoreError) as e:
flat.flat_lines("precombo", {**MINE, "키": key})
assert e.value.status == 400 and why in str(e.value.detail)
def test_초기화_SK_SC_M01_값_옵션_남음(world):
me = world["personal"]
rock = {"보정작업": "암석작업(연암・보통암・경암)"}
entry = {
"종류": "로직",
"로직": PCT,
"입력값": {},
"수량": 1,
"덮개": {"labor:LB000036#0": {"qty": 1}},
}
priced = {**ADD, "단가": 1}
x = _make(
me,
"sangeun",
이름="고친 것",
단위="㎥",
바탕={"로직": GF},
입력값=rock,
덮개={"labor:LB000002#0": {"qty": 1}},
뺀줄=["logic:GC000267#0"],
더한줄=[entry, priced],
)
with store.at(me):
look = flat.reset_doc("sangeun", x, None, True, None)
assert look["걷힘"] == 4 and _read(me, "sangeun", x)["덮개"] # 미리 보기 = 안 씀
done = flat.reset_doc("sangeun", x, None, False, None)
doc = _read(me, "sangeun", x)
assert doc["덮개"] == {} and doc["뺀줄"] == [] and doc["입력값"] == rock
assert [x.get("요소") or x.get("로직") for x in doc["더한줄"]] == [PCT, "LB000002"]
assert "덮개" not in doc["더한줄"][0] and "단가" not in doc["더한줄"][1]
assert done["걷힘"] == 4
plain = {"키": "SK-000099", "이름": "x", "단위": "㎥", "바탕": {"로직": GF}, "입력값": rock}
plain["더한줄"] = [{**entry, "덮개": {}}, ADD]
assert _sk_total(me, doc) == pytest.approx(_sk_total(me, plain), rel=1e-9)
sc = _make(
me,
"precombo",
이름="줄 덮개",
담은로직=[
{"로직": PCT, "패치": {"덮개": {"labor:LB000036#0": {"qty": 1}}, "더한줄": [priced]}}
],
)
with store.at(me):
got = flat.reset_doc("precombo", sc, None, False, None)
line = _read(me, "precombo", sc)["담은로직"][0]
assert got["걷힘"] == 2 and line["패치"] == {"더한줄": [{**ADD, "수량": 0.5, "처음수량": 0.5}]}
# 이음 기계 고정 줄(736-3) = M01 기계 한 시간 값 다시 받음
from M02_MasterTemplete import M02_Machine_Hourly as machine
hour = machine.hourly("EQ000019")
fx = {"이름": "굴착기", "단위": "시간", "단가": {"노무비": 1, "재료비": 1, "경비": 1}}
fx["이음"] = {"기계": "EQ000019"}
mc = _make(me, "precombo", 이름="기계", 담은로직=[{"고정": fx}])
with store.at(me):
assert flat.reset_doc("precombo", mc, None, False, None)["걷힘"] == 1
price = _read(me, "precombo", mc)["담은로직"][0]["고정"]["단가"]
assert price["노무비"] == pytest.approx(hour["노무비"]) and price["경비"] == pytest.approx(
hour["경비"]
)
def test_길_넣기_초기화_권한(world):
app = FastAPI()
app.include_router(same_router.router)
app.dependency_overrides[verify_session] = lambda: USER
client = TestClient(app)
me = world["personal"]
y = _y(me)
url = "/api/m02/layers/personal/{}-{}"
res = client.post(url.format("sangeun", "flat"), json={"참조": {**MINE, "키": y}, "수량": 1})
assert res.status_code == 200, res.text
assert res.json()["더한줄"][0]["본뜬"]["키"] == y
bad = client.post(url.format("precombo", "flat"), json={"참조": {**MINE, "키": "SK-000777"}})
assert bad.status_code == 400
look = client.post(url.format("sangeun", "reset"), json={"키": y})
assert look.status_code == 200 and look.json()["걷힘"] == 1
firm = client.post("/api/m02/layers/company/sangeun-reset", json={"키": y, "미리보기": False})
assert firm.status_code == 403 # 일반 사용자 회사 층 쓰기 막힘
def test_옮기기_함수_전후_같음_사본_블록(world):
me = world["personal"]
y = _y(me)
inner = _make(me, "precombo", 이름="안", 담은로직=[{"로직": GF}])
sk = _raw(
me,
"sangeun",
이름="옛 SK",
단위="㎥",
바탕={"로직": GF},
더한줄=[{"참조": {**MINE, "키": y}, "수량": 1}],
)
sc = _raw(
me,
"precombo",
이름="옛 SC",
담은로직=[{"참조": {**MINE, "키": y}}, {"참조": {**MINE, "키": inner}}],
)
dup = _raw(
me, "precombo", 이름="겹침", 담은로직=[{"로직": GF}, {"참조": {**MINE, "키": inner}}]
)
before = {k: _sc_value(me, k) for k in (sc, dup)} | {
sk: _sk_total(me, _read(me, "sangeun", sk))
}
look = {r["키"]: r for r in flat.migrate(False)}
assert all(look[k]["같음"] and look[k]["펼침"] and not look[k]["씀"] for k in (sk, sc, dup))
rows = {r["키"]: r for r in flat.migrate(True)}
assert all(rows[k]["씀"] for k in (sk, sc, dup))
assert not refs.refs_in(json.loads((me / "sangeun" / f"{sk}.json").read_bytes()))
for key in (sc, dup): # 하위 호표 = 사본 블록만 남음(원본 연결 없음)
left = refs.refs_in(json.loads((me / "precombo" / f"{key}.json").read_bytes()))
assert [(r["층"], r["키"]) for r in left] == [("사본", inner)]
assert _sk_total(me, _read(me, "sangeun", sk)) == pytest.approx(before[sk], rel=1e-9)
assert _sc_value(me, sc) == pytest.approx(before[sc], rel=1e-9)
assert _sc_value(me, dup) == pytest.approx(before[dup], rel=1e-9)
again = flat.migrate(True) # 두 번째 = 바꿀 것 없음
assert all(r["펼침"] and not r["씀"] for r in again)
def test_733_지문_옛_참조_펼친_것_같음(world):
me = world["personal"]
y = _y(me)
old = _raw(me, "precombo", 이름="옛", 담은로직=[{"참조": {**MINE, "키": y}}, {"로직": GF}])
new = _make(me, "precombo", 이름="새", 담은로직=[{"참조": {**MINE, "키": y}}, {"로직": GF}])
assert not refs.refs_in(_read(me, "precombo", new))
fingers = {k: same.finger("precombo", _read(me, "precombo", k)) for k in (old, new)}
assert fingers[old] == fingers[new]
def test_B09_SC_덮개_줄_대가_금액_M02_같음(world, tmp_path):
"""SC 덮개 줄(SK 넣기 꼴) — 옛 B09 = 산근(덮개 안 읽음) · 이제 굳힌 줄 대가 = M02 값."""
from B09_Estimation import B09_Estimation_Bill_Library as library
from resources.tester.test_719_2_sk_project import _b09, _project
me = world["personal"]
cover = {"labor:LB000036#0": {"qty": 0.5}}
sc = _make(
me,
"precombo",
이름="덮은 줄",
담은로직=[{"로직": PCT, "패치": {"덮개": cover, "더한줄": [ADD]}}],
)
m02 = _sc_value(me, sc)
bare = float(labor.run_line(_whole(), PCT, {})["기준"]["계"])
assert m02 > bare * 2 # 덮개 · 더한줄이 값에 듦(옛 B09 산근 = bare)
root = _project(tmp_path / "proj", [])
made = library.import_copy(root, "precombo", "personal", "7/42", sc)
daega, done, _ = _b09(root)
block = done[daega["libs"][made["폴더"]]]
line = block["lines"][0]
assert line["kind"] == "daega" and block["blank"] == 1 # 안 줄 수량 = 이 공사에서 입력(723-4)
cover_qty = {block["cover"]: {line["cid"]: {"qty": 1}}}
block = _b09(root, cover_qty)[1][daega["libs"][made["폴더"]]]
assert block["blank"] == 0
assert block["sum"] == pytest.approx(m02, abs=5) # 끝수(내림)만 다름
@@ -0,0 +1,93 @@
"""740-2 · 738-2 상세 머리 [초기화] [삭제] · 출처 꼬리표 지킴 시험(서버 · 화면 없음)."""
import json
import re
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
M02 = ROOT / "M02_MasterTemplete"
MAIN = (M02 / "M02_MasterTemplete_UI_Main.ts").read_text(encoding="utf-8")
ACTIONS = (M02 / "M02_MasterTemplete_Main_Actions.ts").read_text(encoding="utf-8")
HO = (ROOT / "M01_MasterData" / "M01_MasterData_UI_LogicLab_Ho.ts").read_text(encoding="utf-8")
COVER = (M02 / "logic" / "M02_Sangeun_Cover_Model.ts").read_text(encoding="utf-8")
def _born(expr: str):
"""순수 TS(`logic/M02_Born_Tag.ts`)를 Node 로 돌려 JSON 답을 받음."""
uri = (M02 / "logic" / "M02_Born_Tag.ts").as_uri()
code = (
f"import {{ bornText, bornOfCid, bornNote }} from '{uri}';"
f"console.log(JSON.stringify({expr}));"
)
out = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", "--experimental-strip-types", "--no-warnings", "--input-type=module", "-e", code],
capture_output=True,
text=True,
check=True,
cwd=ROOT,
)
return json.loads(out.stdout)
BORN = {
"층": "personal",
"주인": "u1",
"키": "SK-000012",
"이름": "강관비계",
"때": "2026-11-02T06:30:12Z",
}
def test_꼬리표_글_첫줄_짧게_둘째줄_풍선():
shown, tip = _born(f"bornText({json.dumps(BORN, ensure_ascii=False)})").split("\n")
assert shown == "본뜬 SK-000012"
assert "SK-000012 강관비계" in tip and "개인" in tip
assert re.search(r"\d{4}-\d\d-\d\d \d\d:\d\d", tip) # 이 컴퓨터 시각 · 시간대 따라 값이 다름
def test_꼬리표_없으면_빈칸_기초번호_뒤에_붙음():
assert _born("bornText(null)") == ""
assert _born("bornText(undefined, 'A1')") == "A1"
got = _born(f"bornText({json.dumps(BORN, ensure_ascii=False)}, 'A1')")
assert got.split("\n")[0] == "A1 · 본뜬 SK-000012"
def test_줄_열쇠에서_그_더한줄_출처를_찾음():
adds = json.dumps([{}, {"본뜬": BORN}], ensure_ascii=False)
assert _born(f"bornOfCid('add:#1', {adds})").startswith("본뜬 SK-000012")
assert _born(f"bornOfCid('add:#1/add:#0', {adds})").startswith("본뜬 SK-000012") # 펼침 세부
assert _born(f"bornOfCid('add:#0', {adds})") == ""
assert _born(f"bornOfCid('hopyo:X#3', {adds})") == ""
def test_머리_단추_차례_초기화_삭제_저장():
kids = MAIN[MAIN.index('className: "m02-main__head"') :]
kids = kids[: kids.index("]")]
order = [kids.index(w) for w in ("reload,", "actions.reset,", "actions.del,", "save,")]
assert order == sorted(order)
assert "actions.update(" in MAIN # 쓰기 권한 없는 층 = 안 보임
def test_삭제는_735_경고_창을_그대로_씀():
assert "askLibraryDelete(" in ACTIONS and '"delete"' in ACTIONS
assert "sure ? { 확인: true }" in ACTIONS.replace(
"ans.sure", "sure"
) # 확인 몸 = 쓰는 곳 줄까지
assert "sangeunList(" in ACTIONS and "precomboList(" in ACTIONS # 지운 뒤 목록으로
def test_초기화는_미리보기_뒤_물음_뒤_씀():
assert ACTIONS.index("미리보기: true") < ACTIONS.index("미리보기: false")
assert "ctx.confirmLeave()" in ACTIONS # 저장 안 한 고침 먼저 물음
assert "옵션 · 기본정보는 그대로" in ACTIONS
def test_공용_꼬리표_칸은_첫줄만_보이고_풍선에_나머지():
assert 'text.split("\\n")' in HO
assert "bornOfCid(" in COVER
def test_격자_비고_칸은_첫줄만():
assert _born(f"bornNote({json.dumps(BORN, ensure_ascii=False)})") == "본뜬 SK-000012"
assert _born("bornNote(null)") == ""
+157
View File
@@ -0,0 +1,157 @@
"""불러오기 엑셀 보관 안 함(PLAN 743-1 · 요구 582-3 · 584-2 · `M02_Hopyo_Forget`).
[끝내기] = 남는 파일 0 · 만든 일위대가 · 단가산출 = 출처(파일 · 때 · 누가)만 · 금액 그대로 ·
엑셀 사본(본뜬 키 · 원본 · 확인 · 품셈 · 줄 메모 「실무 …」) 걷음 ·
실무수량 = 저장 수량과 같을 때만 걷음 · SK 또 = 그 HP 몫만 ·
하루 지난 HP = 목록 숨김 → 올림 때 지움 · 옛 파일 정리 미리 보기(쓰기 0) → 고침.
층 = 임시 폴더.
"""
import json
from datetime import datetime, timedelta, timezone
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Hopyo_Fit as fit
from M02_MasterTemplete import M02_Hopyo_Forget as forget
from M02_MasterTemplete import M02_Hopyo_Store as hs
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Template_Layers as layers
from resources.tester.test_m02_hopyo_excel import URL, _post, client # noqa: F401
ALL = "/api/m02/layers/personal/hopyo-build-all"
SK_URL = URL.format("personal") + "/HP-000001/sangeun"
def _files(folder):
return sorted(p.name for p in (folder / hs.DIRNAME).rglob("HP-*.json"))
def _sums(folder):
"""문서마다 1 단위 기준 계 — 끝내기 전후 같아야 함."""
whole = refs.Expand(m01.loaded()[1])
out = {}
with store.at(folder):
for kind in ("precombo", "sangeun"):
for p in sorted((folder / kind).glob("*.json")):
out[p.stem] = refs.summary(folder, kind, p.stem, whole).get("기준금액")
return out
def _docs(folder, kind):
return {p.stem: json.loads(p.read_bytes()) for p in sorted((folder / kind).glob("*.json"))}
# ── 순수 ──────────────────────────────────────────────────────────────
SURE = {"누가": {"id": 42, "이름": "갑"}, "때": "2026-11-02T00:00:00Z"}
HEAD = {"층": "실무자료", "주인": None, "키": "HP-000003 제2호표", "파일": "가.xls"}
def test_SC_사본_걷음_출처만_실무수량은_저장_수량과_같을_때만():
lines = [
{"로직": "L1", "메모": "실무 0.3㎥ × 1 · 규격", "수량": {"손": 0.3}, "별칭": "엑셀 품"},
{"로직": "L2", "메모": "사용자 메모", "수량": {"손": 2}},
]
doc = {
"이름": "가",
"담은로직": lines,
"자재": [{"코드": "MT1", "수량": {"손": 5}}],
"본뜬": {**HEAD, "원본": ["personal", "7/42"], "확인": SURE, "품셈": [0],
"전국자재": [{"이름": "모래"}], "실무수량": {"줄": {"0": 0.3, "1": 2},
"자재": {"MT1": 5}}},
} # fmt: skip
got = forget.strip_sc(doc)
assert got["본뜬"] == {
"층": "실무자료",
"파일": "가.xls",
"전국자재": [{"이름": "모래"}],
"때": SURE["때"],
"누가": SURE["누가"],
}
assert "메모" not in got["담은로직"][0] and got["담은로직"][1]["메모"] == "사용자 메모"
assert got["담은로직"][0]["별칭"] == "엑셀 품" and got["담은로직"][0]["수량"] == {"손": 0.3}
assert got["자재"] == doc["자재"]
# 742 전 저장본(수량 null) = 실무수량이 셈 근거 → 남김
old = {**doc, "담은로직": [{**x, "수량": None} for x in lines]}
assert forget.strip_sc(old)["본뜬"]["실무수량"] == doc["본뜬"]["실무수량"]
def test_SK_또_그_HP_몫만_걷음():
other = {"키": "HP-000004 단산1", "원본": None, "갈래": "SK", "엑셀품": {}, "이름": "나"}
doc = {"본뜬": {**HEAD, "키": "HP-000003 단산1", "확인": SURE, "갈래": "기초", "엑셀품": {},
"못옮김": [], "또": [other]}} # fmt: skip
got = forget.strip_sk(doc, lambda x: str(x.get("키")).startswith("HP-000003 "))
assert got["본뜬"] == {**{"층": "실무자료", "파일": "가.xls"}, **SURE, "또": [other]}
both = forget.strip_sk(doc, lambda x: True)
assert "또" not in both["본뜬"] and "키" not in both["본뜬"]
# ── 길 ───────────────────────────────────────────────────────────────
def test_끝내기_남는_파일_0_금액_그대로_출처만_다시_올림(client):
folder = layers.personal_dir(7, 42)
assert _post(client).json()["키"] == "HP-000001"
fit.run_job(folder, "HP-000001")
assert client.post(SK_URL).status_code == 200
made = client.post(ALL, json={"층": "personal", "키": "HP-000001"}).json()
assert made["만듦"]
before = _sums(folder) # 모두 고정 = 메모 없음(메모 걷음은 순수 시험)
cache = fit.cache_path(folder, "HP-000001")
assert cache.is_file()
res = client.delete(URL.format("personal") + "/HP-000001")
assert res.status_code == 200
assert _files(folder) == [] and not cache.exists() # HP · 격자 · 맞춤셈 캐시 0
assert _sums(folder) == before # 금액 그대로
for kind in ("precombo", "sangeun"):
for key, doc in _docs(folder, kind).items():
got = doc["본뜬"]
assert got["층"] == "실무자료" and got["파일"] and got["때"], key
assert not {"키", "원본", "확인", "품셈", "갈래", "엑셀품", "못옮김", "또"} & set(got)
lines = doc.get("담은로직") or []
assert not any(str(x.get("메모", "")).startswith("실무 ") for x in lines)
assert hs.done(folder)["지문"] == {} # 끝낸 기록 없음
again = _post(client) # 같은 엑셀 다시 올림 = 새 키(409 없음)
assert again.status_code == 200 and again.json()["키"] == "HP-000002"
def test_하루_지난_HP_목록_숨김_올림_때_지움(client):
folder = layers.personal_dir(7, 42)
assert _post(client).json()["키"] == "HP-000001"
path = folder / hs.DIRNAME / "HP-000001.json"
doc = json.loads(path.read_bytes())
old = datetime.now(timezone.utc) - timedelta(days=1, minutes=1)
doc["올린때"] = old.replace(microsecond=0).isoformat().replace("+00:00", "Z")
path.write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8")
assert client.get(URL.format("personal")).json()["줄"] == [] # 숨김
assert forget.expired(folder) == ["HP-000001"]
got = _post(client).json() # 같은 엑셀이어도 지난 것 = 지우고 새로(이어서 아님)
assert got["키"] == "HP-000002" and not got.get("이어서")
assert _files(folder) == ["HP-000002.json", "HP-000002.json"] # 이번 것 + 격자
def test_옛_파일_정리_미리_보기_쓰기_0_고침(client):
folder = layers.personal_dir(7, 42)
assert _post(client).json()["키"] == "HP-000001" # 작업 중 — 그대로
home = folder / hs.DIRNAME
(home / "HP-000007.json").write_text(json.dumps({"키": "HP-000007", "호표": []}), "utf-8")
hs._put_done(folder, {"번": 7, "지문": {"ab": {"파일": "옛.xls", "만든": ["SC-000001"]}}})
(folder / "precombo").mkdir(exist_ok=True)
gone = {"이름": "옛", "단위": "개", "담은로직": [{"로직": "L", "메모": "실무 1개 × 1"}],
"본뜬": {**HEAD, "키": "HP-000005 제1호표", "확인": SURE}} # fmt: skip
live = {**gone, "본뜬": {**HEAD, "키": "HP-000001 제1호표", "확인": SURE}}
for key, doc in (("SC-000001", gone), ("SC-000002", live)):
text = json.dumps(doc, ensure_ascii=False)
(folder / "precombo" / f"{key}.json").write_text(text, "utf-8")
snap = {p: p.read_bytes() for p in folder.rglob("*.json")}
rows = forget.tidy(folder)
assert [r["대상"] for r in rows] == ["실무자료/HP-000007", "불러옴.json", "SC-000001"]
assert {p: p.read_bytes() for p in folder.rglob("*.json")} == snap # 미리 보기 = 쓰기 0
assert forget.tidy(folder, apply=True) == rows
assert _files(folder) == ["HP-000001.json", "HP-000001.json"]
assert hs.done(folder) == {"번": 7, "지문": {}}
docs = _docs(folder, "precombo")
assert "키" not in docs["SC-000001"]["본뜬"] and "메모" not in docs["SC-000001"]["담은로직"][0]
assert docs["SC-000002"] == live # 작업 중 HP 것 그대로
assert forget.tidy(folder) == [] # 두 번째 = 할 일 없음
@@ -0,0 +1,89 @@
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""744-1 서버 — 둘째 기초 세부 품 원문 · 되돌리기 기억(임시 storage · M01 읽기만).
- 둘째 기초 `펼침` 줄 `원문` = 그 기초 1 기준단위 몫(GF000316 품 그대로) · 줄 수량 · 금액 = 머리
수량 곱한 몫(상자 합) 그대로 · 머리 수량 = 더한줄 수량
- 더한줄 `처음수량` = 넣을 때 수량(없으면 저장 때 지금 수량) · 셈 · 지문 안 듦
- `뺀더한줄 [{차례, 줄}]` = [기초 빼기] · 더한 항목 빼기 기억 · 셈 · 지문 안 듦 · 못 읽는 줄 버림 ·
일위대가 줄 패치도 같음
"""
import pytest
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Library_Same as same
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Sangeun_Calc as calc
from resources.tester.test_m02_labor_ref import world # noqa: F401
GF = "GF000099" # 원/㎥
BIG = "GF000316" # 기준단위 100㎥ — ㎥ 바탕에서 처음 수량 0.01
PCT = "GC001109"
MAN = {"종류": "인력", "요소": "LB000002", "수량": 1}
def _sk(folder, **doc):
with store.at(folder):
made = refs.create("sangeun", {"이름": "744", "단위": "㎥", "더한줄": [], **doc})
return store.read("sangeun", made["이름"])["문서"]
def _calc(folder, doc):
with store.at(folder):
return calc.calc_doc(doc)
def test_둘째_기초_세부_원문_품_머리_수량(world):
doc = _sk(
world["personal"], 바탕={"로직": GF}, 더한줄=[{"종류": "로직", "로직": BIG, "수량": 0.01}]
)
got = _calc(world["personal"], doc)
head = next(x for x in got["줄"] if x["cid"] == "add:#0")
m01_lines = labor.run_line(m01.loaded()[1], BIG, {})["줄"]
assert head["수량"] == pytest.approx(0.01)
for row, line in zip(head["펼침"], m01_lines):
assert row["원문"]["수량"] == pytest.approx(float(line["수량"]), rel=1e-9) # 원문 그대로
assert row["수량"] == pytest.approx(float(line["수량"]) * 0.01, rel=1e-9) # 상자 몫
assert sum(x["금액"] for x in head["펼침"]) == pytest.approx(head["금액"], rel=1e-9)
assert sum(x["원문"]["금액"] for x in head["펼침"]) * 0.01 == pytest.approx(head["금액"])
def test_처음수량_넣을때_값_셈_지문_안_듦(world):
me = world["personal"]
plain = _sk(me, 바탕={"로직": PCT}, 단위="㎡", 더한줄=[{**MAN, "수량": 3}])
assert plain["더한줄"][0]["처음수량"] == 3 # 없으면 저장 때 지금 수량
kept = _sk(me, 바탕={"로직": PCT}, 단위="㎡", 더한줄=[{**MAN, "수량": 3, "처음수량": 1}])
assert kept["더한줄"][0]["처음수량"] == 1
assert _calc(me, kept)["기준"]["계"] == _calc(me, plain)["기준"]["계"]
assert same.finger("sangeun", kept) == same.finger("sangeun", plain)
def test_뺀더한줄_기억_셈_지문_안_듦_못읽는_줄_버림(world):
me = world["personal"]
gone = [
{"차례": 0, "줄": {"종류": "로직", "로직": BIG, "수량": 0.01, "덮개": {}}},
{"차례": 1, "줄": MAN},
{"차례": -1, "줄": MAN}, # 차례 틀림
{"차례": 2, "줄": {"종류": "로직", "로직": "GX999999", "수량": 1}}, # 없는 기초
{"차례": 3}, # 줄 없음
]
doc = _sk(me, 바탕={"로직": GF}, 뺀더한줄=gone)
assert [x["차례"] for x in doc["뺀더한줄"]] == [0, 1]
assert doc["뺀더한줄"][1]["줄"]["요소"] == "LB000002"
bare = _sk(me, 바탕={"로직": GF})
assert _calc(me, doc)["기준"]["계"] == _calc(me, bare)["기준"]["계"]
assert same.finger("sangeun", doc) == same.finger("sangeun", bare)
def test_일위대가_줄_패치_기억_처음수량(world):
me = world["personal"]
patch = {"더한줄": [{**MAN, "수량": 2}], "뺀더한줄": [{"차례": 0, "줄": MAN}]}
with store.at(me):
sc = refs.create(
"precombo", {"이름": "744", "단위": "㎡", "담은로직": [{"로직": PCT, "패치": patch}]}
)
line = store.read("precombo", sc["이름"])["문서"]["담은로직"][0]
assert line["패치"]["더한줄"][0]["처음수량"] == 2
assert line["패치"]["뺀더한줄"][0]["줄"]["요소"] == "LB000002"
@@ -0,0 +1,50 @@
"""SK 바탕 재료 = M01 기초 그대로 셈(PLAN 747-1) — 별도계상 빈 칸 = 직접계산 · 고른 줄만 밖 ·
일위대가 덮개 줄(`M02_Labor_Patch.run`)은 일위대가 기본(별도계상) 그대로."""
import pytest
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Labor_Patch as patch
from M02_MasterTemplete import M02_Sangeun_Calc as calc
from M02_MasterTemplete import M02_Sangeun_Doc as sd
FORM = "GC000211" # 6-3-1 합판거푸집 — 합판 1.03 · 각재 0.038 · 폼타이 0 · 소모자재 = 재료비 %
PLY = "material:합판(내수 12㎜)#0"
def _whole():
return m01.loaded()[1]
def _run(**doc):
whole = _whole()
clean = sd.clean({"키": "SK-000001", "이름": "시험", "단위": "㎡", **doc}, None, whole)
return labor._json(calc.run(clean, whole))
def test_base_materials_count_like_m01():
m01_ = labor._json(labor.run_line(_whole(), FORM, None))
got = _run(바탕={"로직": FORM})
assert got["기준"]["재료비"] == pytest.approx(m01_["기준"]["재료비"])
assert got["기준"]["계"] == pytest.approx(m01_["기준"]["계"])
qty = {x["cid"]: x["수량"] for x in got["줄"]}
assert qty[PLY] == pytest.approx(1.03)
assert qty["material:폼타이 12.7㎜#0"] == 0 # M01 수량 0 = 0 그대로
def test_cover_and_explicit_apart():
base = _run(바탕={"로직": FORM})
more = _run(바탕={"로직": FORM}, 덮개={PLY: {"qty": 2}})
ply = next(x for x in base["줄"] if x["cid"] == PLY)
assert more["기준"]["재료비"] > base["기준"]["재료비"] + ply["단가"] * 0.9
out = _run(바탕={"로직": FORM}, 자재표시={PLY: {"별도계상": "별도계상"}})
assert all(x["cid"] != PLY for x in out["줄"])
assert out["기준"]["재료비"] < base["기준"]["재료비"] - ply["금액"] + 1
def test_hopyo_line_keeps_line_default():
whole = _whole()
line = {"로직": FORM, "입력값": {}, "패치": {"덮개": {PLY: {"qty": 2}}}}
got = labor._json(patch.run(whole, line))
assert all(x["cid"] != PLY for x in got["줄"]) # 일위대가 기본 = 별도계상(222-1)
+3 -1
View File
@@ -253,7 +253,9 @@ def test_하위_호표_먼저_확인_퍼센트_빠짐_통째(client):
mine = layers.personal_dir(7, 42)
made = _doc(mine, got["참조"]["키"])
line = made["담은로직"][0]
assert line["참조"] == child["참조"] and line["별칭"] == "고화매트재료비"
# 740-1 하위 호표 = 사본 블록(원본 연결 없음 · 키 = 출처)
assert line["참조"]["층"] == "사본" and line["참조"]["키"] == child["참조"]["키"]
assert line["본뜬"]["키"] == child["참조"]["키"] and line["별칭"] == "고화매트재료비"
assert line["수량"] == {"손": 2.3} and made["본뜬"]["실무수량"]["줄"]["0"] == 2.3 # 742-1
assert "고화매트재료비||m2" not in _book(mine) # 호표 참조 줄 = 장부 안 씀
whole = _build(client, 3, 통째={"대상": [{"로직": "GF000101", "배": 1}]})
+12
View File
@@ -433,3 +433,15 @@ def test_목록_열_때_뒤_셈_예약(client, monkeypatch):
monkeypatch.setattr(fit, "ensure", lambda folder, key: asked.append(key) or {"끝": False})
rows = client.get(URL.format("personal")).json()["줄"]
assert [r["키"] for r in rows] == asked == ["HP-000001"]
def test_미리_고름_점은_다른_점_1위_출처같음만(client): # 745-1 · 584-3
_ready(client)
got = _hopyo(client, 3)
for name in ("구조물터파기(인력100%)", "구조물되메우기(인력100%)"): # 다른 점 + 출처 같음
cands = got[name]["후보"]
assert got[name]["상태"] == "다른 점" and cands[0]["미리"] is True
assert not any("미리" in c for c in cands[1:]) # 1위만
assert all("미리" not in c for c in got["잔토처리(인력100%)"]["후보"]) # 후보 줄 = 점 없음
ok = _hopyo(client, 1)["레미콘타설(장비사용)"] # 맞음 줄도 점 없음(이미 체크)
assert ok["상태"] == "맞음" and all("미리" not in c for c in ok["후보"])
+6 -2
View File
@@ -538,8 +538,12 @@ def test_명사형_단추와_별칭_장부_풀이_359():
def test_372_2_저장은_개인_층에_삭제는_화면_층_권한():
"""718-1 — 만들 층 = 마법사에서 고른 층(올린 층과 같음) · [삭제] = 끝내기 → 처음 걸음."""
"""718-1 — 만들 층 = 마법사에서 고른 층(올린 층과 같음) · [삭제] = 끝내기 → 처음 걸음 ·
743-1 — 지운 파일은 떠남 지움 목록에서 뺌 · 떠남(화면 바뀜 · 페이지 닫음) = 쥔 파일 지움."""
side = _text("M02_MasterTemplete_Hopyo_Wizard.ts")
open_ = side.split("m.openHopyoSheet({")[1].split("}),")[0]
assert "target: scope," in open_ and "canWrite: true," in open_
assert "onDeleted: () => void first()," in open_
assert "drop(layer, file.키);" in open_ and "void first();" in open_
assert 'addEventListener("pagehide", leave)' in side and "return { root, leave };" in side
panel = _text("M02_MasterTemplete_Hopyo_Side.ts")
assert panel.count("open?.leave?.()") == 1 and panel.count("open.leave?.()") == 1
+14 -13
View File
@@ -179,18 +179,18 @@ def test_사전조합_틀린_줄_400_순환_422(world):
with pytest.raises(store.StoreError) as e:
_combo(world["system"], lines)
assert e.value.status == 400
# 740-1 — 사전조합에 사전조합 = 사본 블록(원본 연결 없음) · 되돌려 담아도 순환 없음
a = _combo(world["system"], [one], "가")
b = _combo(world["system"], [{"참조": {**SYS, "키": a}}], "나")
with store.at(world["system"]):
line = store.read("precombo", b)["문서"]["담은로직"][0]
assert line["참조"]["층"] == "사본" and line["본뜬"]["키"] == a
assert line["참조"]["몸"]["담은로직"][0]["참조"] == one["참조"]
have = store.read("precombo", a)
with pytest.raises(store.StoreError) as e:
store.write(
"precombo",
a,
have["판"],
{**have["문서"], "담은로직": [{"참조": {**SYS, "키": b}}]},
)
assert e.value.status == 422 and f"{a} → {b} → {a}" in e.value.detail
store.write(
"precombo", a, have["판"], {**have["문서"], "담은로직": [{"참조": {**SYS, "키": b}}]}
)
assert store.read("precombo", a)["문서"]["담은로직"][0]["참조"]["층"] == "사본"
def test_층_규칙은_위_층만(world):
@@ -331,11 +331,12 @@ def test_복사는_못_읽는_참조를_같이_복사_본뜬(world):
)
got = _read(world["company"], "precombo", made["이름"])
assert got["본뜬"] == {**MINE, "키": sc}
assert [one["참조"] for one in got["담은로직"]] == [
{**COMPANY, "키": "SC-000001"}, # 회사가 못 읽는 개인 사전조합 → 회사 층 새 키
{**SYS, "키": sys_sp}, # 마스터는 그대로(옛 승격도)
]
assert _read(world["company"], "precombo", "SC-000001")["본뜬"] == {**MINE, "키": inner}
# 740-1 — 개인 사전조합은 넣을 때 사본 블록(본뜬) → 회사로 따로 복사 안 함 ·
# 마스터 옛 승격 그대로
block = got["담은로직"][0]
assert block["참조"]["층"] == "사본" and block["본뜬"]["키"] == inner
assert got["담은로직"][1]["참조"] == {**SYS, "키": sys_sp}
assert made["이름"] == "SC-000001" # 딸림 복사 없음
# 못 읽는 옛 개인 승격은 따라 복사 안 함(134-2 · 승격 서고 없음)
mine_sp = _promote(world["personal"])
with store.at(world["company"]), pytest.raises(store.StoreError) as e:
+3 -1
View File
@@ -119,7 +119,9 @@ def test_분류_고정_폴더_지움_자유_폴더_메뉴_끌어_옮기기():
assert 'item(L("M02_Delete"), () => void remove([target]))' in menu
assert 'opt.scope().canWrite ? undefined : tc("ReadOnly")' in menu # 읽기만 층 = 흐림
remove = side.split("const remove = async (")[1].split("\n };")[0]
assert "showConfirmDialog(ask" in remove and 'op: "delete", 대상:' in remove # 안에 든 것째
assert (
"askLibraryDelete(kind," in remove and 'op: "delete", 대상:' in remove
) # 735 쓰는 곳 경고 뒤 그 줄째
assert 'newHere: tc("Lib_NewHere")' in pre_side
assert '({ 이름: name, 구분: "", 상세구분: "", 단위: null, 담은로직: [] })' in pre_side
make = side.split("const make = (path: string[]): void => {")[1].split("\n };")[0]