Reverse It, Once
An entry was posted with the wrong sign, and the accountant wants it gone. Business Central does not delete posted entries — it reverses them: an offsetting entry with the same document number and posting date, the amount and quantity negated, and both sides flagged and cross-referenced so the audit trail shows what undid what. The hard part is not the arithmetic. It is refusing to reverse anything that must not be reversed, and making sure a refusal in the middle of a document leaves the ledger exactly as it was.
The starter ships the table finished. Do not rename it or its fields — the grading tests bind to every name below character for character.
The table you get
Table "Reversible Ledger Entry" — primary key "Entry No." (Integer), plus a secondary key on "Document No.", "Reversed Entry No."; fields "Document No." (Code[20]), "Posting Date" (Date), "Account No." (Code[20]), Description (Text[50]), Amount (Decimal), Quantity (Decimal), "Applied Amount" (Decimal), Reversed (Boolean), "Reversed by Entry No." (Integer), "Reversed Entry No." (Integer).
The three bookkeeping fields have the same meaning as in the real G/L Entry table: an entry that has been reversed carries Reversed = true and the number of the entry that reversed it in "Reversed by Entry No."; a reversal entry carries Reversed = true and the number of the entry it reversed in "Reversed Entry No.". So "Reversed Entry No." <> 0 is what makes an entry a reversal, and only a reversal has it set.
Requirements
Implement both procedures in the codeunit "Ledger Entry Reversal":
procedure ReverseEntry(EntryNo: Integer): Integer
procedure ReverseDocument(DocumentNo: Code[20]): IntegerPick object IDs in the range 50100–50199 and reference other objects by name, never by ID. Captions are good practice but not graded.
ReverseEntry
- The entry must exist. If there is no
"Reversible Ledger Entry"with that"Entry No.", fail with an error message that containsdoes not exist. - Refuse, in exactly this order, so an entry that trips more than one rule always reports the first of them:
- the entry is itself a reversal (
"Reversed Entry No." <> 0) — error text containsis a reversal; - the entry has already been reversed (
Reversedis true) — error text containsalready reversed; - the entry is partly applied (
"Applied Amount" <> 0) — error text containspartly applied.
- the entry is itself a reversal (
- Otherwise write one new
"Reversible Ledger Entry", the reversal:"Entry No."is the highest entry number currently in the table plus 1 (the table already holds entries — never assume you start at 1);"Document No.","Posting Date","Account No."andDescriptionare copies of the original's;AmountandQuantityare the original's negated;"Applied Amount"stays 0. - Flag both sides. On the reversal:
Reversed= true and"Reversed Entry No."= the original's"Entry No.", with"Reversed by Entry No."left at 0. On the original:Reversed= true and"Reversed by Entry No."= the new entry's number, with"Reversed Entry No."left at 0. - Return the new reversal entry's
"Entry No.".
ReverseDocument
- Its scope is every entry carrying
DocumentNothat is not itself a reversal — that is,"Reversed Entry No." = 0. Entries of other documents, and the reversal entries already sitting in this document, are invisible to it. Note that the reversals you write carry the document number too, so they land inside the document while you are working on it. - If the document has no entry in scope, fail with an error message that contains
nothing to reverse. - Every entry in scope must pass the same refusals as
ReverseEntry, rules 2.2 and 2.3, and is reversed by the same rules 3 and 4 — in ascending"Entry No."order, so the lowest new entry number belongs to the lowest original entry number. - All or nothing. If any entry in scope is refused, the whole call fails and the ledger is left exactly as it was: no reversal entry survives, and no entry has been flagged. Reversing half a document and reporting an error is the one outcome that must never happen.
- Return the number of entries reversed.
What the tests check
The tests reverse a clean entry and assert the negated Amount and Quantity, the copied document number, posting date, account and description, the returned entry number continuing the ledger, both Reversed flags with their two cross-references, and that the entry plus its reversal net to zero on both Amount and Quantity. Each refusal is checked with its error text (matching is case-insensitive, but the quoted phrases must appear exactly as written), including a second attempt on an already reversed entry, which must leave the ledger byte-identical, an attempt on the reversal entry itself, and an entry tripping two refusals at once, which must report the earlier of them. For ReverseDocument: a clean three-entry document (the returned count, one reversal per entry, both totals netting to zero), the reversal order, the two Reversed flags and both cross-references on every entry of the document and on the reversal written for it, the empty document, a neighbouring document that must stay untouched, a second run on an already reversed document, a document holding a partly applied entry — and a five-entry document whose third entry was already reversed, where the call must fail with already reversed and the ledger must afterwards hold exactly the entries it held before, with the other four entries still unflagged. Amounts, quantities, dates and descriptions are generated, so hardcoding the examples will not survive.
Learn More
- Reverse journal postings and undo receipts/shipments — what a reversal means in Business Central, and why an entry may only be reversed once.
- Get, Find, and Next methods — reading one entry by key and walking a filtered set.
- AL error handling — raising the refusals, and what an error does to the work already done.
- Database.Commit() Method — where one write transaction ends and the next begins.
Reversed, "Reversed by Entry No." and "Reversed Entry No." is set on the entry being reversed, and which on the new entry. The tests read both sides, and they read the fields you leave at zero as well.Reversed, so checking Reversed first would report the wrong reason for it. Check "Reversed Entry No." before Reversed, and "Applied Amount" last.ReverseDocument, the all-or-nothing rule needs no cleanup code: one call is already one write transaction, and an error undoes everything it wrote — as long as nothing in your code ends that transaction early. So do not reach for Commit. Scope the run with SetRange on "Document No." plus SetRange("Reversed Entry No.", 0): the reversals you insert carry the same document no., and leaving them out of the filter also keeps them out of the set you are still walking. Number the new entries from one FindLast over the whole table, incrementing per entry.