/// <summary> /// Gets the discount data. /// </summary> /// <param name="tradeAgreements">Trade agreement collection to calculate on.</param> /// <param name="relation">The relation (line, multiline, total).</param> /// <param name="itemRelation">The item relation.</param> /// <param name="accountRelation">The account relation.</param> /// <param name="itemCode">The item code (table, group, all).</param> /// <param name="accountCode">The account code (table, group, all).</param> /// <param name="quantityAmount">The quantity or amount that sets the minimum quantity or amount needed.</param> /// <param name="priceContext">The price context.</param> /// <param name="itemDimensions">The item dimensions.</param> /// <param name="includeDimensions">A value indicating whether to include item dimensions.</param> /// <returns> /// A collection of discount agreement arguments. /// </returns> internal static ReadOnlyCollection <TradeAgreement> GetPriceDiscData( List <TradeAgreement> tradeAgreements, PriceDiscountType relation, string itemRelation, string accountRelation, PriceDiscountItemCode itemCode, PriceDiscountAccountCode accountCode, decimal quantityAmount, PriceContext priceContext, ProductVariant itemDimensions, bool includeDimensions) { accountRelation = accountRelation ?? string.Empty; itemRelation = itemRelation ?? string.Empty; string targetCurrencyCode = priceContext.CurrencyCode ?? string.Empty; string inventColorId = (itemDimensions != null && itemDimensions.ColorId != null && includeDimensions) ? itemDimensions.ColorId : string.Empty; string inventSizeId = (itemDimensions != null && itemDimensions.SizeId != null && includeDimensions) ? itemDimensions.SizeId : string.Empty; string inventStyleId = (itemDimensions != null && itemDimensions.StyleId != null && includeDimensions) ? itemDimensions.StyleId : string.Empty; string inventConfigId = (itemDimensions != null && itemDimensions.ConfigId != null && includeDimensions) ? itemDimensions.ConfigId : string.Empty; DateTime today = priceContext.ActiveDate.DateTime; DateTime noDate = new DateTime(1900, 1, 1); ReadOnlyCollection <TradeAgreement> foundAgreements; foundAgreements = GetAgreementsFromCollection(tradeAgreements, relation, itemRelation, accountRelation, itemCode, accountCode, quantityAmount, targetCurrencyCode, inventColorId, inventSizeId, inventStyleId, inventConfigId, today, noDate); return(foundAgreements); }
/// <summary> /// Returns true or false, whether a certain relation is active for a discount search. /// </summary> /// <param name="relation">The trade agreement relation(price, line discount, multiline discount, total discount).</param> /// <param name="accountCode">The account code(table,group,all).</param> /// <param name="itemCode">The item code(table,group,all).</param> /// <returns>Returns true if the relation is active, else false.</returns> public bool Activation(PriceDiscountType relation, PriceDiscountAccountCode accountCode, PriceDiscountItemCode itemCode) { // if parameters haven't been fetched, return false for all if (this.enabledCombinations == null) { return(false); } // look up whether the combination is enabled return(this.enabledCombinations.Contains(CreateCombo(relation, accountCode, itemCode))); }
/// <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); }
/// <summary> /// True if there is a valid relation between the account code and relation. /// </summary> /// <param name="accountCode">The customer account code to validate against (customer/customer group/all).</param> /// <param name="relation">The relation to validate.</param> /// <returns>True if the relation is compatible with the account code.</returns> private static bool ValidRelation(PriceDiscountAccountCode accountCode, string relation) { bool ok = true; if (!string.IsNullOrEmpty(relation) && (accountCode == PriceDiscountAccountCode.AllCustomers)) { ok = false; } if (string.IsNullOrEmpty(relation) && (accountCode != PriceDiscountAccountCode.AllCustomers)) { ok = false; } return(ok); }
/// <summary> /// Gets price agreement 'account relations' based on arguments and given account relation code. /// </summary> /// <param name="accountCode">Account relation code (customer/price group/all).</param> /// <returns> /// Returns customer if 'customer' code given, price groups if 'group' code given, otherwise empty. /// </returns> public ReadOnlyCollection <string> GetAccountRelations(PriceDiscountAccountCode accountCode) { ReadOnlyCollection <string> accountRelations = new ReadOnlyCollection <string>(new List <string> { string.Empty }); if (accountCode == PriceDiscountAccountCode.Customer && !string.IsNullOrEmpty(this.CustomerId)) { accountRelations = new ReadOnlyCollection <string>(new List <string> { this.CustomerId }); } else if (accountCode == PriceDiscountAccountCode.CustomerGroup && this.PriceGroups.Count > 0) { accountRelations = this.PriceGroups; } return(accountRelations); }
/// <summary> /// True if there is a valid relation between the account code and relation. /// </summary> /// <param name="accountCode">The account code (table,group,all).</param> /// <param name="relation">The account relation.</param> /// <returns>True if the account code allows the given relation.</returns> public static bool ValidRelation(PriceDiscountAccountCode accountCode, string relation) { if (relation == null) { throw new ArgumentNullException("relation"); } bool ok = true; if (!string.IsNullOrEmpty(relation) && (accountCode == PriceDiscountAccountCode.AllCustomers)) { ok = false; } if (string.IsNullOrEmpty(relation) && (accountCode != PriceDiscountAccountCode.AllCustomers)) { ok = false; } return(ok); }
/// <summary> /// Helper method to create new 3-tuple of trade agreement combination. /// </summary> /// <param name="relation">The type of trade agreement (price, line discount, etc.).</param> /// <param name="accountCode">The type of customer (single, group, all).</param> /// <param name="itemCode">The type of item (single, group, all).</param> /// <returns>3-tuple containing the given combination.</returns> private static Tuple <PriceDiscountType, PriceDiscountAccountCode, PriceDiscountItemCode> CreateCombo(PriceDiscountType relation, PriceDiscountAccountCode accountCode, PriceDiscountItemCode itemCode) { return(new Tuple <PriceDiscountType, PriceDiscountAccountCode, PriceDiscountItemCode>(relation, accountCode, itemCode)); }
/// <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; } } }
private static ReadOnlyCollection <TradeAgreement> GetAgreementsFromCollection(List <TradeAgreement> tradeAgreements, PriceDiscountType relation, string itemRelation, string accountRelation, PriceDiscountItemCode itemCode, PriceDiscountAccountCode accountCode, decimal quantityAmount, string targetCurrencyCode, string inventColorId, string inventSizeId, string inventStyleId, string inventConfigId, DateTime today, DateTime noDate) { ReadOnlyCollection <TradeAgreement> foundAgreements; foundAgreements = tradeAgreements .Where(ta => ta.Relation == relation && ta.ItemCode == itemCode && string.Equals(ta.ItemRelation, itemRelation, StringComparison.OrdinalIgnoreCase) && ta.AccountCode == accountCode && string.Equals(ta.AccountRelation, accountRelation, StringComparison.OrdinalIgnoreCase) && string.Equals(ta.Currency, targetCurrencyCode, StringComparison.OrdinalIgnoreCase) && ta.QuantityAmountFrom <= Math.Abs(quantityAmount) && (ta.QuantityAmountTo >= Math.Abs(quantityAmount) || ta.QuantityAmountTo == 0) && ((ta.FromDate <= today || ta.FromDate <= noDate) && (ta.ToDate >= today || ta.ToDate <= noDate)) && (string.IsNullOrWhiteSpace(ta.ColorId) || ta.ColorId.Equals(inventColorId, StringComparison.OrdinalIgnoreCase)) && (string.IsNullOrWhiteSpace(ta.SizeId) || ta.SizeId.Equals(inventSizeId, StringComparison.OrdinalIgnoreCase)) && (string.IsNullOrWhiteSpace(ta.StyleId) || ta.StyleId.Equals(inventStyleId, StringComparison.OrdinalIgnoreCase)) && (string.IsNullOrWhiteSpace(ta.ConfigId) || ta.ConfigId.Equals(inventConfigId, StringComparison.OrdinalIgnoreCase))) .AsReadOnly(); return(foundAgreements); }
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++; } }
private static IEnumerable <TradeAgreement> FindPriceAgreements( IDictionary <string, IList <TradeAgreement> > tradeAgreementRules, PriceAgreementArgs args, PriceDiscountItemCode itemCode, PriceDiscountAccountCode accountCode, IEnumerable <SalesLine> salesLines, PriceContext priceContext, DateTimeOffset activeDate) { string itemRelation = args.GetItemRelation(itemCode); IList <string> accountRelations = args.GetAccountRelations(accountCode); string unitId = args.GetUnitId(itemCode); // price trade agreements are always item-specific, so first filter by itemId. IList <TradeAgreement> rulesForItem; if (!tradeAgreementRules.TryGetValue(itemRelation, out rulesForItem)) { return(new List <TradeAgreement>(0)); } List <TradeAgreement> tradeAgreementsOfVariantUnFilteredByQuantity = new List <TradeAgreement>(); List <TradeAgreement> tradeAgreementsOfMasterOrProduct = new List <TradeAgreement>(); // Get the initial filtered trade agreements, not checking quantity. for (int i = 0; i < rulesForItem.Count; i++) { var t = rulesForItem[i]; if (t.ItemRelation.Equals(itemRelation, StringComparison.OrdinalIgnoreCase) && t.ItemCode == itemCode && t.AccountCode == accountCode && accountRelations.Contains(t.AccountRelation) && t.Currency.Equals(args.CurrencyCode, StringComparison.OrdinalIgnoreCase) && (t.FromDate.DateTime <= activeDate.Date || t.FromDate.DateTime <= NoDate) && (t.ToDate.DateTime >= activeDate.Date || t.ToDate.DateTime <= NoDate) && (string.IsNullOrWhiteSpace(unitId) || t.UnitOfMeasureSymbol.Equals(unitId, StringComparison.OrdinalIgnoreCase)) && t.IsVariantMatch(args.Dimensions)) { if (t.IsVariant) { tradeAgreementsOfVariantUnFilteredByQuantity.Add(t); } else { if (t.IsQuantityMatch(args.Quantity)) { tradeAgreementsOfMasterOrProduct.Add(t); } } } } // For variants if (args.IsVariant) { List <TradeAgreement> tradeAgreementsOfVariant = new List <TradeAgreement>(); List <TradeAgreement> tradeAgreementsOfVariantExactMatch = new List <TradeAgreement>(); foreach (TradeAgreement t in tradeAgreementsOfVariantUnFilteredByQuantity) { if (t.IsVariant) { decimal aggregatedQuantityByAgreementVariant = decimal.Zero; foreach (SalesLine salesLine in salesLines) { if (string.Equals(args.ItemId, salesLine.ItemId, StringComparison.OrdinalIgnoreCase) && t.IsVariantMatch(salesLine.Variant)) { aggregatedQuantityByAgreementVariant += salesLine.Quantity; } } if (aggregatedQuantityByAgreementVariant == decimal.Zero) { aggregatedQuantityByAgreementVariant = 1m; } if (t.IsQuantityMatch(aggregatedQuantityByAgreementVariant)) { if (t.IsVariantExactMatch(args.Dimensions)) { tradeAgreementsOfVariantExactMatch.Add(t); } tradeAgreementsOfVariant.Add(t); } } } // 1. Return exact matches if any if (tradeAgreementsOfVariantExactMatch != null && tradeAgreementsOfVariantExactMatch.Any()) { if (accountCode == PriceDiscountAccountCode.CustomerGroup) { RetainTopPriorityTradeAgreements(tradeAgreementsOfVariantExactMatch, priceContext); } tradeAgreementsOfVariantExactMatch.Sort(AgreementSortMethod); return(tradeAgreementsOfVariantExactMatch); } // 2. Return (partial) variant matches if any. if (tradeAgreementsOfVariant.Count > 0) { if (accountCode == PriceDiscountAccountCode.CustomerGroup) { RetainTopPriorityTradeAgreements(tradeAgreementsOfVariant, priceContext); } TradeAgreementComparer tradeAgreementComparator = new TradeAgreementComparer(tradeAgreementsOfVariant, args.Dimensions); tradeAgreementComparator.SortTradeAgreement(); return(tradeAgreementsOfVariant); } } // 3. Return non-variant matches. if (accountCode == PriceDiscountAccountCode.CustomerGroup) { RetainTopPriorityTradeAgreements(tradeAgreementsOfMasterOrProduct, priceContext); } tradeAgreementsOfMasterOrProduct.Sort(AgreementSortMethod); return(tradeAgreementsOfMasterOrProduct); }
/// <summary> /// The calculation of the total customer discount. /// </summary> /// <param name="tradeAgreements">Trade agreement collection to calculate on. If null, uses the pricing data manager to find agreements.</param> /// <param name="retailTransaction">The retail transaction which needs total discounts.</param> /// <returns> /// The retail transaction. /// </returns> public SalesTransaction CalcTotalCustomerDiscount( List <TradeAgreement> tradeAgreements, SalesTransaction retailTransaction) { if (tradeAgreements != null && tradeAgreements.Any()) { decimal totalAmount = 0; // Find the total amount as a basis for the total discount // Consider calculable lines only. Ignore voided or return-by-receipt lines. var clonedTransaction = retailTransaction.Clone <SalesTransaction>(); foreach (var clonedSalesLine in clonedTransaction.PriceCalculableSalesLines) { if (this.IsTotalDiscountAllowed(clonedSalesLine.ItemId)) { SalesLineTotaller.CalculateLine(clonedTransaction, clonedSalesLine, d => this.priceContext.CurrencyAndRoundingHelper.Round(d)); totalAmount += clonedSalesLine.NetAmountWithAllInclusiveTax; } } decimal absTotalAmount = Math.Abs(totalAmount); // Find the total discounts. PriceDiscountType relation = PriceDiscountType.EndDiscountSales; // Total sales discount - 7 PriceDiscountItemCode itemCode = PriceDiscountItemCode.AllItems; // All items - 2 PriceDiscountAccountCode accountCode = 0; string itemRelation = string.Empty; decimal percent1 = 0m; decimal percent2 = 0m; decimal discountAmount = 0m; ProductVariant dimension = new ProductVariant(); int idx = 0; while (idx < /* Max(PriceDiscAccountCode) */ 3) { // Check discounts for Store Currency accountCode = (PriceDiscountAccountCode)idx; string accountRelation = string.Empty; if (accountCode == PriceDiscountAccountCode.Customer) { accountRelation = retailTransaction.CustomerId; } else if (accountCode == PriceDiscountAccountCode.CustomerGroup) { accountRelation = this.priceContext.CustomerTotalPriceGroup; } accountRelation = accountRelation ?? string.Empty; // Only get Active discount combinations if (this.discountParameters.Activation(relation, (PriceDiscountAccountCode)accountCode, (PriceDiscountItemCode)itemCode)) { var priceDiscTable = Discount.GetPriceDiscData(tradeAgreements, relation, itemRelation, accountRelation, itemCode, accountCode, absTotalAmount, this.priceContext, dimension, false); foreach (TradeAgreement row in priceDiscTable) { percent1 += row.PercentOne; percent2 += row.PercentTwo; discountAmount += row.Amount; if (!row.ShouldSearchAgain) { idx = 3; } } } idx++; } decimal totalPercentage = DiscountLine.GetCompoundedPercentage(percent1, percent2); if (discountAmount != decimal.Zero) { this.AddTotalDiscAmountLines(retailTransaction, DiscountLineType.CustomerDiscount, discountAmount); } if (totalPercentage != 0) { // Update the sale items. // Consider calculable lines only. Ignore voided or return-by-receipt lines. foreach (var saleItem in retailTransaction.PriceCalculableSalesLines) { if (this.IsTotalDiscountAllowed(saleItem.ItemId)) { DiscountLine discountItem = GetCustomerDiscountItem(saleItem, CustomerDiscountType.TotalDiscount, DiscountLineType.CustomerDiscount); discountItem.Percentage = totalPercentage; } } } } return(retailTransaction); }