
When designing IT systems, software architects often have to ensure a single item of goods can use several units of measurement.
What are possible scenarios here? Imagine we have a food packaging business dealing with grain. We buy goods from suppliers, put them in convenient packaging, and deliver packages to retail chains. Naturally, suppliers calculate weight in tons, while we want to deal with kilograms. No doubt, we need to decide which units of measurement to use. Should it be tons, kilograms, or something else?
Here I would like to offer one possible solution to the task.
Start with creating a 1C configuration. As we work with goods, we cannot go without a catalog. Let's create it.
At this stage "Products" catalog does not need any attributes.
As we have an entity called "Unit of Measurement," it is obvious we need a catalog to store it. Time to create the second catalog called "UOMClassifier."
This catalog keeps standard units of measurement, such as kilogram, ton, liter, unit, and others.
Our next step is to link "Products" and "UOMClassifier" catalogs. Here we need some accounting theory before we can proceed.
So we have an item of goods that can use several units of measurement, like tons and kilograms. It is evident that we cannot use both units at a time.
To be honest, we can actually record stock balance in various units, but this will make the software more complicated. And one of the primary architect's tasks is to make architecture as plain as possible, or in order words, "normalize" the data storage process. It means that nobody will use several units of measurement for a single item of goods in real life.
Thus, each item requires a single base unit of measurement. That is, we want to use this one unit of measurement to calculate the balance. In case we need to apply other units in sale or purchase transactions, the application automatically calculates the value through conversion ratio.
Add "MeasurementUnit" attribute, type "CatalogRef.UOMClassifier" to "Products" catalog:
The unit of measurement specified in the attribute is considered a base unit for balances (records in registers).
And this brings one important thing to our attention. If the application already contains balances (i.e., registers have records on the amount of the goods), users should be unable to change units of measurement.
Example: We have a 150-meter paper roll in our warehouse. Our employee opens the "Products" catalog and changes units of measurement from meters to kilometers. At that numerical value remains the same. As a result, the system report 150-kilometer (!) paper roll in stock. You can imagine such change will not make the company management happy.
So the golden rule is to prevent any changes to the item's base unit. An employee can assign a unit of measurement only when adding a new item of goods. After that, the unit of measurement cannot be changed. Please note that 1C platform does not perform such checks, and it is the developer's responsibility to add applicable code to the application.
Are we done yet? Not really. Let's take a moment and consider this. We want to use a classifier (catalog) for units of measurement, but these can be very different. Among them are kilograms, kilometers, miles, and pounds, to name a few. Add to it boxes and crates that units of measurement as well. Now imagine employees working on invoices having to scroll through this insane list of units. How happy and efficient will they be? What are the chances of quickly getting an error-free document?
You guessed it. Picking up units of measurement every time a new document is created will obviously slow down the work process and is likely to cause multiple errors. What can we do about it? One of the possible options is to introduce a third entity that can build a relationship between "Products" and "UOMClassifier" catalogs. There are several ways to implement it. Thus, we could utilize an information register for the task. But in our case, it is sufficient to use another catalog as the third entity.
Create "UOM" catalog and add "Ratio" attribute, with type "Number," and "UOMClassifier" attribute, with type "CatalogRef.UOMClassifier":
It is time for a very important thing: assigning an owner for the newly created catalog. The owner here should be "Products" catalog:
This makes it possible to establish a one-to-many relationship that means one item can have several units of measurement. And with the "Ratio" attribute, we are able to convert units.
We want to use the "UOM" catalog to store optional units of measurement, while the item base unit is defined directly in the "Products" catalog ("MeasurementUnit" attribute).
Let me create a very basic "SupplierInvoice" document to demonstrate the concept of units of measurement processing. Create the document within our configuration and add the "Inventory" tabular section to it. Now we need to add two attributes that are "Product" and "Quantity":
In case we choose "pieces" as a base unit (the unit to be used in the database), and a supplier delivers goods in boxes, 10 pieces each, the "SupplierInvoice" should state the number of boxes, not pieces.
At that, the balance register should record the quantity in pieces. To do it, we need one more attribute called "MeasurementUnit."
Its role is to store units of measurement for a particular type of document to make possible the use of proper units of measurement when posting the document.
Thus if our base unit of measurement is set to pieces (database uses this unit for balances), and "SupplierInvoice" specifies 10 pieces, the database records it as 10. But in case the document unit of measurement is set to "boxes" with a "Ratio" of 5 (i.e., 5 pieces per box) and declared quantity is 10, the database records it as 50 pieces (5 x 10).
This is how the "MeasurementUnit" attribute works. But what type should we assign to this attribute? As you remember, we have two catalogs: "UOMClassifier" as a unified classifier for units of measurement and "UOM" as a place to store units of measurement for each item of goods. Again. What type should we assign to the "MeasurementUnit" attribute?
The answer is that we need to use composite data type. This enables the "MeasurementUnit" attribute to store values of both catalogs: "UOMClassifier" and "UOM":
What is the benefit of such an approach? Imagine we have an item of goods calculated in kg. The attribute applied to such an item is the "UOMClassifier" catalog. In order to add the abovementioned item of goods into our document, the "MeasurementUnit" should be attributed to the respective catalog.
Next, in case we want to use an additional unit of measurement, say gram, we need the "UOM" catalog. If we wish to specify the amount of the goods in grams, "MeasurementUnit" should be attributed to the respective catalog type. Now it is getting obvious that the "MeasurementUnit" attribute for the "SupplierInvoice" document should be of the composite type.
And final touch. As the "UOM" catalog is subordinate to the "Products" catalog, when choosing the "UOM" catalog, users should be able to see only units of measurement relating to a particular item of goods. How do we arrange for it? One of the possible options is to create handlers for relevant events. Still, there is an easier way.
Use the "Choice parameters links" property in the "MeasurementUnit" attribute:
By defining the "Choice parameters links" property, we establish a connection between goods and units of measurement. Now users can see only units of measurement relating to a particular item of goods.
Basically, this is all that has to do with the solution architecture, and we are almost ready to proceed with coding.
Start the application and fill in the catalogs. Run configuration, go to the "UOMClassifier" catalog, and fill it in:
Select the "Products" catalog and add a single item of goods. Choose kilogram as a base unit of measurement:
Add gram and ton as extra units of measurement for this item of goods.
Make sure "Ratio" and "UOMClassifier" values are correct.
Go back to Designer and create the "SupplierInvoice" form:
Now we need to write a code that can pick data for the "MeasurementUnit" attribute with the account of an item of goods selected. For this purpose you can use the "OnChange" event for the "Product" attribute in the tabular section of the document:
The event can be processed with the following code:
&AtClient Procedure InventoryProductOnChange(Item)
TabularSectionRow = Items.Inventory.CurrentData;
TabularSectionRow.MeasurementUnit = ReturnDefaultMeasurementUnit(TabularSectionRow.Product);
EndProcedure
&AtServer Function ReturnDefaultMeasurementUnit(Product)
Return Product.MeasurementUnit;
EndFunction |
The idea is simple. When an item of goods in the document's tabular section is changed, the "MeasurementUnit" attribute gets a default unit of measurement for a particular item.
Time to check how it works. Start 1C in dialog mode, create a new "SupplierInvoice" document, and add an item of goods to the document. The application automatically adds a unit of measurement:
Great! We are halfway through. If we try to select another unit of measurement, the application requests data type and only then show the list of available units:
errors. So our next step is to create a code to allow users to select units of measurement with comfort.
Go back to Designer and create a handler in the "SupplierInvoice" form for the "OnChange" event in the document tabular section:
Here is the code.
&AtClient Procedure InventoryOnChange(Item)
TabularSectionRow = Items.Inventory.CurrentData;
If Not TrimAll(TabularSectionRow.Product) = "" Then listChoiceList = ReturnChoiceList(TabularSectionRow.Product); Item.ChildItems.InventoryMeasurementUnit.ChoiceList.LoadValues(listChoiceList); EndIf;
EndProcedure
&AtServer Function ReturnChoiceList(Product)
arrayChoiceList = New Array;
arrayChoiceList.Add(Product.MeasurementUnit);
query = New Query; query.Text = "SELECT | UOM.Ref AS Ref |FROM | Catalog.UOM AS UOM |WHERE | UOM.Owner = &Owner";
query.SetParameter("Owner", Product);
selection = query.Execute().Select();
While selection.Next() Do arrayChoiceList.Add(selection.Ref); EndDo;
Return arrayChoiceList;
EndFunction |
Time to check if the code runs properly. Start 1C, create a new "SupplierInvoice" document, add an item of goods and try picking a unit of measurement:
You automatically get a list of applicable units of measurement and can pick the one you need. This is how the final document might look like:
In means, the supplier has provided 1500 grams of goods. But we have agreed that a kilogram is a base unit of measurement. Thus, the database (balance register) should contain 1.5. There is no need to add a kilogram as a unit of measurement as it is linked to the item of goods and remains unchanged throughout the lifespan of the goods.
And now we are getting to the most exciting part of this article: writing document data in the register.
Let us create "Inventory" accumulation register. This is a place for our balances. Consequently, the register type is "Balances":
Now create "Products" dimension with "CatalogRef.Products" type:
and "Quantity" resource that we need to store data on the quantity of the goods:
We also need to include a recorder in our register. By recorders we mean documents that initiate records in a register.
Now it is time to create a code for the "SupplierInvoice" document. This code should write data into the register we have just created. Open our document in Designer, go to the "Records" tab and run "Record wizard":
You get the wizard form with register record type already set to "Receipt." Select the tabular section:
After that, you can click the "Fill Expressions" button or double click all required attributes in the "Document attributes" to the right to fill the lower form in:
Click "OK." The wizard creates the code and opens it for review. It should look something like this:
Procedure Posting(Cancel, Mode) //{{__REGISTER_REGISTERRECORDS_WIZARD // This fragment was built by the wizard. // Warning! All manually made changes will be lost next time you use the wizard.
// register Inventory Receipt RegisterRecords.Inventory.Write = True; For Each CurRowInventory In Inventory Do Record = RegisterRecords.Inventory.Add(); Record.RecordType = AccumulationRecordType.Receipt; Record.Period = Date; Record.Products = CurRowInventory.Product; Record.Quantity = CurRowInventory.Quantity; EndDo;
//}}__REGISTER_REGISTERRECORDS_WIZARD EndProcedure |
Let's make some adjustments. Make sure you have the "Include in the command interface" flag selected for the "Inventory" register:
Go to "DocumentForm" and set the "Inventory" register to visible state in the "Command interface" section.
This flag makes visible all document register records right in the form.
Start 1C in dialog mode again, and post the previously created "SupplierInvoice" document. Switch to the "Inventory" tab:
As you can see, the code created by the wizard has worked fine, and the "Inventory" register received a record of the amount of the goods.
Still, it states 1,500 instead of 1.5, while the application had to convert 1,500 grams to 1.5 kilograms as kilogram is the base unit of measurement here.
To make it look good, we need a small addition that converts the amount of the goods during document posting. Let me remind you once again. All amounts should be stated in kilograms. So get to the "SupplierInvoice" module and change the code previously created by the wizard as follows:
Procedure Posting(Cancel, Mode) //{{__REGISTER_REGISTERRECORDS_WIZARD // This fragment was built by the wizard. // Warning! All manually made changes will be lost next time you use the wizard.
// register Inventory Receipt RegisterRecords.Inventory.Write = True;
For Each CurRowInventory In Inventory Do Record = RegisterRecords.Inventory.Add(); Record.RecordType = AccumulationRecordType.Receipt; Record.Period = Date; Record.Products = CurRowInventory.Product; //Record.Quantity = CurRowInventory.Quantity;
structureParameters = New Structure; structureParameters.Insert("MeasurementUnit", CurRowInventory.MeasurementUnit); structureParameters.Insert("Quantity", CurRowInventory.Quantity);
Record.Quantity = ReturnCurrentQuantity(structureParameters);
EndDo;
//}}__REGISTER_REGISTERRECORDS_WIZARD EndProcedure
Function ReturnCurrentQuantity(structureParameters)
currentMeasurementUnit = structureParameters.MeasurementUnit; currentQuantity = structureParameters.Quantity;
If TypeOf(currentMeasurementUnit) = Type("CatalogRef.UOMClassifier") Then Return currentQuantity; Else Return currentQuantity * currentMeasurementUnit.Ratio; EndIf;
EndFunction |
As you can see the changes are not that big. The application checks the current "MeasurementUnit" on posting. If it has the "CatalogRef.UOMClassifier" type, we use a base unit, and no conversion is needed. If the unit of measurement is taken from the "UOM" catalog, that amount is recalculated with the account of "Ratio."
Let us start the application in dialog mode and check the code in action. Post the previously created document again and check the record in the "Inventory" register.
As you can see, the document value stated in grams has been converted into kilograms when added to the register.
We can run an additional check by adding one more line for the same item of goods but select a ton as a unit of measurement. Do it:
Well, the code works perfectly fine: one ton has been converted into one thousand kilograms.
This is basically it. We have learned a way to handle several units of measurement for a single item of goods using standard mechanisms of the 1C platform.
And we still have a lot of standard functionality to discuss. Stay tuned. Even more fascinating things are on the way.
As usual, I am glad to share the link to the code we have used in this article. Enjoy!