COM, OLE...

Статья Семенова Дмитрия (AKA Sam13) "Ошибка V82.COMConnector на сервере 64. Решение проблемы"(Статья)

Статья Vladimir'а (AKA wowik) на infostart'e "Некоторая работа с данными через COM" (Статья)

Обработка "Перенос кассовых документов из Торговли в Бухгалтерию" (управляемые формы, практически все механизмы СОМ обмена)


Строка подключения:

Если ПодключениеКФайловойБазе Тогда
	СтрокаПодключения =  "file='" + ПутьКБазе + "'; usr='" + Пользователь + "'; pwd='" + Пароль + "';";
Иначе
	СтрокаПодключения =  "srvr='" + Сервер + "'; ref='" + БазаДанных + "'; usr='" + Пользователь + "'; pwd='" + Пароль + "';";
КонецЕсли; 

Конструктор COM соединения:

СОМ_Коннектор = Новый COMОбъект("V83.COMConnector");

Инициализация:

COM_Соединение = СОМ_Коннектор.Connect(СтрокаПодключения);

Закрытие:

COM_Соединение = Непределено;

Конструктор OLE соединения:

OLE_Объект = Новый COMОбъект("V83.Application");

Инициализация:

Результат = OLE_Объект.Connect(СтрокаПодключения) // возвращает ИСТИНА, если инициализация удалась;

Закрытие:

OLE_Объект = Непределено;

Заготовка подключения к базе через COM интерфейс (упр.формы)


Выполнение Запроса к базе 8.2 через OLE соединение

Получить можно только строковые значения

База = "c:\База\";
Пользователь = "ИмяПользователя";
Пароль = "Пароль";

Попытка
    База8 = Новый COMОбъект("V82.Application");
	СтрокаПодключения = "File=" + База + ";Usr=" + Пользователь + ";Pwd=" + Пароль;
	Соединено = База8.Connect(СтрокаПодключения);
Исключение
    Сообщить("Ошибка соединения!" + ОписаниеОшибки());
КонецПопытки;

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

КонецЕсли;

База8 = Неопределено;

