- POST estimation/import/rates · import/prices (file | file_key · 저장 안 함 · patch 만) - 요율 = 행 이름 → 줄 키 · 손입력 칸(안전 기초액 · 이윤 보정 · 부가세 조정 · 조달수수료율) · 엑셀 대조 - 단가표 = 노임 · 자재 · 기계 → labor_cover · machine_cover · materials.applied · 짝 없음 · 짝 여럿 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
6.1 KiB
Python
145 lines
6.1 KiB
Python
"""B09 경쟁사 결과물 불러오기 길 — 요율(707-2) · 기초 단가표(707-3) · 저장 안 함(제안 → 화면 캐시).
|
|
|
|
POST /api/projects/{id}/estimation/import/rates form: file | file_key · user?(화면 캐시 JSON)
|
|
원가계산서 요율 → {file_key, name, kind, rows, rates, notes, check, patch{estimate}}
|
|
POST /api/projects/{id}/estimation/import/prices form: file | file_key · user?
|
|
노임 · 자재 · 기계 단가표 → 이 공사 단가 책에 이음 →
|
|
{file_key, name, counts, linked, missing, patch{labor_cover, machine_cover, materials}}
|
|
|
|
올린 파일 = 서버 임시 폴더에 내용 지문(`file_key`)으로 한 시간 둠 — 같은 파일을 요율 · 단가표 ·
|
|
내역 줄(707-1 · sub5)이 다시 올리지 않고 `file_key` 로 씀(`load` · `remember`).
|
|
화면은 `patch` 를 캐시에 얹고 [저장] 때 서버가 정본으로 다시 셈(5장).
|
|
`B09_Estimation_Router` 가 끼워 넣음(같은 머리 · 같은 권한).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import tempfile
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
|
|
|
|
from B03_FileInput.B03_FileInput_Engine import save_upload_stream
|
|
from B08_Quantity.B08_Quantity_Router_Hydro import _project_root
|
|
from B09_Estimation import B09_Estimation_Import_Prices as import_prices
|
|
from B09_Estimation import B09_Estimation_Import_Rates as import_rates
|
|
from B09_Estimation import B09_Estimation_Prices as prices
|
|
from B09_Estimation import B09_Estimation_Store as store
|
|
from B09_Estimation.B09_Estimation_Calc_Base import compute_base
|
|
from B09_Estimation.B09_Estimation_Rates import rate_table
|
|
from M02_MasterTemplete.M02_Hopyo_Excel import ExcelError
|
|
|
|
router = APIRouter()
|
|
PATH = "/{project_id}/estimation/import"
|
|
KEEP_S = 3600
|
|
_KEY = re.compile(r"^[0-9a-f]{24}$")
|
|
|
|
|
|
def _dir() -> Path:
|
|
return Path(tempfile.gettempdir()) / "aislo_b09_import"
|
|
|
|
|
|
def load(file_key: str) -> tuple[str, bytes]:
|
|
"""지문 → (파일 이름, 내용) — 없거나 지난 것 = 404."""
|
|
if not _KEY.match(file_key or ""):
|
|
raise HTTPException(400, "file_key 꼴이 아님")
|
|
data, name = _dir() / f"{file_key}.bin", _dir() / f"{file_key}.name"
|
|
if not data.is_file() or time.time() - data.stat().st_mtime > KEEP_S:
|
|
raise HTTPException(404, "올린 파일이 지났음 — 다시 올림")
|
|
return name.read_text(encoding="utf-8"), data.read_bytes()
|
|
|
|
|
|
async def remember(upload: UploadFile) -> tuple[str, str, bytes]:
|
|
"""올린 파일 → (지문, 이름, 내용) · 한 시간 지난 파일 지움."""
|
|
folder = _dir()
|
|
folder.mkdir(parents=True, exist_ok=True)
|
|
for old in folder.glob("*.bin"):
|
|
if time.time() - old.stat().st_mtime > KEEP_S:
|
|
old.unlink(missing_ok=True)
|
|
old.with_suffix(".name").unlink(missing_ok=True)
|
|
temp = folder / f"up-{time.time_ns()}.part"
|
|
try:
|
|
await save_upload_stream(upload, temp)
|
|
except ValueError as error:
|
|
raise HTTPException(400, str(error)) from error
|
|
data = temp.read_bytes()
|
|
key = hashlib.sha256(data).hexdigest()[:24]
|
|
temp.replace(folder / f"{key}.bin")
|
|
name = store.safe_name(upload.filename or "") or "불러온 파일.xlsx"
|
|
(folder / f"{key}.name").write_text(name, encoding="utf-8")
|
|
return key, name, data
|
|
|
|
|
|
async def _file(file: UploadFile | None, file_key: str | None) -> tuple[str, str, bytes]:
|
|
if file is not None:
|
|
return await remember(file)
|
|
if file_key:
|
|
return (file_key, *load(file_key))
|
|
raise HTTPException(400, "file 또는 file_key 가 필요함")
|
|
|
|
|
|
def _draft(user: str | None) -> dict[str, Any] | None:
|
|
try:
|
|
raw = json.loads(user) if user else None
|
|
except ValueError as error:
|
|
raise HTTPException(400, "user 가 JSON 이 아님") from error
|
|
return store.clean_user(raw) if isinstance(raw, dict) else None
|
|
|
|
|
|
@router.post(PATH + "/rates")
|
|
async def import_rate_sheet(
|
|
project_id: UUID,
|
|
file: UploadFile | None = File(None),
|
|
file_key: str | None = Form(None),
|
|
user: str | None = Form(None),
|
|
) -> dict[str, Any]:
|
|
root = await _project_root(project_id)
|
|
key, name, data = await _file(file, file_key)
|
|
draft = _draft(user)
|
|
mine = (draft if draft is not None else await asyncio.to_thread(store.read_user, root))[
|
|
"estimate"
|
|
]
|
|
try:
|
|
read = await asyncio.to_thread(import_rates.read_rates, name, data)
|
|
except ExcelError as error:
|
|
raise HTTPException(400, str(error)) from error
|
|
check = await asyncio.to_thread(import_rates.check, read, mine, rate_table())
|
|
estimate = {**mine, **read["estimate"], "rates": {**(mine.get("rates") or {}), **read["rates"]}}
|
|
keep = ("kind", "sheet", "rows", "rates", "notes")
|
|
return {"file_key": key, "name": name, **{k: read[k] for k in keep}, "check": check,
|
|
"patch": {"estimate": estimate}} # fmt: skip
|
|
|
|
|
|
@router.post(PATH + "/prices")
|
|
async def import_price_sheets(
|
|
project_id: UUID,
|
|
file: UploadFile | None = File(None),
|
|
file_key: str | None = Form(None),
|
|
user: str | None = Form(None),
|
|
) -> dict[str, Any]:
|
|
from B09_Estimation import B09_Estimation_Router as main # 서로 부름 — 부를 때만
|
|
|
|
root = await _project_root(project_id)
|
|
key, name, data = await _file(file, file_key)
|
|
draft = _draft(user)
|
|
mine = draft if draft is not None else await asyncio.to_thread(store.read_user, root)
|
|
try:
|
|
read = await asyncio.to_thread(import_prices.read_prices, name, data)
|
|
except ExcelError as error:
|
|
raise HTTPException(400, str(error)) from error
|
|
keys = await main._demand(root, draft)
|
|
b08 = await main._b08_for(project_id, [], True)
|
|
book = await asyncio.to_thread(store.read_book, root)
|
|
inputs = await asyncio.to_thread(prices.build_inputs, book, keys, b08["materials"])
|
|
base = compute_base(inputs, {}) # 덮개 뺀 이 공사 단가 책
|
|
out = import_prices.propose(read, base, mine)
|
|
sheets = {k: read[f"{k}_sheet"] for k in import_prices.KINDS}
|
|
return {"file_key": key, "name": name, "sheets": sheets, **out}
|