Как исправить ошибки и оптимизировать проведение документа в 1С: подробный разбор тестового задания

Программист 1С v8.3 (Управляемые формы) Управленческий учет Торговля и дистрибуция
← На главную

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

Исходный код обработчика проведения

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


Процедура ОбработкаПроведения(Отказ, РежимПроведения)
    Запрос = Новый Запрос;
    Запрос.Текст = "ВЫБРАТЬ
    | Товары.Номенклатура КАК Номенклатура,
    | Товары.Партия КАК Партия,
    | Товары.Количество КАК Количество,
    | Товары.Сумма КАК Сумма,
    | Заказы.КоличествоОстаток КАК КоличествоОстаток
    |ИЗ
    | Документ.ПоступлениеТоваров.Товары КАК Товары
    |ЛЕВОЕ СОЕДИНЕНИЕ
    |РегистрНакопления.ЗаказыПоставщикам.Остатки(&Период, Контрагент = &Контрагент) КАК Заказы
    | ПО Товары.Номенклатура = Заказы.Номенклатура
    | И Товары.Партия = Заказы.Партия
    |ГДЕ
    | Товары.Ссылка = &Ссылка
    | И (Заказы.Номенклатура, Заказы.Партия) В
    | (ВЫБРАТЬ
    | Товары.Номенклатура,
    | Товары.Партия
    | ИЗ
    | Документ. ПоступлениеТоваров.Товары КАК Товары
    | ГДЕ
    | Товары.Ссылка = &Ссылка)
    |ИТОГИ
    | СУММА(Количество),
    | СУММА(Сумма),
    | СУММА(КоличествоОстаток)
    |ПО
    | Номенклатура,
    | Партия";
    
    ВыборкаНоменклатура = Запрос.Выполнить().Выбрать();
    Пока ВыборкаНоменклатура.Следующий() Цикл
        Если ВыборкаНоменклатура.Количество > ВыборкаНоменклатура.КоличествоОстаток Тогда
            Сообщить("Количество в поступлении "
            + ВыборкаНоменклатура.Количество
            + " " + ВыборкаНоменклатура.Номенклатура.ЕдиницаИзмерения
            + " превышает остаток по заказам "
            + ВыборкаНоменклатура.КоличествоОстаток
            + " " + ВыборкаНоменклатура.Номенклатура.ЕдиницаИзмерения);
        Иначе
            ВыборкаПартия = ВыборкаНоменклатура.Выбрать();
            Пока ВыборкаПартия.Следующий() Цикл
                СуммаВал = СуммаВал
                + РегистрыСведений.КурсыВалют.ПолучитьПоследнее(
                ВыборкаПартия.Партия.Дата
                ,Новый Структура("Валюта", Справочники.Валюты.НайтиПоНаименованию("USD")));
                
                Движение = РегистрыНакопления.ЗаказыПоставщикам.Добавить();
                Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
                Движение.Период = Дата;
                Движение.Номенклатура = ВыборкаПартия.Номенклатура;
                Движение.Количество = ВыборкаПартия.Количество;
            КонецЦикла;
            
            Движение = Движения.ТоварыНаСкладах.Добавить();
            Движение.Период = Дата;
            Движение.Номенклатура = ВыборкаНоменклатура.Номенклатура;
            Движение.Количество = ВыборкаНоменклатура.Количество;
            Движение.Сумма = ВыборкаНоменклатура.Сумма;
            Движение.СуммаВал = СуммаВал;
        КонецЕсли;
    КонецЦикла;
КонецПроцедуры

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

Подробный анализ ошибок и неоптимальных решений

Проанализируем пошагово все проблемные места представленного фрагмента кода.

1. Запросы в цикле к базе данных

Внутри вложенного цикла выполняется обращение к базе данных:

РегистрыСведений.КурсыВалют.ПолучитьПоследнее(...)

