Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 Rec vs xRec distinction 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: var before a parameter passes it by reference, so changes the procedure makes to LoanRepaymentSchedule are visible to the caller. Leave off var for a parameter you only want to read, like the amounts going into CalculateTotal.

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: Rec is “the record as it will be saved” and xRec is “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 need xRec. If your table from Chapter 7 checks Paid instead, 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 plain Insert(), tells AL to run the table’s OnInsert trigger. 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 xRec to see what changed. Never assume Rec alone tells you the previous state.
  • Return simple values with exit, and change records through var parameters. 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 xRec and not Rec
  • 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.

  1. Check Chapter 7. Go back to your Loan Repayment Schedule table’s OnModify trigger and make sure it tests xRec.Paid, not Paid. Once MarkInstallmentPaid exists, confirm you can mark an installment paid without the trigger rejecting it.
  2. Create the codeunit. Create Codeunits/LoanRepaymentMgt.Codeunit.al with ID 50100 and name Loan Repayment Mgt..
  3. Add MarkInstallmentPaid. Add the public procedure from 9.2 that sets "Payment Date" and Paid, then calls Modify(true).
  4. Add GenerateSchedule. Add the procedure from 9.4 that inserts NumberOfInstallments rows for a given loan, spaced one month apart.
  5. Wire it to a page action. On your Loan Repayment Schedule list page (from Chapter 8), add an action that calls MarkInstallmentPaid on the selected record.
  6. Try it yourself. GenerateSchedule doesn’t check whether a schedule already exists for LoanNo before 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.