Sales price agreement effective
Fires when a sales-price trade agreement reaches its From date and becomes active. Nothing is written to the database at that moment, so no table or posting hook can observe it; the event is raised by the Lattice effectivity batch, which on each run reports the sales prices whose From date was reached since the batch's last position. Subscribers can use it to switch the active price in storefronts, point-of-sale or external pricing engines on the day it takes effect.
| Event class | MGCLatticeTradeAgreementPriceEffectiveBusinessEvent |
| Contract class | MGCLatticeTradeAgreementPriceBusinessEventContract |
| Category | Sales orders |
| Module | ModuleAxapta::SalesOrder |
| Label IDs | TradeAgrPriceEffectiveName / TradeAgrPriceEffectiveDesc |
| Trigger | MGCLatticeTradeAgreementPriceEffectivityBatch.run() (the daily Lattice sales price effectivity events background process, in every legal entity; a run from the menu item, in the legal entity it runs in) -> sales-price FromDate after the sweep's position and on or before the run 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.
| 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 | 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 (this event) | 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.
Effectivity batch
- Batch class:
MGCLatticeTradeAgreementPriceEffectivityBatch(extendsRunBaseBatch), run from the action menu itemMGCLatticeTradeAgreementPriceEffectivityBatch("Sales price agreement effectivity (effective/expired) events"). The event is raised only when this batch runs; the Lattice sales price effectivity events background process runs it daily (see Activation). The background process sweeps every legal entity that is not a virtual company, each inside that legal entity, so the activation check, the position and the event are that legal entity's. Legal entities where neither the Effective nor the Expired event is active are skipped, and a failure in one legal entity is reported ("The sales price agreement effectivity run failed in legal entity x. The other legal entities were still processed; this one is retried on the next run.") without stopping the others. A run from the menu item sweeps only the legal entity it runs in. In each legal entity, a run does the Effective sweep first, then the Expired sweep. - Position: each legal entity keeps one
MGCLatticeTradeAgreementPriceEffectivityCursorrow per sweep, holding the From date and record ID of the last sales price the Effective sweep processed (CursorDate,CursorRecId; the highest possible record ID means the whole date is done). - First run: the first run in which this event is active records the starting position - the run date - and sends nothing. Prices whose From date is on or before that date are never reported.
- Firing condition: every sales price whose From date is after the position and on or before the run date (today in the company's time zone), in (From date, record ID) order. A run covers every day since the position, so a gap between runs is caught up in one run. When the run has processed them all, the position moves to the end of the run date.
- The sweep is date based: a price saved with a From date the position has already passed - for example a back-dated price, or a price effective today that is created after today's run - is not reported as effective (the created or modified event reports it), and rows without a From date are never reported. A price whose From date is changed to a later date after it was reported is reported again when that date arrives.
- One price at a time: for each price, the event is sent and the position moves past the price in the same transaction. So an interrupted run resumes after the last price it committed and sends nothing twice; a send that fails is rolled back together with its position move, the sweep stops at that price (the run shows "The Sales price agreement effective event for sales price record n could not be sent. The sweep stopped at this price and will retry it on its next run.") and the next run retries it; and two runs at the same time in the same company never both report a price, because each price's transaction locks the position row and a price another run has already processed is skipped.
- Inactive event: while this event is not activated in the company, the Effective sweep does nothing and keeps its position (the run shows "Sales price agreement effective is not active in this company. Its sweep kept its position and will catch up once the event is activated."). Once the event is activated again, the next run reports the prices that became effective in the meantime. If the event is deactivated during a run, the sweep stops and keeps its position.
- Throttling: the batch pauses 250 ms after every 500 events it sends (Effective and Expired together), outside any transaction.
- Enablement gate: the sweep checks
BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeTradeAgreementPriceEffectiveBusinessEvent))at the start and before each price. Effective and Expired are activated, and keep their positions, independently. - Each run ends with the message "Sales price agreement effectivity run completed. Effective events sent: n. Expired events sent: n."
The Effective sweep in sweep():
while select row
order by FromDate, RecId
where row.Relation == PriceType::PriceSales
&& row.FromDate <= lastDate
&& (row.FromDate > cursorDate || (row.FromDate == cursorDate && row.RecId > cursorRecId))
{
if (!this.processRow(_changeType, row, row.FromDate))
{
break;
}
}
The per-price step, from processRow():
try
{
sentNow = false;
ttsbegin;
cursor = MGCLatticeTradeAgreementPriceEffectivityCursor::find(_changeType, true);
// The row lock above serializes concurrent runs; skip a price another run has already processed.
if (cursor.RecId
&& !MGCLatticeTradeAgreementPriceEffectivityBatch::isAtOrAfter(cursor.CursorDate, cursor.CursorRecId, _rowDate, _row.RecId))
{
if (report)
{
this.send(_changeType, _row);
sentNow = true;
}
cursor.CursorDate = _rowDate;
cursor.CursorRecId = _row.RecId;
cursor.update();
}
ttscommit;
}
catch
{
// The send and the position move were rolled back together, so the position never passes an unsent
// price. Stop here; the next run retries this price.
warning(strFmt("@MGCLattice:TradeAgrPriceEffectivitySendFailed", MGCLatticeTradeAgreementPriceEffectivityBatch::eventName(_changeType), _row.RecId));
sweepStopped = true;
return false;
}
Payload Contract
One row per [DataMember], in declaration order. All six sales price agreement events use this contract; the sources are fields on the PriceDiscTable row as it is when the batch runs, 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 4 for this event |
changeTypeName | Change Type Name | str | enum2Symbol(enumNum(MGCLatticeTradeAgreementPriceChangeType), …) | Change type symbol; always "Effective" 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 | The date the price became effective, in the \/Date(milliseconds)\/ form |
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 that became effective |
oldAmount | Old Amount | PriceDiscAmount | Not used by this event | Always 0 |
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 row |
previousPriceDiscTableRecId | Previous Price Disc Table RecId | RecId | Not used by this event | Always 0 |
changedFields | Changed Fields | list of str | Not used by this event | Always 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": "MGCLatticeTradeAgreementPriceEffectiveBusinessEvent",
"ControlNumber": 5637149320,
"EventId": "629E5013-744B-426D-BD9D-63C768BD7D17",
"EventTime": "\/Date(1790816407000)\/",
"EventTimeIso8601": "2026-10-01T01:00:07.6862844Z",
"MajorVersion": 0,
"MinorVersion": 0,
"BusinessEventLegalEntity": "USMF",
"ContextRecordSubject": "",
"InitiatingUserAADObjectId": "{1F0C417D-3EFE-2637-21FA-6590C0314E86}",
"ParentContextRecordSubjects": [],
"eventSequence": 48517,
"correlationId": "Event:8aae20df-d733-41ee-b207-ce378f51fd89",
"schemaVersion": 1,
"legalEntity": "usmf",
"changeType": 4,
"changeTypeName": "Effective",
"priceType": 4,
"priceTypeName": "PriceSales",
"accountCode": 0,
"accountCodeName": "Table",
"accountRelation": "US-004",
"itemCode": 0,
"itemCodeName": "Table",
"itemRelation": "T0005",
"currencyCode": "USD",
"unitId": "ea",
"quantityAmountFrom": 1,
"quantityAmountTo": 0,
"inventDimId": "AllBlank",
"priceUnit": 1,
"fromDate": "\/Date(1790812800000)\/",
"toDate": "\/Date(-2208988800000)\/",
"amount": 1249.00,
"oldAmount": 0,
"discountPercent": 0,
"priceDiscTableRecId": 22565424071,
"previousPriceDiscTableRecId": 0,
"changedFields": []
}
Activation
- In the D365FO client, go to System administration > Setup > Business events > Business events catalog. Find this event by name ("Sales price agreement effective") 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).
- The Lattice sales price effectivity events background process (System administration > Setup > Process automations, Background processes tab) sends this event once a day in every legal entity where it is active; there is nothing to schedule per legal entity. To send a legal entity's events at once, open System administration > Periodic tasks > Lattice business events > Sales price agreement effectivity (effective/expired) events in that legal entity (available to the Lattice Integration administrator role) and select OK. Such a run covers only that legal entity, and because each legal entity keeps its own position, nothing is sent twice. The batch has no parameters.
- The first run after the event is activated only records its starting position and sends nothing; events start with the next run.
Related Events
- Sales price agreement expired - raised by the same batch
- Sales price agreement created
- Sales price agreement superseded
- Sales price agreement modified
- Sales price agreement deleted
- Sales price trade agreement changed - record-based complement for sales-price rows inserted or updated outside a journal post