Как выполнить отчет на СКД через COM и получить данные отчета? (статья Vladimir A (wowik) на Infostart'e)

    МесяцРасчета = Дата(2018,03,01);
    
    //Програмное формирование отчета СКД    
    СхемаОст = Отчеты.АнализНачисленийРаботникамОрганизаций.ПолучитьМакет("ОсновнаяСхемаКомпоновкиДанных");
    
    КомпоновщикНастроекНастройки = Новый КомпоновщикНастроекКомпоновкиДанных;
    КомпоновщикНастроекНастройки.Инициализировать(Новый ИсточникДоступныхНастроекКомпоновкиДанных(СхемаОст));
    КомпоновщикНастроекНастройки.ЗагрузитьНастройки(СхемаОст.НастройкиПоУмолчанию);
    
    КомпоновщикНастроекНастройки.Настройки.ПараметрыДанных.УстановитьЗначениеПараметра("НачалоПериода", НачалоМесяца(МесяцРасчета));
    КомпоновщикНастроекНастройки.Настройки.ПараметрыДанных.УстановитьЗначениеПараметра("КонецПериода", КонецМесяца(МесяцРасчета));
    
    КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
    МакетКомпоновкиДанных = КомпоновщикМакета.Выполнить(СхемаОст, КомпоновщикНастроекНастройки.Настройки, , , Тип("ГенераторМакетаКомпоновкиДанныхДляКоллекцииЗначений"));
    ПроцессорКомпоновкиДанных = Новый ПроцессорКомпоновкиДанных;
    ПроцессорКомпоновкиДанных.Инициализировать(МакетКомпоновкиДанных);
    ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений;
    
    ДанныеТЗ = Новый ТаблицаЗначений;
    ПроцессорВывода.УстановитьОбъект(ДанныеТЗ);
    ПроцессорВывода.Вывести(ПроцессорКомпоновкиДанных);   

    ЗП = 0;
    ПодразделениеПроизводство = Справочники.ПодразделенияОрганизаций.НайтиПоКоду("000000005");
    Для каждого СтрокаТаблицы из ДанныеТЗ Цикл
        Если не ЗначениеЗаполнено(СтрокаТаблицы.ВидРасчета) Тогда
            Продолжить;
        КонецЕсли;
        
        Если СтрокаТаблицы.ПодразделениеОрганизации = ПодразделениеПроизводство  Тогда
            ЗП = ЗП + СтрокаТаблицы.Начислено;
        КонецЕсли;
    КонецЦикла;

ЗУП - Com - соединение. Как подключиться сказано здесь

МесяцРасчета = Дата(2018,03,01);   
    
    СхемаКомпоновкиДанных = ЗУП.Отчеты.АнализНачисленийРаботникамОрганизаций.ПолучитьМакет("ОсновнаяСхемаКомпоновкиДанных");
    
    КомпоновщикНастроекНастройки = ЗУП.NewObject("КомпоновщикНастроекКомпоновкиДанных");
    ИсточникДоступныхНастроекКомпоновкиДанных =  ЗУП.NewObject("ИсточникДоступныхНастроекКомпоновкиДанных",СхемаКомпоновкиДанных);
    КомпоновщикНастроекНастройки.Инициализировать(ИсточникДоступныхНастроекКомпоновкиДанных);
    КомпоновщикНастроекНастройки.ЗагрузитьНастройки(СхемаКомпоновкиДанных.НастройкиПоУмолчанию);    
    
    
    КомпоновщикНастроекНастройки.Настройки.ПараметрыДанных.УстановитьЗначениеПараметра("НачалоПериода", НачалоМесяца(МесяцРасчета));
    КомпоновщикНастроекНастройки.Настройки.ПараметрыДанных.УстановитьЗначениеПараметра("КонецПериода", КонецМесяца(МесяцРасчета));
    
    КомпоновщикМакета = ЗУП.NewObject("КомпоновщикМакетаКомпоновкиДанных");
    МакетКомпоновкиДанных = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, КомпоновщикНастроекНастройки.Настройки,,,ЗУП.NewObject("ОписаниеТипов", "ГенераторМакетаКомпоновкиДанныхДляКоллекцииЗначений").Типы().Получить(0));
    ПроцессорКомпоновкиДанных = ЗУП.NewObject("ПроцессорКомпоновкиДанных");
    ПроцессорКомпоновкиДанных.Инициализировать(МакетКомпоновкиДанных);
    
    ПроцессорВывода = ЗУП.NewObject("ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений");
    
    ДанныеТЗ = ЗУП.NewObject("ТаблицаЗначений");
    
    ПроцессорВывода.УстановитьОбъект(ДанныеТЗ);
    ПроцессорВывода.Вывести(ПроцессорКомпоновкиДанных);     
    
    ЗП = 0;
     ПодразделениеУИД =ЗУП.XMlСтрока(ЗУП.Справочники.ПодразделенияОрганизаций.НайтиПоКоду("000000005"));
    Для каждого СтрокаТаблицы из ДанныеТЗ Цикл
        Если не ЗУП.ЗначениеЗаполнено(СтрокаТаблицы.ВидРасчета) Тогда
            Продолжить;
        КонецЕсли;
        
        Если ЗУП.XMlСтрока(СтрокаТаблицы.ПодразделениеОрганизации) = ПодразделениеУИД Тогда // сравнить элементы справочника в СОМ подключении можно только так
            ЗП = ЗП + СтрокаТаблицы.Начислено;
        КонецЕсли;
    КонецЦикла;

Передача списка параметров запросу через COM (OLE) соединение

СписокПараметров = База8.NewObject("СписокЗначений");
СписокПараметров.Добавить(Параметр1);
СписокПараметров.Добавить(Параметр2);
СписокПараметров.Добавить(Параметр3);
Запрос.Параметры.Вставить("СписокПараметров", СписокПараметров);

