184 lines
7.0 KiB
Python
184 lines
7.0 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]
|
|
|
|
# #region agent log
|
|
try:
|
|
import pathlib
|
|
_log_path = pathlib.Path("/Users/vlad/Documents/wesp new (1)/.cursor/debug-785e22.log")
|
|
_log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
with _log_path.open("a", encoding="utf-8") as _lf:
|
|
_lf.write(json.dumps({"sessionId":"785e22","hypothesisId":"A,B","location":"wesp_compat_reports.py:list","message":"reports api response","data":{"date_from":date_from,"date_to":date_to,"total_before_filter":len(payload),"returned":len(result),"component_counts":[len(r.get("components") or []) for r in result[:5]]},"timestamp":int(datetime.now(UTC).timestamp()*1000)}, ensure_ascii=False) + "\n")
|
|
except Exception:
|
|
pass
|
|
# #endregion
|
|
|
|
return result
|
|
|
|
|
|
@router.get("/reports/ping")
|
|
def reports_ping_wesp():
|
|
return {"status": "ok"}
|