Нередко разработчики сталкиваются с ситуацией, когда обработка загрузки табличных данных из Microsoft Excel идеально отрабатывает на локальной файловой копии, но при запуске в клиент-серверной базе (SQL) начинает вести себя непредсказуемо. Вместо одного свернутого документа за день создается множество документов за одни и те же даты, причем созданные документы идут вразброс.
Проанализируем детально причины такого поведения платформы 1С:Предприятие и разберем надежные способы решения проблемы с правильными примерами кода.
Рассмотрим распространенный алгоритм: разработчик считывает строки в таблицу значений, выполняет агрегацию через метод Свернуть(), а затем в цикле обходит таблицу. Если текущая дата не совпадает с датой в предыдущей строке, алгоритм создает новый документ ОтчетОРозничныхПродажах.
Здесь кроется критическая архитектурная ловушка: метод ТаблицаЗначений.Свернуть() выполняет группировку и суммирование, но не гарантирует сохранение порядка записей и не выполняет автоматическую сортировку. В файловой базе за счет работы менеджера памяти в рамках одного процесса строки часто случайно сохраняют исходный порядок добавления. В клиент-серверном режиме порядок строк после свертки становится произвольным. Если в выборке строка за 14-е число окажется рядом со строкой за 15-е, а следом вновь появится 14-е число, линейный обход создаст дублирующие документы вразброс.
Разберем, как исправить эту проблему. После выполнения метода Свернуть() необходимо обязательно вызвать явную сортировку:
// Сворачиваем строки по аналитике и дате
ТЗ.Свернуть("Номенклатура, СтавкаНДС, ЕдИзм, Цена, Дата", "Количество");
// Обязательная явная сортировка перед созданием документов
ТЗ.Сортировать("Дата Возр");
Только при наличии явного вызова ТЗ.Сортировать("Дата Возр") строки за один день гарантированно будут идти последовательно друг за другом.
Посмотрим на типичный фрагмент строкового преобразования даты:
Врем.Дата = Лев(Строка(Excel.Cells(НС, 2).Value), 10);
...
текДата = СокрЛП(ход.Дата);
текДата = СтрЗаменить(текДата, ".", "");
текДатаДень = Лев(текДата, 2);
текДатаМесяц = Лев(текДата, 4);
текДатаМесяц = Прав(текДатаМесяц, 2);
текДатаГод = Прав(текДата, 4);
текстр.Дата = Дата(текДатаГод + ТекДатаМесяц + текДатаДень + "000000");
Выясним причину, почему такой код дает сбой. Свойство ячейки Excel.Cells().Value для даты возвращает не строку, а внутренний тип VT_DATE, который COM-соединение 1С автоматически преобразует в примитивный тип Дата. Преобразование даты в строку через Строка() жестко завязано на региональные настройки операционной системы.
Если на локальном компьютере установлен формат ДД.ММ.ГГГГ, то на сервере (особенно без русскоязычного языкового пакета) формат может быть ГГГГ-ММ-ДД или М/Д/ГГГГ. В результате строковые функции Лев() и Прав() вырезают случайные фрагменты времени и разделителей, а конструктор Дата() инициализирует пустые даты либо неверные года, что полностью ломает группировку строк.
Для приведения даты из Excel к началу дня строковый разбор не нужен вовсе. Достаточно использовать встроенную функцию НачалоДня():
// Прямое получение даты без строковых преобразований
ЗначениеЯчейки = Excel.Cells(НС, 2).Value;
Если ЗначениеЗаполнено(ЗначениеЯчейки) Тогда
ТекСтрока.Дата = НачалоДня(ЗначениеЯчейки);
КонецЕсли;
Если таблица значений создается без указания типов колонок (через простой вызов ТЗ.Колонки.Добавить("Дата")), колонка может принимать значения произвольного типа. Если в одну строку попадет значение типа Дата, а в другую — строковое представление, метод Свернуть() не объединит их, так как значения разных типов считаются не равными.
Всегда задавайте явное описание типов при создании промежуточных таблиц:
ТЗ = Новый ТаблицаЗначений;
ТЗ.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТЗ.Колонки.Добавить("ЕдИзм", Новый ОписаниеТипов("СправочникСсылка.КлассификаторЕдиницИзмерения"));
ТЗ.Колонки.Добавить("СтавкаНДС", Новый ОписаниеТипов("ПеречислениеСсылка.СтавкиНДС"));
ТЗ.Колонки.Добавить("Цена", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));
ТЗ.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3)));
ТЗ.Колонки.Добавить("Дата", Новый ОписаниеТипов("Дата", , Новый КвалификаторыДаты(ЧастиДаты.Дата)));
Наиболее надежным и отказоустойчивым способом группировки данных в 1С является передача таблицы значений в качестве параметра во временную таблицу запроса. Механизм запросов работает абсолютно одинаково как в файловом, так и в серверном вариантах, полностью исключая влияние менеджера памяти.
Рассмотрим практическую реализацию:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Т.Номенклатура КАК Номенклатура,
| Т.ЕдИзм КАК ЕдИзм,
| Т.СтавкаНДС КАК СтавкаНДС,
| Т.Цена КАК Цена,
| НАЧАЛОПЕРИОДА(Т.Дата, ДЕНЬ) КАК Дата,
| СУММА(Т.Количество) КАК Количество
|ПОМЕСТИТЬ ВТ_Данные
|ИЗ
| &ТаблицаДанных КАК Т
|СГРУППИРОВАТЬ ПО
| Т.Номенклатура,
| Т.ЕдИзм,
| Т.СтавкаНДС,
| Т.Цена,
| НАЧАЛОПЕРИОДА(Т.Дата, ДЕНЬ)
|;
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ.Дата КАК Дата,
| ВТ.Номенклатура КАК Номенклатура,
| ВТ.ЕдИзм КАК ЕдИзм,
| ВТ.СтавкаНДС КАК СтавкаНДС,
| ВТ.Цена КАК Цена,
| ВТ.Количество КАК Количество
|ИЗ
| ВТ_Данные КАК ВТ
|ИТОГИ ПО
| Дата";
Запрос.УстановитьПараметр("ТаблицаДанных", ТЗ);
Результат = Запрос.Выполнить();
// Обход с группировкой по датам гарантирует создание ровно одного документа на каждый день
ВыборкаДата = Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаДата.Следующий() Цикл
ДокументОбъект = Документы.ОтчетОРозничныхПродажах.СоздатьДокумент();
ДокументОбъект.Дата = ВыборкаДата.Дата;
ВыборкаСтроки = ВыборкаДата.Выбрать();
Пока ВыборкаСтроки.Следующий() Цикл
НоваяСтрока = ДокументОбъект.Товары.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, ВыборкаСтроки);
КонецЦикла;
ДокументОбъект.Записать(РежимЗаписиДокумента.Запись);
КонецЦикла;
Разберем по шагам ключевые правила построения архитектуры загрузки:
ТабличныйДокумент с методом Прочитать(ПутьКФайлу, СпособЧтенияЗначенийТабличногоДокумента.Значение).&НаКлиенте. Выбранный файл помещается во временное хранилище через метод ПоместитьФайлНаСервер(), а серверная процедура распаковывает двоичные данные и заполняет объекты базы данных, не обращаясь напрямую к локальным дискам клиентских ПК.