- 723-1 「안 줄 수량 null」 걷음 — SC 줄 · 자재 수량 {손} 저장(새 줄 1 · 빈 칸 = 「수량 입력」 · 금액 빠짐)
- 셈 차례: 층 = 알약 값 → 저장 수량 · 공사 굳힌 사본 = 굳힌 손 → 알약 → 저장(with_pills 가 {저장} 칸으로 옮김)
- 불러오기로 만든 일위대가 = 엑셀 수량을 저장 수량으로(본뜬.실무수량 같은 값 남김)
- 단가산출 안 일위대가 = 저장 수량으로 금액(수량 없음 없어짐) · B08 자재 · B09 내 자료집 저장도 같은 차례
- 일위대가표 수량 칸 = 문서에 저장(휘발 임시 칸 걷음) · 합계 열 「기준단위 단가」 · 「수량 반영 금액」
- 낡은 시험(723 · 175 · 204-2 · 702-3 · 703 · 717-4 · 719-1 · 719-2 · hopyo build · flow · labor ref)의 null 단언을
저장 수량 단언으로 고침 — 723-1 결정이 742-1(요구 584-1)로 바뀜 · 새 지킴 시험 test_742_1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
228 lines
11 KiB
Python
228 lines
11 KiB
Python
"""703-2 B09 펼친 일위대가 우클릭 — 복사해 새 일위대가 · 내 자료집에 저장.
|
|
|
|
- 복사본 = 덮개 `K:…` 의 `@copy` {from · name · spec} — 셈 짝(`Calc_Labor` 파이썬 · TS 거울)이
|
|
원본 블록을 본떠 끝 번호 새 호표 · 덮개 = 제 것 · 원본 없음 = 짝 잃음 ·
|
|
저장 다듬기 · 서고 사본 지킴(prune)
|
|
- 내 자료집 = 개인 층 사전조합(임시 storage) — 줄 수량 덮개 = 문서 손(쓸 때 걷힘 · 723-1) ·
|
|
품 · 단가 덮개 = 안 담음 수 ·
|
|
같은 이름 409 → 덮어쓰기 · 새 이름
|
|
"""
|
|
|
|
import copy
|
|
import importlib.util
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from B09_Estimation import B09_Estimation_Bill_Library as library # noqa: E402
|
|
from B09_Estimation import B09_Estimation_Daega_Personal as personal # noqa: E402
|
|
from B09_Estimation import B09_Estimation_Store as store # noqa: E402
|
|
from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor # noqa: E402
|
|
from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs # noqa: E402
|
|
from config import config_system # noqa: E402
|
|
from M02_MasterTemplete import M02_MasterTemplete_Items as items_store # noqa: E402
|
|
from M02_MasterTemplete import M02_MasterTemplete_Store as m02 # noqa: E402
|
|
from M02_MasterTemplete import M02_Template_Layers as layers # noqa: E402
|
|
|
|
|
|
def _load(name: str):
|
|
spec = importlib.util.spec_from_file_location(name, Path(__file__).with_name(f"{name}.py"))
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
b145 = _load("test_145_2_b09_labor_calc")
|
|
b702 = _load("test_702_b09_daega_cover")
|
|
|
|
#: DG9 찰붙임 복사 — 석공 품 0.1 → 0.2(복사본만) · 원본 DG9 덮개 0.12 는 원본에만
|
|
COPY = {
|
|
"T:DG9": {"labor:L00005#0": {"qty": 0.12}},
|
|
"K:aa": {
|
|
"@copy": {"from": "T:DG9", "name": "찰붙임 복사", "spec": "규격 B"},
|
|
"labor:L00005#0": {"qty": 0.2},
|
|
},
|
|
"K:bb": {"@copy": {"from": "K:aa", "name": "복사의 복사"}},
|
|
"K:cc": {"@copy": {"from": "T:없음"}, "x#0": {"qty": 1}},
|
|
}
|
|
|
|
|
|
def _block(out, bid):
|
|
return next(b for b in out["daega_blocks"] if b["id"] == bid)
|
|
|
|
|
|
def test_복사본_새_호표_끝_번호_덮개_제것():
|
|
daega = b702._covered_excel()
|
|
out = compute_labor(daega, b145.SHEETS, b145.SANGEUN, COPY)
|
|
src, mine = _block(out, "DG9"), _block(out, "DG:K:aa")
|
|
n = len(daega["blocks"])
|
|
assert [mine["no"], _block(out, "DG:K:bb")["no"]] == [n + 1, n + 2] # 번호 새로(끝)
|
|
assert (mine["name"], mine["spec"], mine["cover"]) == ("찰붙임 복사", "규격 B", "K:aa")
|
|
assert src["lines"][0]["qty"] == 0.12 and mine["lines"][0]["qty"] == 0.2
|
|
assert mine["lines"][0]["base"]["qty"] == 0.1 # 품셈 값 = 원본 기본값
|
|
assert [ln["amount"] for ln in mine["lines"][1:]] == [ln["amount"] for ln in src["lines"][1:]]
|
|
twice = _block(out, "DG:K:bb")
|
|
assert twice["spec"] == "규격 B" and twice["lines"][0]["qty"] == 0.1 # 덮개 안 물려받음
|
|
assert any(r["key"] == "DG:K:aa" for r in out["daega"]) # 목록표 = 내역 단가연결 자리
|
|
assert out["daega_orphans"] == [
|
|
{"cover": "K:cc", "cid": "@copy"},
|
|
{"cover": "K:cc", "cid": "x#0"},
|
|
]
|
|
|
|
|
|
def test_거울_파이썬_TS_복사본():
|
|
daega = b702._covered_excel()
|
|
py = json.loads(json.dumps(compute_labor(daega, b145.SHEETS, b145.SANGEUN, COPY)))
|
|
ts = b145._ts(daega, b145.SHEETS, b145.SANGEUN, COPY)
|
|
assert b145._same(py, ts["out"])
|
|
|
|
|
|
def test_저장_다듬기_복사본_표지():
|
|
raw = {"daega_cover": {**COPY, "K:dd": {"@copy": {"name": "원본 없음"}}}}
|
|
got = store.clean_user(raw)["daega_cover"]
|
|
assert got["K:aa"]["@copy"] == {"from": "T:DG9", "name": "찰붙임 복사", "spec": "규격 B"}
|
|
assert got["K:bb"] == {"@copy": {"from": "K:aa", "name": "복사의 복사"}}
|
|
assert "K:dd" not in got
|
|
|
|
|
|
def test_서고_사본_원본_지킴():
|
|
user = {"daega_cover": {"K:aa": {"@copy": {"from": "L:combo@system@-@SS1"}}}}
|
|
assert library.used_folders(user) == {"combo@system@-@SS1"}
|
|
|
|
|
|
def test_사전조합_원본_복사본_사본_지킴(tmp_path):
|
|
"""통합 점검 — 사전조합(`C:참조`) 복사본 · 복사본의 복사본 = 원본 사본 폴더 지킴(업데이트 수요 · 저장 정리)."""
|
|
base = library.library_dir(tmp_path)
|
|
for name, ref in (
|
|
("precombo@personal@1-3@SC-000001", {"층": "personal", "주인": "1/3", "키": "SC-000001"}),
|
|
("precombo@personal@1-3@SC-000002", {"층": "personal", "주인": "1/3", "키": "SC-000002"}),
|
|
):
|
|
(base / name).mkdir(parents=True)
|
|
manifest = {"종류": "precombo", "원본": {"참조": ref}}
|
|
(base / name / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
|
|
src = "C:참조|personal|1/3|SC-000001"
|
|
user = {
|
|
"daega_cover": {
|
|
"K:aa": {"@copy": {"from": src, "name": "복사"}},
|
|
"K:bb": {"@copy": {"from": "K:aa", "name": "복사의 복사"}},
|
|
}
|
|
}
|
|
assert library.used_folders(user) == set() # 폴더 모름 = 옛 꼴
|
|
assert library.used_folders(user, tmp_path) == {"precombo@personal@1-3@SC-000001"}
|
|
assert library.prune(tmp_path, user) == ["precombo@personal@1-3@SC-000002"]
|
|
|
|
|
|
# ── 내 자료집(개인 층 사전조합 · 임시 storage) ─────────────────────────────
|
|
|
|
|
|
@pytest.fixture()
|
|
def world(tmp_path, monkeypatch) -> dict[str, Any]:
|
|
(tmp_path / "storage").mkdir()
|
|
monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(tmp_path / "storage"))
|
|
root = b145._project(tmp_path / "proj", [(100, "SS000901")])
|
|
b145._snap(root, "pipe@100.0", "SS000901", b145.LINES, b145._values(b145.STONE))
|
|
folder = layers.personal_dir(7, 42)
|
|
folder.mkdir(parents=True)
|
|
items_store.ensure(folder)
|
|
got = daega_inputs(root)
|
|
block = next(b for b in got["blocks"] if b["id"] == got["owners"]["pipe@100.0"])
|
|
return {"root": root, "folder": folder, "block": block}
|
|
|
|
|
|
def test_자료집_문서_덮개_손_안담음_수(world):
|
|
block = world["block"]
|
|
each = block["lines"][1]["qty"] # GF000119 줄 수량(산근 = 손 그대로 0.5)
|
|
cover = {
|
|
block["cover"]: {
|
|
"sangeun:SG:GF000119#0": {"qty": 0.8, "labor": 1000}, # 줄 수량 = 담음 · 단가 = 안 담음
|
|
}
|
|
}
|
|
made = personal.build(world["root"], block["cover"], cover, "내 돌붙임")
|
|
doc = made["문서"]
|
|
assert each == 0.5 and doc["이름"] == "내 돌붙임"
|
|
hands = [ln["수량"]["손"] for ln in doc["담은로직"]]
|
|
assert hands == [2, 0.8, 1] # 덮개 줄만 바뀜
|
|
assert doc["담은로직"][0]["입력값"] == b145.STONE # 넣을 값 얹음
|
|
assert made["담음"] == {"수량": 1, "넣을값": 1, "고른자재": 0}
|
|
assert made["안담음"] == {"품": 0, "단가": 1}
|
|
# 복사본 = 원본까지 따라감 · 이름 기본 = 복사본 이름 · 덮개 = 복사본 것
|
|
cover2 = {"K:aa": {"@copy": {"from": block["cover"], "name": "복사"}}}
|
|
cover2["K:aa"]["sangeun:SG:GF000219#0"] = {"qty": 3}
|
|
made2 = personal.build(world["root"], "K:aa", cover2)
|
|
assert made2["문서"]["이름"] == "복사"
|
|
assert [ln["수량"]["손"] for ln in made2["문서"]["담은로직"]] == [3, 0.5, 1]
|
|
with pytest.raises(LookupError):
|
|
personal.build(world["root"], "S:없음", {})
|
|
|
|
|
|
def test_자료집_같은_이름_물음(world):
|
|
block = world["block"]
|
|
doc = personal.build(world["root"], block["cover"], {}, "내 돌붙임")["문서"]
|
|
with m02.at(world["folder"]):
|
|
first = personal.write(doc)
|
|
with pytest.raises(m02.StoreError) as e:
|
|
personal.write(doc)
|
|
assert e.value.status == 409 and e.value.detail["같은이름"] == first["이름"]
|
|
changed = copy.deepcopy(doc)
|
|
changed["담은로직"][1]["수량"] = {"손": 9}
|
|
again = personal.write(changed, first["이름"]) # 덮어쓰기
|
|
assert again["이름"] == first["이름"]
|
|
with pytest.raises(m02.StoreError) as e: # 733-2 — 새 이름이라도 같은 내용 = 409 물음
|
|
personal.write({**doc, "이름": "내 돌붙임 2"})
|
|
assert (
|
|
e.value.detail["같은것"][0]["키"] == first["이름"] and "같은이름" not in e.value.detail
|
|
)
|
|
other = personal.write({**doc, "이름": "내 돌붙임 2"}, new=True) # 그래도 새로
|
|
used = personal.write({**doc, "이름": "내 돌붙임 3"}, use=first["이름"]) # 그것을 씀
|
|
assert used == {"이름": first["이름"], "씀": True}
|
|
assert other["이름"] != first["이름"]
|
|
saved = json.loads(
|
|
(world["folder"] / "precombo" / f"{first['이름']}.json").read_text(encoding="utf-8")
|
|
)
|
|
# 742-1 = M02 SC 안 줄 수량 저장(덮어쓴 손 9 · 쓰는 곳 알약 · 이 공사 입력이 이어지면 그 값이 앞섬)
|
|
assert saved["이름"] == "내 돌붙임" and saved["담은로직"][1]["수량"] == {"손": 9}
|
|
assert saved["담은로직"][0]["입력값"] == b145.STONE
|
|
assert len(list((world["folder"] / "precombo").glob("SC-*.json"))) == 2
|
|
|
|
|
|
def test_자료집_길_같은_이름_409_덮어쓰기(world, monkeypatch):
|
|
"""서버 길 — 화면 캐시 덮개째 · 개인 층(임시 storage) · 같은 이름 409 {같은이름} → 덮어쓰기."""
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from B09_Estimation import B09_Estimation_Router as b09
|
|
from common_util.common_util_auth import verify_session
|
|
|
|
async def root_of(_pid):
|
|
return world["root"]
|
|
|
|
monkeypatch.setattr(b09, "_project_root", root_of)
|
|
app = FastAPI()
|
|
app.include_router(b09.router)
|
|
session = {"user_id": 42, "company_id": 7, "role": "USER", "is_master": False}
|
|
app.dependency_overrides[verify_session] = lambda: session
|
|
client = TestClient(app)
|
|
block = world["block"]
|
|
url = "/api/projects/8b98ca75-c572-4002-8468-2adae4323414/estimation/daega/personal"
|
|
user = {"daega_cover": {block["cover"]: {"sangeun:SG:GF000219#0": {"qty": 4}}}}
|
|
body = {"cover": block["cover"], "name": "길 시험", "user": user}
|
|
first = client.post(url, json=body)
|
|
assert first.status_code == 200, first.text
|
|
assert first.json()["담음"]["수량"] == 1
|
|
again = client.post(url, json=body)
|
|
assert again.status_code == 409 and again.json()["detail"]["같은이름"] == first.json()["키"]
|
|
over = client.post(url, json={**body, "overwrite": first.json()["키"]})
|
|
assert over.status_code == 200 and over.json()["키"] == first.json()["키"]
|
|
saved = world["folder"] / "precombo" / f"{first.json()['키']}.json"
|
|
assert json.loads(saved.read_text(encoding="utf-8"))["담은로직"][0]["수량"] == {
|
|
"손": 4
|
|
} # 742-1
|
|
assert client.post(url, json={**body, "cover": "S:없음"}).status_code == 404
|