Skip to main content

Sales price agreement superseded

Fires when posting a trade-agreement journal adds a new sales price for an agreement - account, item, currency, unit, quantity break and inventory dimensions - that already had a sales price in effect on the new price's From date: typically a new price with a later From date that takes over from the current one. The payload carries the new row's values plus the amount and record ID of the earlier price it takes over from, so subscribers can announce upcoming price changes, schedule catalog updates or track price history. The earlier price itself is not changed by this: if it has no To date it stays in effect alongside the new one, unless the journal also end-dates it.

Event classMGCLatticeTradeAgreementPriceSupersededBusinessEvent
Contract classMGCLatticeTradeAgreementPriceBusinessEventContract
CategorySales orders
ModuleModuleAxapta::SalesOrder
Label IDsTradeAgrPriceSupersededName / TradeAgrPriceSupersededDesc
TriggerPriceDiscAdmCheckPost.createOrUpdatePriceDiscTableRecords() -> a posted sales-price journal line inserted a row for an agreement that had a sales price in effect on its From date

Trigger / Hook​

Which path raises which event​

The six sales price agreement events share one payload contract and are told apart by changeType / changeTypeName. All six are limited to sales price agreements - PriceDiscTable rows whose relation is Price (sales) (PriceType::PriceSales); purchase prices and discount agreements never raise them, and journal lines with another relation are ignored. Below, agreement means a row's account code and account relation, item code and item relation, currency, unit, quantity break (from quantity) and inventory dimensions; the dates and the amount are not part of it.

EventRaised byFires when
Sales price agreement createdPosting a trade-agreement journalA posted journal line inserted a sales price for an agreement that had no sales price in effect on the new price's From date
Sales price agreement modifiedPosting a trade-agreement journalA posted journal line that references an existing sales price changed at least one of its values
Sales price agreement superseded (this event)Posting a trade-agreement journalA posted journal line inserted a sales price for an agreement that already had a sales price in effect on the new price's From date
Sales price agreement deletedPriceDiscTable.delete(), and posting a journal line marked for deletionA sales price is deleted
Sales price agreement effectiveEffectivity batch (background process, every legal entity)A sales price's From date is after the batch's position and on or before the run date
Sales price agreement expiredEffectivity batch (background process, every legal entity)A sales price's To date is after the batch's position and before the run date, and no other sales price of the same agreement is in effect on the day after that To date

Sales-price rows that are inserted or updated outside a journal post - for example through a data entity or by code that calls PriceDiscTable.insert() or update() - are not classified by these events; the record-based Sales price trade agreement changed event (catalog category Accounts receivable) reports them. The journal post itself writes PriceDiscTable with set-based statements; whether those writes also raise the record-based event depends on the Accounts receivable parameters Skip price agreement insert alerts (rows the post inserts) and Skip price agreement update alerts (rows it updates). Set to Yes, a journal post raises only the events in the table above; set to No, the record-based event is also raised for each such row.

