Closing Dates, the forgotten magic dates in AL

One of the secrets of why FlowFields can work together with closing the income statement is called a Closing Date. Check the video on what a closing date actually is and how it works:

https://youtu.be/RXKBXk7x_y8

In this video, Erik explores one of the lesser-known features of AL and Business Central: closing dates. He explains the accounting concept behind them, demonstrates how they work in the general ledger, and reveals a surprising gotcha when working with closing dates and JSON serialization in AL code.

The Accounting Background: Why Closing Dates Exist

To understand closing dates, you need a bit of accounting context. In Business Central’s Chart of Accounts, there are two main groups of accounts:

  • Balance Sheet accounts — these carry forward from year to year
  • Income Statement accounts — these get reset to zero at the end of each fiscal year

When you close out a year, you reset all the income statement accounts back to zero, and whatever positive or negative result the accounting year produced goes into retained earnings (or losses).

But there’s a problem: every general ledger entry requires a posting date. If you post the year-end reset on a real date like December 31st or January 1st, that entry would show up in date-filtered reports. You wouldn’t be able to run a clean income statement for January 1 through December 31 without the reset entries polluting the results.

The “In-Between” Date

The solution Navision (now Business Central) came up with is an in-between date — a secret date that sits after one day but before the next. In English, it’s called a closing date. In some languages (like Danish), it’s called the Ultimo date (prefixed with “U” instead of “C”).

If you look at general ledger entries for an income statement account sorted by posting date, you’ll see something interesting between December 31, 2022 and January 1, 2023: an extra entry prefixed with “C” for closing. This closing date entry sits logically between those two real dates.

This means if you set a date filter from January 1, 2023 onward, the closing date entry for December 31, 2022 will not be included — which is exactly the desired behavior. The reset entries remain invisible to normal date-range reporting.

How Year-End Close Works in Practice

Erik points out that there are two separate operations involved in year-end closing:

  1. Closing the accounting periods — done via Accounting Periods, this simply marks the dates as closed
  2. Running “Close Income Statement” — this is the batch job that actually resets the income statement accounts, creating the closing date entries

You can also close by dimensions, which may result in multiple closing date entries per account.

Working with Closing Dates in AL

In AL code, you can convert any date to a closing date using the built-in ClosingDate() function:

D1 := ClosingDate(TODAY());

The function takes a date and returns that date as a closing date. For example, if today is May 10th, the closing date of May 10th sits logically between May 10th and May 11th. When displayed, it will be prefixed with “C” (e.g., “C05/10/2023”).

That’s essentially the only built-in function for closing dates — it’s very specific to this one purpose.

The Gotcha: Closing Dates and JSON Serialization

Here’s where Erik discovered a real problem. Consider the following scenario: you have a closing date, you store it in a JsonValue, and then you try to read it back.

pageextension 50105 CustomerListExt extends "Customer List"
{
    trigger OnOpenPage()
    var
        D1: Date;
        D2: Date;
        JValue: JsonValue;
    begin
        D1 := ClosingDate(TODAY());
        JValue.SetValue(format(D1));

        //D2 := JValue.AsDate();
        Evaluate(D2, JValue.AsText());
        Message('Result %1 - %2 - %3', D1, D2, format(D1));
    end;
}

The problem: if you use JValue.SetValue(D1) and then try to retrieve it with JValue.AsDate(), the closing date information is stripped away. What you get back is just a regular date. The “closing” aspect is completely lost during JSON serialization and deserialization.

Erik speculates that closing dates may have been “forgotten” even by Microsoft in this context — the JsonValue type has an overload that accepts dates, but it doesn’t preserve the closing date flag.

The Workaround: Format and Evaluate

The solution is to avoid AsDate() entirely and instead work with the text representation:

  1. Use Format(D1) to convert the closing date to a string (which preserves the “C” prefix)
  2. Store that formatted string in the JSON value
  3. Use Evaluate(D2, JValue.AsText()) to parse it back into a date

Evaluate correctly understands the closing date format and restores the closing date flag. Running the code above produces closing dates for all three values — D1, D2, and the formatted output all show as closing dates.

A Note on Format Specifiers

When using Format with closing dates, be aware that the XML format specifier (format 9, i.e., Format(D1, 0, 9)) does not include closing date information. If you need an XML-compatible format that also conveys closing date status, you’d need to build your own format string. The Format Property documentation shows there is a tag for closing dates in the standard date format options, so a custom format could be constructed if needed.

Summary

Closing dates are a specialized but important feature in Business Central that enables clean year-end accounting. They represent “in-between” dates that sit after one calendar day but before the next, allowing income statement reset entries to be posted without affecting normal date-filtered reports. In AL, the ClosingDate() function creates them, and Evaluate can parse them back from text. However, be cautious with JSON serialization: JsonValue.AsDate() silently drops the closing date flag. The workaround is to use Format to serialize and Evaluate to deserialize, preserving the closing date information through the round trip. It’s a niche topic, but one worth knowing — especially if you’re ever bitten by the silent data loss in JSON handling.