Сортировка выделенных документов в управляемой форме по возрастанию даты и момента времени

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

В практике разработки на платформе «1С:Предприятие 8» часто возникает задача пакетной обработки документов, выбранных пользователем в форме списка или журнала. Типичный сценарий: пользователь выделяет несколько строк в динамическом списке и нажимает кнопку «Провести». Чтобы алгоритм проведения не вызвал ошибок (например, из-за попытки списать товар до его фактического оприходования), документы должны обрабатываться строго в хронологическом порядке.

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

Почему стандартная сортировка в Таблице Значений не работает по умолчанию?

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


СписокДок = Новый ТаблицаЗначений;
СписокДок.Колонки.Добавить("МоментВремени");
СписокДок.Колонки.Добавить("Документ");

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

СписокДок.Сортировать("МоментВремени Убыв");

При выполнении этого кода сортировка по колонке МоментВремени не даст ожидаемого результата. Выясним причину такого поведения платформы. Дело в том, что метод таблицы значений Сортировать() без дополнительных параметров сравнивает сложные объекты (такие как ссылки или объекты типа МоментВремени) по их строковому представлению.

Строковое представление документа или момента времени формируется по принципу «ИмяТипа Номер от Дата». Если в списке присутствуют документы разных типов, они сначала сгруппируются по имени метаданных, а уже внутри этой группы будут отсортированы по дате. Это ломает всю хронологическую последовательность.

Чтобы заставить таблицу значений выполнять физическое сравнение сложных типов данных, необходимо использовать специальный системный объект — СравнениеЗначений. Изменим код сортировки:


// Передаем объект СравнениеЗначений вторым параметром
СписокДок.Сортировать("МоментВремени Возр", Новый СравнениеЗначений);

При использовании объекта СравнениеЗначений платформа меняет логику сравнения элементов:

  1. Объекты типа МоментВремени начинают сравниваться корректно: сначала по дате и времени с точностью до секунды.
  2. При совпадении даты и времени сравнение производится по внутреннему уникальному идентификатору (GUID) ссылки документа.

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

Ловушка производительности: проблема «N+1 запросов»

В приведенном выше коде присутствует критическая архитектурная ошибка. Строка СтрТЗ.Документ.Дата (или обращение к методу МоментВремени() через точку от ссылки) приводит к неявному обращению к базе данных.

Если пользователь выделит 200 документов в списке, система выполнит 200 отдельных, последовательных запросов к СУБД, чтобы получить дату каждого документа. В условиях клиент-серверной архитектуры и работы через веб-клиент это вызовет колоссальные задержки интерфейса. Такое явление в программировании называется проблемой «N+1 запросов».

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

Решение задачи с помощью запроса и механизма автоупорядочивания

Перенос сортировки в запрос к базе данных — наиболее эффективный и профессиональный метод. Рассмотрим, как передать массив выбранных ссылок в запрос и отсортировать их.

Один из самых простых способов упорядочить документы в запросе — использовать ключевое слово АВТОУПОРЯДОЧИВАНИЕ. Проанализируем этот подход на примере:


&НаСервере
Функция ПолучитьОтсортированныйСписок(МассивСсылок)
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	ВыбранныеДокументы.Ссылка КАК Ссылка
        |ПОМЕСТИТЬ ВТ_Документы
        |ИЗ
        |	&МассивСсылок КАК ВыбранныеДокументы
        |;
        |
        |////////////////////////////////////////////////////////////////////////////////
        |ВЫБРАТЬ
        |	ВТ_Документы.Ссылка КАК Ссылка
        |ИЗ
        |	ВТ_Документы КАК ВТ_Документы
        |
        |АВТОУПОРЯДОЧИВАНИЕ";
        
    Запрос.УстановитьПараметр("МассивСсылок", МассивСсылок);
    
    РезультатЗапроса = Запрос.Выполнить();
    Возврат РезультатЗапроса.Выгрузить().ВыгрузитьКолонку("Ссылка");
    
КонецФункции

Инструкция АВТОУПОРЯДОЧИВАНИЕ указывает платформе «1С:Предприятие», что результат запроса должен быть упорядочен по правилам, определенным для конкретных таблиц. Для документов правилом по умолчанию является сортировка по полям Дата и Ссылка.

Однако использование АВТОУПОРЯДОЧИВАНИЕ имеет существенные недостатки:

  1. Оно создает избыточную нагрузку на СУБД. Платформа генерирует сложный SQL-запрос, добавляя скрытые поля и сортировки, что может негативно сказаться на производительности больших баз данных.
  2. В стандартах разработки фирмы «1С» использовать эту инструкцию без крайней необходимости не рекомендуется, так как план выполнения запроса становится менее предсказуемым.

Оптимальное решение: явная сортировка по моменту времени в запросе

Вместо того чтобы полагаться на автоматические механизмы платформы, мы можем явно указать сортировку в запросе, используя встроенную функцию языка запросов МОМЕНТВРЕМЕНИ(). Это решение работает быстрее, стабильнее и полностью контролируется разработчиком.

Рассмотрим по шагам реализацию этого метода:


&НаСервере
Функция ОтсортироватьДокументыПоМоментуВремени(МассивСсылок)
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	ВыбранныеДокументы.Ссылка КАК Ссылка,
        |	МОМЕНТВРЕМЕНИ(ВыбранныеДокументы.Ссылка) КАК Момент
        |ПОМЕСТИТЬ ВТ_Документы
        |ИЗ
        |	&МассивСсылок КАК ВыбранныеДокументы
        |;
        |
        |////////////////////////////////////////////////////////////////////////////////
        |ВЫБРАТЬ
        |	ВТ_Документы.Ссылка КАК Ссылка
        |ИЗ
        |	ВТ_Документы КАК ВТ_Документы
        |
        |УПОРЯДОЧИТЬ ПО
        |	ВТ_Документы.Момент ВОЗР";
        
    Запрос.УстановитьПараметр("МассивСсылок", МассивСсылок);
    
    Выгрузка = Запрос.Выполнить().Выгрузить();
    Возврат Выгрузка.ВыгрузитьКолонку("Ссылка");
    