Это классический антипаттерн в разработке на платформе 1С. При проведении документа, содержащего тысячи строк в табличной части, система выполнит тысячи отдельных запросов к СУБД для получения курса валюты. Это вызовет колоссальную нагрузку на сервер СУБД и резко снизит скорость проведения документа. Получать курсы валют необходимо пакетным запросом вместе с основными данными.

2. Поиск элементов справочника по наименованию

Внутри цикла используется конструкция:

Справочники.Валюты.НайтиПоНаименованию("USD")

Во-первых, это еще один неявный запрос к базе данных внутри цикла. Во-вторых, поиск по наименованию крайне ненадежен. Если в пользовательской базе валюта будет переименована (например, в "Доллар США" или "$"), этот метод вернет пустую ссылку, что приведет к ошибкам в расчете валютной суммы. Подобные системные элементы необходимо искать по коду валюты (например, "840") или хранить ссылку на валюту учета в константах/функциональных опциях.

3. Неявные запросы при обращении к реквизитам через точку

Конструкция ВыборкаНоменклатура.Номенклатура.ЕдиницаИзмерения и обращение ВыборкаПартия.Партия.Дата вызывают разыменование ссылочных полей. При выполнении этого кода платформа 1С делает дополнительные скрытые запросы к соответствующим таблицам СУБД для извлечения значений реквизитов ЕдиницаИзмерения и Дата. Все необходимые свойства ссылочных полей должны извлекаться непосредственно в основном запросе.

4. Отсутствие управляемых блокировок (Блокировка данных)

В процедуре проведения полностью отсутствует механизм управляемых блокировок данных перед чтением остатков. В условиях многопользовательской работы это может привести к считыванию некорректных остатков ("грязное чтение") и возникновению избыточных транзакционных взаимоблокировок (Deadlocks). Перед чтением остатков из регистра ЗаказыПоставщикам необходимо установить управляемую блокировку по номенклатуре и партиям из табличной части документа.

5. Неоптимальное левое соединение и параметры виртуальной таблицы

В запросе левое соединение выполняется с виртуальной таблицей остатков РегистрНакопления.ЗаказыПоставщикам.Остатки. При этом в параметрах виртуальной таблицы указан отбор только по контрагенту. Фильтрация по номенклатуре и партиям накладывается позже в секции ГДЕ. С точки зрения СУБД это крайне неоптимально: СУБД сначала прочитает из базы все остатки заказов по контрагенту, выполнит соединение с табличной частью документа, и только потом отсечет лишние записи. Отборы по номенклатуре и партиям необходимо передавать напрямую в параметры виртуальной таблицы остатков, предварительно выгрузив список товаров во временную таблицу.

6. Ошибки при работе с движениями регистров

В коде допущена критическая синтаксическая ошибка при работе с регистром ЗаказыПоставщикам:

Движение = РегистрыНакопления.ЗаказыПоставщикам.Добавить();

Добавление записи напрямую в менеджер регистра накопления приведет к тому, что у записи не будет заполнен обязательный реквизит Регистратор. Запись движений должна осуществляться через коллекцию движений документа: Движения.ЗаказыПоставщикам.ДобавитьРасход(). Кроме того, в конце процедуры не установлены признаки записи движений (например, Движения.ЗаказыПоставщикам.Записывать = Истина), а старые движения документа не очищаются перед записью новых.

7. Ошибки инициализации и расчета переменной СуммаВал

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

8. Необработанные значения NULL

При левом соединении по позициям, у которых нет остатков по заказам, поле Заказы.КоличествоОстаток вернет значение NULL. Сравнение ВыборкаНоменклатура.Количество > ВыборкаНоменклатура.КоличествоОстаток отработает некорректно в случае встречи с NULL. Поле остатка обязательно должно быть обернуто в функцию ЕСТЬNULL.

9. Нарушение бизнес-логики проведения

Если количество товара в поступлении превышает остаток по заказам поставщику, система выводит сообщение пользователю через метод Сообщить(), однако проведение документа не прерывается. Документ успешно проведется с ошибочными данными. Необходимо устанавливать флаг Отказ = Истина в случае нехватки остатков по заказам.

