klg-asutk-app/backend/app/api/routes/import_export.py

149 lines
6.3 KiB
Python

"""
Import/Export Excel (XLSX) — массовая загрузка и выгрузка данных.
Поддерживает: компоненты, директивы, персонал ПЛГ, дефекты.
"""
import io
import logging
import uuid
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from app.api.deps import get_db, get_current_user
from app.api.helpers import audit
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/import-export", tags=["import-export"])
@router.get("/export/{entity_type}")
def export_xlsx(entity_type: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
"""
Экспорт в XLSX. Типы: components, directives, bulletins, specialists, defects, work_orders.
"""
try:
import openpyxl
except ImportError:
raise HTTPException(500, "openpyxl not installed")
data_map = {
"components": ("airworthiness_core", "_components", ["name", "part_number", "serial_number", "ata_chapter", "manufacturer", "condition", "current_hours", "current_cycles"]),
"directives": ("airworthiness_core", "_directives", ["number", "title", "issuing_authority", "effective_date", "compliance_type", "status"]),
"bulletins": ("airworthiness_core", "_bulletins", ["number", "title", "manufacturer", "category", "issued_date", "status"]),
"specialists": ("personnel_plg", "_specialists", ["full_name", "personnel_number", "position", "category", "license_number", "status"]),
"defects": ("defects", "_defects", ["aircraft_reg", "ata_chapter", "description", "severity", "discovered_during", "status"]),
"work_orders": ("work_orders", "_work_orders", ["wo_number", "aircraft_reg", "wo_type", "title", "priority", "status", "estimated_manhours"]),
}
if entity_type not in data_map:
raise HTTPException(400, f"Unknown entity: {entity_type}. Supported: {list(data_map.keys())}")
module_name, store_name, columns = data_map[entity_type]
mod = __import__(f"app.api.routes.{module_name}", fromlist=[store_name])
items = list(getattr(mod, store_name).values())
wb = openpyxl.Workbook()
ws = wb.active
ws.title = entity_type
ws.append(columns)
for item in items:
row = [str(item.get(col, "")) for col in columns]
ws.append(row)
# Style header
from openpyxl.styles import Font, PatternFill
for cell in ws[1]:
cell.font = Font(bold=True)
cell.fill = PatternFill(start_color="DBEAFE", end_color="DBEAFE", fill_type="solid")
for col in ws.columns:
max_len = max(len(str(cell.value or "")) for cell in col)
ws.column_dimensions[col[0].column_letter].width = min(max_len + 2, 40)
buf = io.BytesIO()
wb.save(buf)
buf.seek(0)
audit(db, user, "export_xlsx", entity_type, description=f"XLSX export: {entity_type} ({len(items)} rows)")
db.commit()
return StreamingResponse(
buf,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename={entity_type}_{datetime.now().strftime('%Y%m%d')}.xlsx"},
)
@router.post("/import/{entity_type}")
async def import_xlsx(
entity_type: str,
file: UploadFile = File(...),
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
"""Импорт из XLSX. Первая строка — заголовки."""
try:
import openpyxl
except ImportError:
raise HTTPException(500, "openpyxl not installed")
if not file.filename.endswith(('.xlsx', '.xls')):
raise HTTPException(400, "Only .xlsx files accepted")
content = await file.read()
wb = openpyxl.load_workbook(io.BytesIO(content))
ws = wb.active
rows = list(ws.iter_rows(values_only=True))
if len(rows) < 2:
raise HTTPException(400, "File must have header + at least 1 data row")
headers = [str(h).strip().lower() if h else "" for h in rows[0]]
imported = 0
errors = []
for i, row in enumerate(rows[1:], start=2):
try:
item = {headers[j]: (str(v).strip() if v is not None else "") for j, v in enumerate(row) if j < len(headers)}
item["id"] = str(uuid.uuid4())
item["created_at"] = datetime.now(timezone.utc).isoformat()
if entity_type == "components":
from app.api.routes.airworthiness_core import _components
if not item.get("name") or not item.get("part_number") or not item.get("serial_number"):
errors.append(f"Row {i}: missing required fields (name, part_number, serial_number)")
continue
item.setdefault("condition", "serviceable")
item["current_hours"] = float(item.get("current_hours", 0) or 0)
item["current_cycles"] = int(item.get("current_cycles", 0) or 0)
_components[item["id"]] = item
elif entity_type == "specialists":
from app.api.routes.personnel_plg import _specialists
if not item.get("full_name") or not item.get("personnel_number"):
errors.append(f"Row {i}: missing full_name or personnel_number")
continue
item.setdefault("status", "active")
item.setdefault("specializations", [])
_specialists[item["id"]] = item
elif entity_type == "directives":
from app.api.routes.airworthiness_core import _directives
if not item.get("number") or not item.get("title"):
errors.append(f"Row {i}: missing number or title")
continue
item.setdefault("status", "open")
item.setdefault("aircraft_types", [])
_directives[item["id"]] = item
else:
errors.append(f"Import not supported for: {entity_type}")
break
imported += 1
except Exception as e:
errors.append(f"Row {i}: {str(e)}")
audit(db, user, "import_xlsx", entity_type, description=f"XLSX import: {entity_type} ({imported} imported, {len(errors)} errors)")
db.commit()
return {"imported": imported, "errors": errors[:20], "total_rows": len(rows) - 1}