Skip to main content

Project invoice posted

Fires when a project invoice is posted in D365FO, the moment a ProjInvoiceJour journal record is created. Pro forma invoices do not raise it. This signals that billing for a project has been finalized and an invoice document now exists, enabling downstream systems to recognize revenue, notify billing/finance teams, or sync the invoice to external AR and reporting platforms.

Event classMGCLatticeProjectInvoicePostedBusinessEvent
Contract classMGCLatticeProjectInvoicePostedBusinessEventContract
CategoryProjects
ModuleModuleAxapta::Project
Label IDsProjectInvoicePostedBusinessEventName / ProjectInvoicePostedBusinessEventDescription
TriggerProjInvoiceJour.insert() -> not pro forma, business event enabled

Trigger / Hook​

  • Extension class: MGCLatticeProjInvoiceJour_Table_Extension, declared [ExtensionOf(tableStr(ProjInvoiceJour))]
  • CoC method signature: void insert() (Chain of Command wrap of ProjInvoiceJour.insert())
  • Firing condition: after next insert() completes (the journal record is persisted), the event is sent when the journal is not pro forma (ProjInvoiceJour.isProforma() is false) and the business event is enabled. There is no orig() transition guard - every other newly inserted project invoice journal record fires the event.
  • When it fires: ProjInvoiceJour headers are written by the project invoice posting engine. ProjInvoiceJournalCreate sets the invoice number and invoice date in initJournalHeader(), then createJournalHeader() fills in the totals (initializeJournalHeader()) and calls insert(), so the payload carries the header values as posted. The event fires once per project invoice journal, inside the posting transaction: it is written to the commit log and delivered after commit, so a failed post never delivers.
  • Pro forma invoices: a pro forma project invoice also inserts a temporary ProjInvoiceJour - flagged IsProforma = Yes with a generated pro forma invoice ID, or without an invoice number - which is removed after the document is printed. ProjInvoiceJour.isProforma() recognizes both forms and the hook sends nothing for them.
  • Limitation: with the Payment criteria option in the Project management and accounting parameters, posting can offset on-account transactions after the journal header is created and then recalculate the invoice's totals. In that case invoiceAmount is the total calculated before that adjustment.
  • Inserts that bypass ProjInvoiceJour.insert() (for example doInsert() or set-based inserts that skip table methods) raise no event.
  • Enablement gate: BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeProjectInvoicePostedBusinessEvent))
  • The event is built from the inserted record via MGCLatticeProjectInvoicePostedBusinessEvent::newFromProjInvoiceJour(this).send().
void insert()
{
next insert();

if (!this.isProforma()
&& BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeProjectInvoicePostedBusinessEvent)))
{
MGCLatticeProjectInvoicePostedBusinessEvent::newFromProjInvoiceJour(this).send();
}
}

Payload Contract​

One row per [DataMember], in declaration order. All sources are the inserted ProjInvoiceJour record, mapped in initialize():

JSON fieldDisplay labelType / EDTSourceNotes
legalEntityLegal EntityDataAreaIdProjInvoiceJour.DataAreaIdCompany/legal entity of the invoice journal, in lower case (for example usmf)
projectIdProject IDProjIdProjInvoiceJour.ProjInvoiceProjIdProject the invoice was raised against
invoiceIdInvoice IDProjInvoiceIdProjInvoiceJour.ProjInvoiceIdPosted project invoice number
invoiceDateInvoice DateInvoiceDateProjInvoiceJour.InvoiceDateInvoice document date; serialized in the \/Date(milliseconds)\/ form
invoiceAccountInvoice AccountCustInvoiceAccountProjInvoiceJour.InvoiceAccountCustomer account the invoice is issued to
invoiceAmountInvoice AmountAmountCurProjInvoiceJour.InvoiceAmountInvoice total in the invoice currency (currencyCode), including sales tax and charges; negative for a credit note
currencyCodeCurrency CodeCurrencyCodeProjInvoiceJour.CurrencyIdInvoice currency

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 raised inside the business transaction, not when it commits; it orders the events of one business record, gaps are permanent and normal, and it must not be used to re-sequence the whole stream), correlationId (String, always Scope:Key - groups emissions that belong to one logical run; an event that joins no run carries Event: followed by a lower-case GUID unique to this emission) 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": "MGCLatticeProjectInvoicePostedBusinessEvent",
"ControlNumber": 5637148802,
"EventId": "45A6B4E8-FF37-4F86-9DB1-376BA67C104C",
"EventTime": "\/Date(1790179533000)\/",
"EventTimeIso8601": "2026-09-23T16:05:33.1608976Z",
"MajorVersion": 0,
"MinorVersion": 0,
"BusinessEventLegalEntity": "USMF",
"ContextRecordSubject": "",
"InitiatingUserAADObjectId": "{66785381-F05D-757C-EC52-AB5498CCD700}",
"ParentContextRecordSubjects": [],
"eventSequence": 48087,
"correlationId": "Event:18ce8659-6e2f-4e72-bdd4-3c22cd165de8",
"schemaVersion": 1,
"legalEntity": "usmf",
"projectId": "000184",
"invoiceId": "000183",
"invoiceDate": "\/Date(1790121600000)\/",
"invoiceAccount": "US-003",
"invoiceAmount": 20156.25,
"currencyCode": "USD"
}

Activation​

In the D365FO client go to System administration > Setup > Business events > Business events catalog. Find the event by name ("Project invoice posted") or filter the Category column to Projects. Select it and activate it against the desired endpoint (for example an Azure Service Bus queue, Event Grid, or HTTPS/Power Automate endpoint) to begin receiving payloads.