diamond АШ Tlg

Как заполнить программно документы 1C ЗУП 3.1 на сервере и правильно рассчитать их

Подход к решению задачи через имитацию формы с помощью ООП. Применим так же и в конфигурации 1С ERP.

Постановка задачи и описание проблемы

Требуется программно сформировать на сервере (например при обменах) документ Доход в натуральной форме, причём документ должен быть заполнен и рассчитан так, чтобы сам документ и его движения были полностью идентичны такому же документу, заполненного пользователем вручную.

Задача на самом деле не такая тривиальная, как кажется. Корень зла в том, что архитекторы конфигурации полностью отвергли паттерн MVC. Cправедливости ради, и сама платформа этому 1С не способствует, но прикладные программисты современных типовых конфигураций приложили все силы чтобы уйти от стандартных паттернов ещё дальше.

Вышеописанная проблема привела в ЗУПе к тому, что часть бизнес-логики реализуется на форме документа (уровень View и Controller в MVC) и часть процедур заполнения и расчёта документа, которые нам и нужны для выполнения задачи, находятся в модуле формы, и их не вызвать из сервера, будь они даже экспортными. Более того, многие общие модули для расчетов также является клиентскими и требуют аргументом объект типа Форма или различного вида коллекции формы. По слухам, фирма 1С признаёт проблему и обещала даже переписать бизнес-логику в общие модули, но в данный момент существенных подвижек пока не замечено и нам придётся попотеть.

Также сделаем допущение, что у нас нет времени на изучение Менеджера расчета зарплаты и мы хотим решить задачу побыстрее "в лоб", либо документ вообще другого вида и в другой конфигурации, например в ERP.

При вышеописанных условиях встречается такой подход для решения проблемы: программист создает структуру, которая повторяет реквизиты формы, копирует в свой серверный модуль все нужные ему процедуры и функции из этого модуля, все задействованные переменные модуля, при этом добавляя в каждую из процедур новый параметр для передачи структуры-обманки. В конце приходится делать полный ревью перенесенного типового кода, чтобы убрать многочисленные ошибки и везде вместо контекста формы подставлять структуру. Согласитесь - это довольно трудоёмко.

Реализация с помощью ООП

В качестве носителя решения будем применять объект конфигурации - обработку. Создаем в расширении новую обработку, назовём к примеру расш1_ДоходВНатуральнойФорме. Наша задача - написать класс (в терминах ООП), имитирующий Форму. Всю разработку ведём исключительно в модуле объекта. Создаём главный реквизит формы Объект и заготовку конструктора (в терминах ООП), в котором будем заполнять данные из ссылки на документ:

Перем Объект Экспорт;

Процедура Конструктор(ДокументСсылка) Экспорт

    Объект = Новый Структура;
    Объект.Вставить("Ссылка", ДокументСсылка);
	
    // Реквизиты объекта
    Для Каждого Реквизит Из ДокументСсылка.Метаданные().Реквизиты Цикл
        Объект.Вставить(Реквизит.Имя, ДокументСсылка[Реквизит.Имя]);
    КонецЦикла;
	
    // Табличные части объекта
    Для Каждого ТЧ Из ДокументСсылка.Метаданные().ТабличныеЧасти Цикл
        Объект.Вставить(ТЧ.Имя, Новый ТаблицаЗначений);
        Для Каждого Реквизит Из ТЧ.Реквизиты Цикл
            Объект[ТЧ.Имя].Колонки.Добавить(Реквизит.Имя);
        КонецЦикла;
        Объект[ТЧ.Имя].Колонки.Добавить("НомерСтроки", Новый ОписаниеТипов("Число"));
        Для Каждого Строка Из ДокументСсылка[ТЧ.Имя] Цикл
            НСтр = Объект[ТЧ.Имя].Добавить();
            ЗаполнитьЗначенияСвойств(НСтр, Строка); 
        КонецЦикла;
    КонецЦикла; 	
		
КонецПроцедуры

Для дальнейшей работы нам нужно постоянно держать открытым форму типового документа. Видим, что там есть реквизит ИзмененныеДанные в виде таблицы значений, который почти гарантированно будет нужен в расчёте (забегая вперед - интуиция не обманула), поэтому тоже добавляем его в наш класс. Остальными реквизитами формы пока не заморачиваемся - будем добавлять их по мере необходимости:

