Sales price agreement modified
Fires when posting a trade-agreement journal changes an existing sales price: a journal line that was created from an existing sales-price trade agreement (so it references that PriceDiscTable row) is posted, and the standard posting code overwrites the row with the line's values. It fires only when at least one of the row's values actually changed. The payload carries the new values, the amount before the post and the names of the fields that changed, so subscribers can drive price-change notifications, margin checks or price synchronization to storefronts and external pricing engines.
| Event class | MGCLatticeTradeAgreementPriceModifiedBusinessEvent |
| Contract class | MGCLatticeTradeAgreementPriceBusinessEventContract |
| Category | Sales orders |
| Module | ModuleAxapta::SalesOrder |
| Label IDs | TradeAgrPriceModifiedName / TradeAgrPriceModifiedDesc |
| Trigger | PriceDiscAdmCheckPost.createOrUpdatePriceDiscTableRecords() -> a posted sales-price journal line updated the row it references (PriceDiscTableRef > 0) and changed at least one value |
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.
| Event | Raised by | Fires when |
|---|---|---|
| Sales price agreement created | Posting a trade-agreement journal | A 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 modified (this event) | Posting a trade-agreement journal | A posted journal line that references an existing sales price changed at least one of its values |
| Sales price agreement superseded | Posting a trade-agreement journal | A 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 deleted | PriceDiscTable.delete(), and posting a journal line marked for deletion | A sales price is deleted |
| Sales price agreement effective | Effectivity 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 expired | Effectivity 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 byMGCLatticeTradeAgreementPriceJournalClassifier. - CoC methods:
public void initJournalNum(PriceDiscJournalNum _journalNum)- records the number of the trade-agreement journal being posted.protected void createOrUpdatePriceDiscTableRecords()- beforenext, captures every sales-price row that a sales-price journal line references (the image of the row before the post); callsnext(the standard post, which updates the rows that journal lines reference and inserts the new rows with set-based statements that skip the table'sinsert()andupdate()methods, so a hook on those methods cannot see them); then classifies the posted journal's lines (PriceDiscAdmTranswith that journal number) and sends the events.
- Firing condition: the journal line has the sales price relation, is not marked for deletion and references an existing row (
PriceDiscTableRef> 0); after the post that row is a sales price; and at least one of the values the post writes differs from the captured image. A line posted without changing anything raises nothing. If two lines reference the same row, the row is reported once. - Compared values:
Amount,Currency,PriceUnit,Markup,AllocateMarkup,FromDate,ToDate,QuantityAmountFrom,QuantityAmountTo,AccountCode,AccountRelation,ItemCode,ItemRelation,UnitId,InventDimId,Relation,Module,Agreement,GenericCurrency,Percent1,Percent2,DeliveryTime,CalendarDays,DisregardLeadTime,SearchAgain,PDSCalculationId,MaximumRetailPrice_IN,AgreementHeaderExt_RUandInventBaileeFreeDays_RU- the fields the standard post writes. The ones that changed are listed, with these names, inchangedFields. Text comparisons ignore case, as elsewhere in the application. - Lines marked for deletion do not raise this event: the standard post removes the rows they reference before the classification runs (see 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()), sosend()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++catchblock 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 classifyAfterPost() that handles journal lines referencing an existing row:
while select RecId from line
where line.JournalNum == journalNum
&& line.MustBeDeleted == NoYes::No
&& line.PriceDiscTableRef != 0
&& line.Relation == PriceType::PriceSales
join row
where row.RecId == line.PriceDiscTableRef
&& row.Relation == PriceType::PriceSales
{
if (reported.in(row.RecId) || !preImageByRecId.exists(row.RecId))
{
continue;
}
before = con2Buf(preImageByRecId.lookup(row.RecId));
changedFields = MGCLatticeTradeAgreementPriceJournalClassifier::changedFields(before, row);
// A line posted without changing anything is not a modification.
if (conLen(changedFields) == 0)
{
continue;
}
reported.add(row.RecId);
if (!this.addResult(results, MGCLatticeTradeAgreementPriceChangeType::Modified, row, before.Amount, 0, changedFields))
{
break;
}
}
Payload Contract
One row per [DataMember], in declaration order. All six sales price agreement events use this contract; the sources are fields on the updated PriceDiscTable row (as it is after the post), mapped in initialize().
| JSON field | Display label | Type / EDT | Source | Notes |
|---|---|---|---|---|
legalEntity | Legal Entity | DataAreaId | PriceDiscTable.DataAreaId | Company of the trade agreement row |
changeType | Change Type | int | enum2int(MGCLatticeTradeAgreementPriceChangeType) | Integer change type: Created 0, Modified 1, Superseded 2, Deleted 3, Effective 4, Expired 5. Always 1 for this event |
changeTypeName | Change Type Name | str | enum2Symbol(enumNum(MGCLatticeTradeAgreementPriceChangeType), …) | Change type symbol; always "Modified" for this event |
priceType | Price Type | int | enum2int(PriceDiscTable.Relation) | Integer value of the PriceType enum; always 4 (PriceSales) |
priceTypeName | Price Type Name | str | enum2Symbol(enumNum(PriceType), enum2int(PriceDiscTable.Relation)) | Always "PriceSales" |
accountCode | Account Code | int | enum2int(PriceDiscTable.AccountCode) | PriceDiscPartyCodeType: 0 = Table (one customer), 1 = GroupId (customer price group), 2 = All (all customers) |
accountCodeName | Account Code Name | str | enum2Symbol(enumNum(PriceDiscPartyCodeType), …) | Symbol of accountCode: Table, GroupId or All |
accountRelation | Account Relation | PriceDiscAccountRelation | PriceDiscTable.AccountRelation | Customer account or price group; empty when accountCode is 2 |
itemCode | Item Code | int | enum2int(PriceDiscTable.ItemCode) | PriceDiscProductCodeType: 0 = Table (one item), 1 = GroupId, 2 = All |
itemCodeName | Item Code Name | str | enum2Symbol(enumNum(PriceDiscProductCodeType), …) | Symbol of itemCode: Table, GroupId or All |
itemRelation | Item Relation | PriceDiscItemRelation | PriceDiscTable.ItemRelation | Item number (or item group) |
currencyCode | Currency Code | CurrencyCode | PriceDiscTable.Currency | Currency of the price |
unitId | Unit | UnitOfMeasureSymbol | PriceDiscTable.UnitId | Unit of measure the price applies to |
quantityAmountFrom | Quantity From | AmountQty | PriceDiscTable.QuantityAmountFrom | From quantity of the row's quantity break |
quantityAmountTo | Quantity To | AmountQty | PriceDiscTable.QuantityAmountTo | To quantity of the row's quantity break; 0 when the break has no upper limit |
inventDimId | Inventory Dimensions | InventDimId | PriceDiscTable.InventDimId | Inventory dimension combination the price applies to; AllBlank when the price is not dimension-specific |
priceUnit | Price Unit | PriceUnit | PriceDiscTable.PriceUnit | Number of units the amount is quoted for |
fromDate | From Date | FromDate | PriceDiscTable.FromDate | Start of the validity period, in the \/Date(milliseconds)\/ form. A row without a start date carries 1900-01-01 (\/Date(-2208988800000)\/) |
toDate | To Date | ToDate | PriceDiscTable.ToDate | Last valid day, in the same form. An open-ended price carries 1900-01-01 (\/Date(-2208988800000)\/) |
amount | Amount | PriceDiscAmount | PriceDiscTable.Amount | The price after the post |
oldAmount | Old Amount | PriceDiscAmount | Image of the same row captured just before the post | The row's amount before the post; equal to amount when only other values changed |
discountPercent | Discount Percent | DiscPct | PriceDiscTable.Percent1 | Discount percentage stored on the row (Percent1); normally 0 on a sales price row |
priceDiscTableRecId | Price Disc Table RecId | RecId | PriceDiscTable.RecId | Record ID of the updated row (the row the journal line references) |
previousPriceDiscTableRecId | Previous Price Disc Table RecId | RecId | Not used by this event | Always 0 |
changedFields | Changed Fields | list of str | Comparison of the row before and after the post | Names of the values the post changed, for example ["Amount"] or ["ToDate"]; never empty for this event |
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": "MGCLatticeTradeAgreementPriceModifiedBusinessEvent",
"ControlNumber": 5637148826,
"EventId": "8FBC931E-8682-4572-884B-08EAC27F76D1",
"EventTime": "\/Date(1790239872000)\/",
"EventTimeIso8601": "2026-09-24T08:51:12.4031520Z",
"MajorVersion": 0,
"MinorVersion": 0,
"BusinessEventLegalEntity": "USMF",
"ContextRecordSubject": "",
"InitiatingUserAADObjectId": "{998F80C1-BD30-8C38-3B7A-2DF97276C2C2}",
"ParentContextRecordSubjects": [],
"eventSequence": 48174,
"correlationId": "Event:06159b5e-6af6-4a57-a84a-2da84772c44b",
"schemaVersion": 1,
"legalEntity": "usmf",
"changeType": 1,
"changeTypeName": "Modified",
"priceType": 4,
"priceTypeName": "PriceSales",
"accountCode": 0,
"accountCodeName": "Table",
"accountRelation": "US-004",
"itemCode": 0,
"itemCodeName": "Table",
"itemRelation": "T0002",
"currencyCode": "USD",
"unitId": "ea",
"quantityAmountFrom": 0,
"quantityAmountTo": 0,
"inventDimId": "AllBlank",
"priceUnit": 1,
"fromDate": "\/Date(-2208988800000)\/",
"toDate": "\/Date(-2208988800000)\/",
"amount": 1899.00,
"oldAmount": 1999.00,
"discountPercent": 0,
"priceDiscTableRecId": 22565424061,
"previousPriceDiscTableRecId": 0,
"changedFields": ["Amount"]
}
Activation
In the D365FO client, go to System administration > Setup > Business events > Business events catalog. Find this event by name ("Sales price agreement modified") 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.
Related Events
- Sales price agreement created
- Sales price agreement superseded
- Sales price agreement deleted
- Sales price agreement effective
- Sales price agreement expired
- Sales price trade agreement changed - record-based complement for sales-price rows inserted or updated outside a journal post