/// <summary> /// Gets maximum retail price from trade agreement. /// </summary> /// <param name="salesLine">The sales line.</param> /// <param name="context">The request context.</param> /// <returns>Maximum retail price from trade agreement.</returns> public decimal GetMaximumRetailPriceFromTradeAgreement(SalesLine salesLine, RequestContext context) { if (salesLine == null) { throw new ArgumentNullException("salesLine"); } decimal quantity = this.salesTransaction.ActiveSalesLines.Where(x => (x.ItemId == salesLine.ItemId) && (x.InventoryDimensionId == salesLine.InventoryDimensionId)).Sum(x => x.Quantity); if (quantity == decimal.Zero) { quantity = 1; } if (!salesLine.BeginDateTime.IsValidAxDateTime()) { salesLine.BeginDateTime = context.GetNowInChannelTimeZone(); } PriceResult priceResult = PricingEngine.GetActiveTradeAgreement( this.pricingDataManager, DiscountParameters.CreateAndInitialize(this.pricingDataManager), this.channelConfiguration.Currency, salesLine, quantity, this.customerId, this.priceGroup, salesLine.BeginDateTime); this.maxRetailPrice = priceResult.MaximumRetailPriceIndia; return(this.maxRetailPrice); }
/// <summary> /// Create a new DiscountParameters object and initialize from database if possible. /// </summary> /// <param name="pricingDataManager">Data manager to access pricing data.</param> /// <returns>Newly fetched and initialized DiscountParameters object.</returns> public static DiscountParameters CreateAndInitialize(IPricingDataAccessor pricingDataManager) { DiscountParameters parameters = new DiscountParameters(); parameters.GetDiscountParameters(pricingDataManager); return(parameters); }
/// <summary> /// Create a new DiscountParameters object from the passed in data. /// </summary> /// <param name="priceParameters">Data model object representing price activation options.</param> /// <returns>Newly fetched and initialized DiscountParameters object.</returns> public static DiscountParameters CreateAndInitialize(PriceParameters priceParameters) { DiscountParameters parameters = new DiscountParameters(); parameters.GetDiscountParameters(priceParameters); return(parameters); }
/// <summary> /// Get struct PriceResult from active trade agreement. /// Struct PriceResult contains India MaxRetailPrice. Currently there is a field �Max. retail price� in the form price/discount agreement journal /// (Navigation path: Main Menu > Sales and marketing > Journal > price/discount agreement journal). /// The field will be visible only when the logged on company is an India company. And it is optional. /// User can use this field to specify different MRP values on different sites and warehouses for the same item. And when the trade agreement applies to a transaction, /// the MRP value should flow to the MRP field of the transaction as the default value. /// So current change is when fetching the superset of trade agreements which could apply to all of these items and customer for the given date, /// also takes field MAXIMUMRETAILPRICE_IN through the stored procedures GETALLDISCOUNTTRADEAGREEMENTS/ GETALLTRADEAGREEMENTS/ GETTRADEAGREEMENTS. /// Then return the whole struct PriceResult rather than PriceResult.Price. /// </summary> /// <param name="tradeAgreementRules">The trade agreement rules.</param> /// <param name="priceParameters">The price parameters.</param> /// <param name="currencyCode">The currency code.</param> /// <param name="itemId">The item Id.</param> /// <param name="defaultSalesUnit">The default sales unit.</param> /// <param name="salesUnit">The sales unit.</param> /// <param name="variantLine">The variant line.</param> /// <param name="unitOfMeasureConversion">The UnitOfMeasure Conversion.</param> /// <param name="quantity">The quantity.</param> /// <param name="customerId">The customer Id.</param> /// <param name="customerPriceGroup">The customer price group.</param> /// <param name="channelPriceGroupIds">The channel price group Ids.</param> /// <param name="salesLines">Optional sales lines.</param> /// <param name="priceContext">Price context.</param> /// <param name="activeDate">The active date.</param> /// <returns>The PriceResult of active trade agreement.</returns> internal static PriceResult GetPriceResultOfActiveTradeAgreement( IDictionary <string, IList <TradeAgreement> > tradeAgreementRules, DiscountParameters priceParameters, string currencyCode, string itemId, string defaultSalesUnit, string salesUnit, ProductVariant variantLine, UnitOfMeasureConversion unitOfMeasureConversion, decimal quantity, string customerId, string customerPriceGroup, IEnumerable <string> channelPriceGroupIds, IEnumerable <SalesLine> salesLines, PriceContext priceContext, DateTimeOffset activeDate) { PriceResult result; variantLine = variantLine ?? new ProductVariant(); // Get basic arguments for Price evaluation RetailPriceArgs args = new RetailPriceArgs() { Barcode = string.Empty, CurrencyCode = currencyCode, CustomerId = customerId, Dimensions = variantLine, DefaultSalesUnitOfMeasure = defaultSalesUnit, ItemId = itemId, PriceGroups = channelPriceGroupIds.AsReadOnly(), Quantity = quantity, SalesUOM = salesUnit, UnitOfMeasureConversion = unitOfMeasureConversion, }; // Get the active retail price - checks following prices brackets in order: Customer TAs, Store price group TAs, 'All' TAs. // First bracket to return a price 'wins'. Each bracket returns the lowest price it can find. result = FindPriceAgreement(tradeAgreementRules, priceParameters, args, salesLines, priceContext, activeDate); // Direct customer TA price would have been caught above. // Compare against customer price group TAs now and override if lower than previously found price (or if previously found price was 0). if (!string.IsNullOrEmpty(customerId) && !string.IsNullOrEmpty(customerPriceGroup) && !channelPriceGroupIds.Contains(customerPriceGroup)) { // Customer price group args.PriceGroups = new ReadOnlyCollection <string>(new[] { customerPriceGroup }); PriceResult customerResult = FindPriceAgreement(tradeAgreementRules, priceParameters, args, salesLines, priceContext, activeDate); // Pick the Customer price if either the Retail price is ZERO, or the Customer Price is non-zero AND lower if ((result.Price == decimal.Zero) || ((customerResult.Price > decimal.Zero) && (customerResult.Price <= result.Price))) { result = customerResult; } } return(result); }
private static PriceResult FindPriceAgreement( IDictionary <string, IList <TradeAgreement> > tradeAgreementRules, DiscountParameters priceParameters, RetailPriceArgs args, IEnumerable <SalesLine> salesLines, PriceContext priceContext, DateTimeOffset activeDate) { // First we get the price according to the base UOM PriceAgreementArgs p = args.AgreementArgsForDefaultSales(); PriceResult result = ApplyPriceTradeAgreements(tradeAgreementRules, p, priceParameters, salesLines, priceContext, activeDate); // Is the current UOM something different than the base UOM? if (args.SalesUOM != args.DefaultSalesUnitOfMeasure) { // Then let's see if we find some price agreement for that UOM p = args.ArgreementArgsForSale(); PriceResult salesUOMResult = ApplyPriceTradeAgreements(tradeAgreementRules, p, priceParameters, salesLines, priceContext, activeDate); // If there is a price found then we return that as the price if (salesUOMResult.Price > decimal.Zero) { return(salesUOMResult); } else { return(new PriceResult(result.Price * args.UnitOfMeasureConversion.GetFactorForQuantity(args.Quantity), result.IncludesTax, custPriceGroup: result.CustPriceGroup)); } } // else we return baseUOM price mulitplied with the unit qty factor. return(result); }
/// <summary> /// Implements the IPricingCalculator interface to calculate item price trade agreement prices. /// </summary> /// <param name="salesLines">The item lines which need prices.</param> /// <param name="priceContext">The configuration of the overall pricing context for the calculation.</param> /// <param name="pricingDataManager">Instance of pricing data manager to access pricing data.</param> /// <returns>Sets of possible price lines keyed by item line Id.</returns> public Dictionary <string, IEnumerable <PriceLine> > CalculatePriceLines( IEnumerable <SalesLine> salesLines, PriceContext priceContext, IPricingDataAccessor pricingDataManager) { Tuple <DateTimeOffset, DateTimeOffset> dateRange = PricingEngine.GetMinAndMaxActiveDates(salesLines, priceContext.ActiveDate); // look up all trade agreements for given items and context HashSet <string> itemIds = new HashSet <string>(salesLines.Select(s => s.ItemId).Distinct(), StringComparer.OrdinalIgnoreCase); ReadOnlyCollection <TradeAgreement> tradeAgreements = pricingDataManager.ReadPriceTradeAgreements( itemIds, PriceContextHelper.GetAllPriceGroupsForPrice(priceContext), priceContext.CustomerAccount, dateRange.Item1, dateRange.Item2, priceContext.CurrencyCode, QueryResultSettings.AllRecords) as ReadOnlyCollection <TradeAgreement>; if (priceContext.IsDiagnosticsCollected && tradeAgreements.Any()) { priceContext.PricingEngineDiagnosticsObject.AddTradeAgreementsConsidered(tradeAgreements.ToList()); } var agreementsByItemId = IndexAgreementsByItemId(tradeAgreements); var discountParameters = DiscountParameters.CreateAndInitialize(priceContext.PriceParameters); Dictionary <string, IEnumerable <PriceLine> > itemPriceLines; Dictionary <string, decimal> itemQuantites = null; if (priceContext.PriceCalculationMode == PricingCalculationMode.Transaction) { itemQuantites = GetItemQuantities(salesLines); } itemPriceLines = new Dictionary <string, IEnumerable <PriceLine> >(StringComparer.OrdinalIgnoreCase); foreach (SalesLine salesLine in salesLines) { Tuple <decimal, string> priceCustPriceGroup = CalculateAgreementPriceLine(salesLines, salesLine, priceContext, agreementsByItemId, discountParameters, itemQuantites); if (priceCustPriceGroup.Item1 != decimal.Zero) { itemPriceLines.Add(salesLine.LineId, new List <PriceLine>(1) { ConstructTradeAgreementPriceLine(priceCustPriceGroup) }); } } return(itemPriceLines); }
/// <summary> /// This function takes arguments (customer, item, currency, etc.) related to price (trade) agreement /// as well as the set of currently enabled trade agreement types. It returns the best trade agreement /// price for the given constraints. /// As in AX, the method searches for a price on the given item which has been given to a /// customer, price group, or anyone (in given precedence order). If a price is found and marked as /// SearchAgain=False, the search will terminate. Otherwise, search for lowest price will continue. /// To recap, the logic is that three searches are done for customer, price group, and all, each bracket /// will return the lowest price it has for the constraints. If it has SearchAgain=True, then the search /// for lowest price continues to the next bracket. /// </summary> /// <param name="tradeAgreementRules">Trade agreements applicable to each item, keyed by item relation (i.e. item Id).</param> /// <param name="args">Arguments for price agreement search.</param> /// <param name="priceParameters">Set of enabled price agreement types.</param> /// <param name="salesLines">Sales lines.</param> /// <param name="priceContext">Price context.</param> /// <param name="activeDate">Date to use for querying trade agreement rules.</param> /// <returns> /// Most applicable price for the given price agreement constraints. /// </returns> private static PriceResult ApplyPriceTradeAgreements( IDictionary <string, IList <TradeAgreement> > tradeAgreementRules, PriceAgreementArgs args, DiscountParameters priceParameters, IEnumerable <SalesLine> salesLines, PriceContext priceContext, DateTimeOffset activeDate) { PriceResult priceResult = new PriceResult(0M, PriceGroupIncludesTax.NotSpecified); var itemCodes = new PriceDiscountItemCode[] { PriceDiscountItemCode.Item, PriceDiscountItemCode.ItemGroup, PriceDiscountItemCode.AllItems }; var accountCodes = new PriceDiscountAccountCode[] { PriceDiscountAccountCode.Customer, PriceDiscountAccountCode.CustomerGroup, PriceDiscountAccountCode.AllCustomers }; // Search through combinations of item/account codes from most to least specific. // This needs to match the behavior of AX code PriceDisc.findPriceAgreement(). foreach (var accountCode in accountCodes) { foreach (var itemCode in itemCodes) { if (priceParameters.Activation(PriceDiscountType.PriceSales, accountCode, itemCode)) { IList <string> accountRelations = args.GetAccountRelations(accountCode); string itemRelation = args.GetItemRelation(itemCode); if (accountRelations.All(a => ValidRelation(accountCode, a)) && ValidRelation(itemCode, itemRelation)) { bool searchAgain; IEnumerable <TradeAgreement> tradeAgreements = FindPriceAgreements(tradeAgreementRules, args, itemCode, accountCode, salesLines, priceContext, activeDate); PriceResult currentPriceResult = GetBestPriceAgreement(tradeAgreements, out searchAgain); if (priceResult.Price == 0M || (currentPriceResult.Price > 0M && currentPriceResult.Price < priceResult.Price)) { priceResult = currentPriceResult; } if (!searchAgain) { break; } } } } } return(priceResult); }
private static Tuple <decimal, string> CalculateAgreementPriceLine( IEnumerable <SalesLine> salesLines, SalesLine salesLine, PriceContext priceContext, Dictionary <string, IList <TradeAgreement> > agreementsByItemId, DiscountParameters discountParameters, Dictionary <string, decimal> itemQuantites) { var quantity = salesLine.Quantity; // count all occurrences for this item if this is a transaction if (priceContext.PriceCalculationMode == PricingCalculationMode.Transaction) { itemQuantites.TryGetValue(salesLine.ItemId, out quantity); if (quantity == decimal.Zero) { quantity = 1m; } } var activeDate = (salesLine.SalesDate != null) ? salesLine.SalesDate.Value : priceContext.ActiveDate; return(GetActiveTradeAgreementPriceAndGroup( agreementsByItemId, discountParameters, priceContext.CurrencyCode, salesLine.ItemId, salesLine.OriginalSalesOrderUnitOfMeasure, Discount.GetUnitOfMeasure(salesLine), salesLine.Variant, salesLine.UnitOfMeasureConversion, quantity, priceContext.CustomerAccount, priceContext.CustomerPriceGroup, PriceContextHelper.GetApplicablePriceGroupsForPrice(priceContext, salesLine.CatalogIds), salesLines, priceContext, activeDate)); }
private static Tuple <decimal, string> GetActiveTradeAgreementPriceAndGroup( IDictionary <string, IList <TradeAgreement> > tradeAgreementRules, DiscountParameters priceParameters, string currencyCode, string itemId, string defaultSalesUnit, string salesUnit, ProductVariant variantLine, UnitOfMeasureConversion unitOfMeasureConversion, decimal quantity, string customerId, string customerPriceGroup, IEnumerable <string> channelPriceGroupIds, IEnumerable <SalesLine> salesLines, PriceContext priceContext, DateTimeOffset activeDate) { PriceResult result = GetPriceResultOfActiveTradeAgreement( tradeAgreementRules, priceParameters, currencyCode, itemId, defaultSalesUnit, salesUnit, variantLine, unitOfMeasureConversion, quantity, customerId, customerPriceGroup, channelPriceGroupIds, salesLines, priceContext, activeDate); return(new Tuple <decimal, string>(result.Price, result.CustPriceGroup)); }
/// <summary> /// Initializes a new instance of the <see cref="MultilineDiscountCalculator"/> class with given configurations. /// </summary> /// <param name="discountParameters">Configuration dictating which trade agreement discount combinations are active.</param> /// <param name="priceContext">Price context.</param> public MultilineDiscountCalculator(DiscountParameters discountParameters, PriceContext priceContext) { this.discountParameters = discountParameters; this.priceContext = priceContext; }
/// <summary> /// Find and total all multiline discount trade agreements that match the given relations and quantity. /// </summary> /// <param name="tradeAgreements">Trade agreement collection to calculate on.</param> /// <param name="itemCode">The item code to search by (item group or all).</param> /// <param name="retailTransaction">The transaction context with Id and customer Id.</param> /// <param name="priceGroup">Multiline price group.</param> /// <param name="salesQuantity">Aggregated quantity for multiline price group.</param> /// <param name="percent1">Percentage one.</param> /// <param name="percent2">Percentage two.</param> /// <param name="discountAmount">Discount amount.</param> private void GetMultiLineDiscountLine( List <TradeAgreement> tradeAgreements, PriceDiscountItemCode itemCode, SalesTransaction retailTransaction, string priceGroup, decimal salesQuantity, out decimal percent1, out decimal percent2, out decimal discountAmount) { PriceDiscountType relation = PriceDiscountType.MultilineDiscountSales; // Sales multiline discount - 6 ProductVariant dimension = new ProductVariant(); percent1 = decimal.Zero; percent2 = decimal.Zero; discountAmount = decimal.Zero; bool searchAgain = true; var codes = new PriceDiscountAccountCode[] { PriceDiscountAccountCode.Customer, PriceDiscountAccountCode.CustomerGroup, PriceDiscountAccountCode.AllCustomers }; foreach (var accountCode in codes) { // skip to next configuration if this one isn't enabled if (!this.discountParameters.Activation(relation, accountCode, itemCode)) { continue; } // get item relation based on item code string itemRelation = (itemCode == PriceDiscountItemCode.ItemGroup) ? priceGroup : string.Empty; itemRelation = itemRelation ?? string.Empty; // get customer relation based on account code string accountRelation = string.Empty; if (accountCode == PriceDiscountAccountCode.Customer) { accountRelation = retailTransaction.CustomerId; } else if (accountCode == PriceDiscountAccountCode.CustomerGroup) { accountRelation = this.priceContext.CustomerMultipleLinePriceGroup; } accountRelation = accountRelation ?? string.Empty; // if both relations are valid for the given item and account codes, look for trade agreements matching these relations if (DiscountParameters.ValidRelation(accountCode, accountRelation) && DiscountParameters.ValidRelation(itemCode, itemRelation)) { // get any active multiline discount trade agreement matching relations and quantity var priceDiscTable = Discount.GetPriceDiscData(tradeAgreements, relation, itemRelation, accountRelation, itemCode, accountCode, salesQuantity, this.priceContext, dimension, false); // compute running sum of discount values found foreach (TradeAgreement row in priceDiscTable) { percent1 += row.PercentOne; percent2 += row.PercentTwo; discountAmount += row.Amount; // stop search when we find a trade agreement set to not find next trade agreement if (!row.ShouldSearchAgain) { searchAgain = false; } } } // stop search if we found a discount without "find next" marked if (!searchAgain) { break; } } }
/// <summary> /// Initializes the specified runtime. /// </summary> /// <param name="pricingDataManager">Provides data access to the calculation.</param> public void Initialize(IPricingDataAccessor pricingDataManager) { this.DiscountParameters = DiscountParameters.CreateAndInitialize(pricingDataManager); }
private void GetLineDiscountLines( List <TradeAgreement> tradeAgreements, SalesLine saleItem, ref decimal absQty, ref decimal discountAmount, ref decimal percent1, ref decimal percent2, ref decimal minQty) { int idx = 0; while (idx < 9) { PriceDiscountItemCode itemCode = (PriceDiscountItemCode)(idx % 3); // Mod divsion PriceDiscountAccountCode accountCode = (PriceDiscountAccountCode)(idx / 3); string accountRelation = string.Empty; if (accountCode == PriceDiscountAccountCode.Customer) { accountRelation = this.priceContext.CustomerAccount; } else if (accountCode == PriceDiscountAccountCode.CustomerGroup) { accountRelation = this.priceContext.CustomerLinePriceGroup; } accountRelation = accountRelation ?? string.Empty; string itemRelation; if (itemCode == PriceDiscountItemCode.Item) { itemRelation = saleItem.ItemId; } else { Item item = PriceContextHelper.GetItem(this.priceContext, saleItem.ItemId); itemRelation = item != null ? item.LineDiscountGroupId : string.Empty; } itemRelation = itemRelation ?? string.Empty; PriceDiscountType relation = PriceDiscountType.LineDiscountSales; // Sales line discount - 5 if (this.discountParameters.Activation(relation, accountCode, itemCode)) { if (DiscountParameters.ValidRelation(accountCode, accountRelation) && DiscountParameters.ValidRelation(itemCode, itemRelation)) { bool dimensionDiscountFound = false; if (saleItem.Variant != null && !string.IsNullOrEmpty(saleItem.Variant.VariantId)) { var dimensionPriceDiscTable = Discount.GetPriceDiscData(tradeAgreements, relation, itemRelation, accountRelation, itemCode, accountCode, absQty, this.priceContext, saleItem.Variant, true); foreach (TradeAgreement row in dimensionPriceDiscTable) { bool unitsAreUndefinedOrEqual = string.IsNullOrEmpty(row.UnitOfMeasureSymbol) || string.Equals(row.UnitOfMeasureSymbol, saleItem.SalesOrderUnitOfMeasure, StringComparison.OrdinalIgnoreCase); if (unitsAreUndefinedOrEqual) { percent1 += row.PercentOne; percent2 += row.PercentTwo; discountAmount += row.Amount; minQty += row.QuantityAmountFrom; } if (percent1 > 0M || percent2 > 0M || discountAmount > 0M) { dimensionDiscountFound = true; } if (!row.ShouldSearchAgain) { idx = 9; } } } if (!dimensionDiscountFound) { var priceDiscTable = Discount.GetPriceDiscData(tradeAgreements, relation, itemRelation, accountRelation, itemCode, accountCode, absQty, this.priceContext, saleItem.Variant, false); foreach (TradeAgreement row in priceDiscTable) { // Apply default if the unit of measure is not set from the cart. string unitOfMeasure = Discount.GetUnitOfMeasure(saleItem); bool unitsAreUndefinedOrEqual = string.IsNullOrEmpty(row.UnitOfMeasureSymbol) || string.Equals(row.UnitOfMeasureSymbol, unitOfMeasure, StringComparison.OrdinalIgnoreCase); if (unitsAreUndefinedOrEqual) { percent1 += row.PercentOne; percent2 += row.PercentTwo; discountAmount += row.Amount; minQty += row.QuantityAmountFrom; } if (!row.ShouldSearchAgain) { idx = 9; } } } } } idx++; } }
internal static PriceResult GetActiveTradeAgreement(IPricingDataAccessor pricingDataManager, DiscountParameters priceParameters, string currencyCode, SalesLine saleItem, decimal quantity, string customerId, string customerPriceGroup, DateTimeOffset dateToCheck) { dateToCheck = saleItem.SalesDate ?? dateToCheck; IEnumerable <PriceGroup> priceGroups = pricingDataManager.GetChannelPriceGroups() as IEnumerable <PriceGroup>; HashSet <string> priceGroupIds = new HashSet <string>(priceGroups.Select(pg => pg.GroupId).Distinct(StringComparer.OrdinalIgnoreCase)); PriceResult result; ProductVariant variantLine = GetVariantFromLineOrDatabase(pricingDataManager, saleItem); variantLine = variantLine ?? new ProductVariant(); Tuple <DateTimeOffset, DateTimeOffset> dateRange = GetMinAndMaxActiveDates(new SalesLine[] { saleItem }, dateToCheck); ReadOnlyCollection <TradeAgreement> agreements = pricingDataManager.ReadPriceTradeAgreements( new HashSet <string> { saleItem.ItemId }, priceGroupIds, customerId, dateRange.Item1, dateRange.Item2, currencyCode, QueryResultSettings.AllRecords) as ReadOnlyCollection <TradeAgreement>; var agreementDict = new Dictionary <string, IList <TradeAgreement> >(StringComparer.OrdinalIgnoreCase); agreementDict.Add(saleItem.ItemId, agreements); result = TradeAgreementCalculator.GetPriceResultOfActiveTradeAgreement( agreementDict, priceParameters, currencyCode, saleItem.ItemId, saleItem.OriginalSalesOrderUnitOfMeasure, Discount.GetUnitOfMeasure(saleItem), variantLine, saleItem.UnitOfMeasureConversion, quantity, customerId, customerPriceGroup, priceGroupIds, new List <SalesLine> { saleItem }, new PriceContext(), dateToCheck); return(result); }