Счётчик в 1С на регистре накопления: создаём с нуля
Содержание
Счётчик в 1С — это не отдельный объект метаданных, а связка из трёх частей: регистр накопления хранит текущее значение, документ меняет его движениями, а запрос к остаткам показывает, сколько накопилось на нужную дату. Сделать такой счётчик можно только в Конфигураторе (или в режиме расширения конфигурации ), на встроенном языке, в платформе 8.3.
Ниже — рабочая схема: какой регистр взять, как спроектировать измерения и ресурсы, как написать проведение документа, как прочитать текущее значение и чем закрыть счётчик от ухода в минус при параллельной работе пользователей.
Пример будем собирать на простом счётчике количества — скажем, остатка бонусных баллов по клиенту или числа выданных пропусков. Логика одинакова для любого числового показателя, который нужно складывать и вычитать.
Какой вид регистра накопления взять
Регистр накопления в 1С бывает двух видов, и это первое решение, от которого всё зависит:
- 🟢 Остатки — хранит текущее значение, которое можно увеличивать (приход) и уменьшать (расход). Платформа сама считает остаток на любую дату. Это и есть счётчик в привычном смысле.
- 🔵 Обороты — хранит только накопление «сколько прошло за период», без понятия текущего остатка. Берут его для статистики: сколько продали, сколько начислили за месяц.
Для счётчика, у которого есть «сколько сейчас», нужен вид «Остатки». Если перепутать и поставить «Обороты», виртуальной таблицы остатков просто не будет, и текущее значение придётся считать вручную — это тупиковый путь.
Выбор вида задаётся свойством Вид регистра в палитре свойств регистра и меняется потом болезненно: при смене вида структура движений перестраивается. Определитесь на старте.
Регистр накопления всегда работает в паре с документом-регистратором. Сам по себе он значения не меняет — только документ пишет в него движения.
Иногда под «счётчиком» понимают не накопитель, а простой изменяемый показатель без прихода и расхода — например, последний использованный порядковый номер. Для такого случая регистр накопления избыточен: берут регистр сведений (периодический или непериодический) и хранят одно значение по ключу. Но как только появляется логика «прибавить/вычесть» и нужен остаток на дату — это именно регистр накопления вида «Остатки», и дальше речь о нём.
Проектируем измерения, ресурсы и реквизиты
Структура регистра — это ответ на вопрос «счётчик в разрезе чего». Три типа полей:
| Поле | Что хранит | Пример для счётчика баллов |
|---|---|---|
| Измерение | Разрез, по которому ведём учёт | Клиент (или Клиент + Карта) |
| Ресурс | Само число, которое накапливаем | Количество (баллы) |
| Реквизит | Доп. информация без влияния на остаток | Комментарий, Основание |
Правила, которые экономят потом часы отладки:
- Измерений — минимум необходимого. Каждое лишнее измерение раздувает таблицу итогов и замедляет запросы. Если счётчик ведётся по клиенту — одно измерение Клиент, и всё.
- Ресурс — только числовой тип. Для баллов достаточно длины 10 и точности 0 (целые). Для денег — точность 2.
- Порядок измерений важен. Ставьте первым то измерение, по которому чаще всего фильтруете остатки. Платформа строит итоги по порядку измерений слева направо.
Создаётся регистр в дереве конфигурации: правой кнопкой по ветке Регистры накопления → Добавить. На вкладке Данные добавляете измерения и ресурсы, на вкладке Регистраторы позже отметите документ.
Документ, который двигает счётчик
Менять значение счётчика будет документ. Сначала свяжите его с регистром: откройте документ, вкладка Движения, в списке регистров накопления установите флажок напротив своего регистра. Платформа предложит сконструировать движения — можно согласиться для черновика, но боевой код пишут руками.
Движения формируются в процедуре ОбработкаПроведения модуля документа. Простой приход (увеличение счётчика):
Процедура ОбработкаПроведения(Отказ, Режим)
Движения.Счётчик.Записывать = Истина;
Для Каждого СтрокаТаблицы Из Товары Цикл
Движение = Движения.Счётчик.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Приход;
Движение.Период = Дата;
Движение.Клиент = Клиент;
Движение.Количество = СтрокаТаблицы.Количество;
КонецЦикла;
КонецПроцедуры
Разберём ключевые строки:
Движения.Счётчик.Записывать = Истина— говорит платформе записать набор движений при проведении. Без этой строки записи не появятся.ВидДвижения—Приходувеличивает остаток,Расходуменьшает. Для счётчика-накопителя обычно только приход; если значение надо и списывать — добавьте расход.Период— дата, на которую ложится движение. Как правило, это дата самого документа.
Проводить документ в рабочей базе ради проверки через копирование соседнего документа — рискованно: копия тоже делает движения и задваивает счётчик. Создавайте тестовый документ и удаляйте его после проверки.
Если счётчик должен уметь и прибавлять, и убавлять (списание баллов), для расхода меняется только вид движения:
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Как узнать текущее значение счётчика запросом
Текущее значение читают не из таблицы движений, а из виртуальной таблицы остатков — её платформа считает сама. Имя таблицы: РегистрНакопления.<Имя>.Остатки. Ресурс превращается в колонку <Ресурс>Остаток — для ресурса Количество это КоличествоОстаток.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СчётчикОстатки.Клиент КАК Клиент,
| СчётчикОстатки.КоличествоОстаток КАК Остаток
|ИЗ
| РегистрНакопления.Счётчик.Остатки(&Период, Клиент = &Клиент) КАК СчётчикОстатки";
Запрос.УстановитьПараметр("Период", ТекущаяДата());
Запрос.УстановитьПараметр("Клиент", Клиент);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
ТекущееЗначение = Выборка.Остаток;
Иначе
ТекущееЗначение = 0;
КонецЕсли;
Два момента, на которых спотыкаются чаще всего:
- Пустой результат — это ноль, а не ошибка. Если по клиенту движений ещё не было, виртуальная таблица вернёт пустую выборку. Поэтому ветка
Иначеобязательна: нет строки — значение0. - Параметр периода. Первый параметр таблицы
Остатки()— момент, на который нужен остаток. Второй — условие отбора. Передавайте границу периода точкой илиНеопределено, если нужен остаток на текущий момент.
Для остатка «прямо сейчас» в реальном времени вместо ТекущаяДата() на сервере корректнее Неопределено в качестве периода — тогда платформа возьмёт самую свежую точку расчёта итогов.
Если текущее значение нужно сразу по многим клиентам (список, отчёт), не гоняйте запрос в цикле. Уберите отбор по конкретному клиенту — виртуальная таблица вернёт остатки по всем разрезам одним запросом:
"ВЫБРАТЬ
| СчётчикОстатки.Клиент КАК Клиент,
| СчётчикОстатки.КоличествоОстаток КАК Остаток
|ИЗ
| РегистрНакопления.Счётчик.Остатки КАК СчётчикОстатки"
Когда вместе с текущим значением нужна и динамика (сколько пришло и ушло за период), есть третья виртуальная таблица — ОстаткиИОбороты. Она отдаёт начальный остаток, приход, расход и конечный остаток в одной выборке, и это дешевле, чем считать то же тремя отдельными запросами.
Блокировки и контроль: чтобы счётчик не ушёл в минус
Самая частая боль счётчика — списали больше, чем было. Два пользователя одновременно списывают баллы, каждый видит старый остаток, и в сумме счётчик уходит в отрицательное значение. Защита — контроль остатка с блокировкой, и порядок действий здесь жёсткий.
В управляемом режиме блокировок (8.3 по умолчанию) схема такая:
- Сформировать движения расхода в
ОбработкаПроведения. - Поставить
Движения.Счётчик.БлокироватьДляИзменения = Истина— это наложит управляемую блокировку на нужные комбинации измерений. - Записать движения:
Движения.Записать(). - Только после записи запросить остатки виртуальной таблицы.
- Если остаток отрицательный — установить
Отказ = Истинаи сообщить пользователю.
Движения.Счётчик.Записывать = Истина;
Движения.Счётчик.БлокироватьДляИзменения = Истина;
// ... заполнение движений расхода ...
Движения.Записать();
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СчётчикОстатки.Клиент КАК Клиент,
| СчётчикОстатки.КоличествоОстаток КАК Остаток
|ИЗ
| РегистрНакопления.Счётчик.Остатки(, Клиент = &Клиент) КАК СчётчикОстатки
|ГДЕ
| СчётчикОстатки.КоличествоОстаток < 0";
Запрос.УстановитьПараметр("Клиент", Клиент);
Если НЕ Запрос.Выполнить().Пустой() Тогда
Отказ = Истина;
Сообщить("Недостаточно баллов у клиента " + Клиент);
КонецЕсли;
Почему порядок именно такой: БлокироватьДляИзменения фиксирует, что никто другой в эту секунду не тронет те же комбинации измерений. Запись движений до чтения остатков даёт нам уже актуальную картину с учётом собственного расхода — поэтому отрицательный остаток поймается сразу.
Проведение документа выполняется на сервере, и блокировка живёт в рамках транзакции проведения. Это важно помнить: тяжёлые вычисления и обращения к внешним данным внутри ОбработкаПроведения удерживают блокировку дольше и создают очередь из ожидающих сеансов. Держите проведение лёгким — заполнение движений и проверка остатка, без лишнего.
Отдельно про отрицательный остаток как бизнес-правило: иногда счётчик в минус уходить должен (например, это аванс или кредитный лимит). Тогда контроль не нужен — просто не ставьте проверку. Решение принимается один раз на этапе проектирования и зависит от смысла счётчика, а не от технических ограничений регистра.
Контроль остатка живёт в проведении документа. Если кто-то поменяет движения напрямую через набор записей регистра в обход документа, проверка не сработает — оставьте запись движений за документом.
Готовое решение по контролю отрицательных остатков есть в типовых конфигурациях (БСП), но для своего регистра логику пишут под его структуру.
Частые ошибки при создании счётчика
Отдельно собрал то, что не видно из кода, но ломает счётчик на практике:
Движения.<Имя>.Записывать = Истина или документ не отмечен регистратором на вкладке «Движения». Проверьте оба места.БлокироватьДляИзменения, либо остатки читаются ДО записи движений. Ставьте блокировку и читайте остаток после Движения.Записать().Что ускоряет счётчик и итог
Когда счётчик заработал, остаётся сделать его быстрым и предсказуемым. Короткий список приёмов:
- 📉 Минимум измерений. Таблица итогов — это произведение всех значений измерений. Чем их меньше, тем быстрее запрос остатков.
- 🗓 Итоги и агрегаты. У регистра накопления есть механизм хранения итогов. Он включён по умолчанию; для больших регистров проверьте, что пересчёт итогов не отключён, иначе каждый запрос остатков будет суммировать движения с нуля.
- 🔍 Отбор в виртуальной таблице, а не во внешнем ГДЕ. Условие передавайте вторым параметром
Остатки(, Условие)— платформа применит его до расчёта итогов, это быстрее, чем фильтровать результат. - 🧮 Остаток на момент. Для текущего значения период оставляйте пустым (
Неопределено) — платформа возьмёт готовую точку итогов, не пересчитывая на произвольную дату.
Итого, чтобы сделать счётчик в 1С: создаёте регистр накопления вида «Остатки», задаёте измерения (разрез учёта) и числовой ресурс, пишете движения в проведении документа, читаете значение запросом к виртуальной таблице остатков и закрываете его от гонки блокировкой БлокироватьДляИзменения с проверкой отрицательного остатка. Этого достаточно для любого счётчика — от бонусных баллов до учёта выданных пропусков.