Как перевести строковую колонку с динамическим именем в числовой тип в таблице значений 1С и получить итог

Программист 1С v8.3 (Обычные формы)
← На главную

При импорте данных из внешних источников, таких как файлы Microsoft Excel, разработчики часто сталкиваются с ситуацией, когда числовые показатели (например, количество или сумма) загружаются в объект ТаблицаЗначений как обычные строки. Прямая попытка рассчитать сумму по такой колонке с помощью стандартного метода Итог() приводит к ошибке или нулевому результату, поскольку платформа 1С:Предприятие не умеет суммировать строковые типы данных.

Ситуация усложняется, когда имя целевой колонки заранее неизвестно и формируется динамически (например, Количество(Склад_А), Количество(Склад_Б)). В этой статье мы подробно разберем, как динамически найти нужную колонку, изменить её тип на числовой и рассчитать итоговые показатели, а также проанализируем критические ошибки производительности в коде загрузки.

Почему нельзя просто переназначить тип существующей колонки?

В платформе 1С свойство ТипЗначения у уже существующей колонки таблицы значений доступно только для чтения. Мы не можем напрямую написать конструкцию вида Таблица.Колонки.Количество.ТипЗначения = Новый ОписаниеТипов("Число"). Чтобы изменить тип данных колонки, нам необходимо применить обходной маневр: выгрузить данные во временный массив, удалить старую строковую колонку, создать на её месте новую с числовым типом и заполнить её обработанными данными.

---

Решение 1: Динамический поиск, реструктуризация колонки и приведение типов

Рассмотрим детальный алгоритм решения этой задачи по шагам. Этот способ является наиболее правильным, так как он физически меняет структуру таблицы значений, позволяя в дальнейшем использовать быстрый платформенный метод Итог(), а также корректно сортировать или сворачивать таблицу.

Шаг 1. Динамическое определение имени колонки. Поскольку имя колонки может меняться в зависимости от файла импорта (например, содержать имя конкретного склада в скобках), мы выполним обход коллекции Колонки нашей таблицы значений и найдем точное имя по вхождению подстроки "количество".

Шаг 2. Выгрузка данных и удаление старой колонки. Мы сохраним индекс старой колонки, чтобы новая числовая колонка встала строго на то же место в таблице. Данные выгрузим в массив через метод ВыгрузитьКолонку().

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

Давайте посмотрим на практический пример реализации этого алгоритма:


// Предположим, что ТабЗначенийОтчетОСкладах — это наша исходная таблица значений

ИмяКолонкиКоличество = "";

// Шаг 1. Находим динамическое имя колонки по подстроке
Для Каждого Колонка Из ТабЗначенийОтчетОСкладах.Колонки Цикл
    Если Найти(НРег(Колонка.Имя), "количество") > 0 Тогда
        ИмяКолонкиКоличество = Колонка.Имя;
        Прервать; // Искомая колонка успешно найдена
    КонецЕсли;
КонецЦикла;