Передача таблицы значений запросу через COM (OLE) соединение

(пример)

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

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

Еще вариант создания таблицы значений:

Таблица = v8.NewObject("ТаблицаЗначений");
Таблица.Колонки.Добавить("Номенклатура", v8.NewObject("ОписаниеТипов", "СправочникСсылка.Номенклатура")); //
КвалификаторСтроки = v8.NewObject("КвалификаторыСтроки", "100");
ДоступныеТипы = v8.NewObject("ОписаниеТипов", "Строка", КвалификаторСтроки);
Таблица.Колонки.Добавить("КолонкаСтрока", ДоступныеТипы);

Варианты получения данных из COM объекта

Процедура ПерегрузитьТаблицуСОМвТаблицуЗначений(сом_ТЗ, ТаблицаРезультатаВрем)

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

		
		 
	
	КонецЦикла; 

КонецПроцедуры

только для простых данных!


Непроверено: ;-)

  • Варианты передачи таблицы из базы по COM:

В базе-источнике надо преобразовать ТЗ в строку.

Функция ЗначениеВСтрокуXML( Значение )
    ЗаписьXML = Новый ЗаписьXML;
    ЗаписьXML.УстановитьСтроку();
    СериализаторXDTO.ЗаписатьXML( ЗаписьXML, Значение, НазначениеТипаXML.Явное );
    
    Возврат ЗаписьXML.Закрыть();
КонецФункции
Эта функция должна выполниться в базе-источнике и её результат надо возвращать вместо таблицы значений. 

Потом в базе-приёмнике это разворачивается в таблицу значений:
Функция ЗначениеИзСтрокиXML( СтрокаXML )
    ЧтениеXML = Новый ЧтениеXML;
    ЧтениеXML.УстановитьСтроку( СтрокаXML );
    Возврат СериализаторXDTO.ПрочитатьXML( ЧтениеXML );
КонецФункции

В ТЗ можно передавать только значения примитивных типов.

ТЗРезультатСтр = Соединение.ЗначениеВСтрокуВнутр(ТЗРезультат); //Преобразуем в строку
ТЗРез = ЗначениеИзСтрокиВнутр(ТЗРезультатСтр); // Преобразуем из Строки в ТаблицаЗначений
  • Перенести ссылки из другой базы можно:
ОбъектТекущейБД = ЗначениеИзСтрокиВнутр(COMСоединение.ЗначениеВСтрокуВнутр(COMОбъект))

при условии, что и идентификатор вида объекта и сама ссылка в двух базах совпадают;

ОбъектТекущейБД = XMLЗначение(Тип("СправочникСсылка.СтатьиЗатрат"), COMСоединение.XMLСтрока(COMОбъект))

если совпадают только ссылки;

ОбъектТекущейБД = Справочники.СтатьиЗатрат.НайтиПоНаименованию(COMОбъект.Наименование)

если ни идентификатор вида объекта ни ссылка на объект не совпадают, то остается поиск по коду, наименованию и прочим полям.


Чтение листа Excel через OLE соединение