Перем Объект Экспорт;
Перем ИзмененныеДанные Экспорт;

Процедура Конструктор(ДокументСсылка) Экспорт

    ...

    // Данные к пересчету
    ИзмененныеДанные = Объект.Начисления.Скопировать(, "Сотрудник");
    ИзмененныеДанные.Колонки.Добавить("ИмяТаблицы");
    ИзмененныеДанные.Колонки.Добавить("ФизическоеЛицо");
    ИзмененныеДанные.Колонки.Добавить("ВидРасчета");
    Для Каждого Строка Из ИзмененныеДанные Цикл
        Строка.ИмяТаблицы = "Начисления";
        Строка.ВидРасчета = Объект.Начисление; 
    КонецЦикла;

КонецПроцедуры

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

// ТерриториальныеУсловияТруда
Если ЗарплатаКадрыРасширенный.ИспользоватьРаспределениеПоТерриториямУсловиямТруда(Объект.Организация) Тогда
    Объект.Начисления.Колонки.Добавить("РаспределениеПоТерриториямУсловиямТруда");
    Объект.НачисленияПерерасчет.Колонки.Добавить("РаспределениеПоТерриториямУсловиямТруда");
    Объект.ЗависимыеНачисления.Колонки.Добавить("РаспределениеПоТерриториямУсловиямТруда");
КонецЕсли;
	
// Корректировки выплаты
Объект.КорректировкиВыплаты.Колонки.Добавить("РезультатРаспределения"); 
Объект.КорректировкиВыплаты.Колонки.Добавить("КомандаРедактированияРаспределения"); 
Объект.КорректировкиВыплаты.Колонки.Добавить("РаспределениеПоСтатьям"); 
	
// НДФЛ и прочие поля
Объект.Начисления.Колонки.Добавить("НДФЛ", Новый ОписаниеТипов("Число"));
Объект.Начисления.Колонки.Добавить("КорректировкаВыплаты", Новый ОписаниеТипов("Число"));
Объект.Начисления.Колонки.Добавить("ПредставлениеРаспределенияПоТерриториямУсловиямТруда", Новый ОписаниеТипов("Строка"));
Объект.Начисления.Колонки.Добавить("ПредставлениеПериодаДействия", Новый ОписаниеТипов("Строка"));
Объект.Начисления.Колонки.Добавить("ПустаяСтрокаЗаголовка", Новый ОписаниеТипов("Строка"));
Объект.Начисления.Колонки.Добавить("РезультатЗависимыхНачислений1", Новый ОписаниеТипов("Число"));
Объект.Начисления.Колонки.Добавить("РезультатЗависимыхНачислений2", Новый ОписаниеТипов("Число"));
Объект.Начисления.Колонки.Добавить("РезультатЗависимыхНачислений3", Новый ОписаниеТипов("Число"));
Объект.Начисления.Колонки.Добавить("РезультатЗависимыхНачислений4", Новый ОписаниеТипов("Число"));
Объект.Начисления.Колонки.Добавить("РезультатЗависимыхНачислений5", Новый ОписаниеТипов("Число"));
Объект.Начисления.Колонки.Добавить("РезультатЗависимыхНачислений6", Новый ОписаниеТипов("Число"));

Всё что пока мы написали, повторяет старый подход - там тоже пришлось бы всё это писать. Теперь начинаются различия: перетаскиваем из формы главный вызов, который нам нужен на сервере для расчета:

