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 class | MGCLatticeProjectInvoicePostedBusinessEvent |
| Contract class | MGCLatticeProjectInvoicePostedBusinessEventContract |
| Category | Projects |
| Module | ModuleAxapta::Project |
| Label IDs | ProjectInvoicePostedBusinessEventName / ProjectInvoicePostedBusinessEventDescription |
| Trigger | ProjInvoiceJour.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 ofProjInvoiceJour.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 noorig()transition guard - every other newly inserted project invoice journal record fires the event. - When it fires:
ProjInvoiceJourheaders are written by the project invoice posting engine.ProjInvoiceJournalCreatesets the invoice number and invoice date ininitJournalHeader(), thencreateJournalHeader()fills in the totals (initializeJournalHeader()) and callsinsert(), 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- flaggedIsProforma= 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
invoiceAmountis the total calculated before that adjustment. - Inserts that bypass
ProjInvoiceJour.insert()(for exampledoInsert()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 field | Display label | Type / EDT | Source | Notes |
|---|---|---|---|---|
legalEntity | Legal Entity | DataAreaId | ProjInvoiceJour.DataAreaId | Company/legal entity of the invoice journal, in lower case (for example usmf) |
projectId | Project ID | ProjId | ProjInvoiceJour.ProjInvoiceProjId | Project the invoice was raised against |
invoiceId | Invoice ID | ProjInvoiceId | ProjInvoiceJour.ProjInvoiceId | Posted project invoice number |
invoiceDate | Invoice Date | InvoiceDate | ProjInvoiceJour.InvoiceDate | Invoice document date; serialized in the \/Date(milliseconds)\/ form |
invoiceAccount | Invoice Account | CustInvoiceAccount | ProjInvoiceJour.InvoiceAccount | Customer account the invoice is issued to |
invoiceAmount | Invoice Amount | AmountCur | ProjInvoiceJour.InvoiceAmount | Invoice total in the invoice currency (currencyCode), including sales tax and charges; negative for a credit note |
currencyCode | Currency Code | CurrencyCode | ProjInvoiceJour.CurrencyId | Invoice 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.
Related Events
- Project task assigned - the other Projects event
- Free text invoice posted - sibling invoice-posting event for non-project (free text) invoices
- Sales order invoice posted - sibling invoice-posting event for sales orders
- General journal posted - related financial posting event