📜 ⬆️ ⬇️

We program 1C on Ruby

The thick client 1C OLE control mechanism gives you full access to all functions and data. This makes it possible with the customization of 1C or its integration with external systems not to use the built-in programming language at all and, accordingly, not to limit itself to its syntax, capabilities and execution environment.

Instead, you can use any modern language that has a library for working with Win32 OLE. For example, JavaScript (Win32 OLE supports Node.JS) or Ruby (the required library is included in the set of standard language libraries).

Below will be described some practical experience with the OLE-interface on Ruby. The description does not pretend to be complete, only what is needed for simple automation or integration at the data level is selected and described: reading and writing of directories and documents, execution of queries.

What for?


1C uses a huge number of Russian enterprises, and most of them feel the need to change something in their 1C or integrate it with something. At the same time, I often want it to be quick and cheap.
')
To do this, 1C has regular features:


But here, as always, there are some "but".

First of all, I really don’t want to change the configuration, because it seems that for this it will have to be removed from support and later it will be difficult to update.

Secondly, I really do not want to learn the 1C programming language to solve a couple of particular tasks because of its uselessness and hopelessness somewhere outside 1C.

And here OLE-access helps a lot, which, as it turned out, allows the 1C client to do almost everything that a user sitting at a computer can do (and even a bit more), and this OLE-access works fine from Ruby through the standard library win32ole.

There are many songs about Ruby that can not be repeated. This is a beautiful, convenient and promising developing language with a great ecosystem, low entry threshold, a huge variety of open libraries, frameworks, products, etc.

And from it it turns out a powerful tool 1) operational small near-by-one automation on its own, 2) integration with corporate systems and external bases, 3), etc. etc. And given the power of the Ruby frameworks, take Rails though, the automation may not be small.

Running OLE-server and 1C client


To get started, you need a Windows-based computer with Ruby installed on it and a 1C thick client. Versions of Ruby and 1C are not fundamental, everything described below has been tested on different Ruby and different 1C (8.1, 8.2, 8.3). Understandably, the 1C client should have access to some 1C database, and you need to know the server address, the database name on this server, the username and password of the user in this database.

Now we can start the cmd.exe console, open the irb Ruby interpreter in it, load the win32ole library and get to work. But here comes the first subtlety: irb works in console encoding (for a standard windows console, this is encoding 866), and OLE wants the encoding of WINDOWS-1251, and errors, by the way, will produce in it. Therefore, you should switch the console encoding with the command:

chcp 1251 

The first step is to get the OLE server (by example 8.2). There is a second subtlety. "Normal" servers allow you to connect to an already open application, for example:

 server=WIN32OLE.connect 'Excel.Application' 

For some reason, it doesn't work with 1C, at least it didn't work out for me. Therefore, we will raise the OLE server, and we will open the client 1C not through our hands, but through the OLE server:

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

(The server opens the 1C client in a hidden state, in the last line we made it visible.)

Work with global context


In the server variable, we got a certain root object of the Win32OLE class. According to the documentation, it has the only Visible property and three methods: Connect , NewObject and String .

However, through it, and only through it, you can get access to the methods of the global context 1C (which in 1C itself are called directly without any objects).

Initially, these methods (and all methods) are named in Russian, and therefore they cannot be accessed directly (through a dot), but the invoke method should be used (if called with parameters) or square brackets can be used (if without parameters):

 server.invoke(''[, ...]) #  server[''] 

Example - search for an item in the counterparty directory:

 element=server[''][''].invoke('','000000001') 

It should be noted that in 8.2 almost all methods have English synonyms, whereas in 8.1 only a few. If there is an English synonym, call it as a normal method through a dot.

There is another subtlety: the methods of the “wrapped” object 1C are case-insensitive, but they can be written in capital letters, as they are described in the documentation and the 1C configurator. And if so, then it is convenient to write them in Ruby-code in the same way - with a capital letter, so you can immediately see which methods are native Ruby methods, and which are actually wrapped in WIN32OLE 1C methods.

Calling ['Directories'] gives the object Directories Manager, calling ['Contracts'] - object Reference Manager for the directory Counterparties, and invoke ('FindCode') - object ReferenceObject. All these objects are wrapped in objects of the WIN32OLE class, and this creates certain inconveniences - the usual methods for working with Ruby objects do not work, for example, you cannot view the class of the wrapped object, the list of methods, etc. You have to use specific calls, for example:

 server.String(element) #      element.Metadata.Name #   element.Metadata.FullName #  ,     uuid=server.String(element.Ref.UUID) #  UUID  element=server[''][''].GetRef(server.NewObject('UUID',uuid)) #     UUID 

