Registering a price of materials bought
Every time we buy materials we register this transaction by creating a new GoodsReceipt document. By now Materials tabular section of the document has the following attributes:
Seems that we have missed something important - as an amount of money we’ve spent on the transaction. We should register a price for the unit for every line of Materials tabular section so let’s add a correspondent attribute.
Now we can specify the unit price but it would also be great to know a total price for every line in the tabular section as well as the total price of the entire document. The total per line can be calculated by this formula: Unit price * Quantity and the overall document total is just a sum of all line totals. This kind of data are called “secondary”, meaning that there is no new information in there - the data contain something that we’ve already known but in another form (as a multiplication of two already known values in our case).
The question is how do we deal with secondary data?
OK, here is a rule #1: never let a user change secondary data. Otherwise:
- you make users do an unnecessary job
- users can make a mistake and compromise data integrity
Instead, you need to implement an automatic calculation of these data.
Another key question is: do we need actually to store the secondary data in the infobase? What if we just calculate and show them on-the-fly and never even bother to save them?
So, there are two ways. We can store the secondary data (let’s call them “persistent” in this case) or we can calculate them when we need to show them (in this case we will call them “virtual”).
Let’s try both approaches and learn their strengths and weaknesses.
Persistent secondary data
It would be great if a user could see totals in real time, so they should be recalculated whenever the price or quantity is changed. Let’s see how we can calculate a total for every tabular section line.
This is source code we’ve got:
&AtClient Procedure MaterialsQuantityOnChange(Item) ThisForm.Items.Materials.CurrentData.Total = ThisForm.Items.Materials.CurrentData.Quantity * ThisForm.Items.Materials.CurrentData.Price; EndProcedure
No, we didn’t, actually. We did it for this form, but there are a lot of opportunities to circumvent this restriction. Let’s take a look at this simple example.
Note that there are much more than just this one way to ruin your totals - this is just a simple example.
Another important thing to be mentioned is that we are not trying to protect our data from hackers or intruders of any kind. Breaches of database security are possible, but the much more possible scenario is somebody’s incidental mistake.
Just think about endless possibilities to make a mistake and get wrong secondary data. Wouldn’t it be great if we could intercept any update document operation to check and correct the totals? Is there a place in 1C:Enterprise where all write document commands ended up?
Document event subscriptions
Most of the 1C:Enterprise metadata objects can generate events every time some specific action are performed with the object. A developer can create event handlers (subscriptions for the events) and implement a code doing whatever they need. Good thing about these events is that there is no way to perform the action without occurrence of the corresponding event - every time the action is performed the event occurs, and our code is here to do what we need. This leaves no space for mistakes.
There are two ways to subscribe to the event and implement its handler:
- Subscribe for the event in the object module;
- Create a global subscription and implement the handler in a common module.
You want to use the first option to implement “local” event handlers meaningful only for the current object. If you need to implement one handler for an event in more than one object, you have to use the global subscriptions. That said, you can use a global subscription for a single object if you want.
Here is where you can find a local document events’ subscriptions:
The source code will be the same in both cases. Let’s consider how to create a handler using global events subscriptions. In this case event, a handler can be implemented only in a common module, so let’s get acquainted with this first.
The source code we were writing so far was stored in form modules - the place where we implement handlers for forms’ events. In real world systems, we quite often need to use one code in more that one place. For instance, we would need to calculate totals in more than one document. We can repeat the same code twice, but the better solution is to place it somewhere else - where it is accessible for both documents. This place is a common module.
You can create as many common modules as you need, and you can make them accessible to client or server (or both).
For a common module procedures and functions to be accessible from outside of the module, you need to use “Export” keyword after the procedure or function definition. You also can write internal procedures that will be accessible only from the inside of the module. In this case, you don’t need to use “Export” keyword.
Let’s create the event subscription we need.
Here is the resulting handler code we’ve got:
Procedure GoodsReceiptMaterialsTotalBeforeWrite( Source, Cancel, WriteMode, PostingMode) Export For Each Material In Source.Materials Do Material.Total = Material.Price * Material.Quantity; EndDo; EndProcedure;
Document’s total price
Now let’s calculate and save the document’s total. First of all we need to add another attribute to the document:
Then we need to add this new attribute to the document form (I put it in the bottom - below the tabular section):
Now we can modify the handler we’ve implemented for it to calculate the overall total in the same cycle. It could be something like that:
Procedure GoodsReceiptMaterialsTotalBeforeWrite( Source, Cancel, WriteMode, PostingMode) Export Source.Total = 0; For Each Material In Source.Materials Do Material.Total = Material.Price * Material.Quantity; Source.Total = Source.Total + Material.Total; EndDo; EndProcedure
The Source parameter in the fragment above contains DocumentObject.GoodsReceipt we are saving, so we can address our new field as “Source.Total”. After we open, save and reopen our document we will see this:
So, the document total is calculated and saved to the infobase. But if we change price or quantity of any line the total remains unchanged. Let’s fix this too.
We need to cycle through the tabular section, calculating the overall total, which sounds exactly like what GoodsReceiptMaterialsTotalBeforeWrite does. Can we use it? Yes, but we don’t need all its parameters, so let’s rewrite it in this way:
Procedure GoodsReceiptMaterialsTotalBeforeWrite( Source, Cancel, WriteMode, PostingMode) Export CalcTotals(Source); EndProcedure Procedure CalcTotals(Document) Export Document.Total = 0; For Each Material In Document.Materials Do Material.Total = Material.Price * Material.Quantity; Document.Total = Document.Total + Material.Total; EndDo; EndProcedure
So, we have added another export procedure and move the entire total calculation cycle in there, leaving only this procedure call in GoodsReceiptMaterialsTotalBeforeWrite handler. Note that we need to declare the procedure using Export keyword to make it accessible from outside the module.
We pass the forms attribute Object having DocumentsObject.GoodsReceipt type as a parameter to this new procedure.
Now let’s call the procedure from GoodsReceipt document form module:
&AtClient Procedure MaterialsQuantityOnChange(Item) //ThisForm.Items.Materials.CurrentData.Total = // ThisForm.Items.Materials.CurrentData.Quantity * // ThisForm.Items.Materials.CurrentData.Price; Subscriptions.CalcTotals(ThisForm.Object); EndProcedure
This procedure used to calculate the current line total but now we commented this line and called CalcTotals procedure instead. It will recalculate all lines’ totals (including the current line total) and calculate the overall document total as well.
Note that we use the module name as a part of a procedure name, i.e. put “Subscriptions.CalcTotals”. Otherwise, the procedure won’t be accessible outside the module. However, if you check the syntax there will be an error message:
This error message tells us that we are making a call from the thin client, but the procedure we are calling isn’t exist or isn’t available. In other words our client code “cannot see” this procedure.
The thing is that the module has settings affecting the accessibility of its procedures and functions from client and server code. To make these module procedures visible for client code, we need to select “Client” checkbox in the modules properties.
Now all totals are calculated and stored in the infobase.
Lesson 1-5 | Course description| Lesson 2-2