Механизм управления «толстым клиентом» 1С по OLE дает полный доступ ко всем функциям и данным. Это дает возможность при кастомизации 1С или ее интеграции с внешними системами вообще не использовать встроенный язык программирования и, соответственно, не ограничивать себя его синтаксисом, возможностями и средой исполнения.

Вместо этого можно воспользоваться любым современным языком, имеющим библиотеку для работы с Win32 OLE. Например, JavaScript (Win32 OLE поддерживает Node.JS) или Ruby (нужная библиотека входит в набор стандартных библиотек языка).

Ниже будет описан некоторый практический опыт работы с OLE-интерфейсом на Ruby. Описание не претендует на полноту, отобрано и описано только то, что нужно для простой автоматизации или интеграции на уровне данных: чтение-запись справочников и документов, выполнение запросов.

Зачем?


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

Для этого в 1С есть штатные возможности:

  • Изменение структуры данных системы путем изменения ее конфигурации с помощью конфигуратора.
  • Изменение логики работы системы через программные модули в составе конфигурации или внешних «обработок».
  • Взаимодействие с «толстым клиентом» системы на рабочем месте пользователя через интерфейс OLE или COM.

Но здесь, как всегда, есть некоторые «но».

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

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

И тут очень выручает OLE-доступ, который, как выяснилось, позволяет сделать с клиентом 1С практически все, что в нем может сделать пользователь, сидящий за компьютером (и даже немного больше), и этот OLE-доступ прекрасно работает из Ruby через стандартную библиотеку win32ole.

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

И из него получается могучий инструмент 1) оперативной малой около1с-ной автоматизации своими силами, 2) интеграции с корпоративными системами и внешними базами, 3) и т.д. и т.п. А учитывая мощь фреймворков Ruby, взять хоть Rails, то автоматизация может быть и не малой.

Запуск OLE-сервера и клиента 1С


Для начала работы нужен Windows-компьютер с установленными на нем Ruby и толстым клиентом 1С. Версии Ruby и 1С не принципиальны, все нижесказанное было опробовано на разных Ruby и разных 1С (8.1, 8.2, 8.3). Понятное дело, клиент 1С должен иметь доступ к какой-нибудь базе 1С, и надо знать адрес сервера, имя базы на этом сервере, логин и пароль пользователя в этой базе.

Теперь мы можем запустить консоль cmd.exe, открыть в ней Ruby-интерпретатор irb, подгрузить библиотеку win32ole и приступить к работе. Но тут возникает первая тонкость: irb работает в кодировке консоли (для стандартной windows-консоли это кодировка 866), а OLE захочет кодировку WINDOWS-1251, и ошибки, кстати, будет выдавать в ней же. Поэтому стоит переключить кодировку консоли командой:

chcp 1251

Первый шаг – получение OLE-сервера (на примере 8.2). Тут есть вторая тонкость. «Обычные» сервера позволяют подключиться к уже открытому приложению, например:

server=WIN32OLE.connect 'Excel.Application'

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

server=WIN32OLE.new "V82.Application"
server.Connect("Srvr=\”myserver\”;Ref=\”mybase\”;Usr=\"me\";Pwd=\"mypassword\"")
server.Visible=true

(Сервер открывает клиента 1С в скрытом состоянии, в последней строке мы сделали его видимым.)

Работа с глобальным контекстом


В переменную server мы получили некий корневой объект класса Win32OLE. По документации он имеет единственное свойство Visible и три метода: Сonnect, NewObject и String.

Однако через него, и только через него, можно получить доступ к методам глобального контекста 1С (которые в самом 1С вызываются напрямую без всяких объектов).

Изначально эти методы (и все методы) именуются по-русски, и поэтому к ним нельзя обратиться напрямую (через точку), а надо использовать метод invoke (если вызов с параметрами) либо можно квадратные скобки (если без параметров):

server.invoke('ИмяМетодаНаРусскомЯзыке'[,параметры ...])
#    или
server['ИмяМетодаНаРусскомЯзыке']

Пример – поиск элемента в справочнике контрагентов:

element=server['Справочники']['Контрагенты'].invoke('НайтиПоКоду','000000001')

