В практической работе разработчика 1С часто возникает задача синхронизации и автоматического пересчета связанных данных. Типичный пример: в системе ведется справочник технологических операций или работ, стоимость которых напрямую зависит от базовых тарифных ставок. При изменении тарифа система должна автоматически обновить расценки для всех связанных работ.
В этой статье мы подробно разберем, как решить эту задачу на примере конфигурации «КАМИН:Расчет заработной платы 3.0» (или любой другой аналогичной конфигурации на платформе «1С:Предприятие 8»). Мы проанализируем архитектурные ошибки, которые часто допускают начинающие специалисты, и выработаем надежное, быстродействующее и отказоустойчивое решение с использованием подписок на события.
---
Прежде чем перейти к программированию, давайте проанализируем ситуацию и разберем архитектурные нюансы, которые упустили участники обсуждения на форуме. Это убережет нас от критических ошибок в реальной базе данных.
Ошибка №1: Подписка на событие для справочника вместо регистра
В ходе обсуждения автору посоветовали создать подписку на событие ПриЗаписи для справочника ТарифныеСетки. Это не сработает. В конфигурациях расчета зарплаты сам справочник «Тарифные сетки» содержит только общую информацию (например, наименование). Конкретные значения ставок (тарифы по разрядам) хранятся в подчиненном периодическом регистре сведений (в нашем случае это РазрядыТарифныхСеток). Когда расчетчик меняет ставку, он вносит изменения в регистр сведений, а сам элемент справочника тарифных сеток не перезаписывается. Следовательно, подписка на справочник просто не запустится.
Правильное решение: Нам необходимо создавать подписку на событие для источника РегистрСведенийНаборЗаписей.РазрядыТарифныхСеток.
Ошибка №2: Игнорирование периодичности и истории изменений
Если мы будем записывать вычисленную «Расценку» напрямую в реквизит справочника Работы, мы потеряем историю изменений. Тарифные ставки периодически индексируются. Например, до 1 июня ставка составляла 200 рублей, а с 1 июня — 250 рублей. Если мы перезапишем расценку прямо в карточке работы, то при попытке пересчитать или проанализировать документы за прошлые периоды программа возьмет новую (неактуальную для того периода) расценку, что приведет к искажению учета.
Правильное решение: Мы рассмотрим два варианта реализации — динамический расчет «на лету» без хранения расценки (Вариант А) и использование нового периодического регистра сведений для хранения истории расценок (Вариант Б).
---
Выясним причину и разберем по шагам правильный алгоритм реализации задачи.
Для того чтобы система знала, какую ставку применять к конкретной работе, нам нужно связать справочник Работы со справочником ТарифныеСетки. Добавим в справочник Работы новый реквизит:
ТарифнаяСеткаСправочникСсылка.ТарифныеСеткиЕсли в вашей тарифной сетке используется разрядность, также потребуется добавить реквизит Разряд (тип данных зависит от структуры регистра ставок в вашей конфигурации, обычно это справочник или число).
Вариант А (Рекомендуемый): Динамический расчет без хранения в справочнике
Самый чистый архитектурный подход — вообще не хранить расценку в справочнике Работы как статический реквизит. Вместо этого в документах начисления или отчетах мы получаем расценку динамически с помощью запроса. Запрос соединяет справочник Работы со срезом последних регистра сведений РазрядыТарифныхСеток на нужную дату документа.
Формула расчета в запросе будет выглядеть следующим образом:
Расценка = РазрядыТарифныхСетокСрезПоследних.Ставка * Работы.НормаЧасов
Этот способ исключает дублирование данных в базе и гарантирует 100% актуальность расценок на любую дату в прошлом или будущем.
Вариант Б: Хранение истории расценок в периодическом регистре сведений
Если бизнес-логика требует физического хранения уже рассчитанных расценок в базе данных, создадим новый периодический регистр сведений РасценкиРабот со следующей структурой:
Работа (тип СправочникСсылка.Работы).Расценка (тип Число, длина 15, точность 2).---
Разберем процесс создания подписки в дереве метаданных конфигуратора:
ПриЗаписиСтавокТарифныхСеток.РегистрСведенийНаборЗаписей.РазрядыТарифныхСеток.ПриЗаписи.При создании общего модуля (назовем его ОбработчикиСобытийКастомный) критически важно правильно выставить свойства в палитре свойств:
---
Посмотрим на пример реализации обработчика подписки в общем модуле. Код должен быть максимально оптимизирован, не вызывать лишних блокировок и корректно обрабатывать пакетную загрузку данных.
Проанализируем структуру кода:
Процедура ПриЗаписиСтавокТарифныхСетокПриЗаписи(Источник, Отказ) Экспорт
// Критически важное правило: если идет загрузка данных (обмен, перенос),
// отключаем выполнение бизнес-логики во избежание падения производительности.
Если Источник.ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
// Получаем измененные записи регистра ставок.
// Так как Источник — это НаборЗаписей, мы можем проанализировать, для каких именно
// тарифных сеток и на какие даты поменялись ставки.
ТаблицаИзменений = Источник.Выгрузить();
Если ТаблицаИзменений.Количество() = 0 Тогда
Возврат;
КонецЕсли;
// Чтобы избежать дублирования запросов в цикле, соберем все уникальные тарифные сетки
// и даты (периоды), для которых изменились ставки.
ТарифныеСеткиМассив = Новый Массив;
Для Каждого СтрокаИзменений Из ТаблицаИзменений Цикл
Если ТарифныеСеткиМассив.Найти(СтрокаИзменений.ТарифнаяСетка) = Неопределено Тогда
ТарифныеСеткиМассив.Добавить(СтрокаИзменений.ТарифнаяСетка);
КонецЕсли;
КонецЦикла;
// Запросом выбираем все работы, привязанные к измененным тарифным сеткам,
// и получаем актуальные ставки из регистра на даты изменения.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Работы.Ссылка КАК Работа,
| Работы.НормаЧасов КАК НормаЧасов,
| РазрядыТарифныхСеток.Период КАК Период,
| РазрядыТарифныхСеток.Ставка КАК Ставка
|ИЗ
| Справочник.Работы КАК Работы
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.РазрядыТарифныхСеток КАК РазрядыТарифныхСеток
| ПО Работы.ТарифнаяСетка = РазрядыТарифныхСеток.ТарифнаяСетка
|ГДЕ
| Работы.ТарифнаяСетка В(&ТарифныеСеткиМассив)";
Запрос.УстановитьПараметр("ТарифныеСеткиМассив", ТарифныеСеткиМассив);
Выборка = Запрос.Выполнить().Выбрать();
// Записываем новые рассчитанные значения расценок.
// В данном примере реализуем запись в созданный нами периодический регистр "РасценкиРабот".
Пока Выборка.Следующий() Цикл
Если Выборка.НормаЧасов = 0 Или Выборка.Ставка = 0 Тогда
Продолжить; // Пропускаем некорректные данные
КонецЕсли;
НоваяРасценка = Выборка.Ставка * Выборка.НормаЧасов;
// Используем менеджер записи для точечного обновления/создания записи в регистре расценок
МенеджерЗаписи = РегистрыСведений.РасценкиРабот.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Период = Выборка.Period;
МенеджерЗаписи.Работа = Выборка.Работа;
// Попытаемся прочитать существующую запись, чтобы избежать лишней перезаписи
МенеджерЗаписи.Прочитать();
Если МенеджерЗаписи.Выбран() Тогда
Если МенеджерЗаписи.Расценка = НоваяРасценка Тогда
Продолжить; // Если расценка не изменилась, ничего не делаем
КонецЕсли;
КонецЕсли;
// Заполняем новые значения и записываем
МенеджерЗаписи.Период = Выборка.Период;
МенеджерЗаписи.Работа = Выборка.Работа;
МенеджерЗаписи.Расценка = НоваяРасценка;
Попытка
МенеджерЗаписи.Записать(Истина);
Исключение
ЗаписьЖурналаРегистрации("Ошибка автоматического пересчета расценок",
УровеньЖурналаРегистрации.Ошибка,
Выборка.Работа.Метаданные(),
Выборка.Работа,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
КонецЦикла;
КонецПроцедуры
---
Рассмотрим подробнее ключевые правила, которые гарантируют стабильность системы при работе с подписками:
Если Источник.ОбменДанными.Загрузка Тогда Возврат; КонецЕсли; должна быть первой в любом обработчике. При обмене данными (РИБ, интеграции) система переносит готовые согласованные данные. Повторный запуск расчетов в этот момент может вызвать рассинхронизацию баз, ошибки блокировок и замедление работы в сотни раз.ПриЗаписи выполняется внутри транзакции записи основного объекта. Старайтесь писать максимально быстрые и легкие запросы. Никогда не делайте длительных вычислений, обращений к веб-сервисам или ресурсоемких операций внутри обработчика подписки.Попытка...Исключение с выводом информации в Журнал регистрации. Ошибка в коде подписки не должна блокировать пользователю возможность изменить ставку тарифа, если по какой-то причине одна из сотен связанных работ оказалась повреждена или заблокирована.Мы успешно проанализировали ситуацию, исправили концептуальные ошибки форума и создали надежное расширяемое решение для автоматического расчета расценок в системе 1С.