# 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) # 끝수(내림)만 다름 def test_747_SK_재료_직접계산_일위대가에_넣어도_같은_금액(world): """747-1 SK 바탕 재료 = 직접계산 · 일위대가 줄 기본 = 별도계상 → 펼친 줄에 적어 둠.""" me = world["personal"] form = "GC000211" # 합판거푸집 — 재료 줄 있음 plain = _make(me, "sangeun", 이름="거푸집", 바탕={"로직": form}) cover = {"material:합판(내수 12㎜)#0": {"qty": 2}} covered = _make(me, "sangeun", 이름="거푸집 고침", 바탕={"로직": form}, 덮개=cover) for sk in (plain, covered): lines = [{"참조": {**MINE, "키": sk}}] old = _raw(me, "precombo", 이름="옛", 담은로직=lines) before = _sc_value(me, old) host = _make(me, "precombo", 이름="새", 담은로직=lines) line = _read(me, "precombo", host)["담은로직"][0] assert line["자재표시"] and all( v["별도계상"] == "직접계산" for v in line["자재표시"].values() ) assert _sc_value(me, host) == pytest.approx(before, rel=1e-9)