"""Уведомления центра «К» при изменениях плана на день.""" from __future__ import annotations import logging from datetime import date from typing import Optional from sqlalchemy import select from app import db from app.models import Component, Ingredient, Recipe, UnloadingGroup from app.services.notification_center_service import create_notification, format_detail_timestamp logger = logging.getLogger(__name__) def _q(name: Optional[str]) -> str: text = (name or "").strip() return f"«{text}»" if text else "«без названия»" def _recipe_name(recipe_id: str) -> str: recipe = db.session.execute( select(Recipe.name).where(Recipe.id == recipe_id, Recipe.is_deleted.is_(False)) ).scalar_one_or_none() return recipe or "без названия" def _ingredient_name(ingredient_id: str) -> str: row = db.session.execute( select(Ingredient.name).where( Ingredient.id == ingredient_id, Ingredient.is_deleted.is_(False), ) ).scalar_one_or_none() return row or "без названия" def _group_name(group_id: str) -> str: row = db.session.execute( select(UnloadingGroup.name).where( UnloadingGroup.id == group_id, UnloadingGroup.is_deleted.is_(False), ) ).scalar_one_or_none() return row or "без названия" def _component_name(component_id: str) -> str: row = db.session.execute( select(Component.name).where( Component.id == component_id, Component.is_deleted.is_(False), ) ).scalar_one_or_none() return row or "без названия" def _plan_date_label(plan_date: date) -> str: months = ( "января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря", ) return f"{plan_date.day} {months[plan_date.month - 1]} {plan_date.year}" def _duration_hint(*, plan_date: date, valid_until: Optional[date]) -> str: if valid_until and valid_until > plan_date: return f", действует до {_plan_date_label(valid_until)}" return "" def _on_date(plan_date: date) -> str: return f"на {_plan_date_label(plan_date)}" def _actor(user: str) -> str: name = (user or "").strip() if not name or name == "system": return "система" return name def _detail_body(*parts: str, when: str, user: str) -> str: core = ". ".join(p for p in parts if p) return f"{core}. {when}, {_actor(user)}" def _notify( *, title: str, detail: str, kind: str, plan_date: date, user: str, ) -> None: try: create_notification( title=title, detail=detail, kind=kind, category="daily_plan", page="daily_plan", link_kind="daily_plan", link_id=plan_date.isoformat(), user_login=user or None, ) except Exception: logger.exception("[DAILY-PLAN-NOTIFY] %s", title) def notify_trip_skipped( recipe_id: str, plan_date: date, *, valid_until: Optional[date] = None, user: str = "system", ) -> None: when = format_detail_timestamp() recipe = _q(_recipe_name(recipe_id)) dur = _duration_hint(plan_date=plan_date, valid_until=valid_until) _notify( title=f"Рейс {recipe} убран из плана", detail=_detail_body( f"Рейс {recipe} исключён из плана {_on_date(plan_date)}{dur}", when=when, user=user, ), kind="warning", plan_date=plan_date, user=user, ) def notify_trip_unskipped(recipe_id: str, plan_date: date, *, user: str = "system") -> None: when = format_detail_timestamp() recipe = _q(_recipe_name(recipe_id)) _notify( title=f"Рейс {recipe} снова в плане", detail=_detail_body( f"Рейс {recipe} снова включён в план {_on_date(plan_date)}", when=when, user=user, ), kind="info", plan_date=plan_date, user=user, ) def notify_ingredient_skipped( recipe_id: str, ingredient_id: str, plan_date: date, *, valid_until: Optional[date] = None, user: str = "system", ) -> None: when = format_detail_timestamp() recipe = _q(_recipe_name(recipe_id)) component = _q(_ingredient_name(ingredient_id)) dur = _duration_hint(plan_date=plan_date, valid_until=valid_until) _notify( title=f"{component} убран из плана {recipe}", detail=_detail_body( f"Компонент {component} убран из плана рейса {recipe} {_on_date(plan_date)}{dur}", when=when, user=user, ), kind="warning", plan_date=plan_date, user=user, ) def notify_ingredient_unskipped( recipe_id: str, ingredient_id: str, plan_date: date, *, user: str = "system", ) -> None: when = format_detail_timestamp() recipe = _q(_recipe_name(recipe_id)) component = _q(_ingredient_name(ingredient_id)) _notify( title=f"{component} снова в плане {recipe}", detail=_detail_body( f"Компонент {component} снова в плане рейса {recipe} {_on_date(plan_date)}", when=when, user=user, ), kind="info", plan_date=plan_date, user=user, ) def notify_ingredients_unskipped_all(recipe_id: str, plan_date: date, *, count: int, user: str) -> None: when = format_detail_timestamp() recipe = _q(_recipe_name(recipe_id)) _notify( title=f"Компоненты снова в плане {recipe}", detail=_detail_body( f"Сняты все исключения компонентов ({count}) рейса {recipe} {_on_date(plan_date)}", when=when, user=user, ), kind="info", plan_date=plan_date, user=user, ) def notify_unloading_group_skipped( recipe_id: str, group_id: str, plan_date: date, *, valid_until: Optional[date] = None, user: str = "system", ) -> None: when = format_detail_timestamp() recipe = _q(_recipe_name(recipe_id)) group = _q(_group_name(group_id)) dur = _duration_hint(plan_date=plan_date, valid_until=valid_until) _notify( title=f"Группа {group} убрана из плана {recipe}", detail=_detail_body( f"Группа {group} убрана из плана рейса {recipe} {_on_date(plan_date)}{dur}", when=when, user=user, ), kind="warning", plan_date=plan_date, user=user, ) def notify_unloading_group_unskipped( recipe_id: str, group_id: str, plan_date: date, *, user: str = "system", ) -> None: when = format_detail_timestamp() recipe = _q(_recipe_name(recipe_id)) group = _q(_group_name(group_id)) _notify( title=f"Группа {group} снова в плане {recipe}", detail=_detail_body( f"Группа {group} снова в плане рейса {recipe} {_on_date(plan_date)}", when=when, user=user, ), kind="info", plan_date=plan_date, user=user, ) def notify_unloading_groups_unskipped_all(recipe_id: str, plan_date: date, *, count: int, user: str) -> None: when = format_detail_timestamp() recipe = _q(_recipe_name(recipe_id)) _notify( title=f"Группы снова в плане {recipe}", detail=_detail_body( f"Сняты все исключения групп ({count}) рейса {recipe} {_on_date(plan_date)}", when=when, user=user, ), kind="info", plan_date=plan_date, user=user, ) def notify_ingredient_replaced( recipe_id: str, ingredient_id: str, replacement_component_id: str, plan_date: date, *, valid_until: Optional[date] = None, user: str = "system", ) -> None: when = format_detail_timestamp() recipe = _q(_recipe_name(recipe_id)) original = _q(_ingredient_name(ingredient_id)) replacement = _q(_component_name(replacement_component_id)) dur = _duration_hint(plan_date=plan_date, valid_until=valid_until) _notify( title=f"{original} → {replacement} в {recipe}", detail=_detail_body( f"В рейсе {recipe} вместо {original} {_on_date(plan_date)} будет {replacement}{dur}", when=when, user=user, ), kind="info", plan_date=plan_date, user=user, ) def notify_component_replaced_in_plan( component_id: str, replacement_component_id: str, plan_date: date, *, recipe_count: int, valid_until: Optional[date] = None, user: str = "system", ) -> None: when = format_detail_timestamp() original = _q(_component_name(component_id)) replacement = _q(_component_name(replacement_component_id)) dur = _duration_hint(plan_date=plan_date, valid_until=valid_until) count = max(int(recipe_count or 0), 1) recipes_word = "рецепте" if count == 1 else "рецептах" _notify( title=f"{original} → {replacement} в {count} {recipes_word}", detail=_detail_body( f"Во всех рейсах плана вместо {original} {_on_date(plan_date)} будет {replacement}{dur}", when=when, user=user, ), kind="info", plan_date=plan_date, user=user, ) def notify_ingredient_replacement_undone( recipe_id: str, ingredient_id: str, plan_date: date, *, user: str = "system", ) -> None: when = format_detail_timestamp() recipe = _q(_recipe_name(recipe_id)) original = _q(_ingredient_name(ingredient_id)) _notify( title=f"{original} снова в плане {recipe}", detail=_detail_body( f"Замена отменена: {original} снова в плане рейса {recipe} {_on_date(plan_date)}", when=when, user=user, ), kind="info", plan_date=plan_date, user=user, )