Функция ПрочитатьТабличныйДокументИзExcel(ТабличныйДокумент, ИмяФайла, НомерЛистаExcel = 1) Экспорт
	
	xLastCell = 11;
	//В 1С при работе с экселем через COM нельзя задавать константы в понятном для экселя виде (xLastCell ).
	//Но есть возможность использовать числовые параметры этих констант
	//(для каждой константы вычисляются гуглением или методом научного тыка в отладчике).
	//Конкретно xLastCell это константа экселя, которая позволяет получить последнюю заполненную ячейку в файле.
	//Порядковый номер константы 11, им и оперируем при обращении к COM объекту.
	
	ВыбФайл = Новый Файл(ИмяФайла);
	Если НЕ ВыбФайл.Существует() Тогда
		Сообщить("Файл не существует!");
		Возврат Ложь;
	КонецЕсли;
	
	Попытка
		Excel = Новый COMОбъект("Excel.Application");
		Excel.WorkBooks.Open(ИмяФайла);
		Состояние("Обработка файла Microsoft Excel...");
		ExcelЛист = Excel.Sheets(НомерЛистаExcel);
	Исключение
		Сообщить("Ошибка открытия файла");
		Попытка
			Excel.WorkBooks.Close();
		Исключение
		
		КонецПопытки; 
		Excel = 0;
		Возврат ложь;
	КонецПопытки;
	
	ActiveCell = Excel.ActiveCell.SpecialCells(xLastCell);
	RowCount = ActiveCell.Row;
	ColumnCount = ActiveCell.Column;
	Для Column = 1 По ColumnCount Цикл
		ТабличныйДокумент.Область("C" + Формат(Column, "ЧГ=")).ШиринаКолонки = ExcelЛист.Columns(Column).ColumnWidth;
	КонецЦикла;
	Для Row = 1 По RowCount Цикл
		
		Для Column = 1 По ColumnCount Цикл
			ТабличныйДокумент.Область("R" + Формат(Row, "ЧГ=") +"C" + Формат(Column, "ЧГ=")).Текст = ExcelЛист.Cells(Row,Column).Value;
		КонецЦикла;
		
	КонецЦикла;
	
	Excel.WorkBooks.Close();
	Excel = 0;
	
	Возврат Истина;
	
КонецФункции

Получение значений перечислений через внешнее соединение или Automation сервер

Системные перечисления и их элементы

Системные перечисления предназначены для определения некоторого набора предопределенных значений и используются в качестве значений параметров некоторых методов, а также в качестве значений некоторых свойств объектов 1С:Педприятия. Доступ к системным перечислениям осуществляется как к свойствам глобального контекста. Значения элементов системных перечислений указываются через точку от имени системного перечисления. Например:

ОбъектДокумент.Записать(РежимЗаписиДокумента.Проведение, РежимПроведенияДокумента.Оперативный);

В качестве значений параметров метода Записать() заданы два значения элементов системных перечислений: элемент «Проведение» системного перечисления «РежимЗаписиДокумента» и элемент «Оперативный» системного перечисления «РежимПроведенияДокумента».

И системные перечисления, и значения их элементов являются объектами, и могут быть переданы в другие приложения через COM посредством интерфейса IDispatch. При этом любое системное перечисление имеет набор свойств, каждое из которых позволяет получить соответствующее значение элемента данного системного перечисления. Элемент системного перечисления также является объектом и доступен через интерфейс IDispatch, однако ни свойств, ни методов он не имеет.

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

// Элемент системного перечисления как собственный объект 1С:Предприятия
ЭлементПеречисления = Метаданные.Справочники.Справочник1.ВидИерархии;
Сообщить(ЭлементПеречисления);

// Элемент системного перечисления, полученный через COM.
Соединитель = Новый COMОбъект("V8.COMConnector");
Соединение = Соединитель.connect("File=c:\InfoBaseDirectory");
ЭлементПеречисления = Соединение.Метаданные.Справочники.Справочник1.ВидИерархии;
// Здесь будет выведено "COMОбъект".
Сообщить(ЭлементПеречисления);
// Образцы значений элементов системного перечисления для сравнения нужно получить
// из того же COM соединения.
Образцы = Соединение.Метаданные.СвойстваОбъектов.ВидИерархии;
Если ЭлементПеречисления = Образцы.ИерархияГруппИЭлементов Тогда
    Сообщить("ИерархияГруппИЭлементов");
ИначеЕсли ЭлементПеречисления = Образцы.ИерархияЭлементов Тогда
    Сообщить("ИерархияЭлементов");
КонецЕсли;

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

Перечисления, определенные в конфигурации

В отличие от системных перечислений, определенные в конфигурации перечисления являются объектами базы данных. Элемент такого перечисления является ссылкой на объект базы данных. Например, если в конфигурации определено перечисление «Перечисление1» с элементами «ЗначениеПеречисления1» и «ЗначениеПеречисления2», а реквизит «Реквизит1» справочника «Справочник1» имеет тип «ПеречислениеСсылка.Перечисление1», то следующий код на встроенном языке 1С:Предприятия:

