Files
Aislo/resources/tester/test_b08_structure_unit_price_edit.py
T
eomsangdonandClaude Opus 5 aab43d652c feat(b08): 구조물도 일위대가 줄 고치기 — 줄 더하기·빼기·고르개·단가 수동, 저장 자리 둘
- 줄 조합은 양식+프로젝트(종류별), [내 라이브러리에 저장] 때 양식에 실림
- 수동 단가는 프로젝트만(값·출처·넣은 날짜) — 빨간 테두리 + 「미확정 N건」
- 고르개는 이 프로젝트 단가표에서 품셈·자원을 낱말로 찾음
- 다른 양식을 가져오면 그 종류의 줄 조합·수동 단가도 비움

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-13 19:46:29 +09:00

182 lines
7.4 KiB
Python

"""구조물도 일위대가 줄 고치기 창구 (2026-09-13, PLAN 3장 ③).
겨누는 것
① 줄 더하기·빼기를 저장하면 표가 그 조합으로 서고, 양식대로 보내면 저장본이 지워짐
② 수동 단가는 막힌 줄을 세우고 「미확정」으로 셈 · 넣은 날짜는 서버가 붙임
③ ⛔ 저장 자리는 둘 — [내 라이브러리에 저장]은 줄 조합만 싣고 수동 단가는 안 실음
④ 다른 양식을 가져오면 그 종류의 줄 조합·수동 단가가 비워짐
⑤ 고르개가 실제 단가표에서 품셈·자원을 찾음 · 차례가 겹치면 거절
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
import B08_Quantity.B08_Quantity_Engine_StructureLibrary as library_module # noqa: E402
import B08_Quantity.B08_Quantity_Router_Material as material_module # noqa: E402
import B08_Quantity.B08_Quantity_Router_StructureSheet as router_module # noqa: E402
from B05_Profile.B05_Profile_Structures_Repository import save_structures # noqa: E402
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance # noqa: E402
from B08_Quantity.B08_Quantity_Engine_StructureTemplate import load_template # noqa: E402
from common_util.common_util_auth import verify_session # noqa: E402
from common_util.common_util_project_settings import quantity_settings # noqa: E402
PROJECT_ID = "55555555-5555-5555-5555-555555555555"
SHEETS = f"/api/projects/{PROJECT_ID}/quantity/structure-sheets"
@pytest.fixture()
def project(tmp_path: Path) -> Path:
root = tmp_path / "project"
root.mkdir()
wall = StructureInstance.model_validate(
{
"type_id": "masonry_wet",
"placement": "interval",
"start_m": 0.0,
"end_m": 10.0,
"options": {"height_m": 2.5, "back_len_cm": 45},
}
)
save_structures(str(root), [wall], base_revision=0)
return root
@pytest.fixture()
def client(project: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> TestClient:
async def fake_root(project_id):
return str(project)
async def no_route(project_id):
return {}
async def real_build(project_id):
from B09_Estimation.B09_Estimation_UnitPrice import cached_build
return cached_build()
monkeypatch.setattr(library_module, "STORAGE_BASE_DIR", str(tmp_path / "storage"))
monkeypatch.setattr(router_module, "_project_root", fake_root)
monkeypatch.setattr(router_module, "_price_build", real_build)
monkeypatch.setattr(material_module, "_section_modes", no_route)
monkeypatch.setattr(material_module, "_ground_types", no_route)
app = FastAPI()
app.include_router(router_module.router)
app.dependency_overrides[verify_session] = lambda: {"company_id": 7, "user_id": 42}
return TestClient(app)
def _key(client: TestClient) -> str:
return client.get(SHEETS).json()["sheets"][0]["key"]
def _table(client: TestClient, key: str) -> dict:
response = client.get(f"{SHEETS}/unit-price", params={"sheet_key": key})
assert response.status_code == 200, response.text
return response.json()
def test_줄을_고치고_수동_단가를_넣으면_저장_자리가_둘로_갈린다(
client: TestClient, project: Path, tmp_path: Path
) -> None:
key = _key(client)
before = _table(client, key)
assert before["editor"]["edited"] is False and before["unit_price"]["unconfirmed"] == 0
rows = before["editor"]["rows"]
# 기초잡석(3) 빼고 · 고정 수량 1.5 줄 더하기 · 모르터(2)에 수동 단가.
edited = [
rows[0],
rows[1],
{"seq": 4, "name": "추가 잡석", "quantity": 1.5, "ref_code": "B-FP-12-25"},
]
saved = client.put(
f"{SHEETS}/unit-price",
json={
"sheet_key": key,
"rows": edited,
"manual_prices": {
"2": {"material": 80000, "source": "견적 3곳 평균"},
"3": {"labor": 1},
},
},
)
assert saved.status_code == 200, saved.text
assert saved.json()["edited"] is True and saved.json()["manual_prices"] == 1
after = _table(client, key)
table = after["unit_price"]
assert [row["seq"] for row in table["rows"]] == [1, 2, 4]
mortar = table["rows"][1]
assert mortar["manual"] is True and mortar["manual_entered_at"]
assert table["unconfirmed"] == 1 and table["complete"] is True
assert table["rows"][2]["quantity"] == pytest.approx(1.5) and table["rows"][2]["total"] > 0
# 저장 자리 둘 — 줄 조합은 종류별, 수동 단가는 프로젝트 칸.
settings = quantity_settings(str(project))
assert [row["seq"] for row in settings["structure_unit_price_rows"]["masonry_wet"]] == [1, 2, 4]
assert settings["structure_manual_prices"]["masonry_wet"]["2"]["source"] == "견적 3곳 평균"
# ⛔ 내 라이브러리에는 줄 조합만 — 수동 단가 흔적이 없음.
mine = client.put(f"{SHEETS}/library/personal", json={"sheet_key": key})
assert mine.status_code == 200, mine.text
folder = tmp_path / "storage" / "7" / "42" / "library"
item = json.loads((folder / f"{mine.json()['code']}.json").read_text(encoding="utf-8"))
assert [row["seq"] for row in item["unit_price"]["rows"]] == [1, 2, 4]
assert "80000" not in json.dumps(item) and "견적" not in json.dumps(item)
# 양식대로 되돌리면 줄 조합 저장본이 지워짐.
back = client.put(
f"{SHEETS}/unit-price",
json={"sheet_key": key, "rows": after["editor"]["default_rows"], "manual_prices": {}},
)
assert back.json()["edited"] is False
assert "masonry_wet" not in quantity_settings(str(project))["structure_unit_price_rows"]
def test_다른_양식을_가져오면_줄_조합과_수동_단가가_비워진다(
client: TestClient, project: Path
) -> None:
key = _key(client)
rows = load_template("masonry_wet")["unit_price"]["rows"][:2]
client.put(
f"{SHEETS}/unit-price",
json={"sheet_key": key, "rows": rows, "manual_prices": {"2": {"material": 1}}},
)
taken = client.put(
f"{SHEETS}/library/import",
json={
"type_id": "masonry_wet",
"tier": "program",
"code": load_template("masonry_wet")["code"],
},
)
assert taken.status_code == 200, taken.text
assert (
taken.json()["cleared_unit_price_rows"] is True
and taken.json()["cleared_manual_prices"] == 1
)
settings = quantity_settings(str(project))
assert "masonry_wet" not in settings["structure_unit_price_rows"]
assert "masonry_wet" not in settings["structure_manual_prices"]
def test_고르개와_차례_겹침_거절(client: TestClient) -> None:
found = client.get(f"{SHEETS}/price-search", params={"q": "FP-12-25", "kind": "work"})
assert found.status_code == 200, found.text
assert any(item["code"] == "B-FP-12-25" for item in found.json()["items"])
resource = client.get(f"{SHEETS}/price-search", params={"q": "보통인부", "kind": "resource"})
assert resource.json()["items"], "노임 보통인부가 자원 갈래에 있어야 함"
twice = [{"seq": 1, "name": "a", "quantity": 1}, {"seq": 1, "name": "b", "quantity": 1}]
bad = client.put(f"{SHEETS}/unit-price", json={"sheet_key": _key(client), "rows": twice})
assert bad.status_code == 422