Files
Aislo/resources/tester/test_740_1_library_flat.py
T
eomsangdonandClaude Opus 5.5 bf7f31fa63 fix(740-1): 747-1 겹침 맞춤 — SK 를 일위대가 줄로 펼칠 때 바탕 재료 별도계상 빈 칸 = 직접계산 적어 둠
- 까닭: 747-1 뒤 SK 셈은 바탕 재료 빈 칸 = 직접계산 · 일위대가 줄(덮개 줄 line=True)은 별도계상 기본 → 펼친 줄이 재료비를 잃음(합판거푸집 GC000211 SK 를 일위대가에 넣으면 금액 줄어듦)
- Library_Flat.sk_line: 바탕 재료 줄마다 자재표시 {별도계상: 직접계산} + 고른 두 칸 그대로
- 지킴 시험: 740-1 에 747 겹침(덮개 있음 · 없음 두 갈래 전후 금액 같음 · 고치기 전 실패 확인)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 21:44:49 +09:00

394 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""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)