ЭлементПеречисления1 = Перечисления.Перечисление1.ЗначениеПеречисления1;
Сообщить(ЭлементПеречисления1);
ЭлементПеречисления2 = Справочники.Справочник1.НайтиПоКоду(1).Реквизит1;
Сообщить(ЭлементПеречисления2);
Если ЭлементПеречисления1 = ЭлементПеречисления2 Тогда
    Сообщить("Равно");
Иначе
    Сообщить("Не равно");
КонецЕсли;

выведет «Равно», если значением реквизита «Реквизит1» является «ЗначениеПеречисления1» и «Не равно» в противном случае.

Однако если значение элемента перечисления получить через внешнее соединение, то оно будет являться COM объектом, и подобное сравнение будет всегда приводить к отрицательному результату. Например, следующий код на встроенном языке 1С:Предприятия:

Соединитель = Новый COMОбъект("V8.COMConnector");
Соединение = Соединитель.connect("File=c:\InfoBaseDirectory");
ЭлементПеречисления1 = Соединение.Перечисления.Перечисление1.ЗначениеПеречисления1;
Сообщить(ЭлементПеречисления1);
ЭлементПеречисления2 = Соединение.Справочники.Справочник1.НайтиПоКоду(1).Реквизит1;
Сообщить(ЭлементПеречисления2);
Если ЭлементПеречисления1 = ЭлементПеречисления2 Тогда
    Сообщить("Равно");
Иначе
    Сообщить("Не равно");
КонецЕсли;

будет всегда выводить «Не равно». Это происходит потому, что значение элемента перечисления является объектом типа ссылка на объект базы данных. Для объектов ссылок определена операция сравнения значений, которая может работать только для «своих» объектов 1С:Предприятия. Объекты, полученные через внешнее соединение «своими» не являются, поэтому операция их сравнения является проверкой идентичности. Поскольку для объектов ссылок идентичность не поддерживается, их сравнение может дать отрицательный результат, даже если значения ссылок, заключенные в этих объектах, одинаковые.

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

Соединитель = Новый COMОбъект("V8.COMConnector");
Соединение = Соединитель.connect("File=c:\InfoBaseDirectory");
ЭлементПеречисления = Соединение.Справочники.Справочник1.НайтиПоКоду(1).Реквизит1;
ВозможныеЗначения = ЭлементПеречисления.Метаданные().ЗначенияПеречисления;
НомерЭлементаПеречисления = ВозможныеЗначения.Индекс(ВозможныеЗначения.Найти(Соединение.XMLString(ЭлементПеречисления)));
Если НомерЭлементаПеречисления = 0 Тогда
    Сообщить("ЗначениеПеречисления1");
ИначеЕсли НомерЭлементаПеречисления = 1 Тогда
    Сообщить("ЗначениеПеречисления2");
КонецЕсли;

Если тип перечисления, значение которого может содежать переменная «ЭлементПеречисления», известен заранее, то приведенный выше пример может быть записан проще:

НомерЭлементаПеречисления = Соединение.Перечисления.Перечисление1.Индекс(ЭлементПеречисления);

Значение элемента перечисления может быть приведено к строковому типу, например, при помощи следующего фрагмента кода на встроенном языке 1С:Предприятия:

Соединитель = Новый COMОбъект("V8.COMConnector");
Соединение = Соединитель.connect("File=c:\InfoBaseDirectory");
ЭлементПеречисления = Соединение.Справочники.Справочник1.НайтиПоКоду(1).Реквизит1;
СтрокаЭлементПеречисления = ЭлементПеречисления.Метаданные().Имя;
СтрокаЭлементПеречисления = СтрокаЭлементПеречисления + "." + Соединение.XMLString(ЭлементПеречисления);
Сообщить(СтрокаЭлементПеречисления);

Работа с 7.7 через OLE