Клиенты
Список клиентов и папки, карта подключений, лицевые счета и финансовые операции.
Содержание раздела
- 8. Клиенты
- Папки клиентов — назначение и роль
- 8.1. Клиенты
- 8.2. На карте
- Обещанный платёж (Promise Pay) — назначение и роль
- Блокировки (AbonentsBlock) — назначение и роль
- Лицевые счета (AdminAccounts) — назначение и роль
- Финансовые операции (FinanceOperations) — назначение и роль
- 8.3. Счета
- 8.4. Финансовые операции
8. Клиенты
Раздел меню «Клиенты» — основной рабочий раздел оператора.
Папка и глобальный поиск, счётчики-фильтры и цвет строки, одиннадцать массовых действий, создание клиента с учётной записью и IP, карточка клиента и карта.
Папки клиентов — назначение и роль
Содержание раздела
Папка клиентов — это «контейнер-узел» в дереве клиентов с флагом IS_FOLDER=true. Хранится в той же таблице abonents (поле parent_id), но не имеет тарифа, счёта и финансовых атрибутов. Корневая папка имеет ID=9001.
Зачем нужны папки
Папки решают три задачи:
- Логическая группировка — клиенты раскладываются по агентам, филиалам, типам клиентов, географическим зонам. Это удобно для навигации и фильтрации в админке;
- Контроль доступа операторов — через
AbonentGroupAccessDjango-группа привязывается к конкретным папкам, и оператор видит только своих клиентов; - Корзина удалённых — обычно это спец-папка (#9003 «Удалённые»), куда перемещаются
deleted=true-клиенты для возможного восстановления.
Как устроено дерево
Иерархия держится на parent_id — FK таблицы на саму себя:
parent_id—NULLу корня (#9001), у остальных — ссылка на родителя. Это единственный источник правды об иерархии;- Подпапки поддерживаются, глубина не ограничена одним уровнем;
- FK с PROTECT — нельзя удалить папку, у которой есть дочерние записи. Сначала перенесите или удалите детей.
lft / rght / tree_id / level не используйте.
У новых записей они не заполняются: модель
Abonents не подключена к MPTT-менеджеру, а значит
rebuild() и get_descendants() недоступны. Код,
который читает эти поля, обязан иметь fallback на parent_id —
как это сделано в выборке клиентов по папкам
(views/messages_global.py). Обход поддерева пишется обходом
по уровням через parent_id.
Группы и контроль доступа
Связь «Группа Django ↔ Папка клиента» хранится в AbonentGroupAccess:
- Каждая запись — это «группа GroupX имеет доступ к папке #N и её потомкам»;
- Один оператор может состоять в нескольких группах — тогда видит клиентов из объединения папок;
- Настраивается в
/admin/settings/abonent_group_permissions/(дерево с чекбоксами); - Если у оператора нет ни одной записи AbonentGroupAccess — он видит всех клиентов (нет ограничений по умолчанию);
- Поиск по папкам, экспорт CSV, bulk-actions — все они учитывают этот фильтр.
Папка организации
При включённой мультиорганизации папку верхнего уровня (лежащую внутри корневой «Все») можно закрепить за компанией — она становится её «домом»: туда складываются новые клиенты этой организации, и по ней строятся списки директорий в карточках тарифов.
- Где: карточка папки → кнопка «Изменить» рядом с заголовком. Там же переименование. Вложенным папкам организация не назначается — её задаёт ближайший закреплённый предок;
- Каскад: закрепление проходит вглубь — подпапки, клиенты и их финансовые операции получают организацию папки. Без этого у вложенных записей оставалась бы прежняя компания: в списках горел бы чужой бейдж, а фильтр «Орг.» их бы не находил;
- Счета собственной организации не имеют — они скоупятся через клиента, поэтому в каскад не входят;
- Цвет: иконка закреплённой папки красится акцентным цветом организации из брендинга и наследуется вложенными папками. Служебные папки (Корзина, Операторы связи) организации не имеют и остаются серыми.

Что делать со старыми папками
- Пустая папка без активных детей — можно удалить через UI. FK PROTECT защитит если в ней не считая deleted-клиентов есть живые;
- Папка с deleted-клиентами — нельзя удалить, soft-deleted записи всё ещё ссылаются на parent_id. Сначала «жёстко» удалить deleted-клиентов или перенести в общую корзину;
- Папка «Корзина» (#9003) — не удалять. Это служебная папка-приёмник для восстановления клиентов из архива;
- Перемещение клиента в другую папку — bulk action
move_folderв глобальном поиске (11 операций).
Abonents.delete() в коде выполняется soft-delete: deleted=true + enabled=false, клиент остаётся на своём месте в дереве. Если хотите централизованную «Корзину» — настройте автоматическое перемещение в #9003 через bulk action или добавьте логику в delete() модели.
8.1. Клиенты
Главный список клиентов. Можно работать в двух режимах: дерево папок (по умолчанию) и глобальный поиск. Папки удобны для иерархической навигации по агентам/филиалам/адресам, глобальный поиск — для массовых операций над всей базой.
Папка клиентов
Папка клиентов (/admin/Abonents/<id>/, корень — /admin/Abonents/9001/) — список клиентов конкретной папки с поддержкой подпапок. Каждая папка имеет свой набор быстрых фильтров и массовых действий, рассчитанный на ежедневную работу оператора с одним сегментом базы.

Содержание раздела
KPI и статистика
Над таблицей выводятся 4–5 счётчиков по клиентам текущей папки (без учёта подпапок): Активных, Сейчас онлайн, Выключенных, Всего, Юр. лиц. Числа обновляются вместе с применённым фильтром-чипом.
Чипы быстрых фильтров
Под счётчиками — ряд чипов с готовыми фильтрами. Активен может быть только один. Повторный клик снимает фильтр. Состояние сохраняется в URL — ссылку можно отправить коллеге:
- Должники — баланс счёта < 0.
- Бизнес — юр. лица (
company=true). - Без email / Без телефона — пустые поля контактов (чтобы дополнить данные).
- Онлайн — сейчас в RADIUS-сессии (< 15 мин с last update).
- Заблокированные — есть активная запись в
ABONENTS_BLOCK(admin / negbal / own). - Обещ. платёж — активен ОП (Usluga 30353).
Поиск, сортировка и multi-tarif
- Поиск в шапке таблицы по ФИО, номеру договора, e-mail, телефону. Минимум 2 символа, мгновенный фильтр.
- Шорткат Ctrl+K — фокус в поле поиска из любого места страницы.
- Сортировка по любой колонке кликом на заголовок (▲▼). Стрелка показывает направление.
- Multi-tarif фильтр — checkbox-список тарифов с поиском по названию. Позволяет ограничить выборку папки одним или несколькими тарифами одновременно.
Колонки таблицы
- ☑ — чекбокс выбора для массовых операций.
- № — внутренний ID клиента.
- ФИО — клик открывает карточку.
- Дог. — номер договора.
- Тариф — с иконкой 💼 для бизнес-тарифов.
- Адрес — короткий формат (через фильтр
home_short: ВО → Волгоградская обл., Влг → Волгоград). - Контакты — иконки ✉ / 📞 с tooltip-значениями. Серая иконка = пустое поле.
- Баланс — баланс счёта. Рядом 🤝 при активном обещанном платеже.
- Подкл. — дата подключения.
- 📡 — флаг «онлайн сейчас» (RADIUS-сессия < 15 мин).
- Статус — цветной бейдж: Активен / Заблок. / Удалён.
- ℹ Подробнее — кнопка открывает drawer-панель без перехода на полную карточку.
Подсветка строк (класс строки задаётся в abonents_folder_rows.html):
- Розовый фон — блокировка за неуплату: есть запись
в
ABONENTS_BLOCKс флагомb_negbal. Просто минус на счёте строку не красит — пока автоблокировка не сработала, клиент выглядит обычным; - Жёлтый фон — любая другая блокировка: административная или добровольная («отпуск»);
- Красный фон — клиент выключен (
enabled=false) и при этом не заблокирован; - Голубой фон — это не клиент, а папка;
- Усиленный красноватый фон добавляется поверх остальных при долге
более 500 ₽ (
is_deep_debt).
Тумблер «Цвет маркер» над таблицей гасит подсветку целиком; выбор запоминается за оператором.
Бесконечная подгрузка
Папка не использует пагинацию. При прокрутке вниз автоматически догружается следующая порция (по 100 записей) через AJAX-endpoint /admin/Abonents/<id>/children_json/. На больших папках (1000+ клиентов) загрузка остаётся плавной — браузер рендерит только видимый кусок.
Массовые действия и экспорт
Чекбокс в первой колонке + «Выбрать все на странице» в шапке. После выделения появляется меню «Действия (N)» — те же 11 операций, что и в глобальном поиске: включить / выключить / удалить / изменить тариф / точку подключения / перенести в другую папку / добавить услугу / бонус / финоперацию / лояльность / отправить сообщение. Destructive-операции защищены confirm-модалкой с предпросмотром имён, для большинства — кнопка «Откатить» в баннере на 1 час.
Экспорт CSV — кнопка «📥 CSV» в шапке выгружает текущий срез папки (/admin/Abonents/<id>/export/): UTF-8 BOM, разделитель «;» для Excel RU. Колонки совпадают с видимыми в таблице плюс расшифровка тарифа, оператора, баланс в рублях. Учитываются текущий фильтр-чип и текстовый поиск.
Навигация по подпапкам
Если у текущей папки есть подпапки — они выводятся отдельным блоком над таблицей клиентов: карточки с именем подпапки и счётчиком клиентов внутри (включая глубоких потомков). Клик переходит в подпапку (отдельный URL). Хлебные крошки в шапке страницы показывают полный путь от корня (#9001 → город → улица → дом), кликабельны.
Мультиорганизация
Выбор организации в шапке — это фильтр отображения. Пока выбрана конкретная компания, раздел показывает только её данные:
- Дерево слева — папки чужих компаний скрыты. Корневая «Все» видна всегда, иначе дерево схлопнулось бы в пустоту;
- Содержимое папки — и первая отрисовка, и подгрузка при прокрутке;
- Колонка «Орг.» — показывается при мульти-орг и режиме «Все организации», в папке после номера строки, в глобальном поиске — после «ФИО / Компания»;
- Записи без организации остаются видимыми в любом режиме. Иначе неразобранное исчезло бы из интерфейса вместе со способом это починить.
Модалка «Новый клиент»
Открывается зелёной кнопкой над деревом. Что важно знать при мульти-орг:
- Организация выбирается в шапке модалки — селектом, когда в навбаре стоит «Все организации». Выбор здесь главнее папки и значения по умолчанию;
- Списки следуют за организацией: тарифы и NAS чужих компаний скрыты. Если у компании нет тарифов нужного типа, вместо пустого списка — пояснение (например, у организации все тарифы для юр.лиц, а создаётся физлицо);
- NAS показываются все, если своих у компании нет — оборудование обычно общее на сеть, и жёсткий фильтр просто не дал бы создать клиента;
- Подсказка «Попадёт в папку» под тарифом появляется, если у тарифа задана директория.

Раскладка новых клиентов
Куда попадёт клиент при создании, задаёт настройка
Настройки → Система → Профиль → «Раскладка новых клиентов»
(ABONENT_FOLDER_ROUTING):
| Режим | Поведение |
|---|---|
| По директории тарифа, иначе по организации | значение по умолчанию: сначала папка тарифа, при её отсутствии — папка организации |
| Только по папке организации | директория тарифа игнорируется |
| Не раскладывать | клиент остаётся там, где его создают |

flowchart TD
ADD["Создание клиента"] --> WHERE{"Откуда добавляют"}
WHERE -->|"из конкретной папки"| KEEP["Остаётся в ней"]
WHERE -->|"из корневой Все"| MODE{"Режим раскладки"}
MODE -->|"Не раскладывать"| ROOT["Остаётся в корне"]
MODE -->|"Только организация"| ORG["Папка организации"]
MODE -->|"По тарифу"| HAS{"У тарифа есть директория"}
HAS -->|"да"| TAR["Папка тарифа"]
HAS -->|"нет"| ORG
Если папка тарифа принадлежит другой компании, организация клиента синхронизируется по папке: иначе он лежал бы в чужой папке со своей меткой и пропадал из неё при фильтре по организации. На практике случай редкий — тарифы уже отфильтрованы по организации, и чужой выбрать нельзя.
Глобальный поиск
Глобальный поиск (Клиенты → 🔍 Глобальный поиск,
URL /admin/Abonents/search/) — плоский список всех клиентов
без иерархии папок, с расширенными фильтрами и массовыми операциями.
Папки в выборку не попадают (is_folder=false).
Раздел рассчитан на сценарии, когда нужно работать со всей базой сразу: найти клиента по обрывку информации (часть телефона, дома, фамилии), сделать массовую рассылку по тарифу, выгрузить срез для бухгалтерии.

Содержание раздела
KPI-бейджи в шапке
Над таблицей выводится 4 счётчика по результатам текущей выборки:
Активных (enabled=true) /
Сейчас онлайн (RADIUS-сессия < 15 мин) /
Выкл (enabled=false) /
Всего. Цифры пересчитываются на каждой смене фильтра.
Чипы быстрых фильтров (7 штук)
Чипы взаимоисключающие — активен может быть только один. Кликом по уже
активному чипу фильтр сбрасывается. Состояние записывается в URL
(?chip=debtors) — ссылку можно отправить коллеге.
| Чип | Иконка | Что показывает | SQL-условие |
|---|---|---|---|
| Должники | ⚠ | Клиенты с отрицательным балансом счёта | admin_account.ostatok < 0 |
| Бизнес | 💼 | Юридические лица | abonents.company = true |
| Без email | ✉ | Не указан e-mail для уведомлений | email IS NULL OR email = '' |
| Без телефона | 📵 | Не указан номер телефона (SMS не уйдут) | sms IS NULL OR sms = '' |
| Онлайн | 📡 | Сейчас в RADIUS-сессии (< 15 минут с last update) | USERS_RADIUSAUTH.LOGGED = 1 AND RADIUS_UPDATE >= now() - 15 min |
| Заблокированные | 🚫 | Любая активная блокировка (admin / negbal / own) | есть запись в ABONENTS_BLOCK с активным флагом |
| Обещ. платёж | 🤝 | Активен обещанный платёж (Usluga 30353) | users_usluga.usluga_id = 30353 AND enabled = true |
Multi-select фильтры
- Тариф — checkbox-список с поиском по названию.
Можно выбрать несколько тарифов одновременно. URL:
?tarif=131&tarif=43. Применение через кнопку «Применить», сброс — «Очистить». - Точка подключения — checkbox-список ConnectionPoints
(NAS-узлов и магистралей). URL:
?cp=Узел 1&cp=Узел 2. Полезно для оператора-инсталлятора, обслуживающего несколько домов / улиц / районов.
Текстовый поиск
Поле в шапке таблицы. Серверный поиск по: ФИО, номер договора, e-mail, телефон, адрес (город / улица / дом / квартира). Минимум 2 символа, debounce 300 мс. Шорткат Ctrl+K ставит фокус в поле поиска из любого места страницы.
Колонки таблицы (12)
- ☑ — чекбокс выбора для bulk-операций.
- № — внутренний ID клиента.
- ФИО — имя, кликабельно (открывает карточку).
- Дог. — номер договора.
- Тариф — название текущего тарифа; для бизнес-тарифов иконка 💼.
- Адрес — короткий формат через фильтр
home_short(ВО → Волгоградская обл., Влг → Волгоград, район сокращённо). - Точка подкл. — название ConnectionPoint.
- Контакты — иконки ✉ / 📞 с tooltip-значениями. Серая иконка = не заполнено.
- Баланс — баланс счёта; рядом 🤝 при активном ОП.
- Подкл. — дата подключения.
- 📡 — флаг «онлайн сейчас» (RADIUS-сессия < 15 мин).
- Статус — цветной бейдж (Активен / Заблок. / Удалён).
Подсветка строк — та же, что в папке: розовый фон — блокировка за неуплату, жёлтый — другая блокировка, красный — выключен, усиленный красноватый — долг более 500 ₽. Разбор классов — в описании папки.
Бесконечный скролл
Глобальный поиск не использует пагинацию: при прокрутке вниз подгружается
следующая порция результатов. Папочные страницы (/admin/Abonents/<id>/)
работают по тому же принципу через AJAX-endpoint
/admin/Abonents/<id>/children_json/ по 100 записей за запрос.
Массовые действия — 11 операций
Выбираете клиентов галочками в первой колонке (или «Выбрать все»
в шапке) — в шапке появляется выпадающее меню «Действия (N)»
с дюжиной операций. Endpoint: POST /admin/Abonents/bulk_action/
(payload: action + ids[] + параметры).
| Действие | Что делает | Undo |
|---|---|---|
Включить (enable) |
Снимает флаг enabled=false, шлёт CoA-Disconnect для переподключения. |
✅ |
Выключить (disable) |
Ставит enabled=false, разрывает RADIUS-сессии. |
✅ |
Удалить (delete) |
Soft-delete: deleted=true, все Users отключаются, CoA-Disconnect. |
✅ |
Изменить тариф (change_tarif) |
Смена тарифа (Select2 в модалке). Срабатывает сразу, не «с 1-го числа». | ✅ |
Изменить точку подключения (change_cp) |
Перевешивает клиента на другую ConnectionPoint. | — |
Перенести в папку (move_folder) |
Меняет parent_id в MPTT-дереве (Select2 по папкам). |
✅ |
Добавить услугу (add_service) |
Подключает разовую/периодическую услугу (Usluga из Select2). | — |
Добавить бонус (add_bonus) |
Подключает бонус (компенсация / скидка с отрицательной суммой). | — |
Добавить операцию (add_finop) |
Создаёт FinanceOperations на счёт (тип, сумма, описание). |
— |
Добавить лояльность (add_loyalty) |
Включает программу лояльности (AbonentsLoyalty). |
— |
Отправить сообщение (send_msg) |
Рассылка по выбранным каналам: e-mail / SMS / Push / Telegram. | — |
Для destructive-операций (Включить / Выключить / Удалить / Изменить тариф /
Перенести в папку) после выполнения в шапке появляется баннер
«Откатить (доступно N мин)» с таймером 1 час. Клик отменяет
изменение для всех затронутых клиентов через
POST /admin/Abonents/bulk_undo/. Undo-токен хранится в
sessionStorage браузера. Create-операции (Добавить услугу / бонус /
финоперацию / лояльность / отправить сообщение) не откатываются — придётся
вручную удалять созданные записи.
Подтверждение опасных операций
Удаление, отключение, включение и смена тарифа открывают модальное окно подтверждения: показывает превью первых 5 имён выбранных клиентов, общее их количество и требует ввести точное число выбранных записей для активации кнопки «Подтвердить». Это защита от случайной массовой операции по невнимательности.
CSV-экспорт
Кнопка «📥 CSV» в шапке выгружает текущий видимый срез
(/admin/Abonents/search/export/): UTF-8 BOM, разделитель «;»
для Excel RU. Колонки те же что в таблице плюс расшифровка тарифа,
оператор и баланс в рублях.
Слайд-панель клиента (drawer)
В строках таблиц списков клиентов (папка, глобальный поиск, должники) у каждой записи справа есть кнопка ℹ Подробнее (на mobile — клик по имени). Клик открывает выезжающую справа панель (slide-panel / drawer) с краткой сводкой по клиенту — без ухода со списка и потери контекста (фильтры, прокрутка, выделение сохраняются).
Панель решает задачу «быстро глянуть и сделать», когда полная карточка с десятком вкладок избыточна. Типичные сценарии за 5–30 секунд: проверить баланс и активировать ОП, посмотреть статус сессии при жалобе на интернет, отправить SMS-напоминание, добавить комментарий-заметку для коллег.

Содержание раздела
- Где доступна панель
- Шапка с плашками статуса
- Финансовый KPI-блок
- Аккордеон разделов и smart-defaults
- Сброс сессии из блока «Последняя сессия»
- Раздел «Поддержка» (HelpDesk)
- Sticky footer — отправка сообщений
- Каналы отправки (SMS/Email/Telegram/ЛК/ВК/Push)
- Email в фирменном брендинге
- Доступность (WCAG colour independence)
- Mobile-режим
Где доступна панель
- Папка клиентов (
/admin/Abonents/<id>/) — кнопка ℹ в каждой строке. - Глобальный поиск (
/admin/Abonents/search/) — кнопка ℹ после колонки «Статус». - Должники (
/admin/debtors/) — последняя колонка таблицы. - На mobile — клик по имени клиента в любом списке (
.abd-name-link).
Программный API из любой страницы: window.AbDrawer.open(abonentId).
Шапка с плашками статуса
Вверху панели — компактные плашки с ключевыми атрибутами клиента (видно с одного взгляда, без скролла):
| Плашка | Что показывает | Цвет |
|---|---|---|
| #<номер договора> | Номер договора клиента | — |
| Активен / Отключён | Состояние клиента целиком | зелёный / жёлтый |
| Физ. лицо / Бизнес | Тип клиента | серый |
| 🔑 Логин | Основной логин учётной записи (скрыт на mobile) | серый |
| 🌐 IP | IP-адрес учётной записи | серый |
| 🔓 Вкл. / 🔒 Выкл. | Состояние учётной записи (отдельно от клиента) | зелёный / жёлтый |
| 📶 Онлайн / 🔌 Офлайн | Сейчас в RADIUS-сессии или нет | зелёный / жёлтый |
| 🪪 СОРМ N/9 | Полнота персональных данных по СОРМ-требованиям | зелёный (9/9) / жёлтый (5–8) / красный (0–4) |
Шапка имеет фон заметно темнее тела (light #e8edf2 / dark
#14171b) — оператор сразу различает «где статус, где данные».
Финансовый KPI-блок
Сразу под шапкой — KPI-сетка 2×2 с самыми важными цифрами:
- Баланс — с цветным акцентом (зелёный для +, красный для −).
- Дней долга или Хватит на N дней — label
меняется в зависимости от знака баланса. При плюсе показываем
«Хватит на N дней» =
balance / стоимость_тарифа × 30. Цвет: зелёный ≥30 дней, нейтральный 7–29, красный <7 дней (нужен платёж). - Тариф — название текущего тарифа (truncate с tooltip).
- Посл. платёж — дата последнего входящего платежа.
Если активен обещанный платёж — отдельная плашка с суммой и датой окончания под KPI-сеткой.
Аккордеон разделов и smart-defaults
Разделы панели объединены в аккордеон — оператор сам
открывает что нужно, не скроллит мимо. Состояние свёрнутости запоминается
в localStorage отдельно по каждой секции — выбор оператора сохраняется.
| Раздел | Smart-default (открыт если…) |
|---|---|
| 👤 Учётные записи | Учётка отключена (!primary_enabled) — оператор сразу видит проблему |
| 🌐 Последняя сессия | Клиент онлайн или есть активная сессия |
| 📈 Платежи за 12 мес. | Всегда свёрнут — открывается по запросу |
| 📋 Последние операции | Должник (balance < 0) — оператор смотрит откуда долг |
| 🏛 Банк | Раздел показывается только при наличии хотя бы одного платежа из банковских выписок; открывается смотря по контексту вызова |
| 📝 Комментарии операторов | Уже есть комментарии — значит история важна |
| 🎧 Поддержка | Клиент ждёт ответа в HelpDesk (waiting_for_us > 0) |
Раздел «Банк» показывает последние платежи клиента из банковских выписок (банк, дата, сумма, плательщик). При открытии панели кликом по имени клиента в очереди чеков («Очередь чеков») панель автоматически прокручивается к разделу «Банк» и раскрывает его — оператор сразу видит, откуда пришёл платёж по разбираемой операции.

Сброс сессии из блока «Последняя сессия»
Если у клиента есть активная RADIUS-сессия, в блоке
«Последняя сессия» рядом с бейджем «Активная сессия» появляется
кнопка «🔄 Сброс сессии». Она нужна при зависшей сессии
(NAS не прислал Accounting-Stop, клиент «висит онлайн», но интернета нет).
- Клик открывает модалку подтверждения: «Сбросить активную сессию? Сессия будет завершена, абонент переподключится. Использовать при зависшей сессии.» — с кнопками Отмена / Сбросить.
- Подтверждение шлёт
POST /admin/users_api/<user_id>/reset_session/: сбрасываетUSERS_RADIUSAUTH.LOGGED, закрывает открытыеRADIUS_SESSIONSи рассылает CoA Disconnect на NAS — клиент переавторизуется. - Действие записывается в аудит (
AuditOperations).

Если оператор однажды раскрыл/свернул секцию вручную — это его выбор сохраняется
в localStorage и smart-defaults больше не перетирают.

Раздел «Поддержка» (HelpDesk)
Шестой раздел аккордеона — интеграция с HelpDesk. Биллинг показывает все обращения клиента в техподдержку прямо в слайд-панели, без перехода в HelpDesk:
- Pill-счётчик в заголовке: «Поддержка 3»
показывает количество открытых тикетов. Если есть тикеты, ожидающие
ответа от нас (
customerWaitingSinceиз FS), счётчик становится красным с pulse-анимацией — оператор сразу видит «горящие» обращения. - Summary-stats внутри: «Ждёт ответа N · Открыто M · Ожидание K · Закрыто L».
- До 5 свежих тикетов карточками: # · статус · «ждёт N часов» бейдж → клик открывает обращение в HelpDesk (новая вкладка).
- Smart-default: раздел открыт если
waiting_for_us > 0. - Резильентность: если HelpDesk недоступен — раздел просто пуст, ошибки в drawer не пробрасываются.

Sticky footer — форма отправки всегда видна
Форма отправки сообщений и табы каналов прилеплены ко дну панели (не уезжают при скролле истории сообщений). Оператор не скроллит чтобы написать SMS — форма всегда на виду, как в современных мессенджерах.

Каналы отправки (SMS / Email / Telegram / ЛК / ВК / Push)
Над формой ввода — chip-табы с каналами отправки. Показываются только доступные клиенту каналы:
- SMS — если у клиента указан телефон.
- Email — если указан email.
- Telegram — если привязан Telegram-чат (
telegram_chat_id). - ЛК — всегда доступен (push в личный кабинет через MsgStack).
- ВК — если привязан VK-аккаунт.
- Push — если установлено мобильное приложение и есть push-токен.
При выборе таба Email над формой появляется поле «Тема письма» — если оператор не укажет, дефолтная тема возьмётся из первой строки текста (до 80 символов).

SMS-форма поддерживает дефолтный шаблон: при пустом тексте у должника автоматически отправляется напоминание «Договор XXX. Задолженность YYY ₽. После пополнения счёта доступ к услугам восстановится. Узнать баланс или взять ОП — отправьте «Баланс» или «Оп» в ответ на это сообщение. ЛК: lk.вашдомен.ru».
Для клиентов с положительным балансом дефолтный шаблон не отправляется —
backend возвращает ошибку no_text_positive_balance, оператор должен
ввести текст вручную.
Email в фирменном брендинге
Email-сообщения отправляются HTML в фирменном стиле — не plain text, а полноценное письмо с шаблоном emails/generic.html:
- Header: brand-color
#43b77a, имя оператора, дата. - Персональное приветствие: «Здравствуйте, <Имя>!» —
имя берётся из ФИО клиента (
parts[1]для физ.лиц, generic для юр.лиц). - Тело письма с текстом оператора (форматирование переносов).
- Footer: реквизиты компании, телефон поддержки, договор,
баланс — из
_build_msg_email_context. - Без CTA-кнопки «Открыть личный кабинет» (
hide_cta=True) — она не нужна для ручных сообщений оператора, только для авто-уведомлений.
Авто-детект шаблона по теме/тексту: если содержит «обещанн», «оплат», «блокир»,
«возобновл», «баланс+низк» — выбирается специализированный пресет
(promise_pay/payment_received/blocked/
unblocked/low_balance), иначе generic.
Доступность (WCAG colour independence)
Все цветовые статусы дублируются иконками — пользователи с deuteranopia (8% мужчин) различают информацию без полагания на цвет:
- FinOps: знак
+/−+ иконка↑/↓перед суммой. - Учётные записи:
fa-check-circle(online),fa-circleoutline (offline),fa-times-circle(disabled). - Сессия:
fa-play-circle(active) /fa-stop-circle(closed). - SMS-статусы: цветные плашки «доставлен» (зелёный), «отправл.» (синий), «ожидание» (серый), «ошибка» (красный).
Mobile-режим
На телефоне (≤768px) панель раскрывается на весь экран
(width: 100vw, height: 100dvh, без скруглений и отступа
сверху) — не узкая шторка сбоку и не нижний лист, а полноэкранная слайд-панель,
как модалки в мобильной версии проекта. Поведение одинаковое с desktop, только:
- Login-бейдж в шапке скрыт (длинный логин ломает строку).
- Textarea формы отправки одной строки (
height: 38px) сfont-size: 16px— iOS Safari не зумит при фокусе. - Sticky footer с safe-area padding (iPhone home indicator).

Тёмная тема
Drawer полностью адаптирован под тёмную тему. Header ещё темнее тела
(#14171b vs body #1f2227), все секции и формы
имеют тёмные варианты, footer-actions с своим bg.

Карточка клиента
Переход кликом по строке клиента — открывается отдельная страница с вкладками Информация, История, Тариф и услуги, Бонусы, Финансы, Точки подключения, Учётные записи, RADIUS, СОРМ, Реквизиты, HelpDesk, Лояльность. Подробное описание каждой вкладки — в отдельном разделе 9. Карточка клиента.
Bulk-операции и Undo
Выберите клиентов галочками — появится плавающая панель действий внизу экрана.

Доступные действия:
- Включить / Выключить / Удалить (с Bootstrap-confirm и превью имён).
- Сменить тариф / точку подключения / папку.
- Подключить услугу / бонус / финоперацию / программу лояльности.
- Отправить сообщение (e-mail / SMS / Push / Telegram).
Для destructive-операций (delete, disable, enable, change_tarif, move_folder) после выполнения в шапке появляется баннер «Откатить (доступно N мин)» с таймером 1 час. Клик отменяет изменение для всех затронутых клиентов.
8.2. На карте
Карта клиентов в браузере (Leaflet + OSM / 2ГИС / Yandex).
Каждый маркер — клиент; отдельно показываются NAS и
слой заявок Поддержки. URL:
/admin/abonents/map/. Меню сайдбара: Клиенты → 🗺 На карте.

- "На карте"
- Слои
- Клиенты
- Кластеры по плотности
- Цвет = статус
- NAS
- Покрытие
- Онлайн-панель
- Заявки Поддержки
- Фильтры
- Статусы
- Услуги
- Поиск Ctrl+K
- Данные
- Только точные координаты
- Скоуп по организации
- Кеш 5 минут
- Действия
- Попап клиента
- Карточка заявки
- Правка координат
Возможности
- Поиск с автокомплитом в три секции: Адреса, Клиенты, Оборудование. Шорткат Ctrl+K (или ⌘+K на Mac) ставит фокус в поле, не открывая поиск navbar. Минимум 2 символа, debounce 300 мс. Клавиатура: ↑↓ Enter Esc.
- Фильтр статусов (Активные / Заблокированные / Приостановленные) и фильтр услуг (Интернет / Видео / IPTV / Телефония / ЧОП) — выпадающие селекторы с toggle. Минимум один статус выбран всегда; дефолт — Активные + Интернет.
- Кластеры — на низком zoom маркеры группируются. Свёртываемая легенда (правый-нижний угол) объясняет цвета и размеры кластеров (5 / 25 / 120 точек) и статусов.
- NAS на карте — включённые серверы доступа показываются по координатам адреса. Popup NAS: бейджи PROD / CoA / SSH, состояние UP/DOWN, прогресс-бар «онлайн / всего», кнопки «Клиенты» (открывает сайдбар, см. ниже) и «Связи» (покрытие). Инфраструктура (Switch / узлы / трассы) — на отдельной карте сети.
- Слой «Заявки» — тикеты Поддержки СмИТ Биллинг с геопривязкой по адресу клиента. Включён при загрузке; клик по маркеру открывает карточку тикета.
- Popup клиента (см. скриншот ниже): Договор + лейбл организации (при multi-org), NAS ссылкой (→ сайдбар клиентов NAS), тариф, адрес, баланс, иконки услуг. Клик по ФИО открывает карточку клиента с прокруткой к блоку «Услуги»; «Позвонить» / «E-mail» — карточку с прокруткой к блоку сообщений.
- Сохранение viewport в браузере
(
localStorage.map_last_view, TTL 7 дней) + shareable URL?center=lat,lon&zoom=N— карта помнит положение, ссылку можно отправить коллеге. - Тёмная тема — карта переключается на тёмный скин (CARTO Dark Matter), попапы и сайдбары адаптированы под тёмный фон. Адаптив: на мобильном поиск — отдельной строкой, фильтры — в bottom-sheet по кнопке «Фильтры».

Откуда берутся точки
Карта не запрашивает данные при каждом движении — весь набор приходит одним JSON и живёт в памяти вкладки. Отсюда два следствия: панорамирование мгновенное, но состав точек фиксируется в момент загрузки.
flowchart LR
REQ["Открытие карты"] --> CACHE{"Кеш 5 минут
по организации"}
CACHE -->|"есть"| JSON["JSON одним ответом"]
CACHE -->|"нет"| HOMES["Адреса с координатами
qc_geo пусто или ≤ 1"]
HOMES --> SCOPE["Скоуп по организации
клиента"]
SCOPE --> BATCH["Батч-запросы:
NAS, блокировки, услуги"]
BATCH --> JSON
JSON --> RENDER["Кластеры на карте"]
qc_geo ≤ 1 — «точно дом» или «улица». Адреса, которые геокодер
свёл к центру города (qc_geo ≥ 2), исключены: иначе сотни
клиентов слиплись бы в одну точку и карта потеряла бы смысл.
Мультиорганизация
Точки, оборудование и заявки скоупятся по организации клиента. Важная деталь реализации: кеш партиционирован — ключ включает выбранную в шапке организацию и список доступных сотруднику компаний. Без этого первый открывший карту наполнил бы общий кеш своими данными, и остальные увидели бы чужих клиентов.
Если у организации нет ни одного адреса с точными координатами, карта не молчит, а объясняет причину и ведёт в редактор координат:

Сайдбар «Клиенты на NAS»
Клик по NAS-маркеру (кнопка «Клиенты») или по NAS-ссылке в попапе клиента открывает слайд-панель справа со списком клиентов выбранного NAS. Фильтр-чипы Все / Online / Offline со счётчиками, поиск (ФИО / логин / договор / IP), бесконечная прокрутка. Каждая строка — кликабельна (→ карточка клиента) и показывает: ФИО, бейдж «Заблок.» при блокировке, логин·IP, тариф, баланс (цветной ↑/↓) и иконки услуг.

Поток: маркер → попап → карточка / сайдбар
flowchart LR
DATA["/admin/abonents/map/data/
points + NAS + заявки"] --> MAP["Карта Leaflet
кластеры"]
MAP --> ABM(("Маркер
клиента"))
MAP --> NASM(("Маркер
NAS"))
MAP --> TKT(("Маркер
заявки"))
ABM --> POP["Popup клиента
org · NAS · услуги"]
POP -->|"ФИО"| DRAWER["Карточка клиента
→ блок «Услуги»"]
POP -->|"Позвонить / E-mail"| DRAWERM["Карточка клиента
→ сообщения"]
POP -->|"NAS-ссылка"| PANEL["Сайдбар «Клиенты NAS»
Все / Online / Offline"]
NASM -->|"«Клиенты»"| PANEL
PANEL -->|"строка"| DRAWER
TKT --> TICKET["Карточка тикета
Поддержка СмИТ"]
style DATA fill:#1e3a5f,stroke:#5BA89D,color:#fff
style MAP fill:#134e4a,stroke:#43b77a,color:#fff
style POP fill:#1e3a5f,stroke:#5BA89D,color:#fff
style PANEL fill:#43b77a,stroke:#2d9a5f,color:#fff
style DRAWER fill:#43b77a,stroke:#2d9a5f,color:#fff
style DRAWERM fill:#14532d,stroke:#16a34a,color:#fff
style TICKET fill:#78350f,stroke:#d97706,color:#fff
Тёмная тема и мобильный вид
Попап и сайдбар NAS полностью адаптированы под тёмную тему (контраст лейблов/значений ≥ WCAG AA). На мобильном панель фильтров — bottom-sheet.


Попап клиента
Клик по маркеру открывает карточку: договор, организация, NAS, тариф, адрес, баланс, точка подключения и значки подключённых услуг. Кнопки ведут в карточку клиента, звонок и почту — из карты можно начать работу, не открывая список.

Маркеры заявок открывают карточку тикета Поддержки в правой панели: история обращений, расшифровка звонка, приоритет и поле ответа.

Вес карты и скорость
Карта — самый тяжёлый JSON в системе: несколько тысяч точек одним ответом. Две правки изменили порядок величины:
| Что | Было | Стало |
|---|---|---|
| Передача по сети | 4585 КБ | 380 КБ |
| Размер JSON | 4584 КБ | 2750 КБ |
| Загрузка страницы | 11,6 с | 2,6 с |
- gzip для JSON в nginx — директива
gzip_typesбыла закомментирована, поэтому сжимался только HTML. Сжатие включено для всех JSON-ответов биллинга, не только карты; - Ответ без экранирования кириллицы — по умолчанию Django пишет русские буквы escape-последовательностями по 6 байт на символ, из-за чего 2,7 МБ данных превращались в 4,6 МБ.
Геокодирование адресов
Чтобы клиент появился на карте, его адрес (Homes.parent_id)
должен иметь координаты lat и lon. Заполнение
координат происходит автоматически через геокодеры —
внешние сервисы, которые по строке адреса возвращают координаты.
Поддерживаются два провайдера:
| Провайдер | Где настроить | Лимиты | Команда |
|---|---|---|---|
| DaData (рекомендуется для России) | /admin/settings/integrations/?tab=geo →
«Адреса», поле DADATA_TOKEN. |
10 000 запросов/день бесплатно (тариф «Бесплатный»), но не более 10 тыс. в месяц. | manage.py geocode_homes_dadata [--re-geocode] |
| Яндекс.Геокодер (запасной) | /admin/settings/integrations/?tab=geo →
«Адреса», поле YANDEX_GEOCODER_API_KEY. |
25 000 запросов/день бесплатно (HTTP Геокодер). | manage.py geocode_homes_yandex [--re-geocode] [--only-no-qc] |
API-токены задаются в разделе «Настройки → Интеграции → География» (вкладка «География → Адреса»). Здесь же есть кнопка «Проверить» для теста подключения и счётчик геокодированных адресов.
ADDRESS_ADMIN_GROUPS
(по умолчанию root, address). Остальным сотрудникам
сохранение вернёт 403.
Запуск геокодирования
На рабочий сервер сейчас геокодировано ~95% адресов (4045 из 4261), из активных клиентов ~90% на карте. Чтобы геокодировать новый адрес автоматически — введите его в форме клиента, и при следующем прогоне команды (или nightly Celery-задаче) он попадёт на карту. Запустить геокодирование вручную:
docker compose exec web python manage.py geocode_homes_dadata
# или принудительно для всех (включая уже геокодированные):
docker compose exec web python manage.py geocode_homes_dadata --re-geocode
Команда:
- Берёт все
Homesгдеlat IS NULL OR lon IS NULL(или все, если--re-geocode). - Обращается к DaData suggest API
(
https://suggestions.dadata.ru/.../suggest/address) с полным адресом из иерархии Homes (регион → город → улица → дом). - Записывает координаты
geo_lat/geo_lonи полеqc_geo(precision: 0=точно дом, 1=улица, 2=город — чем меньше, тем точнее). - Делает паузу между запросами (по умолчанию 0.1 сек) чтобы не превысить лимит сервиса.
Маркеры клиентов на адресах с qc_geo=2 (только город) на
карте отображаются как «приблизительные» — несколько клиентов одного
города могут лечь на одну точку. Для повышения точности проверьте, что
у адреса заполнены поля «Улица» и «Дом».
Reverse-геокодинг (заполнение города/района по координатам)
Если у адреса есть координаты, но не заполнен город или район
(после миграции из старых биллингов это бывает), используйте команду
manage.py enrich_homes — она через Яндекс.Геокодер
(обратное направление: координаты → адрес) восстанавливает
текстовые поля.
Тайловый слой (фон карты)
Подложка карты выбирается на той же вкладке «География → Карта»:
OpenStreetMap (бесплатно), 2ГИС (требует ключ), Yandex Maps. Дефолт —
OSM. Стиль карточки в /admin/settings/integrations/?tab=geo
показывает количество геокодированных адресов и точность (% домов
с qc_geo=0).
Обещанный платёж (Promise Pay) — назначение и роль
Содержание раздела
Обещанный платёж (ОП) — короткий «кредит» клиенту: биллинг временно увеличивает его баланс, чтобы интернет работал ещё несколько дней до фактической оплаты. Реализован через Usluga id=30353 с system_type=11. Ключевые поля клиента: Abonents.promise_pay (сумма ₽) и promise_date_end (дата истечения).
Что такое ОП и зачем нужен
Сценарий типичного использования:
- Клиент в командировке, ушёл в минус по балансу. Биллинг блокирует (
b_negbal); - Клиент через ЛК / SMS / звонок в поддержку запрашивает ОП;
- Биллинг временно зачисляет ему ₽ на 5 дней (срок настраивается);
- Финансовая блокировка снимается, интернет возвращается через CoA Disconnect;
- Через 5 дней клиент пополняет счёт. Иначе ОП истекает, баланс возвращается к минусу, блокировка повторяется.
Экономика
| Параметр | Значение | Где настраивается |
|---|---|---|
| Лимит | max(месячная цена тарифа, |долг|+1₽) с cap 2× тарифа | Автоматически. Override: LK_PROMISE_PAY_MAX_LIMIT_OVERRIDE |
| Длительность | 5 дней | Usluga.end_count_days=5. Override: LK_PROMISE_PAY_MAX_DURATION_DAYS |
| Стоимость в день | 30 ₽/день — списывается ежедневно через billing_worker, если включено | Usluga 30353.price + флаг LK_PROMISE_PAY_DAILY_CHARGE (default 1) |
| Cooldown | 30 дней между двумя ОП | LK_PROMISE_PAY_COOLDOWN_DAYS (default 30) |
| Min age | 30 дней с момента создания договора | LK_PROMISE_PAY_MIN_AGE_DAYS (default 30) |
| Авто-погашение при оплате | Если YooKassa/W1/Generic webhook зачислил достаточно — ОП авто-отменяется | LK_PROMISE_PAY_AUTO_CLOSE_ON_PAYMENT (default 1) |
| Уведомление за N часов | Push за 24ч до окончания + при истечении | MOBILE_PUSH_PROMISE_PAY_EXPIRE_HOURS (default 24) |
Примеры (тариф 500 ₽):
- долг −7 ₽ → лимит 500 ₽ (тариф ≥ долга)
- долг −800 ₽ → лимит 801 ₽ (покрытие долга +1₽)
- долг −2000 ₽ → лимит 1000 ₽ (cap 2× тарифа упёрся, баланс остался −1000 ₽)
Eligibility — 4 политики
Перед активацией _check_promise_pay_eligibility() в lk/views/promise_pay.py проверяет:
LK_PROMISE_PAY_MIN_DEBT— минимальный долг (например, ОП доступен только при балансе < -100 ₽);LK_PROMISE_PAY_MAX_LIMIT_OVERRIDE— макс. лимит ОП (если задан, перебивает расчёт по тарифу);LK_PROMISE_PAY_MIN_AGE_DAYS— минимальный «возраст» клиента (с моментаcreate_date). Защита от мошеничества: создал договор и сразу взял ОП;LK_PROMISE_PAY_COOLDOWN_DAYS— пауза между ОП. Защита от циклов «брать ОП каждую неделю».
Все политики — настройки SystemSettings. Если значение = 0 — политика отключена. По умолчанию все включены.
Что происходит при активации
В одной транзакции:
- Создаётся
UsersUsluga(usluga_id=30353, system_type=11, end_time=now+5дней, limit=<сумма ОП>); - Создаётся
FinanceOperations(op_type_id=31, op_summa=+limit, descr='[ЛК/SMS/Mobile] Обещанный платёж...'); abonent.account.ostatok += limit(баланс пополняется);abonent.promise_pay = limit,abonent.promise_date_end = now+5дней— для бейджа в UI и баннера;- Если был
AbonentsBlock(b_negbal=true)— удаляется,Abonents.enabled=true; - CoA Disconnect на NAS — текущая Reject-сессия разрывается, клиент переавторизуется, RADIUS отвечает Accept → интернет работает в течение секунд;
- Аудит — запись в
AUDIT_OPERATIONSс подробной descr.
Каналы активации
| Канал | URL/действие |
|---|---|
| ЛК клиента | /lk/promise_pay/ — кнопка с подтверждением |
| Mobile App | POST /mobile-api/v1/finance/promise_pay + GET /finance/promise_pay |
| SMS-команда | Текст «ОП» → биллинг шлёт условия → текст «ДА» в течение 10 мин |
| Админка | Карточка клиента → вкладка «Услуги» → кнопка «+ Обещанный платёж» (modal с условиями) |
| REST API v2 | POST /rest_api/v2/promise_pay/ для внешних интеграций |
Завершение ОП — 4 сценария
Есть четыре пути завершить активный ОП — все приводят к одному финалу: возврат лимита через обратную FinOp, очистка promise_pay/promise_date_end, удаление UsersUsluga, и при отрицательном балансе — авто-блок.
| Сценарий | Триггер | Реализация |
|---|---|---|
| A. Авто-погашение при оплате | Webhook YooKassa/W1/Generic зачислил сумму, достаточную чтобы баланс после возврата лимита остался ≥0 | lk/services/payment.py::_credit_abonent после успешного зачисления вызывает abonent.del_promise_pay(reverse_finance=True, reason='погашен оплатой ...'). Управляется LK_PROMISE_PAY_AUTO_CLOSE_ON_PAYMENT |
| B. Истечение по таймеру | Abonents.promise_date_end < now |
Celery beat check_expired_promises (раз в 15 мин). Вызывает del_promise_pay(reverse_finance=True, reason='истёк срок'). Если баланс < 0 — ставит b_negbal через abonent.block() |
| C. Ручное удаление через UI | Оператор удаляет UU обещанного платежа (кнопка корзины во вкладке «Услуги») | uslugi_delete_ajax детектирует system_type=11 или usluga_id ∈ {-3,-4} → выполняет тот же reverse-сценарий |
| D. Тогглер «Выкл.» во вкладке «Услуги» | Оператор переключает UU.enabled с True на False |
uslugi_edit_ajax детектирует переход True→False для ОП-услуги → вызывает del_promise_pay(reverse_finance=True, reason='отключено вручную'). |
Дневная плата 30 ₽/день
Услуга 30353 имеет price=30 ₽, is_periodic=true, every_day=true.
Что происходит при активации:
- Если
LK_PROMISE_PAY_DAILY_CHARGE=1—UU.sched_date = now+1d 09:00; - Каждый день в 09:00
billing_workerсписываетUsluga.price(30 ₽) с лицевого счёта; - Запись
FinanceOperations(op_type=32, descr='Списание абонплаты ...'); sched_dateсдвигается на следующий день — пока ОП активен, плата идёт ежедневно;- При завершении ОП (любой из 4 сценариев) UU удаляется → списания прекращаются.
Чтобы выключить дневную плату: LK_PROMISE_PAY_DAILY_CHARGE=0 в /admin/settings/system/ + переименовать услугу 30353 (убрать «30 руб/день» из названия).
Уведомления
- Активация: SMS-ответ + Push «ОП {N}₽ активирован до DD.MM.YYYY». Если cap упёрся и баланс остался отрицательным — добавляется «Баланс X ₽ — недостаточно, требуется доплата».
- За 24 часа до окончания:
push.promise_pay_expiringbeat-задача (раз в сутки) шлёт push «Обещанный платёж истекает через N ч». Окно настраивается черезMOBILE_PUSH_PROMISE_PAY_EXPIRE_HOURS(default 24). - В день истечения:
push.promise_pay_expiredbeat-задача шлёт push «Обещанный платёж истёк. Пополните баланс». - При погашении оплатой: уведомление об оплате (стандартное «На счёт зачислено …») + Audit-запись о закрытии ОП.
Audit
Все операции с ОП пишутся в AUDIT_OPERATIONS с TABLE_NAME='USERS_USLUGA' через billing.services.audit_log.write_audit():
- Активация через ЛК / SMS — «Обещанный платёж активирован через ЛК/SMS: {usluga.name} (#id, UU={pk}), лимит {N} ₽, до DD.MM.YYYY»;
- Завершение (любой из 4 сценариев) — «Обещанный платёж завершён ({reason}): {N} ₽, удалено UU={count}»;
- Запись попадает на вкладку Аудит → услуги карточки клиента.
Сводка настроек ОП
Все настройки в SystemSettings (/admin/settings/system/):
| Ключ | Default | Назначение |
|---|---|---|
LK_PROMISE_PAY_MIN_DEBT | 0 | Минимальный долг (₽) для активации. 0 = выкл |
LK_PROMISE_PAY_MIN_AGE_DAYS | 30 | Минимальный возраст клиента (дни) |
LK_PROMISE_PAY_COOLDOWN_DAYS | 30 | Интервал между двумя ОП (дни) |
LK_PROMISE_PAY_MAX_DURATION_DAYS | 0 | Override длительности (0 = берём из Usluga.end_count_days) |
LK_PROMISE_PAY_MAX_LIMIT_OVERRIDE | 0 | Override лимита (0 = автоматический расчёт max(тариф, долг+1)) |
LK_PROMISE_PAY_DAILY_CHARGE | 1 | Списывать Usluga.price ежедневно через billing_worker |
LK_PROMISE_PAY_AUTO_CLOSE_ON_PAYMENT | 1 | Авто-погашение при достаточной оплате |
LK_PROMISE_PAY_NOTIFY_BEFORE_HOURS | 24 | Окно уведомления перед истечением |
Defaults применяются миграцией 0127_promise_pay_defaults. UPSERT — существующие значения не перезаписываются, на проде с 0 (выкл) останется 0 пока админ сам не включит.
Блокировки (AbonentsBlock) — назначение и роль
Содержание раздела
AbonentsBlock — таблица записей о блокировках клиента. Один клиент может иметь одну активную запись с несколькими булевыми флагами (один флаг = одна причина блокировки). Хранится в ABONENTS_BLOCK.
Три типа блокировок
| Флаг | Кто ставит | Когда |
|---|---|---|
b_negbal 🔴 Финансовая | Система (Celery / accounts_payment_ajax) | Баланс ушёл ниже LIMIT на счёте |
b_admin 🟠 Административная | Оператор | Ручное действие — нарушение договора, спам, заявление клиента |
b_own 🟡 Добровольная | Сам клиент | Через ЛК / SMS-команду «ВЫКЛ» — пауза услуг (отпуск, командировка) |
Блокировка vs Abonents.enabled
Это два разных механизма, иногда путают:
| Поле | Что это | Кто меняет |
|---|---|---|
Abonents.enabled | Главный «выключатель» клиента. Если false — RADIUS отвечает Reject, услуги не работают | Метод block() ставит в false, unblock() — в true (если других блокировок нет) |
AbonentsBlock | Запись о причине блокировки (флаги b_negbal/b_admin/b_own) | process_blocks Celery, accounts_payment_ajax, ручные действия |
Корректная схема — блокировка всегда сопровождает enabled=false. Рассинхрон («есть запись блокировки, но enabled=true» или наоборот) — это баг, чинится через метод process_blocks или ручную правку через UI.
Жизненный цикл блокировки
- Создание:
process_blocks(Celery, раз в 5 мин) — сравниваетOSTATOKсLIMITу всех клиентов и создаётb_negbal;accounts_payment_ajax— мгновенно блокирует если расход через UI ушёл в минус;- Кнопка «Заблокировать» в карточке клиента — ставит
b_admin; Abonents.add_promise_pay()при истечении срока ОП — возвращаетb_negbalесли баланс остался отрицательный.
- Активные действия при блокировке (см.
Abonents.block()):- Создаётся / обновляется запись в
ABONENTS_BLOCK; Abonents.enabled = false;- Все
Users.enabled = false(учётки) → RADIUS отвечает Reject при следующей авторизации; - На все NAS параллельно отправляется CoA Disconnect (см. ниже) — текущие сессии разрываются;
- Пишется аудит-запись в
AUDIT_OPERATIONSсtable_name='ABONENTS_BLOCK'; - Опционально — IPTV sync (отключить пакеты в TVIP / LFStream).
- Создаётся / обновляется запись в
- Снятие:
process_blocksпри пополнении баланса убираетb_negbal;accounts_payment_ajax— мгновенно снимаетb_negbalкогда баланс >= 0;- Кнопка «Разблокировать» в карточке — снимает
b_admin; - SMS-команда «ВКЛ» снимает
b_own; - Если все флаги в записи стали
false—AbonentsBlockудаляется,Abonents.enabled=true, услуги возвращаются.
CoA Disconnect — что происходит с активной сессией
Чтобы блокировка отрабатывала немедленно (а не через 5-15 мин когда сессия сама закроется), биллинг шлёт CoA Disconnect:
- Параллельно (
ThreadPoolExecutor) на ВСЕ enabled NAS — потому что биллинг может не знать через какой NAS реально идёт сессия; - NAS отвечает
Disconnect-ACKесли у него есть сессия с этим логином, илиDisconnect-NAKесли нет; - FreeRADIUS закрывает сессию, клиент видит обрыв линии;
- При попытке переподключения FreeRADIUS отвечает
Reject(Users.enabled=false); - Сессия закрывается в
RADIUS_SESSIONSсEND_REASON='Disconnect-ACK'.
Без CoA Disconnect блокировка работала бы только при следующей перерегистрации клиента — а это иногда раз в сутки. С CoA — за 5-10 секунд.
AUDIT_OPERATIONS через Abonents.block / Abonents.unblock(). Все события блокировок видны во вкладке «Аудит» карточки клиента.
Лицевые счета (AdminAccounts) — назначение и роль
Содержание раздела
Лицевой счёт (AdminAccounts) — это финансовый «кошелёк» клиента в биллинге. Хранит баланс, бухгалтерские дебет/кредит, лимит блокировки, валюту. Хранится в таблице admin_accounts.
Зачем нужен лицевой счёт
Счёт решает четыре задачи:
- Учёт денег клиента — пополнения и списания идут через счёт. Текущий баланс — поле
OSTATOK; - Финансовая блокировка по порогу — если
OSTATOK < LIMIT, Celery-задачаprocess_blocksблокирует клиента (см. Блокировки); - Бухучёт — поля
DEBIT(поступления) иCREDIT(расходы) накапливают полные суммы для отчётности; - Группа клиентов на одном счёте — можно привязать несколько
Abonentsк одномуAdminAccounts(черезAbonents.ACCOUNT_ID) для семейных тарифов или корпоративных схем.
Поля и связи
| Поле | Назначение |
|---|---|
OSTATOK | Текущий баланс (×10^10, см. ниже) |
DEBIT | Сумма всех поступлений за всё время |
CREDIT | Сумма всех списаний за всё время |
LIMIT | Минимально допустимый баланс (если UNLIMITED=false) — ниже идёт блокировка |
UNLIMITED | Безлимитный счёт (не блокировать по балансу) |
WARN | Порог предупреждения: при OSTATOK < WARN отправляется уведомление |
BONUS_SCORE | Бонусные баллы (программа лояльности) |
CURRENCY_ID | Валюта счёта (FK → currencies) |
SLEEP_DATE | «Заморозить» счёт до даты (счёт неактивен) |
DB_MONEY_KOEF — почему ×10^10
Все денежные поля биллинга (OSTATOK, DEBIT, CREDIT, LIMIT, op_summa в FINANCE_OPERATIONS, price/summa в usluga) хранятся как целые числа, умноженные на 10^10. Это позволяет избежать ошибок округления плавающей запятой и хранить суммы с точностью до 10 знаков после запятой.
- В БД:
50000000000(целое число, типnumeric); - В UI/коде:
5.00 ₽(после деления на10^10); - Константа:
DB_MONEY_KOEF = Decimal('10000000000')вbilling/models/admin_accounts.py,billing/services/sms_inbound.pyи других местах; - Кастомное поле
MoneyField(наследуется отDecimalField) — при вводе формы умножает на коэф, при отображении делит. UI работает в рублях, а БД хранит в «копейках × 10^8».
Кто меняет баланс
| Источник | Что делает |
|---|---|
billing_worker (Celery) | Ежемесячное списание услуг по тарифу (op_type=32). Минусит OSTATOK |
accounts_payment_ajax | Ручное пополнение/списание оператором через UI «Касса» |
YooKassa / W1 webhooks | Автоматическое пополнение после онлайн-оплаты |
add_promise_pay | Активация ОП — плюс к балансу + UU 30353 |
storno | Сторнирование операции — обратное движение баланса с флагом storno=true |
FINANCE_OPERATIONS и AUDIT_OPERATIONS. Не правьте OSTATOK SQL-запросом — это нарушит инварианты OSTATOK = DEBIT - CREDIT и историю операций. Всегда через UI или сервисный слой (_create_finop_atomic).
Финансовые операции (FinanceOperations) — назначение и роль
Содержание раздела
Финансовая операция (FinanceOperations) — каждое движение денег по лицевому счёту: пополнение, списание, обещанный платёж, начисление бонуса. Хранится в таблице finance_operations.
Зачем нужны финоперации
- Журнал движения денег — каждое изменение баланса должно сопровождаться записью FinOp. Это аудиторский след: всегда понятно почему баланс такой, как сейчас;
- Отчётность — отчёты «сколько собрано за месяц», «топ должников», «расход на услуги» строятся через агрегаты по FinanceOperations;
- Сторнирование — при ошибке оператора можно создать обратную операцию, не теряя истории. Поле
storno=true+ связьrelated_operation_idна оригинал; - Идемпотентность платежей — для онлайн-оплат через webhook (YooKassa/W1) сверяется уникальность по `[txn:<ID>]` в descr — повторный webhook не создаст дубль.
Поля и связи
| Поле | Назначение |
|---|---|
OP_ID | PK операции |
OP_DATE | Дата операции (когда деньги фактически прошли). Может отличаться от SYSTEM_DATE (когда запись создана) |
OP_SUMMA | Сумма ×10^10 (положительная = приход, отрицательная = расход). Знак определяется типом операции через op_sign |
OP_TYPE_ID | FK → fin_types — категория операции (см. ниже) |
ABONENT_ID | FK → abonents — кому принадлежит операция |
ACCOUNT_ID | FK → admin_accounts — на какой лицевой счёт |
OWNER_ID | FK → auth_user — какой оператор провёл (NULL для автоматических операций) |
DESCR | Свободный текст с деталями. Для платежей содержит [txn:<ID>] |
NUMBER | Номер документа (квитанция, чек, № платёжки) |
STORNO | true, если эта операция — обратная сторно к другой |
RELATED_OPERATION_ID | FK на саму себя — связь с оригиналом для пары storno↔original |
USLUGA_ID | FK → usluga — какая услуга была списана/начислена (если применимо) |
AA_OSTATOK / AA_DEBIT / AA_CREDIT | Снимок состояния счёта на момент операции (для отчётов) |
Типы операций
Справочник fin_types определяет:
type_id(PK),op_name(название для UI),op_sign(+1 = приход / −1 = расход),enabled
Основные типы (из чтения кода):
| op_type_id | Назначение | Знак |
|---|---|---|
| 31 | Обещанный платёж — активация ОП (плюс к балансу) | + |
| 32 | Тарифное списание (billing_worker, ежемесячно) | − |
| другие | Ручная проводка, единая касса, YooKassa, W1, корректировки и т.д. — справочник в /admin/settings/system/?tab=params | ± |
Кто создаёт финоперации
| Источник | Что делает |
|---|---|
billing_worker (Celery, ежедневно) | Списание тарифа (op_type=32) у всех активных клиентов |
finops_list.py (UI «Финансовые операции») | Прямое CRUD оператором — модалка add/edit/delete |
abonents.py:add_fin_op (карточка) | Ручная проводка из карточки клиента кнопкой «+ Финоп» |
accounts_list.py:_create_finop_atomic | Касса — пополнение/корректировка через UI «Лицевые счета» |
accounts_storno_last_ajax | Сторно последней операции — кнопка в модалке счёта |
lk/services/payment.py:_credit_abonent | Webhook YooKassa/W1/Generic — пополнение после онлайн-оплаты |
lk/views/promise_pay.py | Активация обещанного платежа (op_type=31) |
billing/services/sms_inbound.py | SMS-команды (ОП через SMS) |
Сторнирование
Сторно — это создание обратной операции, не удаление оригинала:
- Оператор открывает модалку счёта → клик «🔄 Сторно последней» (с подтверждением);
- Создаётся новая
FinanceOperationsсop_summa = -original.op_summa, тот жеop_type,storno=true,related_operation_id = original.op_id; - Оригинал помечается
storno=true(для UI: зачёркнутый текст); - Баланс счёта корректируется на сумму сторно;
- Аудит-запись фиксирует обе таблицы —
FINANCE_OPERATIONS(с привязкой к клиенту) иADMIN_ACCOUNTS(с привязкой к счёту).
AUDIT_OPERATIONS с table_name='FINANCE_OPERATIONS' и object_id=op_id. Видно во вкладке «Аудит» карточки клиента с drill-down на саму операцию.
8.3. Счета
URL: /admin/Abonents/AdminAccounts/
Лицевой счёт (AdminAccounts) — кошелёк клиента. На нём лежат остаток, дебет, кредит, стоплата, лимиты и валюта. К одному счёту можно привязать нескольких клиентов (Abonents.account_id → AdminAccounts, FK PROTECT), а все финансовые операции ссылаются на счёт напрямую — поэтому история денег живёт своей жизнью и не рвётся при смене клиента.
Разделение простое: счёт отвечает на вопрос «сколько денег сейчас», финоперации — «откуда они взялись». На проде 6309 счетов.
Что такое счёт: карта понятий
- Лицевой счёт
- Деньги
- Остаток
- Дебет
- Кредит
- Стоплата
- Баланс = остаток + дебет − кредит
- Правила
- Порог предупреждения
- Порог отключения
- Безлимит и дата
- Валюта
- Связи
- Клиенты много к одному
- Финансовые операции
- Банковские выписки
- Организация через клиента
- Действия
- Пополнить
- Корректировка
- Сторно последней
- Привязать отвязать клиента
Отсюда и главная путаница раздела: «остаток» и «баланс» — разные величины. Остаток — одно поле, баланс считается по формуле. В карточке счёта формула выведена прямо под крупной цифрой, чтобы не приходилось помнить.
Как деньги попадают на счёт
flowchart LR
W["Webhook оплаты
ЮKassa · W1"] --> FO
B["Банковская
выписка"] --> FO
O["Оператор:
Пополнить"] --> FO
C["Оператор:
Корректировка"] --> FO
M["billing_worker
абонплата"] --> FO
FO(["FinanceOperations"]) --> ACC["AdminAccounts.ostatok"]
ACC --> BAL{"Баланс
отрицательный?"}
BAL -->|"да"| BLK["Блокировка b_negbal
+ CoA Disconnect"]
BAL -->|"нет"| OK["Доступ активен"]
ACC --> LIM{"Ниже порога
предупреждения?"}
LIM -->|"да"| MSG["SMS · Push
уведомление"]
Ни одна кнопка в разделе не меняет остаток «напрямую»: любое движение денег создаёт финоперацию, а остаток — её следствие. Поэтому удалить операцию нельзя без последствий, а для отмены есть сторно.
Список счетов

- KPI-бейджи: Всего / Активных / Без клиента / Должников / Сумма балансов выборки. Кликабельны — работают как быстрый фильтр.
- Чипы: Должники (баланс < 0) · Без клиента · Плюс (баланс > 0). Повторный клик снимает фильтр.
- Поиск по ID, имени клиента, договору и тарифу; Ctrl+K ставит курсор в поле.
- Колонка «Орг.» — единый бейдж организации, см. Мультиорганизация.
- Колонка «Счёт» — только номер (
#N). Отдельной колонки «№» нет (номер и так виден). Деньги показывает колонка «Баланс». - Колонка «Тариф» — абонплата в формате
₽/м.без копеек (в биллинге нет цен с копейками). - Колонки «Должник с» и «Последняя операция» — дата ухода баланса в минус (
negative_since, с красной стрелкой) и дата последней финоперации по счёту (один агрегатMax(op_date)на всю страницу, без N+1). - Суммы без копеек: целые значения показываются без
.00, дробные — как есть. - Бесконечная прокрутка: первая порция 100 строк, дальше подгрузка при скролле.
- Массовые действия при выборе строк: установка лимитов, CSV выбранных, удаление «сирот».
- CSV-экспорт по текущим фильтрам.
- Справка — единая i-иконка (
SmitHelpIcon/.smit-help) в заголовке раздела ведёт сюда.
Цвет не единственный сигнал: рядом с балансом стоит стрелка направления, поэтому строка читается и при дальтонизме, и в чёрно-белой печати.
Как счёт может быть «без клиента»
Лицевой счёт (AdminAccounts) и клиент (Abonents) — разные сущности. Обычная связь идёт от клиента к счёту (Abonents.account_id). Но есть и второй путь: отдельный видео-счёт модуля видеонаблюдения привязан к клиенту через VideoObject.account, а не через основной счёт.
Поэтому «без клиента» = на счёт не ссылается ни основной клиент, ни видео-объект. Видео-счета показывают клиента (с маркером видео) и не считаются сиротами — иначе реальные видео-счета клиентов попадали бы в «Удалить сироты». Настоящие сироты — это, как правило, тестовые/демо-записи или счёт, у которого клиента удалили жёстко.
flowchart LR
AB["Клиент
Abonents"] -->|"account_id"| ACC["Лицевой счёт
AdminAccounts"]
VO["Видео-объект
VideoObject"] -->|"abonent"| AB
VO -->|"account (видео-счёт)"| ACC2["Отдельный
видео-счёт"]
ACC2 -.->|"клиент через видео-объект"| AB
ORPHAN["«Без клиента»"] -.->|"нет ни того, ни другого"| ACC3["Счёт-сирота"]
Скорость списка
Название тарифа считается по услугам тарифа, и str(tarif) ради этого ходит в БД дважды на каждый тариф. В списке это превращалось в 299 запросов на 100 строк и 2,7 с ответа, а выгрузка всех 6309 счетов в CSV занимала 47 секунд.
Теперь подписи считаются пачкой (Tarif.bulk_labels(ids)): те же две агрегации выполняются один раз на всю страницу.
| Операция | Было | Стало |
|---|---|---|
| Список, 100 строк | 299 запросов · 2 698 мс | 8 запросов · ~300 мс |
| CSV всех счетов | 47,3 с | 1,3 с |
Мультиорганизация и изоляция доступа
У AdminAccounts нет своего поля организации — она берётся у клиента счёта. Поэтому «счёт без организации» означает «клиент без организации», и гарантия стоит на клиенте.
- Колонка «Орг.» показывается при двух условиях сразу: включена мультиорганизация и выбран режим «Все организации». При выбранной одной компании колонка вырождается в один и тот же бейдж.
- Тот же бейдж — в шапке карточки счёта, сразу после номера.
- Организация проставляется на уровне модели (
Abonents.save(),FinanceOperations.save()): записи создают мастер, быстрое добавление, импорт, webhook'и иbilling_worker— точечные проверки в каждом месте протекали, поэтому гарантия одна и обойти её нельзя. Порядок выбора: организация папки-родителя → организация по умолчанию (СмИТ).
Кто какие счета видит
Сотрудник видит только счета своих организаций. Проверка стоит не только в списке, но и на каждом действии с конкретным счётом — иначе чужой счёт открывался бы подстановкой номера в адрес.
flowchart LR
REQ["Запрос сотрудника"] --> ALLOW{"allowed_orgs"}
ALLOW -->|"None — все"| FULL["без фильтра"]
ALLOW -->|"свои орг."| SCOPE["filter_qs_by_org
через клиента"]
FULL --> ACT{"действие"}
SCOPE --> ACT
ACT -->|"список"| L["accounts_json"]
ACT -->|"карточка"| G["accounts_get"]
ACT -->|"деньги"| P["payment · storno"]
ACT -->|"массово"| BK["bulk · csv"]
G --> GUARD["чужой счёт → 404"]
P --> GUARD
BK --> F403["чужой в списке → 403"]
Ответ на чужой счёт — 404, а не 403: одинаковая реакция на «нет такого» и «есть, но не ваш» не даёт перебором выяснить, какие счета существуют. Массовые действия отвечают 403, если хоть один выбранный счёт вне доступа, — молча пропустить часть выборки хуже, чем отказать целиком.
Карточка счёта
Открывается кликом по строке или по названию. Восемь вкладок слева, действия с деньгами — на первой. Параметр ?edit=<id> открывает карточку сразу при загрузке страницы (для ссылок из других разделов).

flowchart TB
subgraph T["Вкладки карточки"]
direction LR
T1["① Баланс
деньги и действия"]
T2["② Услуги и тарифы
ожидаемое списание"]
T3["③ Платёжные системы
история по каналам"]
T4["④ Параметры
лимиты и пороги"]
T5["⑤ Клиенты
привязка"]
T6["⑥ Финоперации
последние 50"]
T7["⑦ Банк. выписки
связанные строки"]
T8["⑧ Уведомления
SMS · Email · Push"]
end
① Баланс
Крупная цифра баланса, под ней формула баланс = остаток + дебет − кредит, ниже — четыре составляющие и производные величины (бухгалтерский, на конец месяца). Три кнопки: Пополнить, Корректировка, Сторно последней.
② Услуги и тарифы

Услуги всех клиентов счёта с ценами и ожидаемым списанием за месяц. Каскад цены: uu.summ → usluga.summa → usluga.price. Бонусы (отрицательная сумма или «бонус» в названии) вынесены отдельно.
③ Платёжные системы

Приходы за 90 дней, сгруппированные по каналу (ЮKassa, Wallet One, банковская выписка, ручная проводка, EBS). Канал определяется по operator_name, а при пустом — по меткам в описании. Ниже — статус интеграций со ссылками на настройки: видно, настроен ли канал вообще.
④ Параметры

- Порог предупреждения — при пересечении уходит SMS/Push.
- Порог отключения — срабатывает автоблокировка (
b_negbal). - Безлимит + дата окончания: пока активен, автоблокировка не срабатывает.
- Валюта, бонусные баллы, сброс даты последнего оповещения.
⑤ Клиенты

Привязанные клиенты с бейджем «основной» (первый по ID) и статусом. Кнопка Привязать открывает поиск; если клиент уже на другом счёте, появляется чекбокс Перенести — молча увести клиента с чужого счёта нельзя.
⑥ Финансовые операции

Последние 50 операций с иконкой источника и цветом по знаку; сторнированные зачёркнуты. Ссылка Все операции по счёту ведёт в Финансовые операции с фильтром по счёту.
⑦ Банковские выписки

Строки выписок, связанные с операциями счёта (BankStatementOperation.fin_operation_id): банк, плательщик, ИНН, назначение платежа. Нужна, когда клиент говорит «я платил через банк», а платёж не виден.
Пополнение, корректировка, сторно
- Пополнение: список типов отфильтрован по
op_sign > 0— расходный тип сюда не подставишь. Поле ID транзакции даёт идемпотентность: повторный запрос с тем же идентификатором вернёт существующую операцию, а не зачислит деньги дважды. - Корректировка: те же поля, но типы все. Знак берётся из типа операции — положительная сумма при расходном типе инвертируется автоматически.
- Сторно последней: показывает реквизиты последней операции, создаёт обратную и помечает оригинал
storno=True. Повторно сторнировать ту же операцию нельзя.
Под формой — предпросмотр: текущий баланс, сумма, новый баланс. Сервер выполняет всё в одной транзакции с SELECT FOR UPDATE на счёт, поэтому два оператора не «потеряют» одно из зачислений.
Мобильная версия
На телефоне у раздела другая задача: посмотреть баланс и принять оплату, а не разбирать журнал. Поэтому таблица не ужимается, а перестраивается.
- Панель управления — одна строка: поиск, кнопка фильтров со счётчиком активных, зелёная кнопка добавления. Подписи у кнопок убраны: на 390 px они не помещаются рядом с полем поиска. Дубль-поиска в слайд-панели фильтров нет.
- Таблица списка на всю ширину экрана: карточка тянется к краям (full-bleed), без боковых полей — данные не жмутся в узкую колонку. Скрыт только чекбокс (массовых действий с телефона не делают).
- Таблицы внутри карточки счёта — карточками: «Клиенты», «Фин. операции», «Банк. выписки» на телефоне перестраиваются в карточки (подпись столбца слева), а не режутся по ширине.
- Имена не переносятся: длинное ФИО сокращается многоточием, полное — в подсказке.
- Карточка счёта открывается полноэкранной панелью, крестик — в правом верхнем углу, тач-цель 56 px.
- Полоса вкладок прокручивается к выбранной вкладке, чтобы было видно, что идёт дальше.
Тёмная тема

Цвета раздела заданы токенами, поэтому тема переключается без отдельного набора правил.
Защита от удаления
Счёт удаляется только когда выполнены все условия: к нему не привязан ни один клиент, по нему нет ни одной финансовой операции и на него не ссылается видео-объект. Иначе сервер отвечает 400 с понятной причиной («Сначала отвяжите N клиентов…», «У счёта M операций — удаление запрещено для сохранения истории», «видео-объектов: K»).
Отдельная защита у видео-счетов: они принадлежат клиенту через VideoObject и не считаются сиротами, поэтому массовое «Удалить сироты» их не трогает даже при пустом балансе. См. Как счёт может быть «без клиента».
Смысл в том, что счёт — это история денег. Удалив его, вы теряете основание для всех прошлых начислений; в спорной ситуации восстановить картину будет нечем.
Аудит
Каждое действие пишется в AuditOperations (table_name='ADMIN_ACCOUNTS') с оператором и описанием изменения: создание и правка счёта (с diff вида «warn 100→200»), пополнение и корректировка (с op_id и типом), сторно, привязка и отвязка клиента, удаление (с балансом на момент удаления).
Endpoints
| URL | Метод | Назначение |
|---|---|---|
/admin/Abonents/AdminAccounts/ | GET | Страница списка |
/admin/Abonents/accounts_json/ | GET | Список: q, chip, page, per_page, sort. Возвращает items, KPI и show_org |
/admin/Abonents/accounts_get/<id>/ | GET | Счёт + клиенты + 50 операций + 20 уведомлений + организация |
/admin/Abonents/accounts_crud/[<id>/] | POST | Создание / правка / удаление (_delete=1) |
/admin/Abonents/accounts_payment/<id>/ | POST | Пополнение и корректировка |
/admin/Abonents/accounts_storno/<id>/ | POST | Сторно последней операции |
/admin/Abonents/accounts_link/<id>/ | POST | Привязать клиента (force=1 — перенос с другого счёта) |
/admin/Abonents/accounts_unlink/<id>/<abonent_id>/ | POST | Отвязать клиента |
/admin/Abonents/accounts_services/<id>/ | GET | Услуги и тарифы клиентов счёта |
/admin/Abonents/accounts_payments_summary/<id>/ | GET | Приходы по каналам + статус интеграций |
/admin/Abonents/accounts_bank_ops/<id>/ | GET | Связанные строки банковских выписок |
/admin/Abonents/accounts_bulk/ | POST | Массово: валюта / лимиты / удаление сирот |
/admin/Abonents/accounts_csv/ | GET | CSV (UTF-8 BOM, разделитель ;) |
Все endpoint'ы, принимающие <id>, проверяют доступ к счёту по организации сотрудника.
Денежные правила
Суммы в БД хранятся как целое × 1010 (DB_MONEY_KOEF). Конвертация автоматическая в MoneyField: при отображении делится, при сохранении умножается. В UI и API — всегда рубли с двумя знаками.
Обслуживание
manage.py backfill_abonent_org [--apply]— проставляет организацию клиентам, у которых её нет (папка-родитель → организация по умолчанию). От неё зависит и организация счёта.manage.py backfill_finops_org [--apply]— то же для финансовых операций.
Обе идемпотентны — трогают только записи с пустой организацией. После перехода гарантии на уровень модели они нужны разве что после массового импорта.
Смежные разделы
- Финансовые операции — журнал, который двигает остаток счёта.
- Карточка клиента — владелец счёта, там же блокировки и услуги.
- Банковские выписки — автоматические зачисления из выписок.
- Платежи (API) — webhook'и, создающие операции по счёту.
- Обещанный платёж — временный кредит на счёте.
- Фискализация 54-ФЗ — чеки по зачислениям.
8.4. Финансовые операции
URL: /admin/Abonents/FinanceOperations/
Финансовая операция (FinanceOperations, таблица finance_operations) — атомарная запись в журнале движения денег. Привязана к лицевому счёту (account_id → AdminAccounts) и к клиенту (abonent_id → Abonents). Тип операции (op_type → FinTypes) определяет знак (op_sign: +1 приход, −1 расход). Сумма хранится в БД как op_summa × 10^10.
Это самая большая таблица биллинга — на проде более 450 000 записей. Почти всё, что клиент видит в ЛК на вкладке «История», и всё, что бухгалтер сдаёт в отчётности, — строки отсюда.
Откуда берутся операции и на что влияют
Финоперации не создают «просто так»: каждая — след реального события. Обратная сторона в том, что операция почти никогда не существует в одиночку — она двигает баланс, а баланс двигает доступ клиента в сеть.
flowchart LR
W["Webhook оплаты
ЮKassa · W1"] --> FO
B["Банковская
выписка"] --> FO
M["billing_worker
абонплата"] --> FO
P["Обещанный
платёж"] --> FO
O["Оператор
вручную"] --> FO
E["EBS-импорт
история"] --> FO
FO[("finance_operations")] --> BAL["ostatok
+= op_summa"]
BAL --> BLK{"Баланс
отрицательный?"}
BLK -->|"да"| B1["b_negbal
+ CoA"]
BLK -->|"нет"| B2["Доступ активен"]
FO --> KKM["Чек в ОФД
54-ФЗ"]
FO --> LK["История в ЛК
и мобильном"]
Практический вывод: операцию нельзя «просто удалить». Удаление не откатывает остаток счёта автоматически и рвёт связь с банковской выпиской и чеком. Для отмены влияния используйте сторно (обратная операция + флаг storno=True) — см. Сторнирование.
Список операций

Раздел построен как кастомная страница с AJAX-загрузкой и модальным редактированием — иначе журнал такого размера не открыть за приемлемое время.
- Бесконечная загрузка — постраничной навигации нет: список догружается по 50 строк при прокрутке (
IntersectionObserver). Для журнала, где ищут «последнее за сегодня», это быстрее, чем перелистывать страницы. - Период по умолчанию — текущий месяц. Устанавливается на загрузке, чтобы первый запрос не поднимал всю таблицу.
- Фильтры: текстовый поиск (клиент / описание / номер), тип операции, источник, диапазон дат с быстрыми периодами.
- Колонка «Орг.» — единый бейдж организации, см. Мультиорганизация.
- Клик по «Типу операции» открывает карточку операции — то же, что кнопка 👁. Целиться в широкую ячейку удобнее, чем в мелкую иконку.
- Клик по имени клиента открывает выезжающую панель клиента (не карточку операции).
Цветовое кодирование. Цвет не единственный сигнал — рядом с суммой стоит стрелка направления, поэтому строка читается и при дальтонизме, и в чёрно-белой печати:
- 🟢 Зелёный бейдж со стрелкой ↓ — приход (
op_summa > 0). - 🔴 Красный бейдж со стрелкой ↑ — расход (
op_summa < 0). - ⚪ Серый — нулевая операция.
- Зачёркнутая строка — сторнированная (
storno=True).
Пагинация: почему курсор, а не OFFSET
Классический LIMIT/OFFSET заставляет PostgreSQL пройти и выбросить все предыдущие строки. На глубине 100 000 записей это 94 мс против 0,3 мс у курсора (замер EXPLAIN ANALYZE на проде). Поэтому подгрузка идёт по составному курсору (op_date, op_id) — составному потому, что даты повторяются и одной даты для однозначного «продолжи отсюда» не хватает.
sequenceDiagram
participant JS as Список в браузере
participant V as finops_list_json
participant DB as PostgreSQL
JS->>V: первая порция (page=1)
V->>DB: ORDER BY op_date DESC, op_id DESC LIMIT 50
DB-->>V: 50 строк + COUNT(*)
V-->>JS: items + total + next_cursor
Note over JS: оператор прокрутил вниз
JS->>V: after_date + after_id (курсор)
V->>DB: (op_date, op_id) < курсор LIMIT 50
DB-->>V: 50 строк за 0,3 мс
V-->>JS: items + next_cursor, total = -1
total = -1 на подгрузках — это не ошибка: общее число между порциями не меняется, поэтому COUNT(*) (около 0,12 с на 450 тыс. строк) считается только для первой порции, а клиент помнит прежнее значение.
Мультиорганизация
FinanceOperations.organization — денормализованный внешний ключ: он дублирует организацию клиента, чтобы фильтр по организации не делал JOIN на каждую из сотен тысяч строк.
- Колонка «Орг.» в списке показывается только при выполнении двух условий: включена мультиорганизация и в шапке выбран режим «Все организации». При выбранной одной организации колонка вырождается в один и тот же бейдж и только съедает ширину.
- Бейдж рисует общий компонент
OrgBadge— тот же, что в тарифах и в карточке клиента. Сервер отдаёт толькоorg_id, а название, иконку и фирменный цвет клиент берёт из реестраwindow.SMIT_ORGS: лишнего JOIN нет. - Организация проставляется при создании операции — из организации клиента, а если у клиента её нет, берётся организация по умолчанию.
Тот же бейдж выводится в шапке карточки операции, чтобы при разборе обращения было видно, к какой компании относится платёж.
Изоляция доступа
Скоупинг применяется не только к списку, но и к чтению и правке конкретной операции. Иначе сотрудник одной компании мог бы читать и менять чужие платежи простым перебором op_id в адресной строке.
flowchart LR
REQ["Запрос"] --> ALLOW{"allowed_orgs"}
ALLOW -->|"None"| FULL["без фильтра"]
ALLOW -->|"свои орг."| SCOPE["filter_qs_by_org"]
FULL --> ACT{"действие"}
SCOPE --> ACT
ACT -->|"список"| LIST["list_json"]
ACT -->|"карточка"| DET["detail_json"]
ACT -->|"правка"| CRUD["crud_ajax"]
DET --> GUARD["чужая → 404"]
CRUD --> GUARD
CRUD --> ABCHK["чужой клиент → 403"]
Ответ на чужую операцию — 404, а не 403: одинаковое сообщение для «нет такой» и «есть, но не ваша» не позволяет перебором выяснить, какие номера операций существуют.
Карточка операции
Открывается кликом по 👁 или по ячейке «Тип операции». Это карточка на две колонки: слева — сама операция и её связи, справа — клиент, снапшот счёта и аудит. Пустые секции скрываются: у миграционных записей из EBS обычно не заполнены кассовые и 1С-поля, и секция просто не рендерится.

Модалка v915 (variant B)
Двухрежимная карточка пришла на смену мини-форме: до неё была мини-форма из шести полей, по которой нельзя было разобрать обращение, не открывая полную форму Django.
Иконки источника
Рядом с описанием — цветная иконка, показывающая, откуда пришла операция. Источник вычисляет эвристика _resolve_source(op) в billing/views/finops_list.py по комбинации operator_name, меток в descr, usluga_id и op_type_id:
- YooKassa —
operator_name='yookassa'или[txn:N]в описании - Wallet One —
operator_name='w1'или содержит «wallet» - Банковская выписка —
operator_name='bank'или[bank_op:N]в описании - EBS (миграция) —
operator_name='ebs', подавляющая часть исторических данных - Обещанный платёж —
usluga_id IN (30353, -3, -4) - Ручная проводка —
op_type_id IN (7, 9) - Ежемесячное списание —
op_type_id IN (3, 31, 32) - Смена тарифа — описание начинается с «Смена тарифа»
Чипы связей рядом с именем клиента (📎🔗💳💰) — флаги наличия usluga, fin_op, credit_type, operator_pay_id_str. В панели фильтров есть отдельный фильтр «Источник» со всеми восемью видами.
Режим просмотра
- Шапка — номер операции, цветная сумма, бейдж источника, бейдж «Сторно» при
storno=True, бейдж организации, кнопка закрытия. - Левая колонка: Операция (тип, дата, сумма, номер, описание, флаг сторно, ссылка на полную форму); Связи (ответственный, оплата по, услуга, счёт-родитель, счётчик дочерних операций); Платёжный шлюз; Банковская выписка (через
BankStatementOperation.fin_operation_id— банк, плательщик, ИНН, назначение платежа, файл выписки). - Правая колонка: Клиент (имя-ссылка, договор, тариф, баланс с цветом, статус блокировки с флагами
b_negbal,b_admin,b_own,b_sys); снапшот лицевого счёта на момент операции (aa_ostatok,aa_credit,aa_debit,balance_buh); Кассовый чек / 1С; Аудит.
Клик по имени клиента в карточке открывает выезжающую панель клиента — можно посмотреть, кому принадлежит платёж, не теряя открытую операцию.
Режим редактирования

Та же сетка: слева форма основных полей, справа — связи операции через Select2 с подгрузкой по AJAX.
- Клиент — поиск через
/rest_api/v2/Abonents/. Выбранный клиент проверяется на стороне сервера: подставить чужую организацию crafted-запросом нельзя. - Тип операции — справочник
FinTypes. - Ответственный за операцию (
owner) — из/admin/Abonents/finops_staff_users/. - Оплата по (
credit_type) — из/admin/Abonents/finops_credit_types/. - Связанная услуга (
usluga) — из/admin/Abonents/finops_uslugi/. Нужна для обещанного платежа, бонусов и сверки начислений. - Счёт на оплату (
fin_op, родительская операция) — из/admin/Abonents/finops_search/, с защитой от связывания операции самой с собой. - Комментарий администратора (
admin_comment) — заметка, не видимая клиенту.
Создание операции

Кнопка «Добавить» открывает ту же карточку с пустой формой и текущей датой. Организация проставится автоматически из выбранного клиента.
Управление
- Открыть полную форму — на
/admin/Abonents/FinanceOperations/<id>/с 30+ полями (банковские реквизиты, чек, операторские поля). - Удалить — через модалку подтверждения с предупреждением о балансе, дочерних операциях и связи с выпиской. Нативный
confirm()не используется. - Редактировать — переключение режима без перезагрузки данных.
Мобильная версия
На телефоне у раздела другая задача: не «работать с журналом», а быстро посмотреть последние платежи — например, стоя у клиента. Поэтому таблица не ужимается, а перестраивается.
- Строка → карточка (единый компонент SmitTable): на телефоне каждая операция превращается в карточку. Имя клиента — заголовок карточки сверху; ниже поля в формате «ПОДПИСЬ значение» в одну строку слева (Дата, Тип операции, Сумма, Описание). Данные не прячутся и не уезжают за горизонтальный скролл.
- Иконка источника на карточке скрыта — рядом стоит текстовый «Тип операции», значок был бы дублем.
- Дата сокращается до
дд.мм.гг; полная дата со временем доступна по нажатию. - Панель управления — одна строка: поиск, кнопка фильтров со счётчиком активных, кнопка добавления.
- Фильтры уезжают в нижний лист (offcanvas, единый
SmitMobileFilterSheet) — на маленьком экране горизонтальная панель заняла бы весь первый экран. - Карточка операции (просмотр) открывается боковой панелью справа, форма создания/редактирования — панелью снизу.
Тёмная тема

Цвета раздела заданы токенами (tokens.css), поэтому тема переключается без отдельного набора правил. Суммы в тёмной теме используют осветлённые оттенки — тёмно-зелёный и тёмно-красный на тёмном фоне не проходят по контрасту.
Достоверность чисел и защита важных действий
Журнал рассчитан на большие объёмы: около 450 тысяч операций листаются без задержки, суммы хранятся без потери копеек. Доступ закрыт организацией — и к списку, и к каждой операции по прямой ссылке. Отдельно проработаны две вещи: достоверность и понятность чисел и защита действий, которые нельзя отменить.
Восемь источников операции
Источник определяется на лету по комбинации сигналов (operator_name + метки в descr + op_type_id + связи), без отдельного поля в БД:
эквайринг, метка
[txn:…]
эквайринг W1
метка
[bank_op:N]
operator_name=EBS
usluga 30353 / −3 / −4
op_type 7 / 9
op_type 3 / 31 / 32
descr = «Смена тарифа…»
flowchart LR
OP["Операция
FinanceOperations"] --> R{"_resolve_source"}
R -->|"метка txn / оператор"| Y["YooKassa"]
R -->|"оператор wallet"| W["Wallet One"]
R -->|"метка bank_op"| B["Банковская выписка"]
R -->|"оператор EBS"| E["EBS (миграция)"]
R -->|"услуга ОП"| P["Обещанный платёж"]
R -->|"тип 7 или 9"| M["Ручная проводка"]
R -->|"тип 3 / 31 / 32"| MO["Ежемесячное"]
R -->|"описание «Смена тарифа»"| TC["Смена тарифа"]
Сквозной drill-down до чека ОФД
Операцию можно проследить от письма банка до фискального чека и обратно — карточка v915 связывает все звенья:
flowchart LR
IMP["Письмо-импорт
выписки"] <--> QUE["Очередь
модерации"]
QUE --> FO["Финансовая
операция"]
FO --> OFD["Чек ОФД
54-ФЗ"]
FO -->|"обратная ссылка"| BOP["Банковская
операция"]
BOP --> IMP
Пробитие чека: подтверждение и аудит
«Пробить чек» — необратимое действие: чек уходит в ОФД и оттуда в ФНС. Поэтому он требует подтверждения, а результат пишется в журнал аудита. Удаление операции подтверждается так же.
flowchart LR
BTN["«Пробить чек»"] --> SC{"SmitConfirm
предупреждение"}
SC -->|"Отмена"| X["ничего"]
SC -->|"Пробить"| API["finops_fiscalize"]
API --> FM["FiscalManager
в ОФД"]
API --> AUD["AuditOperations"]
Достоверность и понятность чисел
- Колонка «Орг.» — эффективная организация. Раньше показывала денорм-поле
organization_id(у старых/«Единая касса» — NULL → «—»), хотя список скоупится по организации клиента. Теперь:organization_idс fallback на организацию клиента — «—» у атрибутированных записей больше нет. - «Единая касса»: расшифровка комиссии. У платежей «Единая касса» в описании лежит голая сумма оплаты (с комиссией агрегатора), а на баланс идёт нетто — числа не сходились. Теперь в списке у таких строк маркер и подсказка, а в модалке — строка «Расшифровка: Оплачено / Зачислено / Комиссия». Новые записи из EBS-синка приходят уже с подписью; текущий generic-webhook и так пишет внятно.
- Без утечки внутренних значений. Убрано «Единая касса
(id=37)» → «Единая касса»; «pay_id (int)» → «ID платежа».
Типы операций (FinTypes)
Справочник типов хранится в fin_types. Поле op_sign определяет знак суммы:
- +1 (приход) — оплаты через эквайринг (Сбербанк РФ, ЮKassa, EasyPay), пополнение ручной проводкой, обещанные платежи.
- −1 (расход) — периодические и единоразовые услуги, корректирующие списания, списания обещанных платежей.
- 0 (информационный) — редко, для технических записей без движения денег.
UI пополнения счёта (см. Счета → Пополнение) фильтрует список типов по op_sign > 0; UI корректировки показывает все типы, направление выбирает оператор.
Редактирование справочника
URL: /admin/dictionary/FinTypes/ (Справочники → Типы фин. операций).

Список с поиском, чипами-фильтрами по знаку и счётчиком использований в журнале. Создание и редактирование — в модальном окне с радиогруппой Приход (+1) / Нейтр. (0) / Расход (−1).

Поля: type_id (автоинкремент, read-only), op_name (название, до 256 символов), op_sign (+1 / 0 / −1), op_descr (описание), is_common (чекбокс «Основной» — ходовые типы показываются первыми, чтобы оператор не искал их среди 47 legacy-типов).
Защита удаления: тип нельзя удалить, если он использован хотя бы в одной операции. Причина не только в истории: часть type_id жёстко зашита в коде (23 — YooKassa в webhook, 32 — ежемесячное списание тарифа в billing_worker), и удаление сломает интеграции.
Аудит: создание, изменение и удаление пишутся в AuditOperations (table_name='FIN_TYPES').
Когда добавлять новый тип: при подключении новой платёжной системы (СБП, T-Bank) или появлении специфической бухгалтерской операции. Перед использованием нового type_id в коде убедитесь, что значение берётся из FinTypes.objects.get(...), а не зашито числом.
Связи и снапшот
Операция — узел, а не изолированная строка. Ниже — что к ней присоединено:
flowchart TB
FO["FinanceOperations"]
AB["Abonents
клиент"] --- FO
AA["AdminAccounts
лицевой счёт"] --- FO
FT["FinTypes
тип · op_sign ±1"] --- FO
ORG["Organization
денорм."] --- FO
FO --- U["auth_user
кто провёл"]
FO --- US["Usluga
за что списано"]
FO --- CT["CreditType
оплата по"]
FO --- SELF["счёт-родитель
сторно"]
BSO["строка
выписки"] --> FO
FO --> KKM["AtolConfig
чек ОФД"]
owner(FKauth_user) — кто из сотрудников провёл операцию.operator(FK Abonents) — оператор связи, кому выставлять счёт.usluga(FK Usluga) — услуга, по которой проведено списание.fin_op(FK self) — родительский счёт, к которому относится платёж.related_operation(FK self) — связанная операция (например, оригинал для сторно).- Снапшот баланса на момент операции:
aa_debit,aa_credit,aa_ostatok,aa_credit_adjust,aa_limit_warning,aa_limit,aa_unlimited,aa_unlimited_date. Нужен, чтобы восстановить состояние счёта на любой момент прошлого — без него разбор жалобы «а почему меня отключили в марте» превращается в гадание.
Онлайн-касса (ФР)
Поля для интеграции с ОФД и кассовой техникой:
kkm_printed— флаг успешной отправки в кассу.kkm_storno— флаг сторно через ФР.kkm_check_id— идентификатор чека.kkm_check_retry_num— количество попыток отправки.atol_config(FKAtolConfig) — какая ОФД-конфигурация использована.print_form(binary) — распечатанная форма документа.is_cash— оплата через вебкассу.
Настройки фискализации — в /admin/settings/atol_config/.
Внешние операторы
Для операций, пришедших через webhook платёжных систем (/admin/settings/payment/):
operator_pay_id/operator_pay_id_str— ID транзакции у оператора.operator_name— название оператора («yookassa», «w1», «sber»).operator_result— код результата.
Идемпотентность по operator_pay_id_str: повторный webhook на ту же транзакцию не создаёт дубль. Это не теоретическая предосторожность — платёжные системы штатно повторяют доставку, пока не получат подтверждение.
Синхронизация с 1С
sync1c— флаг успешной синхронизации.code1c— ID документа в 1С (до 32 символов).isbuhdoc— признак реального финансового документа: служебные операции в отчётность не попадают.
Endpoints
| URL | Метод | Назначение |
|---|---|---|
/admin/Abonents/FinanceOperations/ | GET | HTML-страница списка |
/admin/Abonents/finops_json/ | GET | Список: q, op_type, source, date_from, date_to, page, per_page, курсор after_date + after_id. Возвращает items, total, next_cursor, has_more, show_org |
/admin/Abonents/finops_detail/<op_id>/ | GET | Полный контекст карточки (операция, клиент, связи, выписка) |
/admin/Abonents/finops_crud/[<op_id>/] | POST | Создание / изменение / удаление (_delete=1) |
/admin/Abonents/finops_credit_types/?q= | GET | Подсказки: типы оплаты |
/admin/Abonents/finops_staff_users/?q= | GET | Подсказки: сотрудники |
/admin/Abonents/finops_uslugi/?q= | GET | Подсказки: услуги |
/admin/Abonents/finops_search/?q= | GET | Поиск операции-родителя |
/rest_api/v2/FinanceOperations/ | GET / POST | REST API v2 (CRUD) |
Обслуживание данных
manage.py backfill_finops_org [--apply] — проставляет организацию операциям, у которых она пустая: сначала из организации клиента, затем организацию по умолчанию для остатка. Без --apply только показывает, что будет сделано.
Команда идемпотентна — трогает только записи с пустой организацией. Это осознанно: у части операций организация уже проставлена верно и отличается от организации по умолчанию, и «проставить всем одну» затёрло бы их и сломало изоляцию доступа.
Чем отличается от Журнала платежей
В системе два раздела поверх одной таблицы finance_operations:
- «Финансовые операции» (этот раздел) — рабочий инструмент кассира и бухгалтера: создание, правка, разбор конкретной операции.
- «Журнал платежей» (
/admin/reports/PayLog/) — отчётный инструмент: грид с экспортом в CSV/XLSX и фильтрами по периоду и типу для отчётности.
Разница только в UI и наборе фильтров — данные одни и те же.
Смежные разделы
- Счета — лицевой счёт, который двигают операции; там же пополнение и корректировка.
- Карточка клиента — вкладка «Операции» показывает тот же журнал в разрезе одного клиента.
- Банковские выписки — автоматическое создание операций из выписок Сбер / Альфа.
- Фискализация 54-ФЗ — чеки в ОФД по этим операциям.
- Платежи (API) — webhooks ЮKassa, Wallet One и Единой кассы, которые эти операции создают.
- Обещанный платёж — пара операций «выдача / закрытие».