Files
site/apps/api/app/modules/zootech/wesp_compat_reports.py
T
2026-07-17 12:57:18 +03:00

173 lines
6.3 KiB
Python

"""WESP-shaped reports API for copied static UI (/api/reports)."""
from __future__ import annotations
import json
from datetime import UTC, datetime, timedelta
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query, status
from app.core.database import session_scope
from app.modules.sync.tenant import TenantContext, require_enterprise_zootech
from app.modules.zootech.report_models import ZootechLoadingReport, ZootechUnloadingReport
from sqlalchemy import select
router = APIRouter()
def _require_ent(enterprise_id: str, tenant: TenantContext) -> None:
if tenant.enterprise_id != enterprise_id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ENTERPRISE_FORBIDDEN")
def _parse_payload(row: ZootechLoadingReport | ZootechUnloadingReport) -> dict[str, Any]:
if not row.payload_json:
return {}
try:
data = json.loads(row.payload_json)
except json.JSONDecodeError:
return {}
return data if isinstance(data, dict) else {}
def _parse_date_range(
date_from: str | None, date_to: str | None
) -> tuple[datetime, datetime] | None:
if not date_from or not date_to:
end = datetime.now(UTC)
return end - timedelta(hours=24), end + timedelta(seconds=1)
try:
start = datetime.strptime(date_from.strip(), "%Y-%m-%d").replace(tzinfo=UTC)
end = datetime.strptime(date_to.strip(), "%Y-%m-%d").replace(tzinfo=UTC) + timedelta(days=1)
return start, end
except ValueError as exc:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"error": True, "message": "Некорректный формат date_from/date_to (YYYY-MM-DD)"},
) from exc
def _parse_report_time(value: Any) -> datetime | None:
if value is None:
return None
if isinstance(value, datetime):
return value if value.tzinfo else value.replace(tzinfo=UTC)
text = str(value).strip()
if not text:
return None
try:
parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
except ValueError:
return None
return parsed if parsed.tzinfo else parsed.replace(tzinfo=UTC)
def _in_range(start_time: datetime | None, window: tuple[datetime, datetime]) -> bool:
if start_time is None:
return False
start, end = window
return start <= start_time < end
def _serialize_unloading_id(unloading_id: str, payload: dict[str, Any]) -> dict[str, Any]:
groups = payload.get("unloading_groups")
if not isinstance(groups, list):
groups = payload.get("groups") if isinstance(payload.get("groups"), list) else []
return {
"id": unloading_id,
"start_time": payload.get("start_time"),
"end_time": payload.get("end_time"),
"total_weight": payload.get("total_weight"),
"total_unloaded_weight": payload.get("total_unloaded_weight"),
"remaining_weight": payload.get("remaining_weight"),
"unloading_groups": groups,
}
def _serialize_unloading(row: ZootechUnloadingReport, payload: dict[str, Any]) -> dict[str, Any]:
return _serialize_unloading_id(row.id, payload)
@router.get("/reports")
def list_reports_wesp(
enterprise_id: str = Query(...),
date_from: str | None = Query(None),
date_to: str | None = Query(None),
limit: int = Query(500, ge=1, le=2000),
offset: int = Query(0, ge=0),
tenant: TenantContext = Depends(require_enterprise_zootech),
):
"""Legacy-compatible aggregated loading reports for reports.html."""
_require_ent(enterprise_id, tenant)
window = _parse_date_range(date_from, date_to)
with session_scope() as db:
loading_rows = list(
db.scalars(
select(ZootechLoadingReport).where(
ZootechLoadingReport.enterprise_id == enterprise_id,
ZootechLoadingReport.is_deleted.is_(False),
)
)
)
unloading_rows = list(
db.scalars(
select(ZootechUnloadingReport).where(
ZootechUnloadingReport.enterprise_id == enterprise_id,
ZootechUnloadingReport.is_deleted.is_(False),
)
)
)
loading_data = [(row.id, _parse_payload(row)) for row in loading_rows]
unloading_data: list[tuple[str, str, dict[str, Any]]] = []
for row in unloading_rows:
payload = _parse_payload(row)
loading_id = str(payload.get("loading_report_id") or "").strip()
if loading_id:
unloading_data.append((loading_id, row.id, payload))
unloading_by_loading: dict[str, tuple[str, dict[str, Any]]] = {
loading_id: (unloading_id, payload) for loading_id, unloading_id, payload in unloading_data
}
payload: list[dict[str, Any]] = []
for row_id, data in loading_data:
start_time = _parse_report_time(data.get("start_time"))
if window and not _in_range(start_time, window):
continue
unloading = unloading_by_loading.get(row_id)
unloading_payload = (
_serialize_unloading_id(unloading[0], unloading[1]) if unloading else None
)
payload.append(
{
"id": row_id,
"recipe_id": data.get("recipe_id"),
"recipe_name": data.get("recipe_name"),
"start_time": data.get("start_time"),
"end_time": data.get("end_time"),
"target_mixing_time": data.get("target_mixing_time"),
"actual_mixing_time": data.get("actual_mixing_time"),
"total_weight": data.get("total_weight"),
"dispenser_type": data.get("dispenser_type") or "dispenser",
"components": data.get("components") if isinstance(data.get("components"), list) else [],
"component_loading_times": (
data.get("component_loading_times")
if isinstance(data.get("component_loading_times"), list)
else []
),
"unloading_data": unloading_payload,
}
)
payload.sort(key=lambda item: str(item.get("start_time") or ""), reverse=True)
result = payload[offset : offset + limit]
return result
@router.get("/reports/ping")
def reports_ping_wesp():
return {"status": "ok"}