Надо заметить, что в 8.2 почти все методы имеют английские синонимы, тогда как в 8.1 – лишь некоторые. Если есть английский синоним, вызываем его как обычный метод через точку.

Тут еще тонкость: методы «завернутого» объекта 1С нечувствительны к регистру букв, но допускают написание с заглавными буквами, как они описаны в документации и конфигураторе 1С. А если так, то удобно писать их в Ruby-коде так же — с большой буквы, так что сразу видно, какие методы – родные методы Ruby, а какие – на самом деле завернутые в WIN32OLE методы 1С.

Вызов ['Справочники'] дает объект СправочникиМенеджер, вызов ['Контрагенты'] – объект СправочникМенеджер для справочника Контрагенты, а вызов invoke('НайтиПоКоду') – объект СправочникОбъект. Все эти объекты завернуты в объекты класса WIN32OLE, и это создает определенные неудобства – не работают обычные приемы работы с объектами Ruby, например, нельзя посмотреть класс завернутого объекта, список методов и т.п. Приходится использовать специфические вызовы, например:

server.String(element) # посмотреть строковое представление завернутого объекта
element.Metadata.Name # посмотреть тип
element.Metadata.FullName # посмотреть тип, включающий типы родительских объектов
uuid=server.String(element.Ref.UUID) # получить UUID элемента
element=server['Справочники']['Контрагенты'].GetRef(server.NewObject('UUID',uuid)) # получить обратно элемент по UUID

Кроме этого, в ходе работы пришлось освоить коллекции основных прикладных объектов (подробно о работе с ними ниже):

  • server['Справочники'] или server.Catalogs,
  • server['Документы'] или server.Documents.

Для более сложных случаев и для повышения производительности иногда надо писать запросы на языке запросов 1С. Они создаются вызовом query=server.NewObject('Запрос').

Перечисленного оказалось достаточно для решения наших задач по интеграции и расширению функций 1С. Если коротко, это выгрузка/синхронизация некоторых справочников во внешние системы, автоматическое оформление пакетов связанных документов в 1С из внешних систем и получение из 1С во внешние системы некоторых сводных данных.

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

Далее подробно по прикладным объектам.

Справочники


Как устроены?

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

parent=element['Родитель'] # или parent=element.Parent

Если справочник иерархический, то тут опять два варианта: иерархия элементов либо иерархия групп и элементов. Причем группы и элементы по умолчанию выдаются кучей, и для их сортировки надо смотреть вызов (возвращающий true или false):

element['ЭтоГруппа'] # или element.IsFolder

Элемент-группа в отличие от элемента-элемента имеет только стандартные атрибуты (или реквизиты, в терминах 1С). Справочник может быть зависимым, т.е. у него может быть справочник-владелец. Тогда у элемента есть владелец – элемент справочника-владельца, получаемый вызовом:

owner=element['Владелец'] # или owner=element.Owner

Тут опять тонкость. Справочников-владельцев может быть несколько разных, т.е. связь 'Владелец' в каком-то смысле полиморфна. Так что, получив владельца, есть смысл еще и узнать его тип, как было показано выше: owner.Metadata.FullName.

Каждый элемент справочника имеет стандартный атрибут Код (Code), в котором ведется нумерация элементов. Обычно элементы автонумеруются, но есть, как всегда, тонкости:

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

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

  • ЭтоГруппа, IsFolder – имеет смысл только для справочников с иерархией группа-элемент,
  • Код, Code – есть всегда,
  • Наименование, Description – есть всегда,
  • Родитель, Parent – имеет смысл только для иерархических справочнико, в
  • Владелец, Owner — имеет смысл только для зависимых справочников,
  • ПометкаУдаления – есть всегда,
  • Ссылка, Ref – есть всегда, возвращает объект СправочникСсылка. Обратно получить из ссылки элемент можно методом ПолучитьОбъект (GetObject). Элемент имеет тип СправочникОбъект. Зачем их разделили, не очень понятно, но всегда можно определить методом научного тыка, что нужно в каждом конкретном случае: если оно не хочет объект, попробуем вместо него ссылку и наоборот.
  • Предопределенный, Predefined – не понадобилось, не пользовался.

