Skip to main content

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 classMGCLatticeSalesOrderCanceledBusinessEvent
Contract classMGCLatticeSalesOrderCanceledBusinessEventContract
CategorySales orders
ModuleModuleAxapta::SalesOrder
Label IDsSalesOrderCanceledBusinessEventName / SalesOrderCanceledBusinessEventDescription
TriggerSalesUpdateRemain.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) and public void cancelMultipleOrdersDeliveryRemainder(List _salesRecIdList)
  • Firing condition: next cancelOpenOrderLinesDeliveryRemainder must return true (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 committed SalesStatus must equal SalesStatus::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 own ttsbegin/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 returns true and 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 calls SalesUpdateRemain.cancelMultipleOrdersDeliveryRemainder) routes orders that are not call-center or Commerce-payments orders through cancelOpenOrderLinesDeliveryRemainder, so they fire (once the action has finished).
    • Customer-order cancellations submitted from Commerce through the Commerce transaction service call cancelOpenOrderLinesDeliveryRemainder directly, 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) or MCRSalesOrderCancellation::cancelOrderFTC (the FTC event batch), which do not go through cancelOpenOrderLinesDeliveryRemainder. They are signaled by a second extension, MGCLatticeMCRSalesOrderCancellation_Class_Extension ([ExtensionOf(classStr(MCRSalesOrderCancellation))]), wrapping public static boolean cancelOrder(SalesTable _salesTable, boolean _displayErrors, boolean _nsfCancel) and cancelOrderFTC with the same signature. When the event is activated (checked first), each wrapper reads the order's status before next; afterwards MGCLatticeSalesOrderCanceledBusinessEvent::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.
  • Transaction: the event is sent after the cancellation's own transaction has completed (after cancelOpenOrderLinesDeliveryRemainder returns, 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 fieldDisplay labelType / EDTSourceNotes
legalEntityLegal EntityDataAreaIdSalesTable.DataAreaIdCompany / data area of the sales order
salesOrderIdSales Order IDSalesIdSalesTable.SalesIdSales order number
customerAccountCustomer AccountCustAccountSalesTable.CustAccountCustomer account on the order
salesStatusSales Statusintenum2int(SalesTable.SalesStatus)Integer value of the SalesStatus enum; always 4 (Canceled) because of the firing condition
salesStatusNameSales Status Namestrenum2Symbol(enumNum(SalesStatus), enum2int(SalesTable.SalesStatus))Symbolic enum name string; always "Canceled"
salesTypeSales Typeintenum2int(SalesTable.SalesType)Integer value of the SalesType enum: 3 = Sales, 4 = ReturnItem (return order), 2 = Subscription, 0 = Journal
salesTypeNameSales Type Namestrenum2Symbol(enumNum(SalesType), enum2int(SalesTable.SalesType))Symbolic enum name string, for example "Sales" or "ReturnItem"
rmaNumberRMA NumberReturnItemNumSalesTable.ReturnItemNum when SalesType is ReturnItemReturn merchandise authorization number; empty for other order types
crossReferencesCross referencesList of MGCLatticeCrossReferenceEventContractMGCLatticeCrossReferenceEventHelper::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.