191 lines
6.4 KiB
Python
191 lines
6.4 KiB
Python
"""Уведомления центра зоотехника при событиях синхронизации."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from typing import Any, Dict, Iterable, List, Optional, Set
|
||
|
||
from sqlalchemy import select
|
||
|
||
from app import db
|
||
from app.models import (
|
||
Ingredient,
|
||
Recipe,
|
||
SyncClient,
|
||
SyncClientDisplayName,
|
||
UnloadingGroup,
|
||
)
|
||
from app.models.report import LoadingReport, UnloadingReport
|
||
from app.models.sync import SyncQueue
|
||
from app.services.notification_center_service import create_notification, format_detail_timestamp
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
RECIPE_DELIVERY_TABLES = frozenset(
|
||
{"recipe", "ingredient", "unloading_group", "period_recipes"}
|
||
)
|
||
REPORT_PARENT_TABLES = frozenset({"loading_report", "unloading_report"})
|
||
|
||
|
||
def resolve_sync_client_label(node_id: Optional[str]) -> str:
|
||
if not node_id:
|
||
return "терминал"
|
||
display = db.session.execute(
|
||
select(SyncClientDisplayName.display_name).where(
|
||
SyncClientDisplayName.node_id == node_id
|
||
)
|
||
).scalar_one_or_none()
|
||
if display and str(display).strip():
|
||
return str(display).strip()
|
||
client_name = db.session.execute(
|
||
select(SyncClient.client_name).where(SyncClient.node_id == node_id)
|
||
).scalar_one_or_none()
|
||
if client_name and str(client_name).strip():
|
||
return str(client_name).strip()
|
||
return f"терминал {node_id[:8]}"
|
||
|
||
|
||
def _q(name: Optional[str]) -> str:
|
||
text = (name or "").strip()
|
||
return f"«{text}»" if text else "«без названия»"
|
||
|
||
|
||
def _recipe_name(recipe_id: Optional[str]) -> str:
|
||
if not recipe_id:
|
||
return "без названия"
|
||
recipe = db.session.get(Recipe, recipe_id)
|
||
if recipe and not getattr(recipe, "is_deleted", False):
|
||
return recipe.name or "без названия"
|
||
return "без названия"
|
||
|
||
|
||
def _recipe_id_from_sync_task(task: SyncQueue) -> Optional[str]:
|
||
table_name = task.table_name
|
||
record_id = task.record_id
|
||
if table_name == "recipe":
|
||
return record_id
|
||
if table_name == "ingredient":
|
||
row = db.session.get(Ingredient, record_id)
|
||
return row.recipe_id if row else None
|
||
if table_name == "unloading_group":
|
||
row = db.session.get(UnloadingGroup, record_id)
|
||
return row.recipe_id if row else None
|
||
if table_name == "period_recipes":
|
||
parts = str(record_id).split(":", 1)
|
||
return parts[1] if len(parts) == 2 else None
|
||
return None
|
||
|
||
|
||
def notify_tasks_delivered_to_client(
|
||
client_node_id: str, tasks: Iterable[SyncQueue]
|
||
) -> None:
|
||
"""Рейс (или его состав) доставлен на терминал клиента."""
|
||
terminal = resolve_sync_client_label(client_node_id)
|
||
when = format_detail_timestamp()
|
||
notified: Set[str] = set()
|
||
for task in tasks:
|
||
if task.table_name not in RECIPE_DELIVERY_TABLES:
|
||
continue
|
||
recipe_id = _recipe_id_from_sync_task(task)
|
||
if not recipe_id or recipe_id in notified:
|
||
continue
|
||
notified.add(recipe_id)
|
||
recipe_label = _q(_recipe_name(recipe_id))
|
||
try:
|
||
create_notification(
|
||
title="Рейс на терминале",
|
||
detail=(
|
||
f"Рейс {recipe_label} передан на терминал {terminal} — {when}"
|
||
),
|
||
kind="info",
|
||
category="sync",
|
||
page="recipes",
|
||
link_kind="recipe",
|
||
link_id=recipe_id,
|
||
)
|
||
except Exception:
|
||
logger.exception(
|
||
"[SYNC-NOTIFY] recipe delivered client=%s recipe=%s",
|
||
client_node_id,
|
||
recipe_id,
|
||
)
|
||
|
||
|
||
def _report_notification_from_row(
|
||
*,
|
||
table_name: str,
|
||
report_id: str,
|
||
client_node_id: str,
|
||
) -> None:
|
||
terminal = resolve_sync_client_label(client_node_id)
|
||
when = format_detail_timestamp()
|
||
if table_name == "loading_report":
|
||
report = db.session.get(LoadingReport, report_id)
|
||
if not report or getattr(report, "is_deleted", False):
|
||
return
|
||
recipe_label = _q(report.recipe_name or _recipe_name(report.recipe_id))
|
||
create_notification(
|
||
title="Отчёт о загрузке",
|
||
detail=(
|
||
f"С терминала {terminal} пришёл отчёт о загрузке по рейсу {recipe_label} — {when}"
|
||
),
|
||
kind="success",
|
||
category="sync",
|
||
page="reports",
|
||
link_kind="report_loading",
|
||
link_id=report_id,
|
||
)
|
||
return
|
||
|
||
if table_name == "unloading_report":
|
||
report = db.session.get(UnloadingReport, report_id)
|
||
if not report or getattr(report, "is_deleted", False):
|
||
return
|
||
recipe_label = _q(report.recipe_name or _recipe_name(report.recipe_id))
|
||
link_id = report.loading_report_id or report_id
|
||
create_notification(
|
||
title="Отчёт о выгрузке",
|
||
detail=(
|
||
f"С терминала {terminal} пришёл отчёт о выгрузке по рейсу {recipe_label} — {when}"
|
||
),
|
||
kind="success",
|
||
category="sync",
|
||
page="reports",
|
||
link_kind="report_loading",
|
||
link_id=link_id,
|
||
)
|
||
|
||
|
||
def notify_reports_pushed_from_client(
|
||
client_node_id: str, applied_changes: Iterable[Dict[str, Any]]
|
||
) -> None:
|
||
"""Отчёт с полевого терминала принят на сервер."""
|
||
notified: Set[str] = set()
|
||
for change in applied_changes:
|
||
table_name = change.get("table_name")
|
||
if table_name not in REPORT_PARENT_TABLES:
|
||
continue
|
||
action = change.get("action")
|
||
if action not in ("create", "update"):
|
||
continue
|
||
record_id = change.get("record_id")
|
||
if not record_id:
|
||
continue
|
||
key = f"{table_name}:{record_id}"
|
||
if key in notified:
|
||
continue
|
||
notified.add(key)
|
||
try:
|
||
_report_notification_from_row(
|
||
table_name=table_name,
|
||
report_id=record_id,
|
||
client_node_id=client_node_id,
|
||
)
|
||
except Exception:
|
||
logger.exception(
|
||
"[SYNC-NOTIFY] report received client=%s table=%s id=%s",
|
||
client_node_id,
|
||
table_name,
|
||
record_id,
|
||
)
|