In addition, during the work I had to master the collections of the main applied objects (in more detail about working with them below):


For more complex cases and to improve performance, it is sometimes necessary to write queries in the 1C query language. They are created by calling query = server.NewObject ('Query') .

This turned out to be enough to solve our problems of integration and expansion of functions of 1C. In short, this is uploading / synchronizing some directories to external systems, automatic registration of packages of related documents in 1C from external systems and receiving some summary data from 1C to external systems.

The remaining 1C funds have not yet been required, and therefore are not considered here, but they can be disassembled in the same way. The main source of information for this is the 1C configurator, where the data structures of all applied objects are painted, and there is also the help system “Syntax Assistant”, where the properties and methods of 1C objects are described.

Further detail on the application objects.

Directories


How are they arranged?

The 1C Directory may (although not necessarily) be hierarchical. Then the element has a parent, received by the call:

 parent=element[''] #  parent=element.Parent 

If the directory is hierarchical, then there are again two options: a hierarchy of elements or a hierarchy of groups and elements. Moreover, groups and elements by default are given out in a heap, and to sort them you must watch the call (returning true or false):

 element[''] #  element.IsFolder 

An element-group, in contrast to an element-element, has only standard attributes (or details, in terms of 1C). Directory may be dependent, i.e. he may have a directory owner. Then the element has an owner — the owner-reference element received by the call:

 owner=element[''] #  owner=element.Owner 

Here again subtlety. Owners-directories can be somewhat different, i.e. the 'Owner' relationship is in some sense polymorphic. So, getting the owner, it makes sense to find out its type, as shown above: owner.Metadata.FullName .

Each element of the directory has a standard attribute Code (Code), which is the numbering of elements. Usually the elements are self-numbered, but there are, as always, subtleties:


So using code as a global directory identifier is not always a good idea. To do this, it is more appropriate to use a UUID that is unique within the directory and cannot be changed.
List of standard attributes:


If the directory has a form, then the directory element can be opened in the 1C client with the element.GetForm.Open request.

Reading list

You can get directory entries by calling Select or Select (for an example, the Counterparty reference book):

 selector= server[''][''].Select(parent,owner) #  parent, owner -   while selector.Next do #  -    end 

In selector, we get the object DirectorySelection. It has the Next method, which returns true / false. If it returns true, it means that the selector now contains the next element of the selection, in the sense that you can ask for the attributes and methods of this element.
If parent is specified, we will get a sample with this parent, if owner, is with this owner. If we filter only by owner, we put nil instead of parent.

If we want to get not only direct descendants, but the entire hierarchy under the given parent, then instead of Select we write Select Hierarchically or SelectHierarchically .

These methods still have the attributes Selection and Order, but I did not understand them, and where there is a need for selection and order, I wrote Query queries (see below).

In addition to the standard attributes, the element has specific ones, their list and types should be viewed in the configurator. Types can be elementary, and can be links to 1C application objects. If details are not needed, any type can be converted to a string using the server.String method.

The attribute may be polymorphic, i.e. several types to choose from. If so, then we look at the type, as usual:

 selector[''].Metadata.FullName 

In addition to attributes, there may be so-called table parts. The tabular part can be obtained in the same way as an attribute, by name, while some strange selector is returned, and almost Array, which has a method of each, which gives to the block the objects “row of the tabular part”. (But still not Array: for example, there are no methods for collect and each_with_index.) The line has the standard attribute Line Number or LineNumber plus the attributes specified in the configurator.

Getting a single item

There are several methods: search by code, by name or by props. It turned out to be enough for me first - search by code.

Call - FindCode or FindByCode (code, isfullcode, parent, owner) . The main argument is the first, the rest are optional and necessary if the uniqueness of the code is limited by the parent or owner. If the second argument is true, then the code must include the codes of all parents of the element, separated by the symbol /.

One item is returned.

Create, write and delete

New items are created using the CreateItem (or CreateItem ) and NewGroup (or CreateFolder ) methods, both of which have no arguments. Example:

 new_element= server[''][''].CreateItem 

An empty unsaved item is returned. If the code is left blank, it will auto-fill when saved.
Values ​​are assigned to attributes by calling element ['Attribute Name'] = < attribute_value > . Attributes links should be assigned as values ​​of objects-references, for example:

 new_element['']=element.Ref 

There is a subtlety with the date attribute attribute: the date must be assigned as the string 'yyyymmddhhmmss' .

There is a subtlety with the record attribute references to the listing. It would seem that this is just a string, but it will not accept a string, you need to fully refer to the object element of the enumeration, for example:

 element['']=server[''][''][''] 

