Перейти к содержанию

Activity — задачи и дедлайны

Модуль activity — лёгкие задачи с дедлайнами, привязанные к любым записям CRM (лидам, сделкам, контактам). При наступлении дедлайна автоматически уведомляет ответственного через системный чат.

Ключевая особенность — полиморфная привязка: одна и та же модель Activity обслуживает все сущности CRM через пару полей res_model + res_id. Не нужны отдельные таблицы lead_activity, sale_activity и т.д.

Архитектура

graph LR
    L[Lead]
    S[Sale]
    P[Partner]
    A[Activity<br/>res_model + res_id]
    AT[ActivityType]
    U[User]
    CR[Cron]
    C[Chat]

    L -.->|"res_model='lead', res_id=42"| A
    S -.->|"res_model='sale', res_id=17"| A
    P -.->|"res_model='partner', res_id=8"| A
    A --> AT
    A -->|user_id| U
    CR -->|каждую минуту| A
    A -->|при дедлайне| C

    style A fill:#dde7ff,stroke:#5170c4

Модель Activity

res_model Char(255) required

Имя модели записи: lead, sale, partner. Совпадает с __table__ соответствующей DotModel.

res_id Integer required

ID записи. Не FK на конкретную таблицу — связь полиморфная.

activity_type_id Many2one<ActivityType> required

Тип активности — справочник: «Звонок», «Встреча», «Письмо», «Задача».

summary Char(255)

Короткое описание для UI и уведомления.

note Text

Подробное описание (markdown / plain).

date_deadline Datetime required indexed

Дедлайн в UTC. Индексирован — cron быстро находит просроченные.

user_id Many2one<User> required indexed

Кому назначена. Default — текущий пользователь из сессии.

state Selection indexed

planned (запланирована) → today (сегодня дедлайн) → overdue (просрочена) → done/cancelled. Cron сам переводит между состояниями.

done / done_datetime bool / Datetime

Выполнена ли + когда. После done=true cron перестаёт интересоваться.

notification_sent bool

Флаг «уведомление отправлено». Защита от повторных уведомлений: cron бежит каждую минуту, но напомнит только один раз.

Полиморфная привязка

(res_model, res_id) — это пара полей, по которой построен составной индекс:

__indexes__ = [("res_model", "res_id")]

Это даёт быстрый поиск всех активностей по конкретной записи:

# Все активности лида #42
activities = await Activity.search(
    filter=[("res_model", "=", "lead"), ("res_id", "=", 42)],
)

# Все активности любого типа на сегодня
today_activities = await Activity.search(
    filter=[("state", "=", "today"), ("user_id", "=", current_user_id)],
)

Нет FK — нет каскада

Поскольку res_model + res_id это не настоящий FK, удаление лида не удаляет его активности автоматически. На практике это решается двумя путями:

  1. Soft delete записей — лид помечается active=false, а не удаляется.
  2. Cron-уборщик — раз в день удаляет активности, у которых нет привязанной записи.

Создание

await Activity.create_for_record(
    res_model="lead",
    res_id=lead.id,
    activity_type_id=type_call_id,
    user_id=manager.id,
    summary="Перезвонить по поводу заказа",
    days=2,  # дедлайн через 2 дня от сейчас
)

days — удобный шорткат. Если нужно точное время — передавай date_deadline=datetime(...) напрямую.

Cron — главный механизм Activity

Каждую минуту запускается Activity.check_deadlines() (cron-задача Activity: check deadlines):

@hybridmethod
async def check_deadlines(self):
    now = datetime.now(timezone.utc)

    # 1) Все просроченные → state='overdue'
    overdue = await self.search(filter=[
        ("date_deadline", "<", now),
        ("done", "=", False),
        ("state", "!=", "overdue"),
        ("state", "!=", "cancelled"),
    ])
    await Activity.update_bulk([a.id for a in overdue], Activity(state="overdue"))

    # 2) Все наступившие, по которым ещё не отправляли уведомление
    pending = await self.search(filter=[
        ("date_deadline", "<=", now),
        ("done", "=", False),
        ("notification_sent", "=", False),
        ("state", "!=", "cancelled"),
    ])
    for activity in pending:
        await self._send_notification(...)
        await activity.update(Activity(notification_sent=True))

Два прохода:

  1. Перевести в overdue всё, что прошло дедлайн (без отправки — может быть давно).
  2. Послать уведомление по тем, что только что наступили (notification_sent=False).

Это разделение нужно, чтобы при первом старте cron'а после простоя система не завалила пользователя 100 уведомлениями о просроченных задачах за месяц — notification_sent=true уже стоит у тех, что были обработаны.

Уведомления — системный чат

_send_notification ищет (или создаёт) системный чат пользователя и пишет в него:

sequenceDiagram
    participant CR as Cron
    participant A as Activity
    participant SC as SystemChat
    participant CM as ChatMessage
    participant WS as WebSocket
    participant U as User

    CR->>A: check_deadlines()
    A->>SC: get_or_create("__system__{user_id}")
    SC-->>A: chat_id
    A->>CM: post_message(<br/>chat_id, <br/>body="🔔 Перезвонить — срок наступил",<br/>res_model='lead', res_id=42)
    CM->>WS: broadcast new_message
    WS->>U: 🔔 Уведомление в углу экрана

Системный чатChat(chat_type='direct', is_internal=true, name='__system__{user_id}'). Только пользователь и система. Уведомления о дедлайнах, событиях системы, ошибках интеграций приходят сюда.

res_model + res_id сохраняются в ChatMessage — клик по уведомлению открывает соответствующую запись (лид, сделку и т.д.).

Поле в модели ChatMessage пока закомментировано

На уровне Activity._send_notification параметры res_model/res_id уже передаются в ChatMessage.post_message, но в самой модели поля закомментированы. После раскомментирования и миграции БД клик по уведомлению начнёт переходить на привязанную запись.

Activity types

Справочник ActivityType — для UI: иконки, цвета, дефолтные дедлайны.

await ActivityType.create(payload=ActivityType(
    name="Звонок",
    icon="phone",
    color="green",
    default_days_offset=1,  # завтра
))

Связь с другими модулями

Модуль Использование
cron Запускает check_deadlines каждую минуту
chat Создаёт системный чат, шлёт сообщения-уведомления
users Системный пользователь (SYSTEM_USER_ID) — автор уведомлений
chat_web_push Если включён — пушит уведомление в браузер/PWA

См. также