Files
Aislo/resources/tester/test_m02_labor_ref.py
T
eomsangdonandClaude Opus 5.5 f4be6782a5 feat(751-3): M01 · M02 셈 끝수 = 품셈 1-2-2 버림 — 줄 0.1 원 · 비목 소계 1 원 · 계 = 셋 합 · B09 와 같은 값
- 끝수 한 곳 `master_cut.py`(B09 `rounddown` 재사용) — 엔진 답에 입힘: 줄 비목마다 0.1 원 버림 · 하위 로직 줄 = 하위 끝수 계 × 수량 · 덧줄 = 버린 줄 합으로 다시 셈 · 비목 합 1 원 버림 · 중간 칸도 버린 값
- 쓰는 곳: M01 시험 계산(`check_master.calc`) · 읽는 식(`master_text.lines`) · 옛 조합 미리보기(`master_combo`) · M02 `run_line`(단가산출 · 일위대가 · 펼침) · 별도계상 뺀 기준 · 켜쌓기율 곱 뒤 · 일위대가 반영 · 반영합 · 비율 줄 · 사전조합 안 줄
- 일위대가 반영 TS 짝(`Labor_Calc.ts`) 같은 끝수 · 거울 헬퍼가 상대 경로 .ts 가져옴
- B09 = `run_line(raw=True)` 엔진 날 값(제 단가로 다시 버림 · % 덧줄 율 비 그대로) — 검증_sub3 B09 247 칸 전후 같음
- `master_text` 꼴 셋 = `common_util_money` 로 갈음(줄 금액 `fmt_line_money` · 식 안 금액 = 버린 0.1 원)
- 화면: 상세 줄 금액 · 시험 계산 줄 · 중간 돈 = `formatLineMoney` · 일위대가 반영 칸 0.1 원 · 상세 바닥 · 박스 · 인력재료경비 비목 소계 = 줄 합 1 원 버림
- 테스트1 네 보기 · 서고 목록 = 124,419(옛 124,420 / 124,419.85) · 재료 줄 11,694.6 + 23,425.6 + 1,404.8 = 소계 36,525
- 새 지킴 시험 test_751_3_money_cut
- 옛 날 금액(끝수 없음)을 대조하던 시험 24 파일 고침 — 까닭 = 금액 규칙이 줄 0.1 원 · 소계 1 원 버림으로 바뀜(고정 값 → 새 값 · 줄 합 = 계 → 줄 합 아래 비목마다 1 원 안 · 곱 비 = 끝수 폭 안) · 실무 xlsx 영월 엑셀 같음 22 → 23 · 이어 붙이는 TS 시험(704_3 · 717_2b)에 `rounddown` 같은 식

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 06:16:01 +09:00

