Files
site/WESP_REL/app/services/sync_notification_hooks.py
2026-07-17 12:57:18 +03:00

191 lines
6.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Уведомления центра зоотехника при событиях синхронизации."""
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,
)