Вместо предисловия
Я знаю про CloudConf, и сервисы хранения кода, но на Инфостарте сижу каждый день, поэтому мне здесь удобнее. Да и возможность поделиться какими-то своими идеями, получив отклик, тоже интересна.
Код я оформляю в соответствии с соглашениями о написании кода.
// Описание разницы времени в формате "чч:мм:сс (ВсегоСекунд.ОстатокМиллисекунд сек.)"
//
// Параметры:
// Старт - Число - результат значения работы функции "ТекущаяУниверсальнаяДатаВМиллисекундах()"
// Финиш - Число - результат значения работы функции "ТекущаяУниверсальнаяДатаВМиллисекундах()"\
//
// Возвращаемое значение:
// Строка
//
Функция ОписаниеЗамераВремени(Старт, Финиш)
КолМлСекундЛог = Финиш - Старт;
КолСекундЛог = ЦЕЛ(КолМлСекундЛог / 1000);
КолМлСекунд = КолМлСекундЛог - КолСекундЛог;
ФорматКолМС = "" + КолМлСекунд;
ФорматКолМС = СтрЗаменить(ФорматКолМС, ",", "");
ФорматКолМС = СтрЗаменить(ФорматКолМС, " ", "");
ФорматКолМС = Лев(ФорматКолМС, 3);
Пока Прав(ФорматКолМС, 1) = "0" Цикл
ФорматКолМС = Сред(ФорматКолМС, 1, СтрДлина(ФорматКолМС)-1);
КонецЦикла;
ФорматСекунд = "" + КолСекундЛог + "." + ФорматКолМС;
чч = ЦЕЛ(КолСекундЛог/3600);
мм = ЦЕЛ((КолСекундЛог - чч*3600)/60);
сс = КолСекундЛог - чч*3600 - мм*60;
СтрокаВремя = Формат(чч, "ЧЦ=2; ЧН=; ЧВН=") + ":" +
Формат(мм, "ЧЦ=2; ЧН=; ЧВН=") + ":" +
Формат(сс, "ЧЦ=2; ЧН=; ЧВН=") + " ("+ ФорматСекунд +" сек.)";
Возврат СтрокаВремя;
КонецФункции
Пример использования:
Старт = ТекущаяУниверсальнаяДатаВМиллисекундах();
//.. код
Финиш = ТекущаяУниверсальнаяДатаВМиллисекундах();
Сообщить("Обработка завершена за " + ОписаниеЗамераВремени(Финиш, Старт));
// -> Обработка завершена за 00:00:03 (3.303 сек.)
// Проверка значения на вхождение в некоторый интервал
//
// Параметры:
// Значение - Число, Дата - Проверяемое значение
// НачалоИнтервала - Число, Дата
// КонецИнтервала - Число, Дата
// СтрогаяПроверка - Булево
//
// Возвращаемое значение:
// Булево
//
Функция ЗначениеВИнтервале(Значение, НачалоИнтервала, КонецИнтервала, СтрогаяПроверка = Ложь)
Если СтрогаяПроверка Тогда
Возврат НачалоИнтервала < Значение И Значение < КонецИнтервала;
Иначе
Возврат НачалоИнтервала <= Значение И Значение <= КонецИнтервала;
КонецЕсли;
КонецФункции
Пример использования:
//.....
Если НЕ ЗначениеВИнтервале(ДатаПоказателя, НачалоПериода, КонецПериода) Тогда
Продолжить;
КонецЕсли;
//...
// Универсальная функция для парсинга строк
//
// Параметры:
// ИсходнаяСтрока - Строка
// СтрокаРазделитель - Строка
// ОбрезатьПробелы - Булево
//
// Возвращаемое значение:
// СтруктураЧастиСтроки - Структура
// * Левая - Строка - Левая часть строки (до начала СтрокаРазделитель)
// * Правая - Строка - Правая часть строки (после окончания СтрокаРазделитель)
// * Разбито - Булево - флаг указывающий что в исходной строке есть СтрокаРазделитель
//
Функция РазбитьСтроку(ИсходнаяСтрока, СтрокаРазделитель, ОбрезатьПробелы = Истина)
поз = Найти(ИсходнаяСтрока, СтрокаРазделитель);
ДлинаРазделителя = СтрДлина(СтрокаРазделитель);
Разбито = поз > 0;
ЛеваяЧастьСтроки = Лев(ИсходнаяСтрока, поз - 1);
Если ОбрезатьПробелы Тогда
ЛеваяЧастьСтроки = СокрЛП(ЛеваяЧастьСтроки);
КонецЕсли;
ПраваяЧастьСтроки = Сред(ИсходнаяСтрока, поз + ДлинаРазделителя);
Если ОбрезатьПробелы Тогда
ПраваяЧастьСтроки = СокрЛП(ПраваяЧастьСтроки);
КонецЕсли;
СтруктураЧастиСтроки = Новый Структура("Левая, Правая, Разбито"
, ЛеваяЧастьСтроки
, ПраваяЧастьСтроки
, Разбито);
Возврат СтруктураЧастиСтроки;
КонецФункции
Пример использования:
//Парсинг строки вида "дд.мм.гггг":
Результат = Дата(1,1,1);
СтруктураЧастиСтроки = РазбитьСтроку(ЗначениеЯчейки, ".");
Если СтруктураЧастиСтроки.Разбито Тогда
ст_дд = Прав(СтруктураЧастиСтроки.Левая, 2);
чс_дд = Число(ст_дд);
СтруктураЧастиСтроки = РазбитьСтроку(СтруктураЧастиСтроки.Правая, ".");
Если СтруктураЧастиСтроки.Разбито Тогда
ст_мм = Прав(СтруктураЧастиСтроки.Левая, 2);
чс_мм = Число(ст_мм);
ст_гггг = Лев(СтруктураЧастиСтроки.Правая, 4);
чс_гггг = Число(ст_гггг);
Результат = Дата(чс_гггг, чс_мм, чс_дд);
КонецЕсли;
КонецЕсли;
Возврат Результат;
В комментарии ниже дан более логичный код, с использованием построителя для этих целей (к сожалению, не пойму сейчас как ссылку на коммент дать). Оставил здесь эту функцию потому что мне иногда нужен перебор табличного документа, и ее использую качестве заготовки.
// Преобразование табличного документа в таблицу значений
//
// Параметры:
// ТабДок - ТабличныйДокумент
//
// Возвращаемое значение:
// ТаблицаЗначений
//
Функция ТабличныйДокументВТаблицуЗначений(ТабДок)
ТаблицаДанныеДокумента = Новый ТаблицаЗначений();
Для СчетчикКолонок = 1 По ТабДок.ШиринаТаблицы Цикл
ИмяКолонки = "_" + СчетчикКолонок;
ТаблицаДанныеДокумента.Колонки.Добавить(ИмяКолонки);
Для СчетчикСтрок = 1 По ТабДок.ВысотаТаблицы Цикл
ИндексСтроки = СчетчикСтрок - 1;
Если СчетчикКолонок = 1 Тогда
СтрокаТаблицы = ТаблицаДанныеДокумента.Добавить();
Иначе
СтрокаТаблицы = ТаблицаДанныеДокумента[ИндексСтроки];
КонецЕсли;
ТекущаяЯчейка = ТабДок.Область(СчетчикСтрок, СчетчикКолонок);
ЗначениеЯчейки = ТекущаяЯчейка.Текст;
СтрокаТаблицы[ИмяКолонки] = ЗначениеЯчейки;
КонецЦикла
КонецЦикла;
Возврат ТаблицаДанныеДокумента;
КонецФункции
Решение задачи чтения иерархии табличного документа. Здесь стоит упомянуть статью Чтение группировок табличного документа:
- Сам алгоритм статьи не отрабатывает корректно, если есть несколько вложенных уровней группировок. Его исправили в последнем комментарии, но автор статью почему-то обновлять не стал.
- Предложенная там реализация отличается плохо читабельным кодом. Кроме этого задействовано три метода, в моем подходе - один. Подход к решению тоже разный: я использую видимость области строки, там используется парсинг таб. дока через документ DOM.
- При этом по скорости оба алгоритма показывают примерно одинаковое время
// Дерево значений по табличному документу, с аналогичной иерархией существующих группировок
//
// Параметры:
// ТабДок - ТабличныйДокумент
// СвойстваКолонок - ТаблицаЗначений
// *Имя - Строка - Имя колонки дерева, в которую будут помещены данные области колонки табличного документа
// *Номер - Число - Номер колонки в табличном документе
// ВысотаШапки - Число - для отсечения незначащих строк
// УдалятьФлаговыеКолонки - Булево - При анализе табличного документа в дерево добавляются служебные колонки
// "Начало" и "Конец", где сохраняются диапазоны группировки. Если Истина - то эти колонки при окончании
// алгоритма будут удалены.
// ИсключатьПустыеСтроки - Булево - Если Истина, то строки в которых значения всех колонок пустые будут исключены
//
// Возвращаемое значение:
// ДеревоЗначений
//
Функция ТабличныйДокументВДеревоЗначений(ТабДок, СвойстваКолонок, ВысотаШапки = 0, УдалятьФлаговыеКолонки = Истина, ИсключатьПустыеСтроки = Истина)
ДеревоДокумента = Новый ДеревоЗначений;
Для каждого ОписаниеКолонки Из СвойстваКолонок Цикл
ДеревоДокумента.Колонки.Добавить(ОписаниеКолонки.Имя);
КонецЦикла;
ДеревоДокумента.Колонки.Добавить("Начало");
ДеревоДокумента.Колонки.Добавить("Конец");
Кэш = Новый ТабличныйДокумент;
Кэш.Вывести(ТабДок);
Для сч = -Кэш.КоличествоУровнейГруппировокСтрок() По 0 Цикл
Кэш.ПоказатьУровеньГруппировокСтрок(-сч);
КонецЦикла;
ПройденныеСтроки = Новый Соответствие;
ПройденныеУровни = Новый Соответствие;
ПустыеСтроки = Новый Соответствие;
КоличествоУровней =Кэш.КоличествоУровнейГруппировокСтрок();
Для СчетчикУровня=1 По КоличествоУровней Цикл
ПройденныеУровни.Вставить(СчетчикУровня, Новый Массив);
Кэш.ПоказатьУровеньГруппировокСтрок(СчетчикУровня-1);
Для СчетчикСтрок=1 По Кэш.ВысотаТаблицы Цикл
Если СчетчикСтрок <= ВысотаШапки Тогда
Продолжить;
КонецЕсли;
Если ПройденныеСтроки.Получить(СчетчикСтрок) = Истина Тогда
Продолжить;
КонецЕсли;
Если ИсключатьПустыеСтроки Тогда
ЭтоПустаяСтрока = ПустыеСтроки.Получить(СчетчикСтрок);
Если ЭтоПустаяСтрока = Неопределено Тогда
ЭтоПустаяСтрока = Истина;
Для каждого ОписаниеКолонки Из СвойстваКолонок Цикл
ОбластьЯчейки = Кэш.Область(СчетчикСтрок, ОписаниеКолонки.Номер);
Если ЗначениеЗаполнено(ОбластьЯчейки.Текст) Тогда
ЭтоПустаяСтрока = Ложь;
Прервать;
КонецЕсли;
КонецЦикла;
ПустыеСтроки.Вставить(СчетчикСтрок, ЭтоПустаяСтрока);
КонецЕсли;
Если ЭтоПустаяСтрока Тогда
Продолжить;
КонецЕсли;
КонецЕсли;
ИмяОбласти = "R" + Формат(СчетчикСтрок, "ЧГ=");
ОбластьСтроки = Кэш.Область(ИмяОбласти);
Если ОбластьСтроки.Видимость Тогда
Если СчетчикУровня = 1 Тогда
Родитель = ДеревоДокумента;
Иначе
МассивНомеровСтрокПредУровня = ПройденныеУровни.Получить(СчетчикУровня-1);
Для сч=-МассивНомеровСтрокПредУровня.ВГраница() По 0 Цикл
НомерСтрокиРодителя = МассивНомеровСтрокПредУровня[-сч];
Если НомерСтрокиРодителя < СчетчикСтрок Тогда
Прервать;
КонецЕсли;
КонецЦикла;
Родитель = ДеревоДокумента.Строки.Найти(НомерСтрокиРодителя, "Начало", Истина);
КонецЕсли;
Узел = Родитель.Строки.Добавить();
Узел.Начало = СчетчикСтрок;
Для каждого ОписаниеКолонки Из СвойстваКолонок Цикл
ОбластьЯчейки = Кэш.Область(СчетчикСтрок, ОписаниеКолонки.Номер);
Узел[ОписаниеКолонки.Имя] = СокрЛП(ОбластьЯчейки.Текст);
КонецЦикла;
Если СчетчикУровня < КоличествоУровней Тогда
ПройденныеСтроки.Вставить(СчетчикСтрок, Истина);
МассивНомеровСтрокУровня = ПройденныеУровни.Получить(СчетчикУровня);
МассивНомеровСтрокУровня.Добавить(СчетчикСтрок);
КонецЕсли;
КонецЕсли;
Если Узел <> Неопределено Тогда
Узел.Конец = СчетчикСтрок;
КонецЕсли;
КонецЦикла;
КонецЦикла;
Если УдалятьФлаговыеКолонки Тогда
ДеревоДокумента.Колонки.Удалить("Начало");
ДеревоДокумента.Колонки.Удалить("Конец");
КонецЕсли;
Возврат ДеревоДокумента;
КонецФункции
Приведенный ниже код иллюстрирует работу с файлом типа "xlsx". Работа ведется из управляемой формы:
-
пользователь выбирает файл;
-
файл помещается в временное хранилище, и становится доступен на сервере в виде двоичных данных;
-
производится запись двоичных данных в временный файл на сервере, с проверкой: если запись временного файла занимает более 30 секунд, пользователь получит уведомление о прерывании загрузки.
// Выбор загружаемого файла, заполнение служебного реквизита формы "АдресФайлаВВременномХранилище"
//
// Возвращаемое значение:
// Булево - Истина при успешном выборе файла
//
&НаКлиенте
Функция ПроизведенУспешныйВыборФайла()
ЭтаФорма.АдресФайлаВВременномХранилище = "";
#Если ВебКлиент Тогда
Если НЕ ПодключитьРасширениеРаботыСФайлами() Тогда
УстановитьРасширениеРаботыСФайлами();
ПодключитьРасширениеРаботыСФайлами();
КонецЕсли;
ПоместитьФайл(ЭтаФорма.АдресФайлаВВременномХранилище);
#Иначе
ДиалогФыбораФайла = Новый ДиалогВыбораФайла(РежимДиалогаВыбораФайла.Открытие);
ДиалогФыбораФайла.Фильтр = "Таблица(*.xlsx)|*.xlsx";
ДиалогФыбораФайла.Заголовок = "Выберите файл";
ДиалогФыбораФайла.ПредварительныйПросмотр = Ложь;
ДиалогФыбораФайла.Расширение = "xlsx";
ДиалогФыбораФайла.МножественныйВыбор = Ложь;
Если ДиалогФыбораФайла.Выбрать() Тогда
ФайлЗагрузки = ДиалогФыбораФайла.ПолноеИмяФайла;
Иначе
Возврат Ложь;
КонецЕсли;
ЭтаФорма.АдресФайлаВВременномХранилище = ПоместитьВоВременноеХранилище(Новый ДвоичныеДанные(ДиалогФыбораФайла.ПолноеИмяФайла));
#КонецЕсли
Возврат НЕ ПустаяСтрока(ЭтаФорма.АдресФайлаВВременномХранилище);
КонецФункции
Пример использования:
//...
&НаКлиенте
Процедура ЗагрузитьФайл(Команда)
Если ПроизведенУспешныйВыборФайла() Тогда
ВыполнитьЗагрузкуФайлаНаСервере();
КонецЕсли;
КонецПроцедуры
//...
&НаСервере
Процедура ВыполнитьЗагрузкуФайлаНаСервере()
АдресВременногоФайла = ПолучитьИмяВременногоФайла("xlsx");
// Запись двоичных данных из вр. хранилища:
ДвДанные = ПолучитьИзВременногоХранилища(ЭтаФорма.АдресФайлаВВременномХранилище);
Если ЗаписьФайлаПроизошлаУспешно(ДвДанные, АдресВременногоФайла) Тогда // см. ниже "Проверка записи файла
// обработка данных файла...
Иначе
ЛОГ("загрузка прервана на сервере - файл """+ АдресВременногоФайла +""" не обнаружен...");
КонецЕсли;
КонецПроцедуры
//...
// Попытка записи данных в файл с таймаутом на время записи
//
// Параметры:
// Источник - Произвольный объект 1С, поддерживающий запись в файл через метод "Записать" - например ДвоичныеДанные или ТекстовыйДокумент
// АдресФайла - Строка
// ОграничениеВремениЗаписи - Число, количество секунд в течение которого будет выполнятся проверка существования файла
// КомандаЗаписи - Строка, для различных случаев сохранения файла, в алгоритме будет параметром метода "Выполнить"
//
// Возвращаемое значение:
// Булево
//
Функция ЗаписьФайлаПроизошлаУспешно(Источник, АдресФайла, ОграничениеВремениЗаписи = 30, КомандаЗаписи = "")
Попытка
Если ПустаяСтрока(КомандаЗаписи) Тогда
Источник.Записать(АдресФайла);
Иначе
Выполнить(КомандаЗаписи);
КонецЕсли;
ТекДата = ТекущаяДата();
ПредСекунда = 0;
Пока Истина Цикл
КолСекунд = ТекущаяДата() - ТекДата;
ТекСекунда = КолСекунд;
Если ПредСекунда < ТекСекунда Тогда
// Раз в секунду - проверка существования записываемого файла
ЗаписываемыйФайл = Новый Файл(АдресФайла);
Если ЗаписываемыйФайл.Существует() Тогда
Возврат Истина;
КонецЕсли;
ПредСекунда = ТекСекунда;
КонецЕсли;
Если КолСекунд > ОграничениеВремениЗаписи Тогда
ВызватьИсключение "время записи файла превысило "+ ОграничениеВремениЗаписи +" секунд";
КонецЕсли;
КонецЦикла;
Исключение
Лог("неудачная попытка записи файла на сервере - " + ОписаниеОшибки());
Возврат Ложь;
КонецПопытки;
КонецФункции
Пример использования:
//...
ТекстXSD = ЭтотОбъект.ПолучитьМакет("МакетXSD");
ИмяВременногоФайла = ПолучитьИмяВременногоФайла("xsd");
// Запись xsd файла с проверкой по тайм-ауту
Если НЕ ЗаписьФайлаПроизошлаУспешно(ТекстXSD, ИмяВременногоФайла, 5) Тогда
Возврат;
КонецЕсли;
// Или, если нужна запись с параметром:
Если НЕ ЗаписьФайлаПроизошлаУспешно(ТекстXSD, ИмяВременногоФайла, 5, "Источник.Записать(АдресФайла, КодировкаТекста.UTF8);") Тогда
Возврат;
КонецЕсли;
//...
// Проверка доступности переданного узла с помощью команды ping
//
// Параметры:
// АдресURL - пингуемый адрес
//
// Возвращаемое значение:
// Булево
//
Функция Пинг(АдресURL = "")
Если ПустаяСтрока(АдресURL) Тогда
АдресURL = "www.ya.ru";
КонецЕсли;
objShell = Новый COMОбъект("WScript.Shell") ;
objScriptExec = objShell.Exec("ping.exe -n 1 " + АдресURL);
strPingResults = НРег(objScriptExec.StdOut.ReadAll());
ЕстьСоединение = Найти(strPingResults, "ttl=") > 0;
Возврат ЕстьСоединение;
КонецФункции
Пример использования:
//...
ЕстьСоединениеСИнтернетом = Пинг(); // По-умолчанию пингуется www.ya.ru
//...
// Упрощенный конструктор описания типа
//
// Параметры:
// ИмяТипа - Строка, Массив элементов типа "Тип" - первый параметр конструктора ОписаниеТипов()
// п1 - Произвольный, первый параметр квалификатора
// п2 - Произвольный, второй параметр квалификатора
// п3 - Произвольный, третий параметр квалификатора
//
// Возвращаемое значение:
// ОписаниеТипов
//
Функция ОписаниеТипа(ИмяТипа, п1 = Неопределено, п2 = Неопределено, п3 = Неопределено)
Перем Результат;
Если ИмяТипа = "Строка" Тогда
п1 = ?(п1 = Неопределено, 0, п1);
Если п2 = Неопределено Тогда
п2 = ДопустимаяДлина.Переменная;
КонецЕсли;
КвалСтроки = Новый КвалификаторыСтроки(п1, п2);
Результат = Новый ОписаниеТипов(ИмяТипа,,,,КвалСтроки);
ИначеЕсли ИмяТипа = "Число" Тогда
п1 = ?(п1 = Неопределено, 0, п1);
п2 = ?(п2 = Неопределено, 0, п2);
Если п3 = Неопределено Тогда
п3 = ДопустимыйЗнак.Любой;
КонецЕсли;
КвалЧисла = Новый КвалификаторыЧисла(п1, п2, п3);
Результат = Новый ОписаниеТипов(ИмяТипа,,,КвалЧисла);
ИначеЕсли ИмяТипа = "Дата" Тогда
Если п1 = Неопределено Тогда
п1 = ЧастиДаты.ДатаВремя;
КонецЕсли;
КвалДаты = Новый КвалификаторыДаты(п1);
Результат = Новый ОписаниеТипов(ИмяТипа,,,,,КвалДаты);
Иначе
Результат = Новый ОписаниеТипов(ИмяТипа);
КонецЕсли;
Возврат Результат;
КонецФункции
Пример использования:
//...
Функция НоваяТаблицаОписанияОпераций()
тз = Новый ТаблицаЗначений;
тз.Колонки.Добавить("Сумма" , ОписаниеТипа("Число", 15, 2));
тз.Колонки.Вставить("Описание" , ОписаниеТипа("Строка", 100));
тз.Колонки.Вставить("ДатаОперации" , ОписаниеТипа("Дата"));
МассивТипов = Новый Массив();
МассивТипов.Добавить(Тип("ДокументСсылка.Встреча"));
МассивТипов.Добавить(Тип("ДокументСсылка.ЗапланированноеВзаимодействие"));
тз.Колонки.Вставить("Событие" , ОписаниеТипа(МассивТипов));
тз.Колонки.Вставить("Ответственный" , ОписаниеТипа("СправочникСсылка.Пользователи"));
Возврат тз;
КонецФункции
//...
ADODBConnection = Новый COMОбъект("ADODB.Connection");
ADODBConnection.Provider = "Microsoft.ACE.OLEDB.12.0";
ADODBConnection.Properties("Data Source").Value = СокрЛП(ЭтотОбъект.ПутьКФайлу);
ADODBConnection.Properties("Extended Properties").Value = "Excel 12.0;HDR=Yes;IMEX=1";
ADODBConnection.Open();
// Получить список имен листов:
// ADODBRecordset = Новый COMОбъект("ADODB.Recordset");
// ADODBRecordset = ADODBConnection.OpenSchema(20);
// СписокЛистов = Новый СписокЗначений;
// Пока НЕ ADODBRecordset.EOF Цикл
// ИмяЛиста = ADODBRecordset.Fields("TABLE_NAME").Value;
// Если Найти(ИмяЛиста, "_xlnm#_FilterDatabase") = 0 Тогда
// СписокЛистов.Добавить(ИмяЛиста);
// КонецЕсли;
// ADODBRecordset.MoveNext();
// КонецЦикла;
// ADODBRecordset.Close();
// СписокЛистов.СортироватьПоЗначению(НаправлениеСортировки.Убыв);
// ИмяЛиста = СписокЛистов[0].Значение;
ТекстЗапроса = "SELECT * FROM [" + ЭтотОбъект.ИмяЛиста + "$]";
ADODBRecordset = Новый COMОбъект("ADODB.Recordset");
ADODBRecordset.Open(ТекстЗапроса, ADODBConnection);
КолвоКолонокExcel = ADODBRecordset.Fields.Count;
СчетчикСтрок = 1;
Пока ADODBRecordset.EOF() = 0 Цикл
Для СчетчикКолонок = 1 ПО КолвоКолонокExcel Цикл
Поле = ADODBRecordset.Fields.Item(СчетчикКолонок - 1);
Если Поле.ActualSize = 0 Тогда// Пустое поле EXCEL.
Продолжить;
КонецЕсли;
ЗначениеЯчейки = Поле.Value;
// Обработка значения ячейки
КонецЦикла;
ADODBRecordset.MoveNext(); // Следующая строка.
СчетчикСтрок = СчетчикСтрок + 1;
КонецЦикла;
ADODBConnection.Close();
ADODBRecordset = Неопределено;
ADODBConnection = Неопределено;
Этот метод служит для решения частной задачи отображения группировок колонок в табличных документах, полученных с помощью СКД. Исходная идея вот здесь: СКД. Как объединить заголовки родительских группировок колонок в таблице, я лишь причесал код. Этот вариант не будет работать с уже объединенными ячейками. Может позже допилю, пока нет нужды. Как использовать: в СКД делаем одну группировку колонок, в которую добавлем необходимые поля. После вывода табличного документа на форму вызываем метод.
// Объединяет ячейки шапки табличного документа с повторяющимся текстом
// Служит для решения задачи отображения группировок колонок в табличных документах, полученных с помощью СКД
//
// Параметры:
// ТабДок - ТабличныйДокумент
// ВысотаШапки - Число, если не передана, высотой шапки считается высота фиксации таблицы
//
Процедура СвернутьЗаголовкиШапкиТабличногоДокумента(ТабДок, ВысотаШапки = 0)
ВысотаШапки = ?(ВысотаШапки = 0, ТабДок.ФиксацияСверху, ВысотаШапки);
НачалоШапки = ?(ТабДок.ФиксацияСлева = 0, 1, ТабДок.ФиксацияСлева);
ЭтоПервоеОбъединение = Истина;
Для СчетчикСтрок=1 По ВысотаШапки Цикл
НомерПервойКолонкиОбъединения = 0;
Для СчетчикКолонок=НачалоШапки По ТабДок.ШиринаТаблицы Цикл
ОбъединятьЯчейки = Ложь;
Ячейка = ТабДок.Область(СчетчикСтрок, СчетчикКолонок);
Если ПустаяСтрока(Ячейка.Текст) Тогда
Продолжить;
КонецЕсли;
ЯчейкаСлед = ТабДок.Область(СчетчикСтрок, СчетчикКолонок+1);
ОбъединятьЯчейки = Ячейка.Текст = ЯчейкаСлед.Текст;
Если ОбъединятьЯчейки Тогда
НомерПервойКолонкиОбъединения = ?(НомерПервойКолонкиОбъединения = 0, СчетчикКолонок, НомерПервойКолонкиОбъединения);
ИначеЕсли НомерПервойКолонкиОбъединения > 0 Тогда
ТекстЗаголовка = ТабДок.Область(СчетчикСтрок, СчетчикКолонок).Текст;
ОбъединяемаяОбласть = ТабДок.Область(СчетчикСтрок, НомерПервойКолонкиОбъединения, СчетчикСтрок, СчетчикКолонок);
ОбъединяемаяОбласть.Объединить();
ОбъединяемаяОбласть.ГоризонтальноеПоложение = ГоризонтальноеПоложение.Центр;
ОбъединяемаяОбласть.Текст = ТекстЗаголовка;
НомерПервойКолонкиОбъединения = 0;
КонецЕсли;
КонецЦикла;
КонецЦикла;
КонецПроцедуры
// Формирование идентификатора по правилам образования имен переменных 1С по входящей строке
// Пример "Статья возмещение НДС" => "СтатьяВозмещениеНДС"
//
// Параметры:
// стр - Строка - от которой необходимо получить идентификатор
//
// Возвращаемое значение:
// Строка
//
Функция ИдентификаторПоСтроке(ЗНАЧ стр)
ДопустимыеСимволы = "ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮЁQWERTYUIOPASDFGHJKLZXCVBNM" +
"йцукенгшщзхъфывапролджэячсмитьбюёqwertyuiopasdfghjklzxcvbnm_ ";
СимволыЦифр = "0987654321";
ДопустимыеСимволы = СимволыЦифр + ДопустимыеСимволы;
Пока Найти(стр, " ") > 0 Цикл
стр = СтрЗаменить(стр, " ", " ");
КонецЦикла;
ДлинаСтроки = СтрДлина(стр);
СледующаяЗаглавная = Истина;
Идентификатор = "";
Для сч=1 По ДлинаСтроки Цикл
сим = Сред(стр, сч, 1);
Если Найти(ДопустимыеСимволы, сим) = 0 Тогда
сим = "_";
КонецЕсли;
Если сч = 1 И сим = "_" Тогда
Продолжить;
КонецЕсли;
Если сим = " " Тогда
СледующаяЗаглавная = Истина;
Продолжить;
КонецЕсли;
Если СледующаяЗаглавная Тогда
СледующаяЗаглавная = Ложь;
Сим = Врег(Сим);
КонецЕсли;
ЭтоЦифра = Найти(СимволыЦифр, сим) > 0;
Если сч = 1 И ЭтоЦифра Тогда
сим = "_" + сим;
КонецЕсли;
Если ЭтоЦифра Тогда
СледующаяЗаглавная = Истина;
КонецЕсли;
Идентификатор = Идентификатор + Сим;
КонецЦикла;
Возврат Идентификатор;
КонецФункции
// Преобразование переданного идентификатора в представление
//
// Параметры:
// Идентификатор - Строка - образованная по правилам формирования идентификаторов 1С
//
// Возвращаемое значение:
// Строка - преобразованный идентификатор.
// - Первая буква всегда заглавная
// - Первое подчеркивание пропускается
// - Прочие заглавные буквы преобразуются в нижний регистр, после вставляется пробел
// - Между цифрой и буквой вставляется пробел, между подряд идущими цифрами пробелов нет
// - Несколько сплошных подчеркиваний воспринимаются как один, превращаются в пробел
// - В строке может присутствовать символ, отличный от допустимых в идентификаторе - не обрабатывается
// - Учитывается капитель (аббревиатуры) и спецсимволы в капители
//
// Примеры:
// "ЭтоДата23_05_06" -> "Это дата 23 05 06"
// "Это____ПримерС_Подчеркиваниями" -> "Это пример с подчеркиваниями"
// "это_ПримерС_ПервойНеЗаглавной" -> "Это пример с первой не заглавной"
// "Это_ПримерС_НевернымИден???тификатором*" -> "Это пример с неверным иден???тификатором*"
// "_Это_ПримерС_первымПодчеркиванием" -> "Это пример с первым подчеркиванием"
// "Это_КАПИ_ТЕЛЬ" -> "Это КАПИТЕЛЬ"
// "Это_КАПИ99ТЕЛЬ" -> "Это КАПИ 99 ТЕЛЬ"
// "Это_КАПИ??ТЕЛЬ" -> "Это КАПИ??ТЕЛЬ"
//
Функция ИдентификаторВПредставление(Идентификатор) Экспорт
Цифры = "0123456789";
Алфавит = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
ИдентификаторКОбработке = СокрЛП(Идентификатор);
Пока Найти(ИдентификаторКОбработке, "__") Цикл
ИдентификаторКОбработке = СтрЗаменить(ИдентификаторКОбработке, "__", "_");
КонецЦикла;
Пока Лев(ИдентификаторКОбработке, 1) = "_" Цикл
ИдентификаторКОбработке = Сред(ИдентификаторКОбработке, 2);
КонецЦикла;
ПредыдущийЗнакЭтоПробел = Ложь;
ПредыдущийЗнакЭтоЦифра = Ложь;
ПредыдущийЗнакЭтоЗаглавная = Ложь;
ПредыдущийЗнакЭтоСпецСимвол = Ложь;
Слово = "";
Для сч=1 По СтрДлина(ИдентификаторКОбработке) Цикл
ДобавитьПробел = Ложь;
Сим = Сред(ИдентификаторКОбработке, сч, 1);
ВрегСим = Врег(Сим);
НрегСим = Нрег(Сим);
СимВСлово = Сим;
ЭтоНижнийСлэш = сим = "_";
ЭтоЦифра = Найти(Цифры, Сим) > 0;
ЭтоБуква = Найти(Алфавит, ВрегСим) > 0;
ЭтоПрочийЗнак = НЕ (ЭтоНижнийСлэш ИЛИ ЭтоЦифра);
ЭтоЗаглавная = (Сим = ВрегСим) И (Сим <> НрегСим);
Если сч=1 Тогда
СимВСлово = ВрегСим;
ИначеЕсли ЭтоЦифра И НЕ ПредыдущийЗнакЭтоЦифра Тогда
ДобавитьПробел = Истина;
ИначеЕсли ЭтоНижнийСлэш Тогда
ДобавитьПробел = Истина;
СимВСлово = "";
ИначеЕсли ЭтоПрочийЗнак И ЭтоЗаглавная И НЕ ПредыдущийЗнакЭтоЗаглавная И НЕ ПредыдущийЗнакЭтоСпецСимвол Тогда
ДобавитьПробел = Истина;
СимВСлово = НрегСим;
КонецЕсли;
ЭтоВторойСимволКапители = ЭтоЗаглавная И ПредыдущийЗнакЭтоЗаглавная;
Если ЭтоВторойСимволКапители Тогда
ПоследнийСимвол = Прав(Слово, 1);
Слово = Сред(Слово, 1, СтрДлина(Слово)-1) + ВРЕГ(ПоследнийСимвол);
КонецЕсли;
ДобавитьПробел = ДобавитьПробел И НЕ ПредыдущийЗнакЭтоПробел;
Если ДобавитьПробел Тогда
СимВСлово = " " + СимВСлово;
КонецЕсли;
Слово = Слово + СимВСлово;
ПредыдущийЗнакЭтоПробел = СимВСлово = " ";
ПредыдущийЗнакЭтоЦифра = ЭтоЦифра;
ПредыдущийЗнакЭтоЗаглавная = ЭтоЗаглавная;
ПредыдущийЗнакЭтоСпецСимвол = НЕ (ЭтоБуква ИЛИ ЭтоНижнийСлэш ИЛИ ЭтоЦифра);
КонецЦикла;
Возврат Слово;
КонецФункции
// Формирование структуры для первой (единственной) записи результата запроса
//
// Параметры:
// РезультатЗапроса - РезультатЗапроса, источник данных
//
// Возвращаемое значение:
// Структура - Набор ключей повторяет набор колонок результат запроса, значения заполняются
// значениями первой записи выборки
//
Функция РезультатЗапросаВСтруктуру(РезультатЗапроса) Экспорт
СтруктураРезультат = Новый Структура;
Выборка = РезультатЗапроса.Выбрать();
Выборка.Следующий();
Для каждого Колонка Из РезультатЗапроса.Колонки Цикл
Если Выборка.Количество() = 0 Тогда
МассивТипов = Колонка.ТипЗначения.Типы();
ОбщегоНазначенияКлиентСервер.УдалитьЗначениеИзМассива(МассивТипов, Тип("Null"));
ОписаниеТиповБезNULL = Новый ОписаниеТипов(МассивТипов);
ЗначениеПоля = ОписаниеТиповБезNULL.ПривестиЗначение();
Иначе
ЗначениеПоля = Выборка[Колонка.Имя];
КонецЕсли;
СтруктураРезультат.Вставить(Колонка.Имя, ЗначениеПоля);
КонецЦикла;
Возврат СтруктураРезультат;
КонецФункции