The item is recorded by calling Write or Write , with no arguments. It may return an error, for example, in case of violation of the code uniqueness rules. The error can be intercepted and processed as usual, or you can simply look through the eyes in the client, it will be shown there.
To remove items, it is necessary to call Set Delete Mark or SetDeletionMark (deletion_mark, mark_descendants) . Both arguments are boolean, the first is the actual value of the deletion flag. Accordingly, it is necessary to remove the deletion mark using the same method, but with the argument false. If the second (optional) argument is true, the elements where the called element is parent or owner are also marked.
You can delete an item completely by calling Delete or Delete . Referential integrity is not checked, so there is a risk of spoiling the base.

Documents


How are they arranged?

The document is characterized by a number and date. The uniqueness of the number is checked within the period of numbering (it may be a year, or maybe a day, or it may be unique without a period, within the base for documents of this type).

To identify the document, you can use the UUID similar to the directory elements. The number plus date is not suitable for this, since they can be changed and, worse, they can suddenly change themselves after editing certain attributes of documents.

The document, similar to the directory element, has standard and specific attributes, may have tabular parts. Work with them is completely similar.

Standard document attributes:


Similar to the directory element, you can open the document form in the client: document.GetForm.Open .
The document can be posted and distributed; this is done when the document is saved with the following calls, respectively:

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


Reading list

Receiving documents of this type is similar to receiving directory elements — by calling Select or Select (for example, Buyer's Order documents):

 doc_selector= server[''][''].Select(from, to) while doc_selector.Next do #  -    end 

Similarly, the directories after each call to Next in the doc_selector object become available the following item. You can specify the date range of documents with optional arguments from and to in the format of the string 'yyyymmddhhmmss'.

Receive a separate document

You can search by number or by props. It turned out to be enough for me first - search by number. Call - Find By Number or FindByNumber (number, interval_date) .

The optional argument interval_date (format - the string 'yyyymmddhhmmss') is needed to set the interval of unique numbers, write any date that falls within the desired interval.

Create, write and delete

Everything is very similar to directories.

The document is created by calling CreateDocument or CreateDocument , while getting a new empty document not stored in the database. Auto-numbering and auto-assignment of the date occurs when saving.

Write the item by calling Write or Write with two optional arguments. The first is the recording mode (with or without holding, see above), possible values:

 server.DocumentWriteMode.Posting # (/ ) server.DocumentWriteMode.UndoPosting # (/ ) 

The second - the mode of the document, the possible values:

 server.DocumentPostingMode.Regular # () server.DocumentPostingMode.RealTime # () 

Deleting documents is similar to deleting directory entries.

Deletion mark - Set Deletion mark or SetDeletionMark (deletion_mark) . The Boolean argument is the value of the deletion flag. Complete deletion of a document — by calling Delete or Delete (this does not check referential integrity).

Requests


Requests to the 1C database are written in its SQL-like query language, where various 1C objects appear as entities and attributes.
On the one hand, it is convenient to use 1C queries in the sense that it is possible to more accurately select and aggregate the necessary data and at the same time avoid solving the subtleties of working from ruby ​​with new (not yet mastered) object types. And yet - examples of requests from the documentation or the Internet can be used as is one-on-one, because the request body is just a string. And code samples must also be translated into Ruby, and this is not always trivial.

For example, when I needed balances in warehouses that are stored in Accumulation Registers. Products in Stores, it turned out to be much easier to find a sample request than to disassemble a new type of objects.

On the other hand, in “Syntax Helper” there is no description of the query language, and you need to have 1C documentation on hand.

So, a new request is created by calling:

 query=server.NewObject('Query') 

Next, enter the text of the request, for example:

 query.Text= ' .  .    .=&  .=&' 

Here & Nomenclature and & CH - query parameters, and they need to assign values, the first - the directory element, the second - the line:

 nom=server[''][''].FindByCode('0001234567') query.SetParameter('',nom) query.SetParameter('','12345678') 

Next we have to execute the request ( Execute ), and the result of the request Unload :

 result=query.Execute.Unload 

The result has the method Number or Count - the number of rows found. We get separate records from the result using the Get or Get method by the line number (starting from 0), and a separate record attribute - again by the Get or Get method by the number of the attribute in the request, starting from 0.

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

Important: if in the SELECT section we requested an object, then in the corresponding attribute we will get the whole object. In the example, we have an array of directory elements of the Directory type reference list Reference. And if we in the SELECT indicated a Series. Name, then instead of an object we would receive the corresponding line.

Thank you for your attention.

Source: https://habr.com/ru/post/271835/


All Articles