Как заполнить программно документы 1C ЗУП 3.1 на сервере и правильно рассчитать их
Постановка задачи и описание проблемы
Требуется программно сформировать на сервере (например при обменах) документ Доход в натуральной форме, причём документ должен быть заполнен и рассчитан так, чтобы сам документ и его движения были полностью идентичны такому же документу, заполненного пользователем вручную.
Задача на самом деле не такая тривиальная, как кажется. Корень зла в том, что архитекторы конфигурации полностью отвергли паттерн MVC. Cправедливости ради, и сама платформа этому 1С не способствует, но прикладные программисты современных типовых конфигураций приложили все силы чтобы уйти от стандартных паттернов ещё дальше.
Вышеописанная проблема привела в ЗУПе к тому, что часть бизнес-логики реализуется на форме документа (уровень View и Controller в MVC) и часть процедур заполнения и расчёта документа, которые нам и нужны для выполнения задачи, находятся в модуле формы, и их не вызвать из сервера, будь они даже экспортными. Более того, многие общие модули для расчетов также является клиентскими и требуют аргументом объект типа Форма или различного вида коллекции формы. По слухам, фирма 1С признаёт проблему и обещала даже переписать бизнес-логику в общие модули, но в данный момент существенных подвижек пока не замечено и нам придётся попотеть.
Также сделаем допущение, что у нас нет времени на изучение Менеджера расчета зарплаты и мы хотим решить задачу побыстрее "в лоб", либо документ вообще другого вида и в другой конфигурации, например в ERP.
При вышеописанных условиях встречается такой подход для решения проблемы: программист создает структуру, которая повторяет реквизиты формы, копирует в свой серверный модуль все нужные ему процедуры и функции из этого модуля, все задействованные переменные модуля, при этом добавляя в каждую из процедур новый параметр для передачи структуры-обманки. В конце приходится делать полный ревью перенесенного типового кода, чтобы убрать многочисленные ошибки и везде вместо контекста формы подставлять структуру. Согласитесь - это довольно трудоёмко.
Реализация с помощью ООП
В качестве носителя решения будем применять объект конфигурации - обработку. Создаем в расширении новую обработку, назовём к примеру расш1_ДоходВНатуральнойФорме. Наша задача - написать класс (в терминах ООП), имитирующий Форму. Всю разработку ведём исключительно в модуле объекта. Создаём главный реквизит формы Объект и заготовку конструктора (в терминах ООП), в котором будем заполнять данные из ссылки на документ:
Перем Объект Экспорт;
Процедура Конструктор(ДокументСсылка) Экспорт
Объект = Новый Структура;
Объект.Вставить("Ссылка", ДокументСсылка);
// Реквизиты объекта
Для Каждого Реквизит Из ДокументСсылка.Метаданные().Реквизиты Цикл
Объект.Вставить(Реквизит.Имя, ДокументСсылка[Реквизит.Имя]);
КонецЦикла;
// Табличные части объекта
Для Каждого ТЧ Из ДокументСсылка.Метаданные().ТабличныеЧасти Цикл
Объект.Вставить(ТЧ.Имя, Новый ТаблицаЗначений);
Для Каждого Реквизит Из ТЧ.Реквизиты Цикл
Объект[ТЧ.Имя].Колонки.Добавить(Реквизит.Имя);
КонецЦикла;
Объект[ТЧ.Имя].Колонки.Добавить("НомерСтроки", Новый ОписаниеТипов("Число"));
Для Каждого Строка Из ДокументСсылка[ТЧ.Имя] Цикл
НСтр = Объект[ТЧ.Имя].Добавить();
ЗаполнитьЗначенияСвойств(НСтр, Строка);
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Для дальнейшей работы нам нужно постоянно держать открытым форму типового документа. Видим, что там есть реквизит ИзмененныеДанные в виде таблицы значений, который почти гарантированно будет нужен в расчёте (забегая вперед - интуиция не обманула), поэтому тоже добавляем его в наш класс. Остальными реквизитами формы пока не заморачиваемся - будем добавлять их по мере необходимости:
Перем Объект Экспорт;
Перем ИзмененныеДанные Экспорт;
Процедура Конструктор(ДокументСсылка) Экспорт
...
// Данные к пересчету
ИзмененныеДанные = Объект.Начисления.Скопировать(, "Сотрудник");
ИзмененныеДанные.Колонки.Добавить("ИмяТаблицы");
ИзмененныеДанные.Колонки.Добавить("ФизическоеЛицо");
ИзмененныеДанные.Колонки.Добавить("ВидРасчета");
Для Каждого Строка Из ИзмененныеДанные Цикл
Строка.ИмяТаблицы = "Начисления";
Строка.ВидРасчета = Объект.Начисление;
КонецЦикла;
КонецПроцедуры
Обязательно на форме документа заглядываем в каждую табличную часть реквизита Объект, т.к. разработчики любят добавлять туда поля, которых нет в настоящем Объекте. Видим, что их очень много! Придётся ещё добавить кода в инициализацию:
// ТерриториальныеУсловияТруда
Если ЗарплатаКадрыРасширенный.ИспользоватьРаспределениеПоТерриториямУсловиямТруда(Объект.Организация) Тогда
Объект.Начисления.Колонки.Добавить("РаспределениеПоТерриториямУсловиямТруда");
Объект.НачисленияПерерасчет.Колонки.Добавить("РаспределениеПоТерриториямУсловиямТруда");
Объект.ЗависимыеНачисления.Колонки.Добавить("РаспределениеПоТерриториямУсловиямТруда");
КонецЕсли;
// Корректировки выплаты
Объект.КорректировкиВыплаты.Колонки.Добавить("РезультатРаспределения");
Объект.КорректировкиВыплаты.Колонки.Добавить("КомандаРедактированияРаспределения");
Объект.КорректировкиВыплаты.Колонки.Добавить("РаспределениеПоСтатьям");
// НДФЛ и прочие поля
Объект.Начисления.Колонки.Добавить("НДФЛ", Новый ОписаниеТипов("Число"));
Объект.Начисления.Колонки.Добавить("КорректировкаВыплаты", Новый ОписаниеТипов("Число"));
Объект.Начисления.Колонки.Добавить("ПредставлениеРаспределенияПоТерриториямУсловиямТруда", Новый ОписаниеТипов("Строка"));
Объект.Начисления.Колонки.Добавить("ПредставлениеПериодаДействия", Новый ОписаниеТипов("Строка"));
Объект.Начисления.Колонки.Добавить("ПустаяСтрокаЗаголовка", Новый ОписаниеТипов("Строка"));
Объект.Начисления.Колонки.Добавить("РезультатЗависимыхНачислений1", Новый ОписаниеТипов("Число"));
Объект.Начисления.Колонки.Добавить("РезультатЗависимыхНачислений2", Новый ОписаниеТипов("Число"));
Объект.Начисления.Колонки.Добавить("РезультатЗависимыхНачислений3", Новый ОписаниеТипов("Число"));
Объект.Начисления.Колонки.Добавить("РезультатЗависимыхНачислений4", Новый ОписаниеТипов("Число"));
Объект.Начисления.Колонки.Добавить("РезультатЗависимыхНачислений5", Новый ОписаниеТипов("Число"));
Объект.Начисления.Колонки.Добавить("РезультатЗависимыхНачислений6", Новый ОписаниеТипов("Число"));
Всё что пока мы написали, повторяет старый подход - там тоже пришлось бы всё это писать. Теперь начинаются различия: перетаскиваем из формы главный вызов, который нам нужен на сервере для расчета:
Процедура ПерезаполнитьДанныеФормыНаСервере(Знач Сотрудники, СохранятьИсправления = Истина) Экспорт
Если ТипЗнч(Сотрудники) <> Тип("Массив") Тогда
Сотрудники = ОбщегоНазначенияКлиентСервер.ЗначениеВМассиве(Сотрудники);
КонецЕсли;
ОписаниеТаблицы = ОписаниеТаблицыНачислений();
ИдентификаторыСтрок = Новый Массив;
Если Не СохранятьИсправления Тогда
Отбор = Новый Структура("Сотрудник");
Для каждого Сотрудник Из Сотрудники Цикл
Отбор.Вставить("Сотрудник", Сотрудник);
// Заполняем поля по итогам заполнения коллекций.
СтрокиПоСотруднику = Объект.Начисления.НайтиСтроки(Отбор);
Для каждого СтрокаПоСотруднику Из СтрокиПоСотруднику Цикл
ИдентификаторыСтрок.Добавить(СтрокаПоСотруднику.ПолучитьИдентификатор());
КонецЦикла;
КонецЦикла;
КонецЕсли;
ДополнитьСтрокиНаСервере(ИдентификаторыСтрок, ОписаниеТаблицы, Не СохранятьИсправления, Не СохранятьИсправления);
РассчитатьСотрудниковНаСервере(Сотрудники, ОписаниеТаблицы, СохранятьИсправления);
Для Каждого ИдентификаторСтроки Из ИдентификаторыСтрок Цикл
СтрокаТаблицыНачислений = Объект.Начисления.НайтиПоИдентификатору(ИдентификаторСтроки);
ЗаполнитьИтогиЗависимыхНачисленийТекущегоСотрудника(СтрокаТаблицыНачислений);
КонецЦикла;
КонецПроцедуры
Обратите внимание: процедуру перенесли полностью без изменений! Никакого код-ревью нам не понадобится!
Переходим в наш серверный общий модуль, где будем использовать наш класс для программного расчета и проведения документа:
Сотрудники = Новый Массив;
НДок = Документы.ДоходВНатуральнойФорме.СоздатьДокумент();
// здесь заполняем документ-объект как обычно (только суммы без налогов)
НДок.Записать(РежимЗаписиДокумента.Запись);
ДокументСсылка = НДок.Ссылка;
ОбработкаЗаполнения = Обработки.расш1_ДоходВНатуральнойФорме.Создать();
ОбработкаЗаполнения.Конструктор(ДокументСсылка);
ОбработкаЗаполнения.ПерезаполнитьДанныеФормыНаСервере(Сотрудники, Ложь);
// перенос результатов расчета в объект документа
ЗаполнитьЗначенияСвойств(НДок, ОбработкаЗаполнения.Объект);
НДок.Начисления.Загрузить(ОбработкаЗаполнения.Объект.Начисления);
НДок.ФизическиеЛица.Загрузить(ОбработкаЗаполнения.Объект.ФизическиеЛица);
НДок.НачисленияПерерасчет.Загрузить(ОбработкаЗаполнения.Объект.НачисленияПерерасчет);
НДок.НДФЛ.Загрузить(ОбработкаЗаполнения.Объект.НДФЛ);
НДок.Показатели.Загрузить(ОбработкаЗаполнения.Объект.Показатели);
НДок.ПримененныеВычетыНаДетейИИмущественные.Загрузить(ОбработкаЗаполнения.Объект.ПримененныеВычетыНаДетейИИмущественные);
НДок.РаспределениеРезультатовНачислений.Загрузить(ОбработкаЗаполнения.Объект.РаспределениеРезультатовНачислений);
НДок.РаспределениеРезультатовУдержаний.Загрузить(ОбработкаЗаполнения.Объект.РаспределениеРезультатовУдержаний);
НДок.РаспределениеПоТерриториямУсловиямТруда.Загрузить(ОбработкаЗаполнения.Объект.РаспределениеПоТерриториямУсловиямТруда);
НДок.КорректировкиВыплаты.Загрузить(ОбработкаЗаполнения.Объект.КорректировкиВыплаты);
НДок.ДополнительныеРеквизиты.Загрузить(ОбработкаЗаполнения.Объект.ДополнительныеРеквизиты);
НДок.ЗависимыеНачисления.Загрузить(ОбработкаЗаполнения.Объект.ЗависимыеНачисления);
// заимствовано из формы (ПередЗаписьюНаСервере):
ОбработкаЗаполнения.РеквизитыВДанные(НДок);
Если ОбработкаЗаполнения.ЗаполнениеВыполнено <> Неопределено Тогда
НДок.ДополнительныеСвойства.Вставить("УдалитьПерерасчетыЗарплаты", Истина);
НДок.ДополнительныеСвойства.Вставить("СотрудникиПерерасчетаЗаработка",
ОбщегоНазначения.ВыгрузитьКолонку(ОбработкаЗаполнения.ЗаполнениеВыполнено, "Ключ"));
КонецЕсли;
НДок.Записать(РежимЗаписиДокумента.Проведение);
Далее начинается самая нудная работа: нужно сохранять или запускать код и последовательно выявлять недостающие реквизиты формы и недостающие процедуры и функции, которые тупо переносим в наш класс без изменений. В итоге после десятка итераций расчет успешно отработал, а модуль класса дополнился всеми необходимыми реквизитами и процедурами:
Перем Объект Экспорт;
Перем ИзмененныеДанные Экспорт;
Перем ОкончательныйРасчетНДФЛ Экспорт;
Перем КоличествоКолонокИтоговЗависимыхНачислений Экспорт;
Перем КолонкиИтоговЗависимыхНачислений Экспорт;
Перем ЗаполнениеВыполнено Экспорт;
#Область ПрограммныйИнтерфейс
Процедура Конструктор(ДокументСсылка) Экспорт
Процедура ПерезаполнитьДанныеФормыНаСервере(Знач Сотрудники, СохранятьИсправления = Истина) Экспорт
Процедура РеквизитыВДанные(ТекущийОбъект) Экспорт
#КонецОбласти
#Область АдаптацияИзФормыДокументаНачислениеНатуральногоДохода
Процедура ДополнитьСтрокиНаСервере(ИдентификаторыСтрок, ОписаниеТаблицы, ЗаполнятьСведенияСотрудников, ЗаполнятьЗначенияПоказателей)
Процедура РассчитатьСотрудниковНаСервере(Знач Сотрудники, ОписаниеТаблицы, СохранятьИсправления = Истина, ВыводитьСообщения = Ложь)
Функция ПолучитьКонтролируемыеПоля() Экспорт
Функция СотрудникиФизическиеЛицаОтбор(Сотрудники)
Процедура ЗаполнитьНастройкиМенеджераРасчета(ФизическиеЛица, МенеджерРасчета, СохранятьИсправления = Истина)
Процедура ДанныеФормыВДанныеМенеджераРасчета(МенеджерРасчета, Отбор = Неопределено, ПозицииВставки = Неопределено)
Процедура РасчетЗарплатыВДанныеФормы(ДанныеМенеджераРасчета, ПозицииВставки = Неопределено)
Процедура ОбновитьНачисленоУдержаноИтог(Сотрудники)
Процедура ЗаполнитьНалогиСотрудника(ДанныеСтроки, ФизическоеЛицо = Неопределено, СотрудникиФизическогоЛица = Неопределено)
Функция СотрудникиФизическихЛиц(Знач ФизическиеЛица)
Процедура ЗаполнитьИтогиЗависимыхНачисленийТекущегоСотрудника(СтрокаНачислений)
#Область Описания
Функция ОписаниеДокумента(Форма)
Функция СтруктураОписанияТаблицДляРаспределенияРезультата()
Функция ОписаниеТаблицыНачислений()
Функция ОписаниеТаблицыПерерасчетов()
Функция ОписаниеТаблицыЗависимыеНачисления()
Функция ОписаниеТаблицыНДФЛ()
Функция ОписаниеТаблицыКорректировкиВыплаты()
Функция ОписанияТаблицСРаспределениемПоТерриториямУсловиямТруда()
Функция ОписанияТаблицФормыСМестомПолученияДохода()
Функция МассивОписанийТаблицФормы()
Функция ОписанияТаблицДляРаспределенияРезультата()
#КонецОбласти
#КонецОбласти
Надеюсь, теперь наглядно видно, от какого количества работы по пересмотру типового кода избавил вас метод и вы оцените паттерн ООП по достоинству. На закуску, вам придётся ещё исправить пару-тройку процедур в типовых модулях, где используется функция для коллекций ПолучитьИдентификатор(), заменяя на Индекс(), но это уже несложно по сравнению с тем что уже сделано.