Если ЗначениеЗаполнено(ИмяКолонкиКоличество) Тогда
    
    // Шаг 2. Выгружаем данные старой колонки в массив
    МассивЗначений = ТабЗначенийОтчетОСкладах.ВыгрузитьКолонку(ИмяКолонкиКоличество);
    
    // Получаем индекс текущей колонки, чтобы сохранить исходный порядок полей
    ИндексКолонки = ТабЗначенийОтчетОСкладах.Колонки.Индекс(ТабЗначенийОтчетОСкладах.Колонки[ИмяКолонкиКоличество]);
    
    // Удаляем старую строковую колонку
    ТабЗначенийОтчетОСкладах.Колонки.Удалить(ИндексКолонки);
    
    // Шаг 3. Создаем описание типов для числового поля (длина 15, точность 3)
    МассивТипов = Новый Массив;
    МассивТипов.Добавить(Тип("Число"));
    Квалификаторы = Новый КвалификаторыЧисла(15, 3);
    ОписаниеТиповЧисло = Новый ОписаниеТипов(МассивТипов, , , Квалификаторы);
    
    // Вставляем новую колонку с тем же именем, но правильным типом данных
    ТабЗначенийОтчетОСкладах.Колонки.Вставить(ИндексКолонки, ИмяКолонкиКоличество, ОписаниеТиповЧисло);
    
    // Шаг 4. Заполняем колонку числовыми значениями с предварительной очисткой строки
    Для Инд = 0 По ТабЗначенийОтчетОСкладах.Количество() - 1 Цикл
        СтрокаТЗ = ТабЗначенийОтчетОСкладах[Инд];
        ЗначениеСтрока = МассивЗначений[Инд];
        
        // Проверяем на Неопределено или пустую строку
        Если ЗначениеСтрока = Неопределено Тогда
            СтрокаТЗ[ИмяКолонкиКоличество] = 0;
            Продолжить;
        КонецЕсли;
        
        // Преобразуем к строковому типу на случай, если там уже было число или другой тип
        ТекСтрокаЗначение = Строка(ЗначениеСтрока);
        
        // Очищаем строку от пробелов и неразрывных пробелов (символ 160 / NPP)
        ЧистаяСтрока = СтрЗаменить(ТекСтрокаЗначение, Символы.НПП, "");
        ЧистаяСтрока = СтрЗаменить(ЧистаяСтрока, " ", "");
        
        // Корректируем разделитель дробной части
        ЧистаяСтрока = СтрЗаменить(ЧистаяСтрока, ".", ","); 
        
        // Безопасное приведение типов с обработкой исключений
        Попытка
            СтрокаТЗ[ИмяКолонкиКоличество] = Число(ЧистаяСтрока);
        Исключение
            // Если в ячейке был текст (например, заголовок или ошибка "Н/Д"), запишем 0
            СтрокаТЗ[ИмяКолонкиКоличество] = 0;
        КонецПопытки;
    КонецЦикла;
    
    // Шаг 5. Получаем корректный итог по таблице значений
    ОбщийИтог = ТабЗначенийОтчетОСкладах.Итог(ИмяКолонкиКоличество);
    Сообщить("Расчет завершен успешно. Общее количество: " + ОбщийИтог);
    
Иначе
    Сообщить("Колонка, содержащая количество, не найдена в таблице значений!");
КонецЕсли;

---

Решение 2: Подсчет суммы «на лету» (без изменения структуры таблицы)

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

Проанализируем пример кода для быстрого расчета суммы:


ИтоговаяСумма = 0;
ИмяКолонкиКоличество = "";

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

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

---

Критический анализ и оптимизация исходного кода загрузки

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

Ошибка 1: Выполнение запроса внутри цикла (Запрос в цикле).
В цикле обхода ячеек Excel автор выполняет следующий код: РезультатЗапроса = Запрос.Выполнить();. Если файл Excel содержит 5 000 строк, система сделает 5 000 отдельных обращений к базе данных (СУБД). Это классический антипаттерн разработки в 1С. Каждый такой запрос порождает накладные расходы на сетевое соединение, компиляцию плана запроса СУБД и получение данных.

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

Ошибка 2: Использование медленного COM-соединения для работы с Excel.
Использование COMОбъект("Excel.Application") требует обязательного наличия установленного пакета Microsoft Office Excel на сервере или локальном компьютере пользователя. Если этот код будет выполняться на сервере под управлением Linux, он вызовет неустранимую ошибку. Кроме того, COM-процессы часто аварийно завершаются и зависают в памяти операционной системы.

Как исправить: Начиная с платформы 8.3, в 1С встроен нативный механизм чтения файлов Excel. Метод ТабличныйДокумент.Прочитать() с указанием способа чтения СпособЧтенияТабличногоДокумента.XLSX работает в десятки раз быстрее, не требует стороннего ПО и стабильно функционирует на любых операционных системах (включая Linux).

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


