Sales order canceled
Fires when a sales order's remaining quantities are canceled and its status becomes Canceled; from the Cancel action it fires once the cancellation is final. Return orders raise it too; salesType tells them apart. This signals downstream systems (fulfillment, billing, demand planning) that the order is no longer active and any reserved inventory, scheduled shipments, or pending invoices tied to it should be released or reconciled.
| Event class | MGCLatticeSalesOrderCanceledBusinessEvent |
| Contract class | MGCLatticeSalesOrderCanceledBusinessEventContract |
| Category | Sales orders |
| Module | ModuleAxapta::SalesOrder |
| Label IDs | SalesOrderCanceledBusinessEventName / SalesOrderCanceledBusinessEventDescription |
| Trigger | SalesUpdateRemain.cancelOpenOrderLinesDeliveryRemainder(SalesTable), or for call-center / Commerce-payments orders MCRSalesOrderCancellation::cancelOrder() / cancelOrderFTC() -> committed SalesStatus == SalesStatus::Canceled; from the Cancel action, after SalesUpdateRemain.cancelMultipleOrdersDeliveryRemainder() when the status is still Canceled |
Trigger / Hook
- Extension class:
MGCLatticeSalesUpdateRemain_Class_Extension,[ExtensionOf(classStr(SalesUpdateRemain))] - CoC method signatures:
public boolean cancelOpenOrderLinesDeliveryRemainder(SalesTable _salesTable)andpublic void cancelMultipleOrdersDeliveryRemainder(List _salesRecIdList) - Firing condition:
next cancelOpenOrderLinesDeliveryRemaindermust returntrue(the cancellation succeeded) and the event must be activated (checked first, a cached lookup; when it is not, the caller's buffer is left untouched); then, after re-reading the buffer, the committedSalesStatusmust equalSalesStatus::Canceled. The standard method only acts on open orders (SalesStatus::Backorder), cancels the delivery remainder of the open lines and updates the order status inside its ownttsbegin/ttscommit. The re-read plus status guard is mandatory because when not every line can be canceled the order keeps a status other than Canceled (the method still returnstrueand shows an informational message), and that case must not fire. The boolean return is preserved on all paths. - The Cancel action waits until the cancellation is final: after canceling an order, the Cancel action (
cancelMultipleOrdersDeliveryRemainder) voids the order's credit card pre-authorization, and when that void fails it restores the order's previous status. While the action runs, the cancellation is only registered (MGCLatticeSalesOrderCancelEventScope); when the action has finished, each registered order is re-read and the event is sent only if its status is still Canceled. An order put back after a failed void raises no event. If the action stops with an error, the orders it had already canceled (and committed) are still reported before the error continues. - Coverage / scope:
- The standard Cancel action on sales orders (
SalesCancelOrder, which callsSalesUpdateRemain.cancelMultipleOrdersDeliveryRemainder) routes orders that are not call-center or Commerce-payments orders throughcancelOpenOrderLinesDeliveryRemainder, so they fire (once the action has finished). - Customer-order cancellations submitted from Commerce through the Commerce transaction service call
cancelOpenOrderLinesDeliveryRemainderdirectly, so they fire as soon as that method returns. - Call-center and Commerce-payments orders are canceled by
MCRSalesOrderCancellation::cancelOrder(from the Cancel action, the call-center customer service and order detail pages, the hold and credit-limit workbenches, and return exchange handling) orMCRSalesOrderCancellation::cancelOrderFTC(the FTC event batch), which do not go throughcancelOpenOrderLinesDeliveryRemainder. They are signaled by a second extension,MGCLatticeMCRSalesOrderCancellation_Class_Extension([ExtensionOf(classStr(MCRSalesOrderCancellation))]), wrappingpublic static boolean cancelOrder(SalesTable _salesTable, boolean _displayErrors, boolean _nsfCancel)andcancelOrderFTCwith the same signature. When the event is activated (checked first), each wrapper reads the order's status beforenext; afterwardsMGCLatticeSalesOrderCanceledBusinessEvent::raiseIfCanceled()reads the order again and sends the event when it moved into Canceled (a partial cancellation that leaves the order on backorder, or an order that was already canceled, raises nothing). Inside the Cancel action the event is held back by the same scope as above.
- The standard Cancel action on sales orders (
- Transaction: the event is sent after the cancellation's own transaction has completed (after
cancelOpenOrderLinesDeliveryRemainderreturns, or after the Cancel action finishes). When the cancellation runs inside an outer transaction, the event is part of that transaction and is delivered only after it commits. - Enablement gate:
BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeSalesOrderCanceledBusinessEvent))must return true (in the order's company) before the event is sent.
public boolean cancelOpenOrderLinesDeliveryRemainder(SalesTable _salesTable)
{
boolean ret = next cancelOpenOrderLinesDeliveryRemainder(_salesTable);
if (ret && BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeSalesOrderCanceledBusinessEvent)))
{
_salesTable.reread();
if (_salesTable.SalesStatus == SalesStatus::Canceled
&& !MGCLatticeSalesOrderCancelEventScope::deferIfActive(_salesTable))
{
MGCLatticeSalesOrderCanceledBusinessEvent::newFromSalesTable(_salesTable).send();
}
}
return ret;
}
public void cancelMultipleOrdersDeliveryRemainder(List _salesRecIdList)
{
using (MGCLatticeSalesOrderCancelEventScope scope = MGCLatticeSalesOrderCancelEventScope::construct())
{
try
{
next cancelMultipleOrdersDeliveryRemainder(_salesRecIdList);
}
catch
{
scope.emitFinalCancellations();
throw;
}
scope.emitFinalCancellations();
}
}
Payload Contract
One row per [DataMember], in declaration order. All sources are fields on the re-read SalesTable record, mapped in initialize().
| JSON field | Display label | Type / EDT | Source | Notes |
|---|---|---|---|---|
legalEntity | Legal Entity | DataAreaId | SalesTable.DataAreaId | Company / data area of the sales order |
salesOrderId | Sales Order ID | SalesId | SalesTable.SalesId | Sales order number |
customerAccount | Customer Account | CustAccount | SalesTable.CustAccount | Customer account on the order |
salesStatus | Sales Status | int | enum2int(SalesTable.SalesStatus) | Integer value of the SalesStatus enum; always 4 (Canceled) because of the firing condition |
salesStatusName | Sales Status Name | str | enum2Symbol(enumNum(SalesStatus), enum2int(SalesTable.SalesStatus)) | Symbolic enum name string; always "Canceled" |
salesType | Sales Type | int | enum2int(SalesTable.SalesType) | Integer value of the SalesType enum: 3 = Sales, 4 = ReturnItem (return order), 2 = Subscription, 0 = Journal |
salesTypeName | Sales Type Name | str | enum2Symbol(enumNum(SalesType), enum2int(SalesTable.SalesType)) | Symbolic enum name string, for example "Sales" or "ReturnItem" |
rmaNumber | RMA Number | ReturnItemNum | SalesTable.ReturnItemNum when SalesType is ReturnItem | Return merchandise authorization number; empty for other order types |
crossReferences | Cross references | List of MGCLatticeCrossReferenceEventContract | MGCLatticeCrossReferenceEventHelper::listFor(SalesTable) | Keys external systems hold for the sales order: one { externalSystem, externalKey } object per external system, ordered by external system, historical keys included; [] when it has none. See Cross-reference keys. |
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": "MGCLatticeSalesOrderCanceledBusinessEvent",
"ControlNumber": 5637148655,
"EventId": "29639574-6FCB-4D84-A0FC-C804FA4A8C72",
"EventTime": "\/Date(1790005338000)\/",
"EventTimeIso8601": "2026-09-21T15:42:18.6356735Z",
"MajorVersion": 0,
"MinorVersion": 0,
"BusinessEventLegalEntity": "USMF",
"ContextRecordSubject": "",
"InitiatingUserAADObjectId": "{B8CCA1B9-0E9B-20D9-FE96-936AA79762AD}",
"ParentContextRecordSubjects": [],
"eventSequence": 47958,
"correlationId": "Event:c77fe410-3f34-4e71-bc66-91e268f15966",
"schemaVersion": 1,
"legalEntity": "usmf",
"salesOrderId": "001785",
"customerAccount": "US-004",
"salesStatus": 4,
"salesStatusName": "Canceled",
"salesType": 3,
"salesTypeName": "Sales",
"rmaNumber": "",
"crossReferences": [
{
"externalSystem": "SHOP",
"externalKey": "WEB-1004871"
}
]
}
Activation
In the D365FO client, go to System administration > Setup > Business events > Business events catalog. Locate the event by name ("Sales order canceled") or filter the Category column to Sales orders. Select it and activate it against the desired endpoint (for example an Azure Service Bus queue, Event Grid, or HTTPS endpoint) to begin receiving payloads.