573 lines
28 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.
"""일위대가 사전조합 서고(PLAN 50-3 · `M02_Labor_Ref`) — 사전조합 · 참조 줄 · 옛 단가승격(134-2).
펼침(재귀 · 순환 · 깊이) · 층 규칙(위 층만) · 쓰는 곳 지우기 409 · 층 사이 따라 복사 ·
프로젝트 굳힘 · 로직 지우기 검사 · TS 짝 settle 거울. 층은 임시 폴더(마스터 · storage).
옛 단가승격(SP) = 새로 못 만듦 · 남은 파일은 읽기 · 셈 · 굳힘만(`_promote` = 옛 파일을 바로 씀) ·
층 문서를 읽을 때 옛 참조 줄 = 로직 줄 + 승격(`unpromote` · 같은 로직이 있으면 그대로).
"""
import json
from decimal import Decimal
import pytest
from M02_MasterTemplete import M02_Labor_Doc as labor
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_labor_calc_mirror import _same, _ts
from resources.tester.test_m02_structure import write_sample_table
TEN = "GC000421" # 원/10㎡ — 배 10
STONE = "GF000219" # 원/㎡
PLAIN = "GC000422" # 돈 아닌 로직
SYS = {"층": "system", "주인": None}
COMPANY = {"층": "company", "주인": "7"}
MINE = {"층": "personal", "주인": "7/42"}
@pytest.fixture()
def world(tmp_path, monkeypatch):
from config import config_system
system = tmp_path / "system"
system.mkdir()
write_sample_table(system)
monkeypatch.setattr(store, "FOLDER", system)
monkeypatch.setattr(layers, "SYSTEM_ROOT", system)
(tmp_path / "storage").mkdir()
monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(tmp_path / "storage"))
with store.at(system):
store.create_structure("rv")
return {
"system": system,
"company": layers.company_dir(7),
"personal": layers.personal_dir(7, 42),
}
def _promote(folder, code=TEN, values=None, **extra):
"""옛 단가승격 파일(134-2 앞 꼴) — 층마다 다음 번호 · 배 = 로직 물량 알약 배."""
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
line = {"로직": code, "입력값": values or {}, **extra}
pill = (combo_store.combo_pills({"담은로직": [line]})["물량"] or [{}])[0]
folder = folder / "promote"
folder.mkdir(parents=True, exist_ok=True)
key = f"SP-{len(list(folder.glob('SP-*.json'))) + 1:06d}"
doc = {
"종류": "단가승격",
"키": key,
"이름": pill.get("이름") or code,
"단위": pill.get("단위"),
"배": pill.get("배") or 1,
"담은로직": [line],
"본뜬": None,
}
(folder / f"{key}.json").write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8")
return key
def _combo(folder, lines, name="조합"):
with store.at(folder):
return refs.create("precombo", {"이름": name, "단위": "㎡", "담은로직": lines})["이름"]
def _area(given):
"""면적산출 사본 꼴 — 알약 키마다 칸 하나(723-1 · 안 줄 수량 = 이은 칸 값)."""
cells = {f"A{i}": {"값": v} for i, v in enumerate(given.values(), 1)}
links = [{"칸": f"A{i}", "키": k} for i, k in enumerate(given, 1)]
return {"시트": [{"id": "s", "칸": cells, "연결": links}]}
def _read(folder, kind, key):
return json.loads((folder / kind / f"{key}.json").read_bytes())
def _base(code):
import M01_MasterData.M01_MasterData_Store as m01
return labor.run_line(m01.loaded()[1], code, {})["기준"]
# ── 자료 틀 ────────────────────────────────────────────────────────────
def test_단가승격_서고는_없음_층_종류에서_뺌(world):
assert "promote" not in layers.KINDS and layers.LIBRARY_KINDS == ("precombo", "sangeun")
with store.at(world["system"]), pytest.raises(KeyError):
refs.create("promote", {"담은로직": [{"로직": TEN}]})
# ── 옛 단가승격 참조 줄 = 읽을 때 새 꼴(134-2) ───────────────────────────
def test_옛_참조_줄은_로직_줄_승격으로_보이고_값_같음(world):
sp = _promote(world["system"], TEN)
old = {
"열": "rv",
"담은로직": [
{"참조": {**SYS, "키": sp}, "별칭": "승", "메모": "m", "수량": {"손": 4}},
{"로직": STONE, "수량": {"손": 1}},
],
"자재": [],
}
got = refs.unpromote("labor", old)
assert got["담은로직"][0] == {
"로직": TEN,
"별칭": "승",
"메모": "m",
"입력값": {},
"수량": {"손": 4},
"승격": True,
}
assert old["담은로직"][0]["참조"]["키"] == sp # 받은 문서는 그대로
held = _promote(world["system"], PLAIN, {"규격": "NX"}, 켜쌓기율=90) # 입력값 · 율도 옮김
one = refs.unpromote("labor", {"담은로직": [{"참조": {**SYS, "키": held}}]})["담은로직"][0]
assert (one["입력값"], one["켜쌓기율"]) == ({"규격": "NX"}, 90)
with store.at(world["system"]):
before = labor.calc(labor.clean(old), {})["반영합"]["계"]
after = labor.clean(got)
assert after["담은로직"][0]["승격"] is True # [저장] 때 새 꼴로 굳음
assert labor.calc(after, {})["반영합"]["계"] == pytest.approx(before, rel=1e-9)
def test_옛_참조_줄은_같은_로직이_있으면_그대로_사전조합은_승격_없이(world):
sp = _promote(world["system"], STONE)
lines = [{"로직": STONE, "수량": None}, {"참조": {**SYS, "키": sp}, "수량": None}]
doc = {"열": "rv", "담은로직": lines, "자재": []}
assert refs.unpromote("labor", doc) is doc # 겹침 = 옛 참조 줄 그대로(저장 됨)
with store.at(world["system"]):
assert labor.clean(doc)["담은로직"][1]["참조"]["키"] == sp
combo = {"담은로직": [{"참조": {**SYS, "키": sp}, "수량": {"손": 2}, "메모": None}]}
one = refs.unpromote("precombo", combo)["담은로직"][0]
assert one["로직"] == STONE and one["수량"] == {"손": 2} and "승격" not in one
assert refs.unpromote("area", combo) is combo # 다른 종류는 안 봄
def test_층_길_읽기는_옛_참조_줄을_새_꼴로_파일은_그대로(world):
from fastapi import FastAPI
from fastapi.testclient import TestClient
from common_util.common_util_auth import verify_session
from M02_MasterTemplete import M02_MasterTemplete_Router_Layers as layer_router
sp = _promote(world["system"])
with store.at(world["personal"]):
line = {"참조": {**SYS, "키": sp}, "수량": {"손": 2}}
store.write("labor", "x1", "", {"열": "x1", "담은로직": [line], "자재": []})
app = FastAPI()
app.include_router(layer_router.router)
app.dependency_overrides[verify_session] = lambda: {
"user_id": 42,
"company_id": 7,
"role": "USER",
"is_master": False,
}
got = TestClient(app).get("/api/m02/layers/personal/templates/labor/x1").json()
assert got["문서"]["담은로직"][0]["로직"] == TEN and got["문서"]["담은로직"][0]["승격"] is True
assert _read(world["personal"], "labor", "x1")["담은로직"][0]["참조"]["키"] == sp
def test_사전조합_틀린_줄_400_순환_422(world):
sp = _promote(world["system"])
one = {"참조": {**SYS, "키": sp}}
for lines in (
[{"로직": "GF999999"}], # 모르는 로직
[{"로직": TEN}, {"로직": TEN}], # 같은 로직 두 번(51-11)
[{"로직": TEN, "입력값": [1]}], # 입력값 꼴
[one, one], # 같은 참조 두 번
[{"참조": {**SYS, "키": "SP-000009"}}], # 없는 참조
[{"참조": {**SYS, "키": "SU-000001"}}], # 서고 키 아님
):
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)
store.write(
"precombo", a, have["판"], {**have["문서"], "담은로직": [{"참조": {**SYS, "키": b}}]}
)
assert store.read("precombo", a)["문서"]["담은로직"][0]["참조"]["층"] == "사본"
def test_층_규칙은_위_층만(world):
_promote(world["system"])
_promote(world["company"])
_promote(world["personal"])
ok = [{"참조": {**s, "키": "SP-000001"}} for s in (SYS, COMPANY, MINE)]
assert _combo(world["personal"], ok) == "SC-000001" # 개인 → 개인 · 회사 · 마스터
for folder, bad in (
(world["company"], MINE), # 회사 → 개인 X
(world["system"], COMPANY), # 마스터 → 회사 X
(world["personal"], {"층": "personal", "주인": "7/43"}), # 남의 개인 X
(world["personal"], {"층": "company", "주인": "8"}), # 남의 회사 X
):
with pytest.raises(store.StoreError) as e:
_combo(folder, [{"참조": {**bad, "키": "SP-000001"}}])
assert e.value.status == 400 and "위 층만" in e.value.detail
def test_일위대가_참조_줄_정리_같은_참조_두_번_400(world):
sp = _promote(world["system"])
line = {"참조": {**SYS, "키": sp}, "별칭": " 승격 ", "수량": {"손": 3}}
with store.at(world["system"]):
got = labor.clean({"열": "rv", "담은로직": [line]})["담은로직"]
assert got == [{"참조": {**SYS, "키": sp}, "별칭": "승격", "메모": None, "수량": {"손": 3}}]
with pytest.raises(store.StoreError) as e:
labor.clean({"열": "rv", "담은로직": [line, line]})
assert e.value.status == 400
# ── 펼침(계산) ─────────────────────────────────────────────────────────
def test_calc_는_참조를_펼침_승격은_기준_나누기_배_사전조합은_수량_곱(world):
sp = _promote(world["system"])
sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}, "수량": {"손": 2}}], "둘")
ref = {**SYS, "키": sc}
with store.at(world["system"]):
doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 5}}]})
bare = labor.calc(doc, {})["줄"][0] # 742-1 — 안 줄 저장 수량(손 2)
got = labor.calc(doc, {f"{refs.ref_id(ref)}#0": 2}) # 안 줄 알약 = 구조물 조합 칸 값
fed = labor.calc(doc, {f"{refs.ref_id(ref)}#0": 4})["줄"][0] # 알약이 저장보다 앞섬
want = Decimal(str(_base(TEN)["계"])) / 10 * 2
near = 3 # 751-3 — 안 줄 0.1 원 · 비목 합 1 원 버림
assert bare["기준"]["계"] == pytest.approx(float(want), abs=near) and bare["안"][0]["수량"] == 2
assert (
fed["기준"]["계"] == pytest.approx(float(want * 2), abs=near) and fed["안"][0]["수량"] == 4
)
one = got["줄"][0]
assert one["기준"]["계"] == pytest.approx(float(want), abs=near)
assert (one["분모"], one["배"], one["알약"]) == (
"㎡",
1,
refs.ref_id(ref),
) # 단위 = 그 사전조합 것
assert one["반영"]["계"] == pytest.approx(float(want * 5), abs=20) # 751-3 끝수 × 5
assert one["안"][0]["수량"] == 2 and one["안"][0]["참조"]["키"] == sp
pills = labor.pills(doc)
assert pills[0] == {"키": refs.ref_id(ref), "이름": "둘", "단위": "㎡", "로직": [], "참조": ref}
assert [p["키"] for p in pills[1:]] == [f"{refs.ref_id(ref)}#0"] # 안 줄 알약(723-1)
assert pills[1]["이름"].startswith("둘 · ")
def test_승격_줄_상세는_원_로직_키_표_사전조합은_안_줄만(world):
sp = _promote(world["system"])
sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}}], "하나")
with store.at(world["system"]):
doc = labor.clean({"열": "rv", "담은로직": [{"참조": {**SYS, "키": k}} for k in (sp, sc)]})
promote, combo = labor.calc(doc, {})["줄"]
assert promote["원로직"] == TEN and promote["줄"]
assert sum(x["금액"] for x in promote["줄"]) == pytest.approx(
promote["기준"]["계"], abs=0.3
) # 1 단위(51-2) · 비목 합 1 원 버림 ÷ 배(751-3)
assert "줄" not in combo and [x["참조"]["키"] for x in combo["안"]] == [sp]
def test_펼침은_순환_깊이_없는_참조를_까닭으로_멈춤(world):
folder = world["system"] / "precombo"
for key, child in (("SC-000001", "SC-000002"), ("SC-000002", "SC-000001")):
body = {"이름": key, "담은로직": [{"참조": {**SYS, "키": child}}]}
(folder / f"{key}.json").write_text(json.dumps(body), encoding="utf-8")
import M01_MasterData.M01_MasterData_Store as m01
expand = refs.Expand(m01.loaded()[1])
got = expand({**SYS, "키": "SC-000001"})
assert got["기준"] is None and "순환" in got["까닭"]
assert "없는 참조" in expand({**SYS, "키": "SC-000009"})["까닭"]
deep = refs.Expand(
m01.loaded()[1], lambda r: {"담은로직": [{"참조": {**SYS, "키": "SC-000001"}}]}
)
assert deep.__call__({**SYS, "키": "SC-000001"}, ())["기준"] is None
def test_돈_아닌_승격은_까닭(world):
sp = _promote(world["system"], PLAIN)
import M01_MasterData.M01_MasterData_Store as m01
got = refs.Expand(m01.loaded()[1])({**SYS, "키": sp})
assert got["기준"] is None and got["까닭"]
def test_참조_줄_settle_파이썬과_TS_가_같은_답(world):
sp = _promote(world["system"])
ref = {**SYS, "키": sp}
with store.at(world["system"]):
doc = labor.clean(
{"열": "rv", "담은로직": [{"로직": STONE, "수량": None}, {"참조": ref, "수량": None}]}
)
answer = labor.calc(doc, {})
values = {refs.ref_id(ref): 4}
py = labor.settle(answer, doc, values)
assert py["줄"][1]["수량출처"] == "칸"
_same(py, _ts(answer, doc, values))
# ── 지우기 · 복사 · 굳힘 · 로직 지우기 ─────────────────────────────────
def test_쓰는_곳이_있으면_지우기_409_없으면_지움(world):
sc = _combo(world["system"], [{"로직": TEN}])
with store.at(world["personal"]):
line = {"참조": {**SYS, "키": sc}}
store.write("labor", "x1", "", {"열": "x1", "담은로직": [line], "자재": []})
with store.at(world["system"]), pytest.raises(store.StoreError) as e:
store.delete("precombo", sc)
assert e.value.status == 409 and "개인 7/42" in e.value.detail
(world["personal"] / "labor" / "x1.json").unlink()
with store.at(world["system"]):
store.delete("precombo", sc)
assert not (world["system"] / "precombo" / f"{sc}.json").exists()
def test_복사는_못_읽는_참조를_같이_복사_본뜬(world):
inner = _combo(world["personal"], [{"로직": TEN}], "안")
sys_sp = _promote(world["system"])
sc = _combo(
world["personal"],
[{"참조": {**MINE, "키": inner}}, {"참조": {**SYS, "키": sys_sp}, "수량": {"손": 2}}],
)
with store.at(world["personal"]):
doc = store.read("precombo", sc)["문서"]
with store.at(world["company"]):
made = refs.create(
"precombo", refs.carry({**doc, "본뜬": {**MINE, "키": sc}}, ("personal", "7/42"))
)
got = _read(world["company"], "precombo", made["이름"])
assert got["본뜬"] == {**MINE, "키": sc}
# 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:
refs.carry({"담은로직": [{"참조": {**MINE, "키": mine_sp}}]}, ("personal", "7/42"))
assert e.value.status == 400
def test_복사_새_키_규격_열_수량도_따라감(world):
"""통합 점검 — 회사에 올리기 승인 · 회사로 복사: 개인 참조가 회사 새 키로 바뀌면 규격 열 수량 열쇠도."""
inner = _combo(world["personal"], [{"로직": TEN}], "안")
old = f"참조|personal|7/42|{inner}"
spec = {
"이름": "H",
"열": ["1.5", "2.0"],
"기본": "1.5",
"수량": {old: {"1.5": 1, "2.0": 1.3}, "고정|돌|가|㎡": {"1.5": 2, "2.0": 2.6}},
}
doc = {"담은로직": [{"참조": {**MINE, "키": inner}}], "규격": spec}
with store.at(world["company"]):
got = refs.carry(doc, ("personal", "7/42"))
new = f"참조|company|7|{got['담은로직'][0]['참조']['키']}"
assert got["규격"]["수량"] == {
new: {"1.5": 1, "2.0": 1.3},
"고정|돌|가|㎡": {"1.5": 2, "2.0": 2.6},
}
assert doc["규격"]["수량"][old] == {"1.5": 1, "2.0": 1.3} # 원본 그대로
def test_길_새로_복사_권한(world):
from fastapi import FastAPI
from fastapi.testclient import TestClient
from common_util.common_util_auth import verify_session
from M02_MasterTemplete import M02_Labor_Router as labor_router
app = FastAPI()
app.include_router(labor_router.router)
app.dependency_overrides[verify_session] = lambda: {
"user_id": 42,
"company_id": 7,
"role": "USER",
"is_master": False,
}
client = TestClient(app)
url = "/api/m02/layers/{}/library/{}"
body = {"문서": {"이름": "가", "단위": "㎡", "담은로직": [{"로직": TEN}]}}
got = client.post(url.format("personal", "precombo"), json=body).json()
assert got["이름"] == "SC-000001"
assert client.post(url.format("system", "precombo"), json=body).status_code == 403
copy = {"본뜬": {**MINE, "키": "SC-000001"}}
assert client.post(url.format("personal", "precombo"), json=copy).json()["이름"] == "SC-000002"
other = {"본뜬": {"층": "personal", "주인": "7/99", "키": "SC-000001"}}
assert client.post(url.format("personal", "precombo"), json=other).status_code == 403
assert client.post(url.format("personal", "promote"), json=body).status_code == 422 # 134-2
def test_프로젝트는_참조사본으로_굳힘_원본을_지워도_같은_값(world):
from M02_MasterTemplete import M02_Item_Prices as prices
from M02_MasterTemplete import M02_Item_Source as source
sp = _promote(world["system"])
sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}, "수량": {"손": 2}}])
ref = {**SYS, "키": sc}
given = {f"{refs.ref_id(ref)}#0": 2} # 723-1 — 안 줄 수량 = 이은 칸 값
with store.at(world["system"]):
doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 1}}]})
live = labor.calc(doc, given)["줄"][0]["기준"]["계"]
rows = source.labor_doc_rows(doc)
assert set(rows["참조사본"]) == {refs.ref_id(ref), refs.ref_id({**SYS, "키": sp})}
assert refs.frozen_logics(rows["참조사본"]) == [TEN]
bundle = prices.to_json(prices.collect([TEN]))
for key in (sc, sp): # 원본이 사라져도 프로젝트는 사본만 봄
kind = refs.kind_of(key)
(world["system"] / kind / f"{key}.json").unlink()
got = prices.calc(rows, bundle, {}, {}, _area(given))
line = got["줄"][0]
assert not line["까닭"] and line["참조"] == ref # 층 셈 꼴(142-1)
assert line["기준"]["계"] == pytest.approx(live, rel=1e-9)
assert line["수량"] == 1 and line["반영"]["계"] == pytest.approx(live, rel=1e-9)
def test_로직_지우기_검사는_승격_줄도_보고_참조_줄은_건너뜀(world):
from M01_MasterData import M01_MasterData_Store_Cache as cache
sp = _promote(world["system"], STONE)
with store.at(world["system"]):
have = store.read("labor", "rv")
line = {"참조": {**SYS, "키": sp}}
store.write("labor", "rv", have["판"], {**have["문서"], "담은로직": [line]})
class Gone: # STONE 을 지운 마스터
def logic(self, code):
if code == STONE:
raise cache.cm.mf.FormulaError("없음")
assert cache.labor_errors(Gone()) == [] # 옛 승격 서고는 안 봄(134-2) · 참조 줄 건너뜀
with store.at(world["system"]):
have = store.read("labor", "rv")
line = {"로직": STONE, "승격": True}
store.write("labor", "rv", have["판"], {**have["문서"], "담은로직": [line]})
number = store.read("labor", "rv")["문서"]["일위대가번"]
got = cache.labor_errors(Gone())
assert got == [f"일위대가 마스터 「{number}」 이 로직 「{STONE}」 을 씀 — 거기서 먼저 뺌"]
def test_새_프로젝트_씨앗은_서고를_안_가져감(world, tmp_path):
_promote(world["system"])
got = layers.seed_project(tmp_path / "p1")
assert not any(k.startswith(layers.LIBRARY_KINDS) for k in got["작업본"])
def test_층_목록_서고_줄은_이름_단위_기준금액을_실음(world):
sp = _promote(world["system"])
sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}, "수량": {"손": 3}}], "셋")
rows = [{"종류": "precombo", "이름": sc}, {"종류": "labor", "이름": "rv"}]
refs.summaries(world["system"], rows)
# 742-1 — 목록 기준금액 = 안 줄 저장 수량(손 3)으로
want = Decimal(str(_base(TEN)["계"])) / 10 * 3
assert rows[0]["제목"] == "셋" and rows[0]["기준금액"] == pytest.approx(
float(want), abs=3
) # 751-3
assert "기준금액" not in rows[1]
def test_사전조합_로직_줄_섞임_합은_손셈_지우기_409_굳힘(world):
from M01_MasterData import M01_MasterData_Store_Cache as cache
from M02_MasterTemplete import M02_Item_Prices as prices
from M02_MasterTemplete import M02_Item_Source as source
sp = _promote(world["system"])
lines = [
{"참조": {**SYS, "키": sp}, "수량": {"손": 2}},
{"로직": STONE, "입력값": {"없는칸": 1}, "수량": {"손": 3}, "메모": "돌"},
{"로직": TEN},
]
sc = _combo(world["system"], lines, "섞음")
saved = _read(world["system"], "precombo", sc)["담은로직"]
assert saved[1:] == [ # 742-1 — 안 줄 수량 저장(빈 수량 = null)
{"로직": STONE, "입력값": {}, "수량": {"손": 3}, "메모": "돌"},
{"로직": TEN, "입력값": {}, "수량": None, "메모": None},
]
ten, stone = (Decimal(str(_base(c)["계"])) for c in (TEN, STONE))
want = ten / 10 * 2 + stone * 3 + ten / 10 * 1 # 이은 수량 — 승격 · TEN 은 원/10㎡(배 10)
ref = {**SYS, "키": sc}
given = {f"{refs.ref_id(ref)}#{i}": q for i, q in enumerate((2, 3, 1))}
with store.at(world["system"]):
doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 5}}]})
one = labor.calc(doc, given)["줄"][0]
assert one["기준"]["계"] == pytest.approx(float(want), abs=4) # 751-3 안 줄 셋 · 비목 합 버림
assert one["반영"]["계"] == pytest.approx(float(want * 5), abs=20) # 751-3 끝수 × 5
assert [x.get("로직") or x["참조"]["키"] for x in one["안"]] == [sp, STONE, TEN]
class Gone: # STONE 을 지운 마스터
def logic(self, code):
if code == STONE:
raise cache.cm.mf.FormulaError("없음")
got = cache.labor_errors(Gone())
assert f"사전조합 마스터 「{sc}」 이 로직 「{STONE}」 을 씀 — 거기서 먼저 뺌" in got
rows = source.labor_doc_rows(doc)
assert sorted(refs.frozen_logics(rows["참조사본"])) == sorted([TEN, STONE, TEN])
bundle = prices.to_json(prices.collect([TEN, STONE]))
for key in (sc, sp): # 원본이 사라져도 굳힌 사본으로 같은 값
(world["system"] / refs.kind_of(key) / f"{key}.json").unlink()
got = prices.calc(rows, bundle, {}, {}, _area(given))["줄"][0]
assert got["기준"]["계"] == pytest.approx(float(want), abs=4) # 751-3 안 줄 셋 · 비목 합 버림
def test_사전조합이_자기_자신을_담으면_422_수량_없음(world):
sp = _promote(world["system"])
a = _combo(world["system"], [{"참조": {**SYS, "키": sp}}], "가")
with store.at(world["system"]):
have = store.read("precombo", a)
assert have["문서"]["담은로직"][0]["수량"] is None # 723-1 — 기본 1 걷음
with pytest.raises(store.StoreError) as e:
store.write(
"precombo",
a,
have["판"],
{
**have["문서"],
"담은로직": [*have["문서"]["담은로직"], {"참조": {**SYS, "키": a}}],
},
)
assert e.value.status == 422 and "자기 자신" in e.value.detail
def test_서고_구분_상세구분_사전조합은_고른_값_옛_문서는_읽을_때(world):
sp = _promote(world["system"])
sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}}])
assert (_read(world["system"], "precombo", sc)["구분"], "") == ("", "")
with store.at(world["system"]):
have = store.read("precombo", sc)
store.write("precombo", sc, have["판"], {**have["문서"], "구분": " 가 ", "상세구분": "나"})
got = _read(world["system"], "precombo", sc)
assert (got["구분"], got["상세구분"]) == ("가", "나")
# 옛 문서(칸 없음) — 사전조합은 빈 글 · 옛 승격은 손대지 않음
old = _read(world["system"], "promote", sp)
assert refs.filled("promote", old) is old
assert refs.filled("precombo", {"담은로직": []})["구분"] == ""
rows = [{"종류": "precombo", "이름": sc}]
refs.summaries(world["system"], rows)
assert (rows[0]["구분"], rows[0]["상세구분"], rows[0]["연결"]) == ("가", "나", None)
def test_사전조합_굳힘은_안_로직_알약도_사본_값_142_2(world, monkeypatch):
"""프로젝트 참조사본 사전조합 = 가져온 때 알약(배 10) — 그 뒤 M01 이 바뀌어도 사본 셈 그대로 ·
층 문서(알약 없음)는 지금 M01 을 읽음."""
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
from M02_MasterTemplete import M02_Item_Prices as prices
from M02_MasterTemplete import M02_Item_Source as source
sc = _combo(world["system"], [{"로직": TEN}], "열배")
ref = {**SYS, "키": sc}
with store.at(world["system"]):
doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 1}}]})
rows = source.labor_doc_rows(doc)
given = {f"{refs.ref_id(ref)}#0": 1}
kept = rows["참조사본"][refs.ref_id(ref)]["알약"]
assert [(p["로직"], p.get("배")) for p in kept] == [([TEN], 10)]
assert "알약" not in _read(world["system"], "precombo", sc) # 층 문서는 그대로
bundle = prices.to_json(prices.collect([TEN]))
before = prices.calc(rows, bundle, {}, {}, _area(given))["줄"][0]["기준"]["계"]
changed = {"물량": [{"키": "x|㎡", "이름": "x", "단위": "㎡", "로직": [TEN]}]} # 배 1 로 바뀜
monkeypatch.setattr(combo_store, "combo_pills", lambda row: changed)
again = prices.calc(rows, bundle, {}, {}, _area(given))["줄"][0]["기준"]["계"]
assert again == pytest.approx(before)
with store.at(world["system"]):
live = labor.calc(doc, given)["줄"][0]["기준"]["계"]
assert live == pytest.approx(before * 10, abs=30) # 층 셈은 지금 M01(배 1) · 751-3 끝수 × 10