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) — это пара полей, по которой построен составной индекс:
Это даёт быстрый поиск всех активностей по конкретной записи:
# Все активности лида #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, удаление лида не удаляет его активности автоматически. На практике это решается двумя путями:
- Soft delete записей — лид помечается
active=false, а не удаляется. - 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))
Два прохода:
- Перевести в
overdueвсё, что прошло дедлайн (без отправки — может быть давно). - Послать уведомление по тем, что только что наступили (
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 |
См. также¶
- Cron — фоновые задачи — общий механизм запуска
- Chat Module — куда приходят уведомления
- Чат и звонки → Архитектура — что такое системный чат