0109

Reverse It, Once

reversal
ledger-entry
atomicity
guard-clauses

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]): Integer

Pick object IDs in the range 50100–50199 and reference other objects by name, never by ID. Captions are good practice but not graded.

ReverseEntry

  1. The entry must exist. If there is no "Reversible Ledger Entry" with that "Entry No.", fail with an error message that contains does not exist.
  2. Refuse, in exactly this order, so an entry that trips more than one rule always reports the first of them:
    1. the entry is itself a reversal ("Reversed Entry No." <> 0) — error text contains is a reversal;
    2. the entry has already been reversed (Reversed is true) — error text contains already reversed;
    3. the entry is partly applied ("Applied Amount" <> 0) — error text contains partly applied.
  3. 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." and Description are copies of the original's; Amount and Quantity are the original's negated; "Applied Amount" stays 0.
  4. 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.
  5. Return the new reversal entry's "Entry No.".

ReverseDocument

  1. Its scope is every entry carrying DocumentNo that 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.
  2. If the document has no entry in scope, fail with an error message that contains nothing to reverse.
  3. 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.
  4. 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.
  5. 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

Hint 1
Two records tell one story. Before writing any code, write down what each side of a reversal stores: which of 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.
Hint 2
The refusals are ordered on purpose: a reversal entry is itself flagged Reversed, so checking Reversed first would report the wrong reason for it. Check "Reversed Entry No." before Reversed, and "Applied Amount" last.
Hint 3
For 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.
ALBusiness Central 28.5
Press Compile to check your code compiles — Submit runs the tests.
The code editor is desktop-only
Open this problem on a computer to write and run code. Reading the description, tests and discussion works fine here.