Функция ЗагрузитьДанныеИзФайлаОптимально(ПутьКФайлу, ИмяЛиста) Экспорт

    ТабличныйДокумент = Новый ТабличныйДокумент;
    
    // Используем встроенный нативный метод чтения XLSX (без использования COM)
    Попытка
        ТабличныйДокумент.Прочитать(ПутьКФайлу, СпособЧтенияТабличногоДокумента.XLSX);
    Исключение
        Сообщить("Не удалось прочитать файл! Ошибка: " + ОписаниеОшибки());
        Возврат Неопределено;
    КонецПопытки;
    
    // Получаем данные один раз из базы данных перед циклом (ОПТИМИЗАЦИЯ ЗАПРОСА)
    СоответствиеСкладов = Новый Соответствие;
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
    "ВЫБРАТЬ
    |	Данные.Наименование КАК Наименование,
    |	Данные.Склад КАК Склад
    |ИЗ
    |	Справочник.Данные КАК Данные
    |ГДЕ
    |	Данные.ЭтоГруппа = ЛОЖЬ";
    
    РезультатЗапроса = Запрос.Выполнить();
    Если Не РезультатЗапроса.Пустой() Тогда
        Выборка = РезультатЗапроса.Выбрать();
        Пока Выборка.Следующий() Цикл
            // Ключ — Склад (строка поиска), Значение — Наименование для подстановки
            СоответствиеСкладов.Вставить(Выборка.Склад, Выборка.Наименование);
        КонецЦикла;
    КонецЕсли;
    
    // Заполняем таблицу значений через ПостроительОтчета (как в исходном коде автора)
    ИсточникДанных = Новый ОписаниеИсточникаДанных(ТабличныйДокумент.Область());
    Построитель = Новый ПостроительОтчета;
    Построитель.ИсточникДанных = ИсточникДанных;
    Построитель.Выполнить();
    
    ТабРезультат = Построитель.Результат.Выгрузить();
    
    // Производим замену наименований складов, используя наше Соответствие в оперативной памяти
    Для Каждого СтрокаТЗ Из ТабРезультат Цикл
        // Допустим, склад находится в первой колонке с индексом 0
        ТекущееЗначениеСклада = СтрокаТЗ[0]; 
        НайденноеНаименование = СоответствиеСкладов.Получить(ТекущееЗначениеСклада);
        Если НайденноеНаименование <> Неопределено Тогда
            СтрокаТЗ[0] = НайденноеНаименование;
        КонецЕсли;
    КонецЦикла;
    
    // Теперь типизируем динамическую колонку количества
    ИмяКолонкиКоличество = "";
    Для Каждого Колонка Из ТабРезультат.Колонки Цикл
        Если Найти(НРег(Колонка.Имя), "количество") > 0 Тогда
            ИмяКолонкиКоличество = Колонка.Имя;
            Прервать;
        КонецЕсли;
    КонецЦикла;
    
    Если ЗначениеЗаполнено(ИмяКолонкиКоличество) Тогда
        
        МассивЗначений = ТабРезультат.ВыгрузитьКолонку(ИмяКолонкиКоличество);
        ИндексКолонки = ТабРезультат.Колонки.Index(ТабРезультат.Колонки[ИмяКолонкиКоличество]);
        
        ТабРезультат.Колонки.Удалить(ИндексКолонки);
        
        МассивТипов = Новый Массив;
        МассивТипов.Добавить(Тип("Число"));
        ОписаниеТиповЧисло = Новый ОписаниеТипов(МассивТипов, , , Новый КвалификаторыЧисла(15, 3));
        
        ТабРезультат.Колонки.Вставить(ИндексКолонки, ИмяКолонкиКоличество, ОписаниеТиповЧисло);
        
        Для Инд = 0 По ТабРезультат.Количество() - 1 Цикл
            СтрокаТЗ = ТабРезультат[Инд];
            ЗначениеСтрока = МассивЗначений[Инд];
            
            Если ЗначениеСтрока = Неопределено Тогда
                СтрокаТЗ[ИмяКолонкиКоличество] = 0;
                Продолжить;
            КонецЕсли;
            
            ЧистаяСтрока = СтрЗаменить(Строка(ЗначениеСтрока), Символы.НПП, "");
            ЧистаяСтрока = СтрЗаменить(ЧистаяСтрока, " ", "");
            ЧистаяСтрока = СтрЗаменить(ЧистаяСтрока, ".", ",");
            
            Попытка
                СтрокаТЗ[ИмяКолонкиКоличество] = Число(ЧистаяСтрока);
            Исключение
                СтрокаТЗ[ИмяКолонкиКоличество] = 0;
            КонецПопытки;
        КонецЦикла;
        
    КонецЕсли;
    
    Возврат ТабРезультат;

КонецФункции

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

← На главную