Sales order invoice posted
Fires when a customer invoice for a sales order is posted in Accounts receivable - once for each invoice journal (CustInvoiceJour) that sales order invoicing posts, after the posting has written the invoice totals. Billing, revenue, logistics and customer-portal systems can use it to learn that an order has been invoiced, with the invoice number, date, total and currency. Credit notes posted from sales orders raise it too (their invoiceAmount is negative); pro forma invoices do not. It is the sales-order counterpart of Free text invoice posted; the two never fire for the same invoice.
| Event class | MGCLatticeSalesOrderInvoicePostedBusinessEvent |
| Contract class | MGCLatticeSalesOrderInvoicePostedBusinessEventContract |
| Category | Sales orders |
| Module | ModuleAxapta::SalesOrder |
| Label IDs | SalesOrderInvoicePostedBusinessEventName / SalesOrderInvoicePostedBusinessEventDescription |
| Trigger | SalesInvoiceJournalPost.postJournalPost() -> a posted (not pro forma) invoice journal with a SalesId |
Trigger / Hook
- Extension class:
MGCLatticeSalesInvoiceJournalPost_Class_Extension,[ExtensionOf(classStr(SalesInvoiceJournalPost))], wrappingpublic void postJournalPost()with Chain of Command. The event is sent byMGCLatticeSalesOrderInvoicePostedEventHandlers::raisePostedInvoiceEvents(). - When it runs: sales order invoicing (
FormletterService) callspostJournalPost()once for every invoice journal it creates, after that journal has been posted. By then the posting has written the header's totals (SalesInvoiceJournalPostBase.updateJournalTable()), and the header is read again by record ID so the payload carries what was stored. This covers invoicing a single order, summary invoicing of several orders, and batch and parallel posting. Project invoicing that posts sales order lines through a project invoice creates noCustInvoiceJourand does not raise it (see Project invoice posted). - Firing condition: the invoice journal exists, belongs to a sales order (
SalesIdis set), and is not pro forma - neither the posting (isProforma()) nor the header (Proforma) is flagged as pro forma. One event per invoice journal: a summary invoice that covers several orders is one journal and raises one event, carrying the order the invoice is posted under. - Credit notes: a credit note posted from a sales order is an invoice journal with a negative total, so it raises this event with a negative
invoiceAmount. When the credit note is for a return order, Return credit note posted is raised for the same journal as well; if you subscribe to both, de-duplicate oninvoiceId. - Pro forma invoices: D365 saves a pro forma invoice as a real invoice journal flagged as pro forma, prints it and deletes it again. Those journals are skipped, so a pro forma invoice never raises this event.
- Timing:
postJournalPost()runs inside the posting transaction (FormletterService.processJournal());send()writes to the business events commit log inside it and delivery happens asynchronously after commit, so a posting that fails, or a simulated posting (which D365 rolls back), never delivers. - Enablement gate:
BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeSalesOrderInvoicePostedBusinessEvent)).
public void postJournalPost()
{
next postJournalPost();
// Null when the posted journal is not an invoice header (project invoicing posts against the parm table).
CustInvoiceJour invoiceJournal = this.parmJournalTable() as CustInvoiceJour;
// The activation checks are cached lookups; re-read the header only when one of the two events is wanted.
if (invoiceJournal && invoiceJournal.RecId && !this.isProforma()
&& (BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeSalesOrderInvoicePostedBusinessEvent))
|| BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeReturnCreditNotePostedBusinessEvent))))
{
// Re-read the header so the events carry what the posting stored, totals included.
MGCLatticeSalesOrderInvoicePostedEventHandlers::raisePostedInvoiceEvents(CustInvoiceJour::findRecId(invoiceJournal.RecId));
}
}
raisePostedInvoiceEvents() checks that the header has a SalesId and is not pro forma, sends this event when it is activated, and then hands the header to the return credit note check.
Payload Contract
One row per [DataMember], in declaration order. All sources are fields on the posted CustInvoiceJour header, read after the posting wrote its totals and mapped in initialize().
| JSON field | Display label | Type / EDT | Source | Notes |
|---|---|---|---|---|
legalEntity | Legal Entity | DataAreaId | CustInvoiceJour.DataAreaId | Company of the invoice, as stored (lowercase, for example usmf). |
invoiceAccount | Invoice Account | CustAccount | CustInvoiceJour.InvoiceAccount | The customer billed, taken from the sales order's invoice account (it can differ from the order account). |
salesOrderId | Sales Order ID | SalesId | CustInvoiceJour.SalesId | The sales order on the invoice header. For a summary invoice this is the order the invoice is posted under; the other orders on the invoice are not listed. |
invoiceId | Invoice ID | InvoiceId | CustInvoiceJour.InvoiceId | Invoice number. |
invoiceDate | Invoice Date | InvoiceDate | CustInvoiceJour.InvoiceDate | Invoice date. A date, serialized in the \/Date(milliseconds)\/ form (midnight UTC). |
invoiceAmount | Invoice Amount | AmountCur | CustInvoiceJour.InvoiceAmount | Invoice total including tax, in currencyCode. Negative for a credit note. |
currencyCode | Currency Code | CurrencyCode | CustInvoiceJour.CurrencyCode | Transaction currency of the invoice (from the sales order). |
crossReferences | Cross references | List of MGCLatticeCrossReferenceEventContract | MGCLatticeCrossReferenceEventHelper::listForSalesOrder(CustInvoiceJour.SalesId, CustInvoiceJour.DataAreaId) | Keys external systems hold for the sales order that was invoiced: one { externalSystem, externalKey } object per external system, ordered by external system, historical keys included; [] when it has none. See Cross-reference keys. The keys are the sales order's, not the journal's. |
Every Lattice event also carries the Lattice ordering and correlation envelope, inherited from MGCLatticeBusinessEventContractBase: eventSequence (Int64 - increasing across the whole catalog and all companies, allocated when the event is sent inside the business transaction; it orders the events of one record, but two transactions can commit in the opposite order of their values, and gaps are normal and permanent - see the events overview), correlationId (String - always Scope:Key: Event: followed by a GUID unique to this emission, unless the event joins a run) and schemaVersion (Int - the payload shape version; 1 for this contract). D365 F&O also adds its standard envelope fields to every business event payload: BusinessEventId, BusinessEventLegalEntity, ContextRecordSubject, ControlNumber, EventId, EventTime, EventTimeIso8601, InitiatingUserAADObjectId, MajorVersion, MinorVersion, and ParentContextRecordSubjects. Do not rely on the order of members in the delivered JSON.
Example Payload
{
"BusinessEventId": "MGCLatticeSalesOrderInvoicePostedBusinessEvent",
"ControlNumber": 5637148720,
"EventId": "4D7A9E12-C3B8-4F06-9E51-8A2B6C0D7F94",
"EventTime": "\/Date(1790270772000)\/",
"EventTimeIso8601": "2026-09-24T17:26:12.4336268Z",
"MajorVersion": 0,
"MinorVersion": 0,
"BusinessEventLegalEntity": "USMF",
"ContextRecordSubject": "",
"InitiatingUserAADObjectId": "{55920BD2-9AE5-F41E-5D62-9DC7EAB242C4}",
"ParentContextRecordSubjects": [],
"eventSequence": 48342,
"correlationId": "Event:0ed82331-b041-4ea9-b4f7-31cb6cc743af",
"schemaVersion": 1,
"legalEntity": "usmf",
"invoiceAccount": "US-001",
"salesOrderId": "000745",
"invoiceId": "CIV-000745",
"invoiceDate": "\/Date(1790208000000)\/",
"invoiceAmount": 4386.75,
"currencyCode": "USD",
"crossReferences": [
{
"externalSystem": "SHOP",
"externalKey": "WEB-1004871"
}
]
}
Activation
In the D365FO client, go to System administration > Setup > Business events > Business events catalog. Find this event by name ("Sales order invoice posted") or by filtering the Category column to Sales orders. Select it and use Activate to bind it to a configured endpoint (for example, an Azure Service Bus queue, Event Grid topic, or HTTPS webhook).
Related Events
- Free text invoice posted - the counterpart for invoice headers without a sales order.
- Return credit note posted - raised in addition to this event when the invoiced order is a return order.
- Sales packing slip posted - the delivery step that usually precedes invoicing.
- Sales order confirmed - confirmation posting for the same order.
- E-invoice submission status changed - follows the invoice through Electronic invoicing submission.