Если у справочника есть форма, то элемент справочника можно открыть в клиенте 1С запросом element.GetForm.Open.

Чтение списком

Получить элементы справочника можно вызовом Выбрать или Select (для примера – справочник Контрагенты):

selector= server['Справочники']['Контрагенты'].Select(parent,owner) # аргументы parent, owner - не обязательны
while selector.Next do
    # делаем что-то с текущим элементом
end

В selector мы получаем объект СправочникВыборка. Он имеет метод Следующий (Next), который возвращает true/false. Если вернул true, это значит, что selector теперь содержит очередной элемент выборки, в том смысле, что с него можно спросить атрибуты и методы этого элемента.
Если задан parent, получим выборку с данным родителем, если owner, — c данным владельцем. Если фильтруем только по владельцу, вместо parent ставим nil.

Если хотим получить не только прямых потомков, а всю иерархию под данным родителем, то вместо Select пишем ВыбратьИерархически или SelectHierarchically.

У этих методов есть еще атрибуты Отбор и Порядок, но я с ними не разбирался, а там, где надо отбор и порядок, писал запросы Query (см. ниже).

Помимо стандартных атрибутов элемент имеет специфические, их список и типы нужно смотреть в конфигураторе. Типы могут быть элементарными, а могут быть ссылками на прикладные объекты 1С. Если не нужны подробности, любой тип можно преобразовать в строку методом server.String.

Атрибут может быть полиморфным, т.е. нескольких типов на выбор. Если так, то смотрим тип, как обычно:

selector['ПолиморфныйАтрибут'].Metadata.FullName

Кроме атрибутов, могут быть так называемые табличные части. Табличную часть можно получить так же, как атрибут, по имени, при этом возвращается, о чудо, не какой-то там странный селектор, а родной рубиевский Array, состоящий из объектов «строка табличной части». Строка имеет стандартный атрибут Номер или Number плюс атрибуты, заданные в конфигураторе.

Получение отдельного элемента

Есть несколько методов: поиск по коду, по наименованию или по реквизиту. Мне оказалось достаточно первого – поиска по коду.

Вызов — НайтиПоКоду или FindByCode(code, isfullcode, parent, owner). Основной аргумент – первый, остальные не обязательны и нужны, если уникальность кода ограничена родителем или владельцем. Если второй аргумент true, то в код нужно включить коды всех родителей элемента, разделенные символом /.

Возвращается один элемент.

Создание, запись и удаление

Новые элементы создаются методами СоздатьЭлемент (или CreateItem) и СоздатьГруппу (или CreateGroup), оба не имеют аргументов. Пример:

new_element= server['Справочники']['Контрагенты'].CreateItem

Возвращается пустой несохраненный элемент. Если код оставить незаполненным, он автозаполнится при сохранении.
Значения атрибутам присваиваются вызовом element[‘ИмяАтрибута’]=<значение_атрибута>. Атрибутам ссылкам нужно присваивать в качестве значений объекты-ссылки, например:

new_element['Родитель']=element.Ref

Есть тонкость с записью атрибута-даты: дату нужно присваивать в виде строки 'yyyymmddhhmmss'.

Есть тонкость с записью атрибута-ссылки на перечисление. Казалось бы, это просто строка, но строку оно не примет, нужно полностью сослаться на объект-элемент перечисления, например:

element['ВидЗаказа']=server['Перечисления']['ВидыВнутреннегоЗаказа']['НаСклад']

Записывается элемент вызовом Записать или Write, без аргументов. Может вернуть ошибку, например, при нарушении правил уникальности кода. Ошибку можно перехватить и обработать как обычно, а можно просто посмотреть глазами в клиенте, ее там покажут.
Удалять элементы полагается вызовом УстановитьПометкуУдаления или SetDeletionMark(deletion_mark,mark_descendants). Оба аргумента булевы, первый – собственно значение пометки удаления. Соответственно, снимать пометку удаления надо этим же методом, но с аргументом false. Если второй (не обязательный) аргумент true, пометятся также элементы, где вызываемый элемент родитель или владелец.
Можно удалить элемент полностью вызовом Удалить или Delete. Ссылочная целостность при этом не проверяется, так что есть риск попортить базу.

