Router_Quotes 새 길(목록 · 올리기 · 내려받기 · 지우기 · v1/quotes/<자재 키>/) · Store quote · quote_page · pick 칸 · 읽을 때 files 목록 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sDZSjFvoiDjdGpgj3muc3
106 lines
4.8 KiB
Python
106 lines
4.8 KiB
Python
"""166-4 자재단가대비표 견적 열 · 적용 출처 고르기 · 견적서 여러 파일(임시 storage) · 거울."""
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
from uuid import uuid4
|
||
|
||
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_Store as store # noqa: E402
|
||
from B09_Estimation.B09_Estimation_Calc_Base import compute_base # noqa: E402
|
||
from resources.tester.test_144_b09_base_calc import _run_ts # noqa: E402
|
||
|
||
MATERIALS = [
|
||
{"key": "MT1", "name": "자갈", "prices": {"물가정보": 23000, "물가자료": 22000}, "qty": 2},
|
||
{"key": "MT2", "name": "폭약", "prices": {"물가자료": 12000}},
|
||
{"key": "b08:번호판", "name": "국가지점번호판", "prices": {}},
|
||
{"key": "MT4", "name": "모래", "prices": {"물가자료": 30000}},
|
||
]
|
||
INPUTS = {"labor": [], "machines": [], "materials": MATERIALS}
|
||
USER = {
|
||
"materials": {
|
||
"MT1": {"quote": 21000, "quote_page": "견적 A"}, # 견적이 최저 → 자동
|
||
"MT2": {"quote": 12712, "pick": "견적"}, # 견적이 높아도 고름
|
||
"b08:번호판": {"quote": 520000}, # 견적만 있는 줄 = 견적 자동
|
||
"MT4": {"quote": 29000, "applied": 31000, "pick": "물가자료"}, # 고름 › 손 수
|
||
}
|
||
}
|
||
|
||
|
||
def test_견적_최저_고름_손수():
|
||
s = compute_base(INPUTS, USER)
|
||
rows = {r["key"]: r for r in s["compare"]}
|
||
got = {
|
||
k: (r["lowest"], r["lowest_from"], r["applied"], r["applied_from"]) for k, r in rows.items()
|
||
}
|
||
assert got == {
|
||
"MT1": (21000, "견적", 21000, "견적"),
|
||
"MT2": (12000, "물가자료", 12712, "견적"),
|
||
"b08:번호판": (520000, "견적", 520000, "견적"),
|
||
"MT4": (29000, "견적", 30000, "물가자료"),
|
||
}
|
||
assert rows["MT1"]["pages"]["견적"] == "견적 A" and rows["MT2"]["pick"] == "견적"
|
||
assert {r["key"]: r["price"] for r in s["material"]}["MT2"] == 12712 # 목록표 단가 따라감
|
||
assert s["material"][0]["amount"] == 42000
|
||
# 손 수만 있으면 「사용자」 · 고름 없으면 최저(견적 포함)
|
||
s2 = compute_base(INPUTS, {"materials": {"MT4": {"quote": 29000, "applied": 31000}}})
|
||
assert s2["compare"][3]["applied_from"] == "사용자"
|
||
|
||
|
||
def test_거울_TS():
|
||
assert _run_ts(INPUTS, USER) == compute_base(INPUTS, USER)
|
||
|
||
|
||
def test_저장_다듬기_파일_목록(tmp_path):
|
||
raw = {"materials": {"MT2": {"quote": 12712, "quote_page": " 견적 B ", "pick": "없는 출처",
|
||
"files": ["x"]}, "MT1": {"pick": "견적"}}} # fmt: skip
|
||
clean = store.write_user(tmp_path, raw)
|
||
assert clean["materials"] == {
|
||
"MT2": {"quote": 12712, "quote_page": "견적 B"},
|
||
"MT1": {"pick": "견적"},
|
||
} # files 는 저장 안 함(폴더가 목록)
|
||
folder = store.quote_dir(tmp_path, "b08:이름|㎥/hr")
|
||
folder.mkdir(parents=True)
|
||
(folder / "a.pdf").write_bytes(b"1")
|
||
(folder / ".a.pdf.123.upload").write_bytes(b"temp")
|
||
assert folder.parent == store.folder(tmp_path) / "quotes"
|
||
user = store.read_user(tmp_path)
|
||
assert user["materials"]["b08:이름|㎥/hr"] == {"files": ["a.pdf"]}
|
||
assert store.safe_name("../../x\\y:z.pdf") == "y_z.pdf" and store.safe_name("..") == ""
|
||
with pytest.raises(ValueError):
|
||
store.quote_dir(tmp_path, "..")
|
||
|
||
|
||
def test_올리기_목록_내려받기_지우기(tmp_path, monkeypatch):
|
||
from fastapi import FastAPI
|
||
from fastapi.testclient import TestClient
|
||
|
||
from B09_Estimation import B09_Estimation_Router_Quotes as quotes
|
||
|
||
async def root(_project_id):
|
||
return tmp_path
|
||
|
||
monkeypatch.setattr(quotes, "_project_root", root)
|
||
app = FastAPI()
|
||
app.include_router(quotes.router, prefix="/api/projects")
|
||
client = TestClient(app)
|
||
base = f"/api/projects/{uuid4()}/estimation/quotes"
|
||
key = {"key": "b08:pipe_600"}
|
||
files = [("files", ("견적1.pdf", b"one", "application/pdf")),
|
||
("files", ("sub/견적2.xlsx", b"two!", "application/octet-stream"))] # fmt: skip
|
||
listed = client.post(base, params=key, files=files).json()
|
||
assert [(f["name"], f["size"]) for f in listed] == [("견적1.pdf", 3), ("견적2.xlsx", 4)]
|
||
assert (store.quote_dir(tmp_path, "b08:pipe_600") / "견적1.pdf").read_bytes() == b"one"
|
||
assert len(client.get(base, params=key).json()) == 2
|
||
got = client.get(base + "/file", params={**key, "name": "견적2.xlsx"})
|
||
assert got.status_code == 200 and got.content == b"two!"
|
||
assert client.get(base + "/file", params={**key, "name": "../x"}).status_code == 404
|
||
assert client.get(base, params={"key": ".."}).status_code == 400
|
||
left = client.delete(base, params={**key, "name": "견적1.pdf"}).json()
|
||
assert [f["name"] for f in left] == ["견적2.xlsx"]
|
||
assert store.read_user(tmp_path)["materials"] == {"b08:pipe_600": {"files": ["견적2.xlsx"]}}
|