Journal posting hook​

  • Extension class: MGCLatticePriceDiscAdmCheckPost_Class_Extension, decorated with [ExtensionOf(classStr(PriceDiscAdmCheckPost))]; the classification is done by MGCLatticeTradeAgreementPriceJournalClassifier.
  • CoC methods:
    • public void initJournalNum(PriceDiscJournalNum _journalNum) - records the number of the trade-agreement journal being posted.
    • protected void createOrUpdatePriceDiscTableRecords() - before next, finds for each inserting sales-price journal line the existing sales price of the same agreement that is in effect on the line's From date (its From date is on or before, and its To date is empty or on or after, the line's From date; the latest-starting one when several are); calls next (the standard post, which updates the rows that journal lines reference and inserts the new rows with set-based statements that skip the table's insert() and update() methods, so a hook on those methods cannot see them); then classifies the posted journal's lines (PriceDiscAdmTrans with that journal number) and sends the events.
  • Firing condition: the journal line has the sales price relation, is not marked for deletion and does not reference an existing row (PriceDiscTableRef is 0); the row the post inserted for it is found exactly, through the journal line it was copied from (PriceDiscTable.OriginalPriceDiscAdmTransRecId), and is a sales price; and a sales price of the same agreement was in effect on the line's From date before the post. Without one the line raises Sales price agreement created instead. Prices of another quantity break or other inventory dimensions, and prices that ended before (or start after) the new From date, do not count.
  • oldAmount and previousPriceDiscTableRecId are the amount and record ID of that earlier price, as captured before the post.
  • The event does not end-date or otherwise change the earlier price, and does not check whether it was ended. When the same journal also edits the earlier price (for example to set its To date), that journal line raises Sales price agreement modified on its own. A price deleted by a line of the same journal marked for deletion is gone before the classification runs, so the new price is Created (and the deleted one raises Sales price agreement deleted).
  • Coverage: every posting route that goes through PriceDiscAdmCheckPost::runFromContract() - Post on the trade-agreement journal, posting in batch, and journals posted programmatically. Validate-only runs post nothing and raise nothing.
  • Volume cap: at most 1,000 events are sent per posted journal, counting Created, Modified and Superseded together (only the activated ones). Updated rows are reported first, then inserted rows, which is the order the standard post writes them. When the cap is reached the post shows the message "Sales-price trade-agreement event emission was capped at 1000 events for journal journal number; remaining changes were not emitted individually." and the remaining changes raise no event.
  • Transaction and failure handling: the hook runs inside the journal-posting transaction (PriceDiscAdmCheckPost::runFromContract()), so send() only writes to the business events commit log and delivery happens after commit; a post that fails or is rolled back never delivers. Inside that transaction an X++ catch block does not run, so the hook does not rely on one: every lookup is guarded, a line whose data is missing is skipped, and nothing in the classification throws on purpose.
  • Enablement gate: the classification checks BusinessEventsConfigurationReader::isBusinessEventEnabled() for each of Created, Modified and Superseded once per post, so the three are activated independently; when none is active the hook does no work at all.
  • No Lattice parameters or other setup are required.

The CoC wrapper:

protected void createOrUpdatePriceDiscTableRecords()
{
MGCLatticeTradeAgreementPriceJournalClassifier classifier = MGCLatticeTradeAgreementPriceJournalClassifier::newForPosting(latticeJournalNum);

classifier.snapshotBeforePost();

next createOrUpdatePriceDiscTableRecords();

classifier.emit(classifier.classifyAfterPost());
}

The part of snapshotBeforePost() that finds the price each inserting line takes over from:

while select line
where line.JournalNum == journalNum
&& line.MustBeDeleted == NoYes::No
&& line.PriceDiscTableRef == 0
&& line.Relation == PriceType::PriceSales
join RecId, Amount, FromDate from prior
where prior.Relation == PriceType::PriceSales
&& prior.ItemCode == line.ItemCode
&& prior.ItemRelation == line.ItemRelation
&& prior.AccountCode == line.AccountCode
&& prior.AccountRelation == line.AccountRelation
&& prior.Currency == line.Currency
&& prior.InventDimId == line.InventDimId
&& prior.UnitId == line.UnitId
&& prior.QuantityAmountFrom == line.QuantityAmountFrom
&& prior.FromDate <= line.FromDate
&& (prior.ToDate == dateNull() || prior.ToDate >= line.FromDate)
{
if (this.isLaterPrior(line.RecId, prior.FromDate, prior.RecId))
{
priorByLineRecId.insert(line.RecId, [prior.RecId, prior.Amount, prior.FromDate]);
}
}

Payload Contract​

One row per [DataMember], in declaration order. All six sales price agreement events use this contract; the sources are fields on the new PriceDiscTable row, mapped in initialize(), plus the earlier price's amount and record ID.

JSON fieldDisplay labelType / EDTSourceNotes
legalEntityLegal EntityDataAreaIdPriceDiscTable.DataAreaIdCompany of the trade agreement row
changeTypeChange Typeintenum2int(MGCLatticeTradeAgreementPriceChangeType)Integer change type: Created 0, Modified 1, Superseded 2, Deleted 3, Effective 4, Expired 5. Always 2 for this event
changeTypeNameChange Type Namestrenum2Symbol(enumNum(MGCLatticeTradeAgreementPriceChangeType), …)Change type symbol; always "Superseded" for this event
priceTypePrice Typeintenum2int(PriceDiscTable.Relation)Integer value of the PriceType enum; always 4 (PriceSales)
priceTypeNamePrice Type Namestrenum2Symbol(enumNum(PriceType), enum2int(PriceDiscTable.Relation))Always "PriceSales"
accountCodeAccount Codeintenum2int(PriceDiscTable.AccountCode)PriceDiscPartyCodeType: 0 = Table (one customer), 1 = GroupId (customer price group), 2 = All (all customers)
accountCodeNameAccount Code Namestrenum2Symbol(enumNum(PriceDiscPartyCodeType), …)Symbol of accountCode: Table, GroupId or All
accountRelationAccount RelationPriceDiscAccountRelationPriceDiscTable.AccountRelationCustomer account or price group; empty when accountCode is 2
itemCodeItem Codeintenum2int(PriceDiscTable.ItemCode)PriceDiscProductCodeType: 0 = Table (one item), 1 = GroupId, 2 = All
itemCodeNameItem Code Namestrenum2Symbol(enumNum(PriceDiscProductCodeType), …)Symbol of itemCode: Table, GroupId or All
itemRelationItem RelationPriceDiscItemRelationPriceDiscTable.ItemRelationItem number (or item group)
currencyCodeCurrency CodeCurrencyCodePriceDiscTable.CurrencyCurrency of the price
unitIdUnitUnitOfMeasureSymbolPriceDiscTable.UnitIdUnit of measure the price applies to
quantityAmountFromQuantity FromAmountQtyPriceDiscTable.QuantityAmountFromFrom quantity of the row's quantity break
quantityAmountToQuantity ToAmountQtyPriceDiscTable.QuantityAmountToTo quantity of the row's quantity break; 0 when the break has no upper limit
inventDimIdInventory DimensionsInventDimIdPriceDiscTable.InventDimIdInventory dimension combination the price applies to; AllBlank when the price is not dimension-specific
priceUnitPrice UnitPriceUnitPriceDiscTable.PriceUnitNumber of units the amount is quoted for
fromDateFrom DateFromDatePriceDiscTable.FromDateStart of the new price's validity period, in the \/Date(milliseconds)\/ form. A row without a start date carries 1900-01-01 (\/Date(-2208988800000)\/)
toDateTo DateToDatePriceDiscTable.ToDateLast valid day, in the same form. An open-ended price carries 1900-01-01 (\/Date(-2208988800000)\/)
amountAmountPriceDiscAmountPriceDiscTable.AmountThe new price
oldAmountOld AmountPriceDiscAmountThe earlier price, captured before the postAmount of the price the new row takes over from
discountPercentDiscount PercentDiscPctPriceDiscTable.Percent1Discount percentage stored on the row (Percent1); normally 0 on a sales price row
priceDiscTableRecIdPrice Disc Table RecIdRecIdPriceDiscTable.RecIdRecord ID of the new row
previousPriceDiscTableRecIdPrevious Price Disc Table RecIdRecIdThe earlier price, captured before the postRecord ID of the price the new row takes over from
changedFieldsChanged Fieldslist of strNot used by this eventAlways an empty array

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": "MGCLatticeTradeAgreementPriceSupersededBusinessEvent",
"ControlNumber": 5637148768,
"EventId": "F67040E9-F6A1-4174-850D-9822976F04C4",
"EventTime": "\/Date(1790172175000)\/",
"EventTimeIso8601": "2026-09-23T14:02:55.0076891Z",
"MajorVersion": 0,
"MinorVersion": 0,
"BusinessEventLegalEntity": "USMF",
"ContextRecordSubject": "",
"InitiatingUserAADObjectId": "{7331806D-539C-0E59-6B9E-A892917043E6}",
"ParentContextRecordSubjects": [],
"eventSequence": 48066,
"correlationId": "Event:57b106f7-048d-47f0-9943-c51f8c66a5b2",
"schemaVersion": 1,
"legalEntity": "usmf",
"changeType": 2,
"changeTypeName": "Superseded",
"priceType": 4,
"priceTypeName": "PriceSales",
"accountCode": 0,
"accountCodeName": "Table",
"accountRelation": "US-040",
"itemCode": 0,
"itemCodeName": "Table",
"itemRelation": "T0004",
"currencyCode": "USD",
"unitId": "ea",
"quantityAmountFrom": 1,
"quantityAmountTo": 0,
"inventDimId": "AllBlank",
"priceUnit": 1,
"fromDate": "\/Date(1798761600000)\/",
"toDate": "\/Date(-2208988800000)\/",
"amount": 229.00,
"oldAmount": 215.00,
"discountPercent": 0,
"priceDiscTableRecId": 22565424075,
"previousPriceDiscTableRecId": 22565424068,
"changedFields": []
}

Activation​

In the D365FO client, go to System administration > Setup > Business events > Business events catalog. Find this event by name ("Sales price agreement superseded") 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). No other setup is needed: the event fires whenever a trade-agreement journal with qualifying sales-price lines is posted.