Процедура ПерезаполнитьДанныеФормыНаСервере(Знач Сотрудники, СохранятьИсправления = Истина) Экспорт
	
    Если ТипЗнч(Сотрудники) <> Тип("Массив") Тогда
        Сотрудники = ОбщегоНазначенияКлиентСервер.ЗначениеВМассиве(Сотрудники);
    КонецЕсли;
	
    ОписаниеТаблицы = ОписаниеТаблицыНачислений();
    ИдентификаторыСтрок = Новый Массив;
    Если Не СохранятьИсправления Тогда
        Отбор = Новый Структура("Сотрудник");
        Для каждого Сотрудник Из Сотрудники Цикл
            Отбор.Вставить("Сотрудник", Сотрудник);
			
            // Заполняем поля по итогам заполнения коллекций.
            СтрокиПоСотруднику = Объект.Начисления.НайтиСтроки(Отбор);
            Для каждого СтрокаПоСотруднику Из СтрокиПоСотруднику Цикл
                ИдентификаторыСтрок.Добавить(СтрокаПоСотруднику.ПолучитьИдентификатор());
            КонецЦикла;
        КонецЦикла;
    КонецЕсли;
	
    ДополнитьСтрокиНаСервере(ИдентификаторыСтрок, ОписаниеТаблицы, Не СохранятьИсправления, Не СохранятьИсправления);
    РассчитатьСотрудниковНаСервере(Сотрудники, ОписаниеТаблицы, СохранятьИсправления);
	
    Для Каждого ИдентификаторСтроки Из ИдентификаторыСтрок Цикл
        СтрокаТаблицыНачислений = Объект.Начисления.НайтиПоИдентификатору(ИдентификаторСтроки);
        ЗаполнитьИтогиЗависимыхНачисленийТекущегоСотрудника(СтрокаТаблицыНачислений);
    КонецЦикла;
	
КонецПроцедуры

Обратите внимание: процедуру перенесли полностью без изменений! Никакого код-ревью нам не понадобится!

Переходим в наш серверный общий модуль, где будем использовать наш класс для программного расчета и проведения документа:

Сотрудники = Новый Массив;
НДок = Документы.ДоходВНатуральнойФорме.СоздатьДокумент();

    // здесь заполняем документ-объект как обычно (только суммы без налогов)

НДок.Записать(РежимЗаписиДокумента.Запись);
ДокументСсылка = НДок.Ссылка;

ОбработкаЗаполнения = Обработки.расш1_ДоходВНатуральнойФорме.Создать();
ОбработкаЗаполнения.Конструктор(ДокументСсылка);
ОбработкаЗаполнения.ПерезаполнитьДанныеФормыНаСервере(Сотрудники, Ложь);

// перенос результатов расчета в объект документа
ЗаполнитьЗначенияСвойств(НДок, ОбработкаЗаполнения.Объект); 
НДок.Начисления.Загрузить(ОбработкаЗаполнения.Объект.Начисления);
НДок.ФизическиеЛица.Загрузить(ОбработкаЗаполнения.Объект.ФизическиеЛица);
НДок.НачисленияПерерасчет.Загрузить(ОбработкаЗаполнения.Объект.НачисленияПерерасчет);
НДок.НДФЛ.Загрузить(ОбработкаЗаполнения.Объект.НДФЛ);
НДок.Показатели.Загрузить(ОбработкаЗаполнения.Объект.Показатели);
НДок.ПримененныеВычетыНаДетейИИмущественные.Загрузить(ОбработкаЗаполнения.Объект.ПримененныеВычетыНаДетейИИмущественные);
НДок.РаспределениеРезультатовНачислений.Загрузить(ОбработкаЗаполнения.Объект.РаспределениеРезультатовНачислений);
НДок.РаспределениеРезультатовУдержаний.Загрузить(ОбработкаЗаполнения.Объект.РаспределениеРезультатовУдержаний);
НДок.РаспределениеПоТерриториямУсловиямТруда.Загрузить(ОбработкаЗаполнения.Объект.РаспределениеПоТерриториямУсловиямТруда);
НДок.КорректировкиВыплаты.Загрузить(ОбработкаЗаполнения.Объект.КорректировкиВыплаты);
НДок.ДополнительныеРеквизиты.Загрузить(ОбработкаЗаполнения.Объект.ДополнительныеРеквизиты);
НДок.ЗависимыеНачисления.Загрузить(ОбработкаЗаполнения.Объект.ЗависимыеНачисления);

