Chapter 9: Developing Business Logic with Codeunits
Objectives
By the end of this chapter you will be able to:
- Explain what a codeunit is and when logic belongs in one instead of a table or page trigger
- Write public and local procedures, with parameters and return values
- Call a codeunit from a page action
- Recognize the
RecvsxRecdistinction and why it matters in triggers - Apply codeunit best practices you can defend in a code review
9.1 What is a codeunit?
A codeunit is a container for procedures. It has no UI and no data structure of its own. It exists only to hold business logic, so that the logic can be reused, tested, and called from more than one place (a page action, another codeunit, a report, an API).
As a rule of thumb, logic that runs to more than a line or two, or that more than one object might need to call, belongs in a codeunit. Don’t duplicate it inside a table trigger or a page action.
codeunit 50100 "Loan Repayment Mgt."
{
// Procedures go here
}
9.2 Public and local procedures
Procedures are public by default: any other object that has a variable
of this codeunit’s type can call them. Mark a procedure local when
it’s only a helper for other procedures in the same codeunit. That keeps
the codeunit’s public surface small and easy to understand from the
outside.
procedure MarkInstallmentPaid(var LoanRepaymentSchedule: Record "Loan Repayment Schedule")
begin
LoanRepaymentSchedule."Payment Date" := Today;
LoanRepaymentSchedule.Paid := true;
LoanRepaymentSchedule.Modify(true);
end;
local procedure CalculateTotal(PrincipalAmount: Decimal; InterestAmount: Decimal): Decimal
begin
exit(PrincipalAmount + InterestAmount);
end;
Hint:
varbefore a parameter passes it by reference, so changes the procedure makes toLoanRepaymentScheduleare visible to the caller. Leave offvarfor a parameter you only want to read, like the amounts going intoCalculateTotal.
9.3 A gotcha: Rec vs xRec
Chapter 7’s OnModify trigger blocks edits to a paid installment by
checking xRec.Paid, not Paid. Here’s why. Suppose it checked
if Paid then Error(...) instead. By the time OnModify runs, Rec
already holds the new values you’re about to save, including the
change from Paid = false to Paid = true that MarkInstallmentPaid
is trying to make. So that trigger would block the switch to paid as
well as any edits after it.
The trigger has to check the record’s state before this change, which
AL gives you as xRec:
trigger OnModify()
begin
if xRec.Paid then
Error('A paid installment cannot be changed.');
"Total Amount" := "Principal Amount" + "Interest Amount";
end;
Hint:
Recis “the record as it will be saved” andxRecis “the record as it was before this change.” Whenever a trigger needs to ask “did this field just change?” instead of “what is this field now?”, you needxRec. If your table from Chapter 7 checksPaidinstead, fix it before you continue.
9.4 A procedure that creates multiple records
Some business logic changes more than one record. Generating a whole loan schedule up front means inserting several rows in a loop, and multi-step work like that doesn’t belong in a table trigger.
procedure GenerateSchedule(LoanNo: Code[20]; NumberOfInstallments: Integer; PrincipalPerInstallment: Decimal; InterestPerInstallment: Decimal; FirstDueDate: Date)
var
LoanRepaymentSchedule: Record "Loan Repayment Schedule";
i: Integer;
begin
for i := 1 to NumberOfInstallments do begin
LoanRepaymentSchedule.Init();
LoanRepaymentSchedule."Loan No." := LoanNo;
LoanRepaymentSchedule."Installment No." := i;
LoanRepaymentSchedule."Due Date" := CalcDate(StrSubstNo('<+%1M>', i - 1), FirstDueDate);
LoanRepaymentSchedule."Principal Amount" := PrincipalPerInstallment;
LoanRepaymentSchedule."Interest Amount" := InterestPerInstallment;
LoanRepaymentSchedule.Insert(true);
end;
end;
Hint:
Insert(true), unlike a plainInsert(), tells AL to run the table’sOnInserttrigger. That way"Total Amount"still gets calculated for every generated row, and you don’t have to repeat that logic here.
9.5 Calling a codeunit from a page
Business logic needs something to trigger it, and a page action is the most common entry point:
trigger OnAction()
var
LoanRepaymentMgt: Codeunit "Loan Repayment Mgt.";
begin
LoanRepaymentMgt.MarkInstallmentPaid(Rec);
CurrPage.Update(false);
end;
9.6 Best practices for codeunits
- Give each codeunit a single responsibility: one business area.
"Loan Repayment Mgt."shouldn’t also contain vendor logic. - Keep triggers thin. A trigger should call a procedure instead of containing the whole calculation itself.
- Check
xRecto see what changed. Never assumeRecalone tells you the previous state. - Return simple values with
exit, and change records throughvarparameters. Don’t mix the two patterns in one procedure.
Chapter summary
In this chapter you:
- Learned when logic belongs in a codeunit instead of a trigger
- Wrote a public procedure and a local helper procedure
- Saw why Chapter 7’s trigger checks
xRecand notRec - Wrote a procedure that generates multiple records in a loop
- Called a codeunit procedure from a page action
Tasks
These tasks continue the Loan Repayment Schedule example from Chapter 7.
- Check Chapter 7. Go back to your
Loan Repayment Scheduletable’sOnModifytrigger and make sure it testsxRec.Paid, notPaid. OnceMarkInstallmentPaidexists, confirm you can mark an installment paid without the trigger rejecting it. - Create the codeunit. Create
Codeunits/LoanRepaymentMgt.Codeunit.alwith ID50100and nameLoan Repayment Mgt.. - Add
MarkInstallmentPaid. Add the public procedure from 9.2 that sets"Payment Date"andPaid, then callsModify(true). - Add
GenerateSchedule. Add the procedure from 9.4 that insertsNumberOfInstallmentsrows for a given loan, spaced one month apart. - Wire it to a page action. On your
Loan Repayment Schedulelist page (from Chapter 8), add an action that callsMarkInstallmentPaidon the selected record. - Try it yourself.
GenerateScheduledoesn’t check whether a schedule already exists forLoanNobefore inserting new rows. What would happen if someone ran it twice for the same loan? Add whatever guard you think is needed.
Check your work: compare your codeunit against the reference solution.