КонецФункции

Этот запрос извлекает ссылки на документы и вычисляет их моменты времени на уровне базы данных. Сортировка выполняется в рамках одного SQL-запроса, что гарантирует максимальное быстродействие.

Важный нюанс: коллизии документов разного типа, созданных в одну секунду

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

Представим ситуацию: в одну и ту же секунду были созданы два документа разных типов — Приходный ордер (поступление товара) и Расходный ордер (списание товара).

В рамках одной секунды их хронологическая последовательность определяется платформой по уникальному идентификатору (GUID). Однако GUID-ы для разных типов документов генерируются независимо в разных таблицах СУБД. Взаимное расположение этих документов на временной оси окажется абсолютно случайным.

Если при сортировке по чистому моменту времени Расходный ордер окажется «раньше» Приходного ордера, система попытается списать товар до его поступления, что приведет к ошибке проведения («Отказ из-за отсутствия остатков»).

Для решения этой проблемы при групповой обработке документов применяется **сортировка по бизнес-приоритетам типов документов**. Мы можем явно задать веса для каждого типа документа в запросе с помощью конструкции ВЫБОР КОГДА. Рассмотрим пример реализации:


&НаСервере
Функция ОтсортироватьСУчетомБизнесЛогики(МассивСсылок)
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	ВыбранныеДокументы.Ссылка КАК Ссылка,
        |	ВыбранныеДокументы.Ссылка.Дата КАК Дата,
        |	ВЫБОР
        |		КОГДА ВыбранныеДокументы.Ссылка ССЫЛКА Документ.ПриходныйОрдер ТОГДА 1
        |		КОГДА ВыбранныеДокументы.Ссылка ССЫЛКА Документ.РасходныйОрдер ТОГДА 2
        |		ИНАЧЕ 3
        |	КОНЕЦ КАК ПриоритетТипа
        |ПОМЕСТИТЬ ВТ_Документы
        |ИЗ
        |	&МассивСсылок КАК ВыбранныеДокументы
        |;
        |
        |////////////////////////////////////////////////////////////////////////////////
        |ВЫБРАТЬ
        |	ВТ_Документы.Ссылка КАК Ссылка
        |ИЗ
        |	ВТ_Документы КАК ВТ_Документы
        |
        |УПОРЯДОЧИТЬ ПО
        |	ВТ_Документы.Дата ВОЗР,
        |	ВТ_Документы.ПриоритетТипа ВОЗР,
        |	ВТ_Документы.Ссылка ВОЗР";
        
    Запрос.УстановитьПараметр("МассивСсылок", МассивСсылок);
    
    Выгрузка = Запрос.Выполнить().Выгрузить();
    Возврат Выгрузка.ВыгрузитьКолонку("Ссылка");
    
КонецФункции

Благодаря такой структуризации запроса, даже если документы прихода и расхода созданы в одну и ту же секунду, система гарантированно расположит сначала приходные документы (приоритет 1), а затем расходные (приоритет 2). Сортировка по полю Ссылка в конце секции УПОРЯДОЧИТЬ ПО обеспечивает воспроизводимость результатов при повторных запусках алгоритма.

Полный пример интеграции алгоритма в управляемую форму

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

Код модуля управляемой формы:


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

&НаСервереБезКонтекста
Функция ПолучитьОтсортированныйСписокСервер(МассивДокументов)
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	ВыбранныеДокументы.Ссылка КАК Ссылка,
        |	МОМЕНТВРЕМЕНИ(ВыбранныеДокументы.Ссылка) КАК Момент
        |ПОМЕСТИТЬ ВТ_Выборка
        |ИЗ
        |	&МассивДокументов КАК ВыбранныеДокументы
        |;
        |
        |////////////////////////////////////////////////////////////////////////////////
        |ВЫБРАТЬ
        |	ВТ_Выборка.Ссылка КАК Ссылка
        |ИЗ
        |	ВТ_Выборка КАК ВТ_Выборка
        |
        |УПОРЯДОЧИТЬ ПО
        |	ВТ_Выборка.Момент ВОЗР";
        
    Запрос.УстановитьПараметр("МассивДокументов", МассивДокументов);
    
    Результат = Запрос.Выполнить();
    Если Результат.Пустой() Тогда
        Возврат Новый Массив;
    КонецЕсли;
    
    Возврат Результат.Выгрузить().ВыгрузитьКолонку("Ссылка");
    
КонецФункции

&НаСервереБезКонтекста
Процедура ОбработатьПроведениеНаСервере(СсылкаДок)
    
    Попытка
        ДокОбъект = СсылкаДок.ПолучитьОбъект();
        Если ДокОбъект <> Неопределено Тогда
            ДокОбъект.Записать(РежимЗаписиДокумента.Проведение, РежимПроведенияДокумента.Неоперативный);
        КонецЕсли;
    Исключение
        ЗаписьЖурналаРегистрации("ПакетноеПроведение", УровеньЖурналаРегистрации.Ошибка,,, 
            "Ошибка проведения документа: " + СсылкаДок + Символы.ПС + ОписаниеОшибки());
    КонецПопытки;
    
КонецПроцедуры

Использование директивы &НаСервереБезКонтекста для функций сортировки и проведения позволяет минимизировать объем данных, передаваемых между клиентом и сервером. В такие процедуры не передаются реквизиты самой управляемой формы, что положительно сказывается на скорости работы интерфейса программы.

← На главную