About this post
When I was writing this post, I read two new (and good) post about AL interfaces. When I finish it, another two, three or four may be published. So, I'm not going to talk about interfaces from an OOP point of view, I'm just going to talk about how to extend BC with interfaces with what I think is a good example and compare with other ways to modify the standard behavior of the application.
When the interfaces arrived, I didn't pay them they any attention. But things changed when I realize that we have a new method to extend the app. I'm more interested in practice than in theory.
An example
The user's case is as follows: we will make a system to set up the calculation of commissions on each salesperson card. Initially we want to implement two methods of commission calculation, and those will be the ones that can initially be configured on the salesperson card:
In the first system, the commission base amount will be the VAT base amount.
In the second, the commission-based amount shall also be the VAT base amount, but only for the sales lines of items, not of other types of lines(account, resource or charge)..
Finally, to show the calculated commissions, we will generate a new sales invoice list page with a commission column to show the commission amount of each sales invoice.
Example coding
The first step is to make the interface. To do this in BC we have a new type of object called interface, an abstract declaration of one or more methods without implementation, I mean name only, its parameters and its return value:
interface "Commission Calculation"
{
procedure GetInvCommission(SalesInvoiceHeader: Record "Sales Invoice Header") ComisionAmount: Decimal;
procedure LookupCommission(SalesInvoiceHeader: Record "Sales Invoice Header")
}
Once we have the interface, the abstract method, we can declare different Codeunits to implement this interface. To do this, we put the implements clause after Codeunit name:
codeunit 69007 "Commission Vat Base" implements "Commission Calculation"
And within the Codeunit, we define the implementation of the interface methods.
We must implement all the methods of the interface with the same name, parameters and return variables, otherwise the compiler will return us an error:
procedure GetInvCommission(SalesInvoiceHeader: Record "Sales Invoice Header"): Decimal;
begin
if not Salesperson.get(SalesInvoiceHeader."Salesperson Code") then
exit;
with SalesInvoiceLine do begin
SetRange("Document No.", SalesInvoiceHeader."No.");
CalcSums(Amount);
BaseCommission := Amount;
exit(Round(BaseCommission * Salesperson."Commission %" / 100));
end;
end;
We make the other implementation of the same interface, the one that takes as commission bas only the item lines(I don´t write here all the code, could be boring, you can see it in the repository):
codeunit 69008 "Commission Items Only" implements "Commission Calculation"
{
procedure GetInvCommission(SalesInvoiceHeader: Record "Sales Invoice Header"): Decimal;
The Enum connection
The last piece of this puzzle is a bit strange, a connection between interfaces and objects of type Enuma:
We define an object of type Enum and declare that it will be an implementation of the interface Commission:
enum 69000 "Calc. Commission Method" implements "Commission Calculation"
And at each value of the enum we assign an implementation of the interface:
value(0; Default)
{
Implementation = "Commission Calculation" = "Commission Vat Base";
}
It is important the meaning of the second line of code. Its structure is: Implementation = <Interface> <"ImplementationCodeunit"”>
This way, we will define a new field in a table with this enumeration, to configure a different implementation in each salesperson record:
tableextension 69000 "COM Salesperson" extends "Salesperson/Purchaser"
………….
field(69000; "Calc. Commission Method"; enum "Calc. Commission Method")
When we calculate the sales invoice commission , we can make a GET from the seller, to obtain their commission calculation method and assign it directly to the interface:
ICommissionCalculation: Interface "Commission Calculation";
SalespersonPurchaser: Record "Salesperson/Purchaser";
Begin
……….
ICommissionCalculation := SalespersonPurchaser."Calc. Commission Method";
exit(ICommissionCalculation.GetInvCommission(Rec));
And this is enough to establish the calculation method implementation.
Recap: We can assign Interface= <Enum value> before calculating. That value of the Enum tells the interface which Codeunit will implement it, because we have set it in the Enum definition this implementation Codeunit. With this code, if the value of the enum is “default” or “VAT base”, it takes the first Codeunit to implement the Interface. If in the salesperson card we have set up the option “Only items", take the second Codeunit to implement the interface code.
The big deal: Extending from another App
This make sense if a third part app arrives and it want to extend the functionality of the commission calculation. These are the steps to extend the interface behavior from another application:
Declare the dependency from the Commission Calculation App on the app.json and then download symbols again. The objective of this new extension will be to be to be able to configure and implement in the salesperson card a new kind of commission based in invoice profit amount, sales amount-Cost in sales invoice lines.
Then we create a new Codeunit to implement the cost-based implementation:
codeunit 69009 "Commission Base Profit" implements "Commission Calculation"
{
procedure GetInvCommission(SalesInvoiceHeader: Record "Sales Invoice Header"): Decimal;
var
SalesInvoiceLine: Record "Sales Invoice Line";
Salesperson: Record "Salesperson/Purchaser";
BaseCommission: Decimal;
CostAmount: decimal;
begin
if not Salesperson.get(SalesInvoiceHeader."Salesperson Code") then
exit;
with SalesInvoiceLine do begin
SetRange("Document No.", SalesInvoiceHeader."No.");
if not FindSet() then
exit;
repeat
CostAmount := "Unit Cost" * Quantity;
BaseCommission := BaseCommission + (Amount - CostAmount)
until next = 0;
exit(Round(BaseCommission * Salesperson."Commission %" / 100));
end;
end;
And the second step, we extend the enum for this new calculation method:
enumextension 69000 "Commission over profit" extends "Calc. Commission Method"
{
value(69009; "Profit based")
{
Implementation = "Commission Calculation" = "Commission Base Profit";
}
}
And in this way it allows us assign on the salesperson card this new calculation method value:
It is important to know how to extend an interface, because we may have to do it to make our own sales price calculation methods.
Opinion: A new idea in the NAV/BC world? A big step in OOP, a small step in extensibility
Although I find it a great contribution to code organization, I don't think so much about extending the application.
Let's see what we had so far to modify the operation of the application:
Events. Ideal for the lazy collective which I belong. I almost never want to do all the code of a process to replace the existing one, I prefer the subscription, take a single step in the process and add small behavior changes. Also, before the interfaces full behavior replacing was possible with events with the Handled pattern. I think NAV almost always gives us the right answer and make no sense to program again a process instead use Microsoft code. I say almost always, except for the idea that the best sale price is the cheapest rather than the more specific (customer better than customer price group). This may have caused that the first standard interface was used for sales prices in the standard.
Setup Codeunits, as in the SEPA bank payment. We set up a Codeunit number in the bank card to implement the management of the export of the banking format. For me that was Interface Beta version. It's not a bad idea and sometimes I used it.
In any case, it is worth knowing how to manage interface implementations, because it will be the main way to extend the calculation of sales prices and more things that will come.
All code in Git repositories:
*This post is locked for comments