Оптимизированный вариант проведения документа

Проанализируем правильный подход к проектированию процедуры проведения. Оптимальный алгоритм должен состоять из следующих этапов:

  1. Создание менеджера временных таблиц.
  2. Помещение состава табличной части документа во временную таблицу со сворачиванием по ключевым аналитикам.
  3. Наложение управляемых блокировок на ключевые аналитики регистров.
  4. Получение остатков по заказам и актуальных курсов валют на даты партий в рамках единого пакетного запроса.
  5. Обработка результатов запроса, контроль остатков и корректное формирование движений через коллекции Движения.

Рассмотрим готовую реализацию оптимизированного алгоритма проведения:


Процедура ОбработкаПроведения(Отказ, РежимПроведения)
    
    // Очищаем движения документа по регистрам
    Движения.ТоварыНаСкладах.Очистить();
    Движения.ЗаказыПоставщикам.Очистить();
    
    // Инициализируем Менеджер временных таблиц для оптимизации запросов
    МВТ = Новый МенеджерВременных Таблиц;
    
    // ЭТАП 1. Выгружаем табличную часть "Товары" во временную таблицу
    ЗапросТовары = Новый Запрос;
    ЗапросТовары.МенеджерВременныхТаблиц = МВТ;
    ЗапросТовары.УстановитьПараметр("Ссылка", Ссылка);
    ЗапросТовары.Текст = 
    "ВЫБРАТЬ
    |	Товары.Номенклатура КАК Номенклатура,
    |	Товары.Партия КАК Партия,
    |	Товары.Номенклатура.ЕдиницаИзмерения КАК ЕдиницаИзмерения,
    |	Партия.Дата КАК ДатаПартии,
    |	СУММА(Товары.Количество) КАК Количество,
    |	СУММА(Товары.Сумма) КАК Сумма
    |ПОМЕСТИТЬ ВТ_ТоварыДокумента
    |ИЗ
    |	Документ.ПоступлениеТоваров.Товары КАК Товары
    |ГДЕ
    |	Товары.Ссылка = &Ссылка
    |СГРУППИРОВАТЬ ПО
    |	Товары.Номенклатура,
    |	Товары.Партия,
    |	Товары.Номенклатура.ЕдиницаИзмерения,
    |	Партия.Дата";
    
    ЗапросТовары.Выполнить();
    
    // ЭТАП 2. Наложение управляемых блокировок для защиты от параллельного изменения данных
    Блокировка = Новый БлокировкаДанных;
    ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ЗаказыПоставщикам");
    ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
    ЭлементБлокировки.ИсточникДанных = "ВТ_ТоварыДокумента";
    ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Номенклатура", "Номенклатура");
    ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Партия", "Партия");
    Блокировка.Заблокировать();
    
    // ЭТАП 3. Пакетный запрос для получения остатков и курсов валют
    ЗапросПакет = Новый Запрос;
    ЗапросПакет.МенеджерВременныхТаблиц = МВТ;
    ЗапросПакет.УстановитьПараметр("Период", ?(РежимПроведения = РежимПроведенияДокумента.Оперативный, Неопределено, МоментВремени()));
    ЗапросПакет.УстановитьПараметр("Контрагент", Контрагент);
    
    // Для примера валюту USD будем получать по предопределенному коду классификатора "840"
    ВалютаUSD = Справочники.Валюты.НайтиПоКоду("840");
    Если ВалютаUSD.Пустая() Тогда
        Сообщить("В справочнике ""Валюты"" не найдена валюта с кодом ""840"" (USD)!");
        Отказ = Истина;
        Возврат;
    КонецЕсли;
    ЗапросПакет.УстановитьПараметр("ВалютаUSD", ВалютаUSD);
    
    ЗапросПакет.Текст = 
    "ВЫБРАТЬ
    |	Товары.Номенклатура КАК Номенклатура,
    |	Товары.Партия КАК Партия,
    |	Товары.ЕдиницаИзмерения КАК ЕдиницаИзмерения,
    |	Товары.ДатаПартии КАК ДатаПартии,
    |	Товары.Количество КАК Количество,
    |	Товары.Сумма КАК Сумма,
    |	ЕСТЬNULL(ЗаказыОстатки.КоличествоОстаток, 0) КАК КоличествоОстаток,
    |	ЕСТЬNULL(КурсыВалют.Курс, 1) КАК КурсВалюты
    |ИЗ
    |	ВТ_ТоварыДокумента КАК Товары
    |	ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ЗаказыПоставщикам.Остатки(
    |			&Период,
    |			Контрагент = &Контрагент
    |				И (Номенклатура, Партия) В (ВЫБРАТЬ ВТ.Номенклатура, ВТ.Партия ИЗ ВТ_ТоварыДокумента КАК ВТ)) КАК ЗаказыОстатки
    |		ПО Товары.Номенклатура = ЗаказыОстатки.Номенклатура
    |			И Товары.Партия = ЗаказыОстатки.Партия
    |	ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КурсыВалют.СрезПоследних(
    |			&Период,
    |			Валюта = &ВалютаUSD) КАК КурсыВалют
    |		ПО ИСТИНА";
    
    РезультатЗапроса = ЗапросПакет.Выполнить();
    Выборка = РезультатЗапроса.Выбрать();
    
    // ЭТАП 4. Обход выборки, контроль остатков и формирование движений
    Пока Выборка.Следующий() Цикл
        
        // Проверяем остаток по заказам поставщикам
        Если Выборка.Количество > Выборка.КоличествоОстаток Тогда
            
            Сообщить(ШаблонСтроки(
                "Недостаточно остатка по заказам поставщику для номенклатуры %1. Требуется: %2 %3, в наличии по заказам: %4 %3",
                Выборка.Номенклатура,
                Выборка.Количество,
                Выборка.ЕдиницаИзмерения,
                Выборка.КоличествоОстаток
            ));
            Отказ = Истина;
            
        КонецЕсли;
        
        // Если уже зафиксирован отказ, продолжать формирование движений нет смысла
        Если Отказ Тогда
            Продолжить;
        КонецЕсли;
        
        // Пересчитываем сумму в валюту (рублевая сумма делится на курс)
        СуммаВал = ?(Выборка.КурсВалюты > 0, Выборка.Сумма / Выборка.КурсВалюты, 0);
        
        // Движения по регистру "ЗаказыПоставщикам"
        ДвижениеЗаказ = Движения.ЗаказыПоставщикам.ДобавитьРасход();
        ДвижениеЗаказ.Период = Дата;
        ДвижениеЗаказ.Номенклатура = Выборка.Номенклатура;
        ДвижениеЗаказ.Партия = Выборка.Партия;
        ДвижениеЗаказ.Количество = Выборка.Количество;
        
        // Движения по регистру "ТоварыНаСкладах"
        ДвижениеТовар = Движения.ТоварыНаСкладах.ДобавитьПриход();
        ДвижениеТовар.Период = Дата;
        ДвижениеТовар.Номенклатура = Выборка.Номенклатура;
        ДвижениеТовар.Партия = Выборка.Партия;
        ДвижениеТовар.Количество = Выборка.Количество;
        ДвижениеТовар.Сумма = Выборка.Сумма;
        ДвижениеТовар.СуммаВал = СуммаВал;
        
    КонецЦикла;
    
    // Включаем признак автоматической записи измененных движений при завершении транзакции
    Если Не Отказ Тогда
        Движения.ЗаказыПоставщикам.Записывать = Истина;
        Движения.ТоварыНаСкладах.Записывать = Истина;
    КонецЕсли;
    
КонецПроцедуры

Преимущества оптимизированного кода

Проанализируем ключевые достоинства представленного решения с точки зрения стандартов разработки фирмы "1С":

← На главную