366 lines
10 KiB
Python
366 lines
10 KiB
Python
"""Уведомления центра «К» при изменениях плана на день."""
|
||
|
||
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,
|
||
)
|