Документы


Как устроены?

Документ характеризуется номером и датой. Уникальность номера проверяется в пределах периода нумерации (может быть год, а может быть и день, а может быть уникальность без периода, в пределах базы для документов данного типа).

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

Документ, аналогично элементу справочника, имеет стандартные и специфические атрибуты, может иметь табличные части. Работа с ними полностью аналогична.

Стандартные атрибуты документа:

  • Номер, Number – есть всегда,
  • Дата, Date – есть всегда, имеет тип дата-врем, я
  • ПометкаУдаления, DeletionMark – есть всегда,
  • Ссылка, Ref – есть всегда, смысл полностью аналогичен ссылке на элемент справочника,
  • Проведен, Posted – есть всегда.

Аналогично элементу справочника, можно открыть форму документа в клиенте: document.GetForm.Open.
Документ можно провести и распровести, это делается при сохранении документа следующими вызовами, соответственно:

document.Write(server.DocumentWriteMode.Posting)
document.Write(server.DocumentWriteMode.UndoPosting)


Чтение списком

Получение документов данного типа аналогично получению элементов справочника — вызовом Выбрать или Select (для примера – документы ЗаказПокупателя):

doc_selector= server['Документы']['ЗаказПокупателя'].Select(from, to)
while doc_selector.Next do
    # делаем что-то с текущим документом
end

Аналогично справочникам после каждого вызова Next в объекте doc_selector становится доступен следующий элемент. Можно задать диапазон дат документов необязательными аргументами from и to в формате строки 'yyyymmddhhmmss'.

Получение отдельного документа

Можно искать по номеру или по реквизиту. Мне оказалось достаточно первого – поиска по номеру. Вызов — НайтиПоНомеру или FindByNumber(number, interval_date).

Необязательный аргумент interval_date (формат — строка 'yyyymmddhhmmss') нужен, чтобы задать интервал уникальности номера, пишется любая дата, попадающая в нужный интервал.

Создание, запись и удаление

Все очень похоже на справочники.

Документ создается вызовом СоздатьДокумент или CreateDocument, при этом получаем новый пустой документ, не сохраненный в базе. Автонумерация и автоприсвоение даты происходит при сохранении.

Записывается элемент вызовом Записать или Write с двумя необязательными аргументами. Первый – режим записи (с проведением или без проведения, см. выше), возможные значения:

server.DocumentWriteMode.Posting # (провести/оставить проведенным)
server.DocumentWriteMode.UndoPosting # (распровести/не проводить)

Второй – режим проведения документа, возможные значения:

server.DocumentPostingMode.Regular # (неоперативный)
server.DocumentPostingMode.RealTime # (оперативный)

Удаление документов аналогично удалению элементов справочника.

Пометка удаления — УстановитьПометкуУдаления или SetDeletionMark(deletion_mark). Аргумент булев — значение пометки удаления. Полное удаление документа — вызовом Удалить или Delete (ссылочная целостность при этом не проверяется).

Запросы


Запросы к базе 1С пишутся на своем SQL-подобном языке запросов, где в качестве сущностей и атрибутов фигурируют разнообразные объекты 1С.
Пользоваться запросами 1С, с одной стороны, удобно в том смысле, что можно точнее отобрать и агрегировать нужные данные и одновременно избежать разгадывания тонкостей работы из ruby с новыми (еще не освоенными) типами объектов. И еще — примеры запросов из документации или интернета можно использовать как есть один в один, ибо тело запроса – просто строка. А примеры кода надо еще переводить на Ruby, и это не всегда тривиально.

Например, когда мне понадобились остатки на складах, которые лежат в РегистрыНакопления.ТоварыНаСкладах, оказалось гораздо проще найти образец запроса, чем разбирать новый тип объектов.

С другой стороны, в «Синтакс-Помощнике» нет описания языка запросов, и надо иметь под рукой документацию 1С.

Итак, новый запрос создается вызовом:

query=server.NewObject('Query')

Далее вводим текст запроса, например:

