При разработке функционала в 1С с использованием расширений часто возникает задача автоматического заполнения полей табличной части нового документа на основе данных из существующих справочников и регистров сведений. Рассмотрим подробнее, как решить такую задачу, на примере заполнения информации об основных средствах в документе, созданном в расширении для конфигурации 1С:Бухгалтерия КОРП.
Предположим, у нас есть новый документ, добавленный в расширение, с табличной частью, в которой мы хотим автоматически заполнять инвентарный номер, стоимость по 01 счету и сумму амортизации по 02 счету при выборе основного средства. Источниками данных будут справочник ОсновныеСредства и регистр сведений РасчетАмортизации.
Прежде всего, мы должны убедиться, что наше расширение настроено корректно. Мы создали расширение и добавили в него новый документ. Для работы с данными основных средств нам необходимо "захватить" в расширение следующие метаданные:
ОсновныеСредстваПодразделенияОрганизаций (если он используется в контексте ОС)РасчетАмортизацииВ форме нашего нового документа, созданного расширением, должна присутствовать табличная часть. В этой табличной части нам нужны следующие реквизиты:
НаименованиеОС: тип СправочникСсылка.ОсновныеСредства (поле, которое выбирает пользователь).ИнвОС: тип Строка (для инвентарного номера).СтоимостьОСпо01Счету: тип Число (для стоимости ОС).СуммаАмортизацииОСпо02Счету: тип Число (для суммы амортизации).Задача состоит в том, чтобы при выборе значения в поле НаименованиеОС автоматически заполнялись поля ИнвОС, СтоимостьОСпо01Счету и СуммаАмортизацииОСпо02Счету.
Одна из первых проблем, с которой мы можем столкнуться, — это правильное извлечение кода (инвентарного номера) из справочника ОсновныеСредства. Частой ошибкой является попытка получить код справочника в целом или некорректное обращение к элементу. Рассмотрим типичные варианты и правильное решение.
Наши первоначальные попытки могли выглядеть так:
// Попытка 1: Некорректное обращение к свойству справочника
&НаСервереБезКонтекста Функция ПолучитьИнвОС(НаименованиеОС)
Возврат Справочники.ОсновныеСредства.Код; // Ошибка: Справочник как объект не имеет свойства "Код"
КонецФункции
// Попытка 2: Использование НайтиПоНаименованию, но с возможными неточностями
// (если НаименованиеОС уже является ссылкой, то поиск по наименованию избыточен и может привести к ошибкам,
// если есть одинаковые наименования)
&НаСервереБезКонтекста Функция ПолучитьИнвОС(НаименованиеОС)
Возврат Справочники.ОсновныеСредства.НайтиПоНаименованию(НаименованиеОС).Код; // Ошибка, если НаименованиеОС - это ссылка, а не строка
КонецФункции
Причина ошибок заключалась в том, что НаименованиеОС в табличной части нашего документа имеет тип СправочникСсылка.ОсновныеСредства. Это означает, что в переменную НаименованиеОС уже передается не строка с наименованием, а ссылка на выбранный элемент справочника. Следовательно, нам не нужно искать элемент по наименованию; мы можем напрямую обратиться к свойствам переданной ссылки.
Самый надежный и корректный способ получить инвентарный номер — это передать в серверную функцию непосредственно ссылку на основной фонд и извлечь его код:
&НаСервереБезКонтекста
Функция ПолучитьИнвОС(СсылкаНаОС)
Если СсылкаНаОС.Пустая() Тогда
Возврат ""; // Возвращаем пустую строку, если ссылка не выбрана
КонецЕсли;
Возврат СсылкаНаОС.Код;
КонецФункции
В этом случае, на клиенте мы будем вызывать функцию, передавая ссылку:
&НаКлиенте
Процедура Расш1ТабЧастьНаименованиеОСПриИзменении(Элемент)
ТекДанные = Элементы.Расш1ТабЧасть.ТекущиеДанные;
ТекДанные.ИнвОС = ПолучитьИнвОС(ТекДанные.НаименованиеОС);
// ... далее вызовы для стоимости
КонецПроцедуры
В исходном сообщении, хотя и использовалось НайтиПоНаименованию, оно все же сработало, потому что НаименованиеОС, вероятно, неявно преобразовывалось в строковое представление для функции НайтиПоНаименованию, которая затем уже возвращала ссылку, из которой брался Код. Однако, это менее оптимально и потенциально ошибочно, чем прямая передача ссылки.
Для получения стоимости по 01 счету (СтоимостьБУ), остаточной стоимости (ОстаточнаяСтоимостьБУ) и суммы амортизации (СуммаАмортизацииБУ) из регистра сведений РасчетАмортизации мы будем использовать метод ПолучитьПоследнее(). Этот метод позволяет получить срез последних значений ресурсов регистра на определенный момент времени по заданному отбору.
Создадим отдельные серверные функции для каждого показателя. Каждая функция будет принимать ссылку на основное средство и дату, на которую необходимо получить срез.
&НаСервереБезКонтекста
Функция ПолучитьСтоимостьБУ(СсылкаНаОС, ДатаСреза)
Если СсылкаНаОС.Пустая() Тогда
Возврат 0;
КонецЕсли;
Отбор = Новый Структура ("Объект", СсылкаНаОС);
ЗначенияРесурсов = РегистрыСведений.РасчетАмортизации.ПолучитьПоследнее(КонецДня(ДатаСреза), Отбор);
Если ЗначенияРесурсов = Неопределено Тогда
Возврат 0;
КонецЕсли;
Возврат ЗначенияРесурсов.СтоимостьБУ;
КонецФункции
&НаСервереБезКонтекста
Функция ПолучитьОстСтоимостьБУ(СсылкаНаОС, ДатаСреза)
Если СсылкаНаОС.Пустая() Тогда
Возврат 0;
КонецЕсли;
Отбор = Новый Структура ("Объект", СсылкаНаОС);
ЗначенияРесурсов = РегистрыСведений.РасчетАмортизации.ПолучитьПоследнее(КонецДня(ДатаСреза), Отбор);
Если ЗначенияРесурсов = Неопределено Тогда
Возврат 0;
КонецЕсли;
Возврат ЗначенияРесурсов.ОстаточнаяСтоимостьБУ; // Исправлено: изначально было СуммаАмортизацииБУ
КонецФункции
&НаСервереБезКонтекста
Функция ПолучитьСуммаАмортизацииБУ(СсылкаНаОС, ДатаСреза)
Если СсылкаНаОС.Пустая() Тогда
Возврат 0;
КонецЕсли;
Отбор = Новый Структура ("Объект", СсылкаНаОС);
ЗначенияРесурсов = РегистрыСведений.РасчетАмортизации.ПолучитьПоследнее(КонецДня(ДатаСреза), Отбор);
Если ЗначенияРесурсов = Неопределено Тогда
Возврат 0;
КонецЕсли;
Возврат ЗначенияРесурсов.СуммаАмортизацииБУ;
КонецФункции
Важный момент: Обратите внимание, что в функции ПолучитьОстСтоимостьБУ в примере из форума была допущена опечатка (возвращалось СуммаАмортизацииБУ вместо ОстаточнаяСтоимостьБУ). В приведенном выше коде мы исправили эту ошибку. Также мы добавили проверку на Неопределено, что является хорошей практикой для предотвращения ошибок в случае отсутствия данных в регистре.
Вместо использования КонецДня(ТекущаяДата) для получения среза регистра, мы должны использовать дату, указанную в документе. Для этого необходимо добавить в документ реквизит, например, ДатаУчетаАрендованныхОС типа Дата, и передавать его значение в наши серверные функции.
Например, если на форме документа есть поле ДатаУчетаАрендованныхОС, то при вызове серверных функций мы будем передавать его значение:
&НаКлиенте
Процедура Расш1ТабЧастьНаименованиеОСПриИзменении(Элемент)
ТекДанные = Элементы.Расш1ТабЧасть.ТекущиеДанные;
// Получаем ссылку на основной фонд
СсылкаНаОС = ТекДанные.НаименованиеОС;
// Получаем дату для среза
ДатаСреза = Элементы.ДатаУчетаАрендованныхОС.Значение; // Предполагается, что на форме есть элемент с именем ДатаУчетаАрендованныхОС
Если ДатаСреза = Неопределено Тогда
ДатаСреза = ТекущаяДата(); // Если дата не указана, используем текущую
КонецЕсли;
ТекДанные.ИнвОС = ПолучитьИнвОС(СсылкаНаОС);
ТекДанные.СтоимостьОСпо01Счету = ПолучитьСтоимостьБУ(СсылкаНаОС, ДатаСреза);
// Получаем остальные значения для расчета суммы амортизации
СтоимостьБУ = ПолучитьСтоимостьБУ(СсылкаНаОС, ДатаСреза);
ОстаточнаяСтоимостьБУ = ПолучитьОстСтоимостьБУ(СсылкаНаОС, ДатаСреза);
СуммаАмортизацииБУ = ПолучитьСуммаАмортизацииБУ(СсылкаНаОС, ДатаСреза);
ТекДанные.СуммаАмортизацииОСпо02Счету = СтоимостьБУ - ОстаточнаяСтоимостьБУ + СуммаАмортизацииБУ;
КонецПроцедуры
Использование КонецДня(ДатаСреза) в серверных функциях гарантирует, что мы получим данные по конец дня указанной даты, что критически важно для корректной работы с периодическими регистрами сведений.
Для небольшого количества строк в табличной части использование РегистрыСведений.ПолучитьПоследнее() для каждой строки может быть приемлемым. Однако, если табличная часть может содержать большое количество строк, многократные вызовы к регистру могут замедлить работу. В таких случаях более эффективным будет использование запросов.
Мы можем собрать все ссылки на основные средства из табличной части в массив, а затем выполнить один запрос, который получит все необходимые данные для всех ОС за один раз. Полученные данные можно поместить во временную таблицу или структуру для быстрого доступа.
&НаСервереБезКонтекста
Функция ПолучитьДанныеОСИзРегистраЗапросом(СписокОбъектовОС, ДатаСреза)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| РасчетАмортизацииСрезПоследних.Объект КАК Объект,
| РасчетАмортизацииСрезПоследних.СтоимостьБУ КАК СтоимостьБУ,
| РасчетАмортизацииСрезПоследних.ОстаточнаяСтоимостьБУ КАК ОстаточнаяСтоимостьБУ,
| РасчетАмортизацииСрезПоследних.СуммаАмортизацииБУ КАК СуммаАмортизацииБУ
|ИЗ
| РегистрСведений.РасчетАмортизации.СрезПоследних(&ДатаСреза, Объект В (&СписокОбъектов)) КАК РасчетАмортизацииСрезПоследних";
Запрос.УстановитьПараметр("ДатаСреза", КонецДня(ДатаСреза));
Запрос.УстановитьПараметр("СписокОбъектов", СписокОбъектовОС);
РезультатЗапроса = Запрос.Выполнить().Выгрузить();
// Преобразуем результат запроса в карту для быстрого доступа
КартаДанных = Новый Соответствие;
Для Каждого СтрокаРезультата Из РезультатЗапроса Цикл
КартаДанных.Вставить(СтрокаРезультата.Объект, СтрокаРезультата);
КонецЦикла;
Возврат КартаДанных;
КонецФункции
Затем в процедуре ПриИзменении на клиенте мы могли бы сначала собрать список всех ОС из табличной части, вызвать эту функцию, а затем использовать полученную карту для заполнения данных:
&НаКлиенте
Процедура Расш1ТабЧастьНаименованиеОСПриИзменении(Элемент)
// ... получение ТекДанные, СсылкаНаОС, ДатаСреза ...
// Пример: Сбор всех ОС из табличной части (можно сделать и для всех строк сразу)
СписокОбъектовОС = Новый Массив;
Для Каждого СтрокаТЧ Из Элементы.Расш1ТабЧасть.Значение Цикл
Если Не СтрокаТЧ.НаименованиеОС.Пустая() И Не СписокОбъектовОС.Содержит(СтрокаТЧ.НаименованиеОС) Тогда
СписокОбъектовОС.Добавить(СтрокаТЧ.НаименованиеОС);
КонецЕсли;
КонецЦикла;
КартаДанныхРегистра = ПолучитьДанныеОСИзРегистраЗапросом(СписокОбъектовОС, ДатаСреза);
// Теперь используем КартаДанныхРегистра для заполнения текущей строки
ТекДанные = Элементы.Расш1ТабЧасть.ТекущиеДанные;
СсылкаНаОС = ТекДанные.НаименованиеОС;
ТекДанные.ИнвОС = ПолучитьИнвОС(СсылкаНаОС);
Если КартаДанныхРегистра.Содержит(СсылкаНаОС) Тогда
ДанныеОС = КартаДанныхРегистра.Получить(СсылкаНаОС);
ТекДанные.СтоимостьОСпо01Счету = ДанныеОС.СтоимостьБУ;
ТекДанные.СуммаАмортизацииОСпо02Счету = ДанныеОС.СтоимостьБУ - ДанныеОС.ОстаточнаяСтоимостьБУ + ДанныеОС.СуммаАмортизацииБУ;
Иначе
ТекДанные.СтоимостьОСпо01Счету = 0;
ТекДанные.СуммаАмортизацииОСпо02Счету = 0;
КонецЕсли;
КонецПроцедуры
Такой подход позволяет значительно уменьшить количество обращений к базе данных, что повышает производительность.
СправочникСсылка, ДокументСсылка и т.д.) вместо строковых наименований. Это исключает проблемы с поиском по неточным или повторяющимся наименованиям и повышает производительность.Неопределено: После любого обращения к методам получения объектов (НайтиПоНаименованию, ПолучитьПоследнее и т.п.) всегда проверяйте результат на Неопределено. Это предотвратит ошибки при попытке обращения к свойствам несуществующего объекта.ТекущаяДата(), чтобы обеспечить корректность данных в историческом контексте.ПолучитьПоследнее().Следуя этим рекомендациям, мы сможем создать надежный и производительный механизм автозаполнения табличных частей документов в расширениях 1С.