feat(B09 불러오기): 원가계산서 요율 · 기초 단가표 읽기 · 이 공사 단가 책 이음 · 올린 파일 열쇠 길 (PLAN 707-2 · 707-3)
- 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>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
"""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}
|
||||
Reference in New Issue
Block a user