query.Text= 'ВЫБРАТЬ Серия.Ссылка ИЗ Справочник.СерииНоменклатуры КАК Серия ГДЕ Серия.Владелец=&Номенклатура И Серия.СерийныйНомер=&СН'

Здесь &Номенклатура и &СН – параметры запроса, и им надо присвоить значения, первому – элемент справочника, второму – строку:

nom=server['Справочники']['Номенклатура'].FindByCode('0001234567')
query.SetParameter('Номенклатура',nom)
query.SetParameter('СН','12345678')

Дальше мы должны запрос Выполнить (Execute), а результат запроса Выгрузить (Unload):

result=query.Execute.Unload

У результата есть метод Количество или Count – количество найденных строк. Отдельные записи из результата мы получаем методом Получить или Get по номеру строки (начиная с 0), а отдельный атрибут записи – опять же методом Получить или Get по номеру атрибута в запросе, начиная с 0.

sers=(0..result.Count).collect do |i|
    record=result.Get(i)
    record.Get(0)
end

Важно: если в разделе ВЫБРАТЬ мы запросили объект, то в соответствующем атрибуте мы и получим объект целиком. В примере у нас получился массив элементов справочника СерииНоменклатуры типа СправочникСсылка. А если бы мы в ВЫБРАТЬ указали Серия.Наименование, то вместо объекта получили бы соответствующую строку.

Cпасибо за внимание.

Комментарии (8)


  1. nemilya
    27.11.2015 16:09
    +1

    Алексей, приветствую!
    Спасибо за подробную статью! Направление интересное, и думаю можно будет со временем добавить примеры из жизни.

    Были бы так же интересны примеры на простые сценарии:
    — получение элемента
    — получение списка
    — добавление/редактирование/удаление элемента

    Или пример решения несложной задачи — отображение в вебе справочников, и выложить это в github.

    Просто это некоторый другой подход к 1c системе — работа с ней без каких-либо изменений в конфигурации.


    1. 27.11.2015 23:41

      Иногда проще сделать связку «внешняя обработка в 1с со всеми нужными методами, вшитая в базу» — вызовы из своей среды программирования. И думаю, что разработка окажется менее затратной в человеко-часах.


  1. alexey-lustin
    27.11.2015 17:04
    +1

    Очень опасное направление — уже переживали это.

    Например тут github.com/dancingbytes/s41c

    В качестве экспериментов классное, но в итоге вы придете к двум проблемам:

    1. OLE 1С будет периодически оставаться в памяти, даже после перезапуска процесса Ruby. Это связано с особенностями как самой 1С в режиме OLE, так и с особенностями gem win32ole. Единственный способ победить такое поведение — COM+ сервер infostart.ru/public/93643

    2. В 1C мире уже вовсю шагает OData — поэтому правильный путь следующий: использовать gem подобный этому github.com/visoft/ruby_odata и подглядывать вот сюда github.com/oknosoft/metadata.js, за идеями


    1. asiventsev
      27.11.2015 20:35
      +1

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

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


  1. Sirion
    27.11.2015 19:11

    Win32 OLE поддерживает Node.JS
    Вы про вот этот мёртвый проект, который у меня отказался работать? Если нет, скажите, пожалуйста, о чём вы. А то я с горя уже свою либу для работы с xlsx накидал.


    1. asiventsev
      27.11.2015 20:48

      Да, имелся в виду этот пакет. Работать с ним я еще не пробовал, а упомянул исходя из того, что он есть. Насколько он рабочий и что можно сделать с его использованием, надеюсь посмотреть в ближайшем будущем.


  1. leemuar
    28.11.2015 00:36

    Спасибо за статью! Прекрасный пример антипаттерна Golden Hammer


  1. TreyLav
    28.11.2015 11:18

    Все отлично пока не сменится тот, кто эту систему обслуживает. Человек, приходящий поддерживать 1С может не ожидать «вкраплений» на других языках. Я надеюсь, что Вы хорошо все документируете и Ваше руководство понимает, какой квалификации следует требовать от Ваших «наследников».
    Именно поэтому эта тема очень грустна: концепция 1С как языка явно не попала в цель, но, увы, промышленный стандарт.
    Простите за стиль Капитана Очевидность, наболело.