- 줄 조합은 양식+프로젝트(종류별), [내 라이브러리에 저장] 때 양식에 실림 - 수동 단가는 프로젝트만(값·출처·넣은 날짜) — 빨간 테두리 + 「미확정 N건」 - 고르개는 이 프로젝트 단가표에서 품셈·자원을 낱말로 찾음 - 다른 양식을 가져오면 그 종류의 줄 조합·수동 단가도 비움 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
182 lines
7.4 KiB
Python
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
|