Переход на редакцию 1С:Зарплата и управление персоналом 3.1 принес разработчикам множество преимуществ в плане модульности и универсализации кода. Однако оборотной стороной медали стало значительное усложнение бизнес-логики. Многие разработчики, пытающиеся автоматизировать рутинные процессы (например, создание документов начисления зарплаты из внешних обработок или через фоновые задания), сталкиваются с серьезным препятствием: расчет зарплаты жестко привязан к контексту управляемой формы. Попытки просто создать документ, заполнить его реквизиты и записать приводят к тому, что документ оказывается пустым, не рассчитывается или делает некорректные движения по регистрам.
В этой статье мы подробно разберем, как обойти эти ограничения, заглянем внутрь программных механизмов расчета ЗУП 3.1, научимся использовать встроенный менеджер расчета и напишем готовый рабочий код для программного заполнения и проведения документа НачислениеЗарплаты.
Давайте проанализируем ситуацию и выясним, почему обычный метод Заполнить() не решает задачу. В отличие от старой редакции 2.5, где расчеты происходили преимущественно в модуле объекта, в ЗУП 3.1 логика расчета распределена между общими модулями, обработкой МенеджерРасчетаЗарплаты и модулем управляемой формы документа. Это сделано для повышения интерактивности: пользователь на форме видит расчет «на лету».
При этом база данных хранит информацию в так называемом «плоском» виде, а на форме данные трансформируются. Например, показатели начислений (такие как Оклад, Тарифная ставка, Районный коэффициент) на форме отображаются в виде динамических колонок в строке сотрудника. Но в базе данных они хранятся в отдельной подчиненной табличной части Показатели. Связующим звеном между строкой начисления и строкой показателей выступает специальный реквизит ИдентификаторСтрокиВидаРасчета (или ИдентификаторСтроки).
Если мы просто заполним документ программно без учета этих связей, то:
ЗначенияПоказателейНачисления не сформируются движения. Это сломает последующие отчеты и перерасчеты.Для решения этой задачи нам потребуется сымитировать работу формы программным путем на сервере.
Для корректного заполнения параметров расчета системе требуется структура ОписаниеДокумента. В модуле формы документа НачислениеЗарплаты функция ОписаниеДокумента() не является экспортной. Чтобы не переносить весь этот код вручную в свои внешние обработки (что сделает их крайне неустойчивыми к обновлениям конфигурации), мы можем использовать штатные экспортные функции платформы.
Рассмотрим, как инициализировать параметры и подготовить описание документа программно:
ПараметрыОписания = Новый Структура;
ПараметрыОписания.Вставить("ИзменениеБезОграничений", Истина);
ПараметрыОписания.Вставить("РегистрацияНачисленийДоступна", Истина);
ПараметрыОписания.Вставить("СпособыРасчетаСреднегоЗаработка", Неопределено);
ОписаниеДокумента = КадровыйУчетРасширенныйКлиентСервер.ОписаниеДокумента(ПараметрыОписания);
Используя этот подход, мы гарантируем совместимость нашего кода с будущими релизами 1С:ЗУП.
Теперь нам необходимо сформировать структуру параметров, аналогичную той, которую форма отправляет в длительную операцию расчета при нажатии кнопки «Заполнить». Для этого мы воспользуемся функцией общего модуля РасчетЗарплатыРасширенныйКлиентСервер.ПараметрыПолученияДанныхЗаполненияДокумента().
Давайте разберем по шагам заполнение этой структуры:
СтруктураПараметров = РасчетЗарплатыРасширенныйКлиентСервер.ПараметрыПолученияДанныхЗаполненияДокумента();
СтруктураПараметров.ОписаниеДокумента = ОписаниеДокумента;
СтруктураПараметров.Организация = ОбъектДокумент.Организация;
СтруктураПараметров.ДокументСсылка = ОбъектДокумент.Ссылка;
СтруктураПараметров.Подразделение = ОбъектДокумент.Подразделение;
СтруктураПараметров.МесяцНачисления = ОбъектДокумент.МесяцНачисления;
СтруктураПараметров.ОкончаниеПериода = КонецМесяца(ОбъектДокумент.МесяцНачисления);
СтруктураПараметров.РежимНачисления = Перечисления.РежимНачисленияЗарплаты.ОкончательныйРасчет;
СтруктураПараметров.ОкончательныйРасчетНДФЛ = Истина;
СтруктураПараметров.ПроверятьРегистрациюПроцентаЕНВД = Истина;
СтруктураПараметров.ДатаВыплаты = РасчетЗарплатыРасширенныйКлиентСервер.ПланируемаяДатаВыплатыЗарплаты(ОбъектДокумент.Организация, ОбъектДокумент.МесяцНачисления);
После подготовки параметров мы вызываем основной метод заполнения табличных частей. Платформа 1С делает это через фоновые задания БСП с использованием временного хранилища. Мы воспользуемся методом ПодготовитьДанныеДляЗаполнения менеджера документа НачислениеЗарплаты:
АдресХранилища = ПоместитьВоВременноеХранилище(Неопределено, Новый УникальныйИдентификатор);
Документы.НачислениеЗарплаты.ПодготовитьДанныеДляЗаполнения(СтруктураПараметров, АдресХранилища);
РезультатЗаполнения = ПолучитьИзВременногоХранилища(АдресХранилища);
В переменной РезультатЗаполнения мы получаем структуру со всеми рассчитанными таблицами: начислениями, удержаниями, НДФЛ, взносами и показателями. Наша следующая задача — правильно перенести эти таблицы в создаваемый документ.
Проанализируем структуру полученного результата. Большинство таблиц можно загрузить напрямую в соответствующие табличные части документа с помощью метода Загрузить(). Однако с показателями расчетов все обстоит сложнее.
В структуре РезультатЗаполнения.ДанныеДляЗаполненияТаблицДокумента начисления лежат в виде таблицы значений. Внутри каждой строки этой таблицы в колонке Показатели содержится вложенная таблица значений со значениями конкретных показателей для данной строки начисления. Нам необходимо «развернуть» эти вложенные таблицы во флэт-структуру подчиненной ТЧ Показатели документа, при этом строго соблюдая равенство идентификаторов строк.
Выполним заполнение начислений и показателей с помощью следующего алгоритма:
// Очищаем существующие таблицы документа перед заполнением
ОбъектДокумент.Начисления.Очистить();
ОбъектДокумент.Показатели.Очистить();
// Переносим таблицы значений из результата расчета в ТЧ документа
Для Каждого КлючИЗначение Из РезультатЗаполнения.ДанныеДляЗаполненияТаблицДокумента Цикл
Если ТипЗнч(КлючИЗначение.Value) = Тип("ТаблицаЗначений") Тогда
// Проверяем наличие ТЧ в документе
МетаданныеТЧ = ОбъектДокумент.Метаданные().ТабличныеЧасти.Найти(КлючИЗначение.Key);
Если МетаданныеТЧ <> Неопределено Тогда
ОбъектДокумент[КлючИЗначение.Key].Загрузить(КлючИЗначение.Value);
КонецЕсли;
КонецЕсли;
КонецЦикла;
// Заполняем показатели из вложенных таблиц строк начислений
Для Каждого СтрокаНачисления Из РезультатЗаполнения.ДанныеДляЗаполненияТаблицДокумента.Начисления Цикл
// Ищем соответствующую вложенную ТЗ показателей для текущей строки
Для Каждого СтрокаПоказателя Из СтрокаНачисления.Показатели Цикл
НоваяСтрокаПоказателей = ОбъектДокумент.Показатели.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрокаПоказателей, СтрокаПоказателя);
// Связываем строку показателя со строкой начисления по идентификатору
НоваяСтрокаПоказателей.ИдентификаторСтрокиВидаРасчета = СтрокаНачисления.ИдентификаторСтроки;
КонецЦикла;
КонецЦикла;
В процессе тестирования можно заметить, что движения по НДФЛ и страховым взносам могут формироваться некорректно, если не заполнены идентификаторы строк в соответствующих ТЧ. В отличие от начислений, где идентификаторы уникальны и генерируются системой автоматически, для НДФЛ часто используется сквозная порядковая нумерация.
Если после заполнения вы обнаружили, что реквизит ИдентификаторСтроки в ТЧ НДФЛ остался пустым, выполните принудительное индексирование строк перед проведением документа:
ИндексСтроки = 1;
Для Каждого СтрокаНДФЛ Из ОбъектДокумент.НДФЛ Цикл
Если СтрокаНДФЛ.ИдентификаторСтроки = 0 Тогда
СтрокаНДФЛ.ИдентификаторСтроки = ИндексСтроки;
КонецЕсли;
ИндексСтроки = ИндексСтроки + 1;
КонецЦикла;
Объединим все вышеописанные шаги в единую законченную экспортную функцию. Вы можете разместить этот код в общем модуле или модуле внешней обработки для автоматического начисления зарплаты по расписанию:
Функция ПрограммноеНачислениеЗарплаты(Организация, МесяцНачисления) Экспорт
// 1. Создаем новый документ
ОбъектДокумент = Документы.НачислениеЗарплаты.СоздатьДокумент();
ОбъектДокумент.Дата = ТекущаяДата();
ОбъектДокумент.МесяцНачисления = НачалоМесяца(МесяцНачисления);
ОбъектДокумент.Организация = Организация;
// Заполняем первоначальные значения (ответственный, подразделение и т.д.)
ЗначенияДляЗаполнения = Новый Структура("Организация, Ответственный", Организация, ПараметрыСеанса.ТекущийПользователь);
ЗарплатаКадры.ЗаполнитьПервоначальныеЗначенияВФорме(ОбъектДокумент, ЗначенияДляЗаполнения);
// Записываем документ, чтобы получить ссылку (необходимо для расчетов)
ОбъектДокумент.Записать(РежимЗаписиДокумента.Запись);
// 2. Формируем описание документа
ПараметрыОписания = Новый Структура;
ПараметрыОписания.Вставить("ИзменениеБезОграничений", Истина);
ПараметрыОписания.Вставить("РегистрацияНачисленийДоступна", Истина);
ПараметрыОписания.Вставить("СпособыРасчетаСреднегоЗаработка", Неопределено);
ОписаниеДокумента = КадровыйУчетРасширенныйКлиентСервер.ОписаниеДокумента(ПараметрыОписания);
// 3. Настраиваем структуру параметров расчета
СтруктураПараметров = РасчетЗарплатыРасширенныйКлиентСервер.ПараметрыПолученияДанныхЗаполненияДокумента();
СтруктураПараметров.ОписаниеДокумента = ОписаниеДокумента;
СтруктураПараметров.Организация = ОбъектДокумент.Organзация;
СтруктураПараметров.ДокументСсылка = ОбъектДокумент.Ссылка;
СтруктураПараметров.МесяцНачисления = ОбъектДокумент.МесяцНачисления;
СтруктураПараметров.ОкончаниеПериода = КонецМесяца(ОбъектДокумент.МесяцНачисления);
СтруктураПараметров.РежимНачисления = Перечисления.РежимНачисленияЗарплаты.ОкончательныйРасчет;
СтруктураПараметров.ОкончательныйРасчетНДФЛ = Истина;
СтруктураПараметров.ПроверятьРегистрациюПроцентаЕНВД = Истина;
СтруктураПараметров.ДатаВыплаты = РасчетЗарплатыРасширенныйКлиентСервер.ПланируемаяДатаВыплатыЗарплаты(Организация, МесяцНачисления);
// 4. Запускаем фоновый расчет данных
АдресХранилища = ПоместитьВоВременноеХранилище(Неопределено, Новый УникальныйИдентификатор);
Документы.НачислениеЗарплаты.ПодготовитьДанныеДляЗаполнения(СтруктураПараметров, АдресХранилища);
РезультатЗаполнения = ПолучитьИзВременногоХранилища(АдресХранилища);
Если РезультатЗаполнения = Неопределено Тогда
ВызватьИсключение "Не удалось получить данные для заполнения документа начисления зарплаты.";
КонецЕсли;
// 5. Загружаем основные таблицы
Для Каждого КлючИЗначение Из РезультатЗаполнения.ДанныеДляЗаполненияТаблицДокумента Цикл
Если ТипЗнч(КлючИЗначение.Value) = Тип("ТаблицаЗначений") Тогда
МетаданныеТЧ = ОбъектДокумент.Метаданные().ТабличныеЧасти.Найти(КлючИЗначение.Key);
Если МетаданныеТЧ <> Неопределено Тогда
ОбъектДокумент[КлючИЗначение.Key].Загрузить(КлючИЗначение.Value);
КонецЕсли;
КонецЕсли;
КонецЦикла;
// 6. Переносим показатели начислений
ОбъектДокумент.Показатели.Очистить();
Для Каждого СтрокаНачисления Из РезультатЗаполнения.ДанныеДляЗаполненияТаблицДокумента.Начисления Цикл
Для Каждого СтрокаПоказателя Из СтрокаНачисления.Показатели Цикл
НоваяСтрокаПоказателей = ОбъектДокумент.Показатели.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрокаПоказателей, СтрокаПоказателя);
НоваяСтрокаПоказателей.ИдентификаторСтрокиВидаРасчета = СтрокаНачисления.ИдентификаторСтроки;
КонецЦикла;
КонецЦикла;
// 7. Корректируем идентификаторы НДФЛ
ИндексСтроки = 1;
Для Каждого СтрокаНДФЛ Из ОбъектДокумент.НДФЛ Цикл
СтрокаНДФЛ.ИдентификаторСтроки = ИндексСтроки;
ИндексСтроки = IndexСтроки + 1;
КонецЦикла;
// 8. Проводим готовый документ
ОбъектДокумент.Записать(РежимЗаписиДокумента.Проведение);
Возврат ОбъектДокумент.Ссылка;
КонецФункции
Для более тонких и кастомных расчетов можно использовать объект МенеджерРасчетаЗарплаты напрямую, минуя процедуры подготовки данных документа. Это полезно, если вам нужно рассчитать зарплату, не создавая сам документ в базе данных (например, для построения прогнозных отчетов или интеграции со сторонними CRM-системами).
Проанализируем пример такого использования:
МенеджерРасчета = Обработки.МенеджерРасчетаЗарплаты.Создать();
МенеджерРасчета.Инициализировать(МесяцНачисления, Организация);
// Настраиваем состав расчетов
МенеджерРасчета.НастройкиРасчета.РассчитыватьНачисления = Истина;
МенеджерРасчета.НастройкиРасчета.РассчитыватьУдержания = Истина;
МенеджерРасчета.НастройкиРасчета.РассчитыватьНДФЛ = Истина;
// Выполняем расчет в оперативной памяти
МенеджерРасчета.РассчитатьЗарплату();
// Анализируем результаты расчета
ТаблицаРезультатов = МенеджерРасчета.Зарплата.Начисления;
// Здесь можно обработать полученную таблицу значений
Этот способ работает быстрее, так как не производит запись промежуточных данных в СУБД, но требует от разработчика самостоятельной сборки итогового документа из таблиц менеджера расчета.
Поскольку ЗУП 3.1 постоянно обновляется в связи с частыми изменениями в законодательстве (НДФЛ, взносы, ЕФС-1 и т.д.), при написании программного кода автоматизации важно следовать нескольким правилам:
ПолучитьФорму() и имитировать нажатие кнопок. Это крайне медленно, не работает в регламентных заданиях без контекста клиента и ломается при любом визуальном редизайне формы фирмой 1С.РасчетЗарплатыРасширенный, КадровыйУчетРасширенныйКлиентСервер и ЗарплатаКадры. Эти модули считаются частью внутреннего API конфигурации и реже подвергаются кардинальным изменениям при обновлениях.Рассмотренный нами алгоритм позволяет решить задачу программного начисления зарплаты красиво, быстро и с соблюдением всех стандартов разработки фирмы «1С».