// заимствовано из формы (ПередЗаписьюНаСервере):
ОбработкаЗаполнения.РеквизитыВДанные(НДок);
Если ОбработкаЗаполнения.ЗаполнениеВыполнено <> Неопределено Тогда
    НДок.ДополнительныеСвойства.Вставить("УдалитьПерерасчетыЗарплаты", Истина);
    НДок.ДополнительныеСвойства.Вставить("СотрудникиПерерасчетаЗаработка",
        ОбщегоНазначения.ВыгрузитьКолонку(ОбработкаЗаполнения.ЗаполнениеВыполнено, "Ключ"));
КонецЕсли;

НДок.Записать(РежимЗаписиДокумента.Проведение);

Далее начинается самая нудная работа: нужно сохранять или запускать код и последовательно выявлять недостающие реквизиты формы и недостающие процедуры и функции, которые тупо переносим в наш класс без изменений. В итоге после десятка итераций расчет успешно отработал, а модуль класса дополнился всеми необходимыми реквизитами и процедурами:

Перем Объект Экспорт;
Перем ИзмененныеДанные Экспорт;
Перем ОкончательныйРасчетНДФЛ Экспорт;
Перем КоличествоКолонокИтоговЗависимыхНачислений Экспорт;
Перем КолонкиИтоговЗависимыхНачислений Экспорт;
Перем ЗаполнениеВыполнено Экспорт; 

#Область ПрограммныйИнтерфейс

Процедура Конструктор(ДокументСсылка) Экспорт
Процедура ПерезаполнитьДанныеФормыНаСервере(Знач Сотрудники, СохранятьИсправления = Истина) Экспорт
Процедура РеквизитыВДанные(ТекущийОбъект) Экспорт

#КонецОбласти

#Область АдаптацияИзФормыДокументаНачислениеНатуральногоДохода

Процедура ДополнитьСтрокиНаСервере(ИдентификаторыСтрок, ОписаниеТаблицы, ЗаполнятьСведенияСотрудников, ЗаполнятьЗначенияПоказателей)
Процедура РассчитатьСотрудниковНаСервере(Знач Сотрудники, ОписаниеТаблицы, СохранятьИсправления = Истина, ВыводитьСообщения = Ложь)
Функция ПолучитьКонтролируемыеПоля() Экспорт
Функция СотрудникиФизическиеЛицаОтбор(Сотрудники)
Процедура ЗаполнитьНастройкиМенеджераРасчета(ФизическиеЛица, МенеджерРасчета, СохранятьИсправления = Истина)
Процедура ДанныеФормыВДанныеМенеджераРасчета(МенеджерРасчета, Отбор = Неопределено, ПозицииВставки = Неопределено)
Процедура РасчетЗарплатыВДанныеФормы(ДанныеМенеджераРасчета, ПозицииВставки = Неопределено)
Процедура ОбновитьНачисленоУдержаноИтог(Сотрудники)
Процедура ЗаполнитьНалогиСотрудника(ДанныеСтроки, ФизическоеЛицо = Неопределено, СотрудникиФизическогоЛица = Неопределено)
Функция СотрудникиФизическихЛиц(Знач ФизическиеЛица)
Процедура ЗаполнитьИтогиЗависимыхНачисленийТекущегоСотрудника(СтрокаНачислений)

#Область Описания

Функция ОписаниеДокумента(Форма)
Функция СтруктураОписанияТаблицДляРаспределенияРезультата()
Функция ОписаниеТаблицыНачислений()
Функция ОписаниеТаблицыПерерасчетов()
Функция ОписаниеТаблицыЗависимыеНачисления()
Функция ОписаниеТаблицыНДФЛ()
Функция ОписаниеТаблицыКорректировкиВыплаты()
Функция ОписанияТаблицСРаспределениемПоТерриториямУсловиямТруда()
Функция ОписанияТаблицФормыСМестомПолученияДохода()
Функция МассивОписанийТаблицФормы()
Функция ОписанияТаблицДляРаспределенияРезультата()

#КонецОбласти

#КонецОбласти

Надеюсь, теперь наглядно видно, от какого количества работы по пересмотру типового кода избавил вас метод и вы оцените паттерн ООП по достоинству. На закуску, вам придётся ещё исправить пару-тройку процедур в типовых модулях, где используется функция для коллекций ПолучитьИдентификатор(), заменяя на Индекс(), но это уже несложно по сравнению с тем что уже сделано.