/// <summary> /// 查询客户产品折扣表 /// </summary> /// <param name="DiscountDTOList"></param> /// <returns></returns> public List <ReturnDiscountDTO> GetDiscount(List <InDiscountDTO> DiscountDTOList) { List <ReturnDiscountDTO> ReturnDiscountList = new List <ReturnDiscountDTO>(); //查找条件: //客户产品折扣表.客户=销售订单.客户 //客户产品折扣表.产品=销售订单行.料品 //客户产品折扣表行.币种=销售订单.币种 for (int i = 0; i < DiscountDTOList.Count; i++) { //DiscountHead.EntityList DiscountList = DiscountHead.Finder.FindAll("Custmor.ID={0} and DiscountLine.CurrencyType.ID={1} and Product.ID={2}", new OqlParam[] { new OqlParam(DiscountDTOList[i].Customer.ID), new OqlParam(DiscountDTOList[i].Currency.ID), new OqlParam(DiscountDTOList[i].ItemMaster.ID) }); DiscountLine Discount = DiscountLine.Finder.Find(string.Format("DiscountHead.Custmor.ID={0} and DiscountHead.Product.ID={1} and ValidDate<=@ValidDate and UnValidDate>=@UnValidDate", DiscountDTOList[i].Customer.ID, DiscountDTOList[i].ItemMaster.ID), new OqlParam(DateTime.Now.ToString()), new OqlParam(DateTime.Now.ToString())); //and CurrencyType.ID={1} DiscountDTOList[i].Currency.ID, // 折扣表单价。折扣方式,折扣比例,外销价,销售订单行ID if (Discount != null) { ReturnDiscountDTO ReturnDiscount = new ReturnDiscountDTO(); ReturnDiscount.DiscountID = Discount.ID; //折扣表行ID ReturnDiscount.DiscountPrices = Discount.Prices; //单价 ReturnDiscount.DiscountRatio = Discount.Discount; //折扣比例 ReturnDiscount.DiscountType = Discount.DiscountType; //折扣方式 ReturnDiscount.SoLineID = DiscountDTOList[i].SoLineID; //销售订单行ID ReturnDiscount.ExportPrices = DiscountDTOList[i].ExportPrices; //外销价 ReturnDiscount.Currency = DiscountDTOList[i].Currency; //币种 ReturnDiscountList.Add(ReturnDiscount); } } return(ReturnDiscountList); }
private static bool CanApplyCompoundedDiscount( IEnumerable <DiscountLine> existingDiscountLines, DiscountLine newDiscountLine, bool requiresExistingCompounded) { bool canApplyCompoundedDiscount = true; if (newDiscountLine.PeriodicDiscountType == PeriodicDiscountOfferType.Threshold || newDiscountLine.PeriodicDiscountType == PeriodicDiscountOfferType.Offer) { // Threshold and discount offer don't discount for partial quantity, so no need to worry about requiresExistingCompounded; canApplyCompoundedDiscount = CanCompoundTogether(existingDiscountLines, newDiscountLine); } else { // This is really for multiple compounded mix and match discounts, each of which taking partial quantity. // We'd compound them together as much as possible. if (existingDiscountLines.Any()) { canApplyCompoundedDiscount = CanCompoundTogether(existingDiscountLines, newDiscountLine); } else { // No existing discount if (requiresExistingCompounded) { canApplyCompoundedDiscount = false; } } } return(canApplyCompoundedDiscount); }
public void createDiscountLine() { getApiKey(); conekta.Api.version = "2.0.0"; Order order = new conekta.Order().create(@"{ ""currency"":""MXN"", ""customer_info"": { ""name"": ""Jul Ceballos"", ""phone"": ""+5215555555555"", ""email"": ""*****@*****.**"" }, ""line_items"": [{ ""name"": ""Box of Cohiba S1s"", ""unit_price"": 35000, ""quantity"": 1 }] }"); Assert.AreEqual(order.id.GetType().ToString(), "System.String"); order = new Order().find(order.id); DiscountLine discount_line = order.createDiscountLine(@"{ ""code"": ""123"", ""type"": ""loyalty"", ""amount"": 600 }"); Assert.AreEqual(discount_line.code, "123"); Assert.AreEqual(discount_line.type, "loyalty"); }
/// <summary> /// Update the discount items. /// </summary> /// <param name="saleItem">The item line that the discount line is added to.</param> /// <param name="discountItem">The new discount line to add.</param> internal static void UpdateDiscountLines(SalesLine saleItem, DiscountLine discountItem) { // Check if line discount is found, if so then update bool discountLineFound = false; foreach (var discLine in saleItem.DiscountLines) { if (discLine.DiscountLineType == DiscountLineType.CustomerDiscount) { // If found then update if ((discLine.DiscountLineType == discountItem.DiscountLineType) && (discLine.CustomerDiscountType == discountItem.CustomerDiscountType)) { discLine.Percentage = discountItem.Percentage; discLine.Amount = discountItem.Amount; discountLineFound = true; } } } // If line discount is not found then add it. if (!discountLineFound) { saleItem.DiscountLines.Add(discountItem); } if (discountItem.DiscountLineType == DiscountLineType.CustomerDiscount) { saleItem.ResetLineMultilineDiscountOnItem(); } saleItem.WasChanged = true; }
/// <summary> /// Retrieves a customer discount item of the indicated type if it exists and creates one if not. /// </summary> /// <param name="salesLine">The sales line from which to find customer discount lines.</param> /// <param name="customerDiscountType">The customer discount type.</param> /// <param name="lineDiscountType">The line discount type.</param> /// <returns> /// The discount line. /// </returns> private static DiscountLine GetCustomerDiscountItem(SalesLine salesLine, CustomerDiscountType customerDiscountType, DiscountLineType lineDiscountType) { DiscountLine discount; var discounts = from d in salesLine.DiscountLines where d.DiscountLineType == lineDiscountType && d.CustomerDiscountType == customerDiscountType select d; // If the discount doesn't exist create a new one if (discounts.Count() == 0) { discount = new DiscountLine { DiscountLineType = lineDiscountType, CustomerDiscountType = customerDiscountType, }; salesLine.DiscountLines.Add(discount); } else { // otherwise select it. discount = discounts.First(); } return(discount); }
/// <summary> /// Updates the sales transaction with the quantity promotion if applicable. /// </summary> /// <param name="existingTransaction">Existing transaction.</param> /// <param name="tempSalesTransaction">Copy of existing transaction.</param> /// <param name="context">The request context.</param> /// <param name="salesLineIndex">The sales line under consideration.</param> /// <param name="discountLine">The quantity discount under consideration.</param> /// <param name="multiBuyDiscountLines">The multi buy discount lines.</param> private static void GetQuantityPromotions(SalesTransaction existingTransaction, SalesTransaction tempSalesTransaction, RequestContext context, int salesLineIndex, DiscountLine discountLine, IEnumerable <QuantityDiscountLevel> multiBuyDiscountLines) { // Get the multi buy discount lines for the current multi buy discount. IEnumerable <QuantityDiscountLevel> multiBuyLinesForCurrentOffer = multiBuyDiscountLines.Where(j => j.OfferId.Equals(discountLine.OfferId)).OrderBy(l => l.MinimumQuantity); List <SalesLine> salesLinesWithSameProduct = tempSalesTransaction.SalesLines.Where(j => j.ItemId == tempSalesTransaction.SalesLines[salesLineIndex].ItemId && j.InventoryDimensionId == tempSalesTransaction.SalesLines[salesLineIndex].InventoryDimensionId).ToList(); decimal totalQuantity = salesLinesWithSameProduct.Select(j => j.Quantity).Sum(); decimal currentQuantity = tempSalesTransaction.SalesLines[salesLineIndex].Quantity; salesLinesWithSameProduct.Remove(tempSalesTransaction.SalesLines[salesLineIndex]); bool neverApplied = true; foreach (QuantityDiscountLevel multiBuyLine in multiBuyLinesForCurrentOffer) { // removing the quantity discounts that were not applied (because of concurrency rules). if (multiBuyLine.MinimumQuantity <= totalQuantity) { continue; } // Temporarily update the current transaction with the new quantity to see if the quantity discount will be applied. existingTransaction.SalesLines[salesLineIndex].Quantity = multiBuyLine.MinimumQuantity - totalQuantity + currentQuantity; CartWorkflowHelper.Calculate(context, existingTransaction, CalculationModes.All); DiscountLine isApplied = existingTransaction.SalesLines[salesLineIndex].DiscountLines.Where(j => j.OfferId == discountLine.OfferId).SingleOrDefault(); // If the quantity discount will be applied then remove the discount line from the lines with same product and get the min quantity to buy for discount. if (isApplied != null && (isApplied.Amount != 0 || isApplied.Percentage != 0)) { int toBuy = (int)(multiBuyLine.MinimumQuantity - totalQuantity); if (isApplied.Amount != 0) { discountLine.OfferName = string.Format(CultureInfo.CurrentUICulture, Resources.MultiBuyDiscountPricePromotion, toBuy, Math.Round(isApplied.Amount, 2)); } else { discountLine.OfferName = string.Format(CultureInfo.CurrentUICulture, Resources.MultiBuyDiscountPercentagePromotion, toBuy, Math.Round(isApplied.Percentage, 2)); } neverApplied = false; break; } } if (neverApplied) { tempSalesTransaction.SalesLines[salesLineIndex].DiscountLines.Remove(discountLine); } existingTransaction.SalesLines[salesLineIndex].Quantity = currentQuantity; CartWorkflowHelper.Calculate(context, existingTransaction, CalculationModes.All); foreach (SalesLine sameproductCartLine in salesLinesWithSameProduct) { sameproductCartLine.DiscountLines.Remove(sameproductCartLine.DiscountLines.Where(k => k.OfferId == discountLine.OfferId).SingleOrDefault()); } }
/// <summary> /// Attempts to apply a DiscountLine to a SalesLine that has a larger quantity than the required quantity, splitting the line if it is found. /// </summary> /// <param name="transaction">The current transaction containing the lines.</param> /// <param name="availableLines">The available line indices on the transaction.</param> /// <param name="quantityNeeded">The quantity needed for the DiscountLine.</param> /// <param name="discount">The DiscountLine and original quantity needed.</param> /// <param name="isReturn">True if it's return.</param> /// <param name="requiresExistingCompoundedDiscounts">A flag indicating whether it requires existing compounded discounts on the line to compound on top of.</param> /// <returns>True if a match was found and the discount was applied, false otherwise.</returns> private bool ApplyDiscountLineForLargerMatch( SalesTransaction transaction, HashSet <int> availableLines, ref decimal quantityNeeded, DiscountLineQuantity discount, bool isReturn, bool requiresExistingCompoundedDiscounts) { bool discountApplied = false; foreach (int x in availableLines.ToList().OrderBy(p => this.salesLines[p].Quantity)) { SalesLine salesLine = this.salesLines[x]; if (!IsQuantityMatchSalesOrReturn(isReturn, salesLine.Quantity)) { continue; } decimal lineQuantity = Math.Abs(salesLine.Quantity); if (lineQuantity > quantityNeeded) { if (discount.DiscountLine.ConcurrencyMode != ConcurrencyMode.Compounded || CanApplyCompoundedDiscount(this.salesLines[x].DiscountLines, discount.DiscountLine, requiresExistingCompoundedDiscounts)) { // Perform the split of this line SalesLine newLine = this.SplitLine(this.salesLines[x], quantityNeeded); // Add the new line to the transaction and to the available lines for discounts. Set the line number to the next available number. newLine.LineNumber = transaction.SalesLines.Max(p => p.LineNumber) + 1; transaction.SalesLines.Add(newLine); this.salesLines.Add(newLine); DiscountLine discountLine = discount.DiscountLine.Clone <DiscountLine>(); discountLine.SaleLineNumber = newLine.LineNumber; newLine.DiscountLines.Add(discountLine); newLine.QuantityDiscounted = quantityNeeded * Math.Sign(salesLine.Quantity); // If this is a compounding discount, add the new line to the available lines. if (discount.DiscountLine.ConcurrencyMode == ConcurrencyMode.Compounded) { availableLines.Add(this.salesLines.Count - 1); } discountApplied = true; quantityNeeded = 0; break; } } } return(discountApplied); }
/// <summary> /// Calculate the periodic discounts for the transaction. /// </summary> /// <param name="pricingDataManager">Provides data access to the calculation.</param> /// <param name="currencyAndRoundingHelper">Currency and rounding helper.</param> /// <param name="transaction">The sales transaction.</param> /// <param name="customer">Customer for the transaction containing customer discount groups.</param> /// <param name="doesPriceIncludeTax">Does the channel have tax-inclusive prices.</param> /// <param name="currencyCode">Currency code to filter discounts by.</param> /// <param name="channelDateTime">Channel time.</param> internal static void GetAllPeriodicDisc( IPricingDataAccessor pricingDataManager, ICurrencyOperations currencyAndRoundingHelper, SalesTransaction transaction, Customer customer, bool doesPriceIncludeTax, string currencyCode, DateTimeOffset channelDateTime) { if (transaction == null) { throw new ArgumentNullException("transaction"); } // Clear all periodic, customer, standard, total discounts ClearDiscountLinesOfType(transaction, null); PriceContext priceContext = BuildPriceContext(pricingDataManager, currencyAndRoundingHelper, customer, transaction, currencyCode, channelDateTime, doesPriceIncludeTax, DiscountCalculationMode.CalculateAll); PricingEngine.PopulateProductIds(pricingDataManager, priceContext, transaction); ReadOnlyCollection <PeriodicDiscount> periodicDiscountData = GetRetailDiscounts(transaction.ActiveSalesLines, priceContext, pricingDataManager, QueryResultSettings.AllRecords); foreach (SalesLine salesLine in transaction.PriceCalculableSalesLines) { ReadOnlyCollection <PeriodicDiscount> applicablePeriodicDiscounts = Discount.GetPeriodicDiscountData(periodicDiscountData, salesLine.ItemId, salesLine.InventoryDimensionId); foreach (PeriodicDiscount periodicDiscount in applicablePeriodicDiscounts) { var discountLine = new DiscountLine { OfferId = periodicDiscount.OfferId, OfferName = periodicDiscount.Name, Percentage = periodicDiscount.DiscountPercent, Amount = periodicDiscount.DiscountAmount, ConcurrencyMode = periodicDiscount.ConcurrencyMode, ConcurrencyModeValue = (int)periodicDiscount.ConcurrencyMode, DiscountLineType = DiscountLineType.PeriodicDiscount, DiscountLineTypeValue = (int)DiscountLineType.PeriodicDiscount, PeriodicDiscountType = periodicDiscount.PeriodicDiscountType, PeriodicDiscountTypeValue = (int)periodicDiscount.PeriodicDiscountType, DiscountApplicationGroup = salesLine.ItemId, IsDiscountCodeRequired = periodicDiscount.IsDiscountCodeRequired }; salesLine.DiscountLines.Add(discountLine); } } }
/// <summary> /// Apply the given multiline discount row to the given sales line if discount amounts have been specified. /// </summary> /// <param name="salesLine">The sales line which will receive the discount.</param> /// <param name="percent1">Percentage one.</param> /// <param name="percent2">Percentage two.</param> /// <param name="discountAmount">Discount amount.</param> private static void ApplyMultilineDiscount(SalesLine salesLine, decimal percent1, decimal percent2, decimal discountAmount) { if (percent1 > decimal.Zero || percent2 > decimal.Zero || discountAmount > decimal.Zero) { DiscountLine discountItem = new DiscountLine { DiscountLineType = DiscountLineType.CustomerDiscount, CustomerDiscountType = CustomerDiscountType.MultilineDiscount, Percentage = DiscountLine.GetCompoundedPercentage(percent1, percent2), Amount = discountAmount, }; Discount.UpdateDiscountLines(salesLine, discountItem); } }
private static DiscountLine NewDiscountLineCompoundedThreshold(string offerId, decimal salesLineNumber, decimal amount, int priorityNumber) { DiscountLine discountLine = new DiscountLine() { OfferId = offerId, SaleLineNumber = salesLineNumber, DiscountLineType = Microsoft.Dynamics.Commerce.Runtime.DataModel.DiscountLineType.PeriodicDiscount, PeriodicDiscountType = PeriodicDiscountOfferType.Threshold, ConcurrencyMode = ConcurrencyMode.Compounded, IsCompoundable = true, PricingPriorityNumber = priorityNumber, Amount = amount, }; return(discountLine); }
private static void ParseAndCreateDiscountLine(XElement discountDetail, SalesLine lineItem) { DiscountLine lineDiscount = new DiscountLine(); lineDiscount.EffectiveAmount = Convert.ToDecimal(discountDetail.Attribute("Amount").Value); lineDiscount.Amount = Convert.ToDecimal(discountDetail.Attribute("DiscountAmount").Value); lineDiscount.DiscountLineType = (DiscountLineType)Convert.ToInt32(discountDetail.Attribute("DiscountOriginType").Value); lineDiscount.DiscountCode = discountDetail.Attribute("DiscountCode").Value; lineDiscount.CustomerDiscountType = (CustomerDiscountType)Convert.ToInt32(discountDetail.Attribute("CustomerDiscountType").Value); lineDiscount.ManualDiscountType = (ManualDiscountType)Convert.ToInt32(discountDetail.Attribute("ManualDiscountType").Value); lineDiscount.OfferId = discountDetail.Attribute("PeriodicDiscountOfferId").Value; lineDiscount.Percentage = Convert.ToDecimal(discountDetail.Attribute("Percentage").Value); lineDiscount.DealPrice = Convert.ToDecimal(discountDetail.Attribute("DealPrice").Value); lineItem.DiscountLines.Add(lineDiscount); }
/// <summary> /// Attempts to apply a DiscountLine to a SalesLine that has a smaller quantity than the required quantity. /// </summary> /// <param name="availableLines">The available line indices on the transaction.</param> /// <param name="quantityNeeded">The quantity needed for the DiscountLine.</param> /// <param name="discount">The DiscountLine and original quantity needed.</param> /// <param name="isReturn">True if it's return.</param> /// <param name="requiresExistingCompoundedDiscounts">A flag indicating whether it requires existing compounded discounts on the line to compound on top of.</param> /// <returns>True if the discount was applied to a line, false otherwise.</returns> private bool ApplyDiscountLineForSmallerMatch( HashSet <int> availableLines, ref decimal quantityNeeded, DiscountLineQuantity discount, bool isReturn, bool requiresExistingCompoundedDiscounts) { bool discountPartiallyApplied = false; foreach (int x in availableLines.ToList().OrderByDescending(p => this.salesLines[p].Quantity)) { SalesLine salesLine = this.salesLines[x]; if (!IsQuantityMatchSalesOrReturn(isReturn, salesLine.Quantity)) { continue; } decimal lineQuantity = Math.Abs(salesLine.Quantity); if (lineQuantity < quantityNeeded) { if (discount.DiscountLine.ConcurrencyMode != ConcurrencyMode.Compounded || CanApplyCompoundedDiscount(this.salesLines[x].DiscountLines, discount.DiscountLine, requiresExistingCompoundedDiscounts)) { DiscountLine discountLine = discount.DiscountLine.Clone <DiscountLine>(); discountLine.SaleLineNumber = salesLine.LineNumber; salesLine.DiscountLines.Add(discountLine); salesLine.QuantityDiscounted = salesLine.Quantity; if (discount.DiscountLine.ConcurrencyMode != ConcurrencyMode.Compounded) { availableLines.Remove(x); } discountPartiallyApplied = true; quantityNeeded -= lineQuantity; break; } } } return(discountPartiallyApplied); }
public void updateDiscountLine() { conekta.Api.apiKey = "key_eYvWV7gSDkNYXsmr"; conekta.Api.version = "2.0.0"; Order order = new conekta.Order().create(@"{ ""currency"":""MXN"", ""customer_info"": { ""name"": ""Jul Ceballos"", ""phone"": ""5575553324"", ""email"": ""*****@*****.**"" }, ""line_items"": [{ ""name"": ""Box of Cohiba S1s"", ""unit_price"": 35000, ""quantity"": 1 }] }" ); Assert.AreEqual(order.id.GetType().ToString(), "System.String"); order = new Order().find(order.id); DiscountLine discount_line = order.createDiscountLine(@"{ ""code"": ""234"", ""type"": ""loyalty"", ""amount"": 600 }" ); order = new Order().find(order.id); discount_line = (DiscountLine)order.discount_lines.at(0); discount_line = discount_line.update(@"{ ""amount"": 700, ""code"": ""567"", ""type"": ""coupon"" }" ); Assert.AreEqual(discount_line.type, "coupon"); Assert.AreEqual(discount_line.code, "567"); Assert.AreEqual(discount_line.amount, 700); }
/// <summary> /// Calculates manual line discount sent from cashier. /// Should be called only after other discounts are calculated. /// </summary> /// <param name="transaction">Transaction to calculate manual total discounts on.</param> public void CalculateLineManualDiscount(SalesTransaction transaction) { if (transaction == null) { throw new ArgumentNullException("transaction"); } // Consider calculable lines only. Ignore voided or return-by-receipt lines. foreach (var salesLine in transaction.PriceCalculableSalesLines) { Discount.ClearManualDiscountLinesOfType(salesLine, ManualDiscountType.LineDiscountAmount); Discount.ClearManualDiscountLinesOfType(salesLine, ManualDiscountType.LineDiscountPercent); DiscountLine lineDiscountItem = null; if (salesLine.LineManualDiscountPercentage != 0) { // Add a new line discount lineDiscountItem = new DiscountLine { DiscountLineType = DiscountLineType.ManualDiscount, ManualDiscountType = ManualDiscountType.LineDiscountPercent, Percentage = salesLine.LineManualDiscountPercentage, }; this.AddLineDiscount(transaction, salesLine, lineDiscountItem); } if (salesLine.LineManualDiscountAmount != 0) { // Add a new line discount lineDiscountItem = new DiscountLine { DiscountLineType = DiscountLineType.ManualDiscount, ManualDiscountType = ManualDiscountType.LineDiscountAmount, Amount = salesLine.Quantity != decimal.Zero ? salesLine.LineManualDiscountAmount / salesLine.Quantity : decimal.Zero, }; this.AddLineDiscount(transaction, salesLine, lineDiscountItem); } } }
/// <summary> /// Adds total discount lines to the item lines. /// </summary> /// <param name="transaction">The transaction receiving total discount lines.</param> private void AddTotalDiscPctLines(SalesTransaction transaction) { // Consider calculable lines only. Ignore voided or return-by-receipt lines. // Add the total discount to each item. foreach (var saleItem in transaction.PriceCalculableSalesLines) { if (PriceContextHelper.IsDiscountAllowed(this.priceContext, saleItem.ItemId) && saleItem.Quantity > 0) { // Add a new total discount DiscountLine totalDiscountItem = new DiscountLine { DiscountLineType = DiscountLineType.ManualDiscount, ManualDiscountType = ManualDiscountType.TotalDiscountPercent, Percentage = transaction.TotalManualDiscountPercentage, }; saleItem.DiscountLines.Add(totalDiscountItem); SalesLineTotaller.CalculateLine(transaction, saleItem, d => this.priceContext.CurrencyAndRoundingHelper.Round(d)); } } }
/// <summary> /// The calculation of a customer line discount. /// </summary> /// <param name="tradeAgreements">Trade agreement collection to calculate on. If null, uses the pricing data manager to find agreements.</param> /// <param name="transaction">The sales transaction which needs line discounts..</param> /// <returns> /// The sales transaction. /// </returns> public SalesTransaction CalcLineDiscount( List <TradeAgreement> tradeAgreements, SalesTransaction transaction) { // Loop trough all items all calc line discount // Consider calculable lines only. Ignore voided or return-by-receipt lines. if (tradeAgreements != null && tradeAgreements.Any()) { foreach (var saleItem in transaction.PriceCalculableSalesLines) { decimal absQty = Math.Abs(saleItem.Quantity); decimal discountAmount = 0m; decimal percent1 = 0m; decimal percent2 = 0m; decimal minQty = 0m; this.GetLineDiscountLines(tradeAgreements, saleItem, ref absQty, ref discountAmount, ref percent1, ref percent2, ref minQty); decimal totalPercentage = DiscountLine.GetCompoundedPercentage(percent1, percent2); if ((totalPercentage != 0m) || (discountAmount != 0m)) { DiscountLine discountItem = new DiscountLine { DiscountLineType = DiscountLineType.CustomerDiscount, CustomerDiscountType = CustomerDiscountType.LineDiscount, Percentage = totalPercentage, Amount = discountAmount, }; Discount.UpdateDiscountLines(saleItem, discountItem); } } } return(transaction); }
private static void AllocateDiscountAmountToDiscountLines(SalesLine salesLine, LineDiscountCalculationType lineDiscountCalculationType, RoundingRule roundingRule) { if (salesLine.DiscountLines.Count == 0) { return; } // customerLineAmountForMixOrMax and customerLinePercentForMixOrMax are for LineDiscountCalculationType.MaxLineMultiline and MinLineMultiline decimal customerLineAmountForMixOrMax = 0; decimal customerLinePercentForMixOrMax = 0; List <DiscountLine> periodicDiscountItemList = new List <DiscountLine>(); List <DiscountLine> periodicThresholdDiscountItemList = new List <DiscountLine>(); List <DiscountLine> customerLineDiscountItemList = new List <DiscountLine>(); // Manual line discount at most one. DiscountLine manualLineDiscountItem = null; List <DiscountLine> totalDiscountItemList = new List <DiscountLine>(); List <DiscountLine> loyaltyDiscountLineList = new List <DiscountLine>(); //// Step 1: split discount lines into 5 groups: Periodic less threshold, Periodic threshold, Customer line, Manual line and Total and Loyalty //// and figure out customerLineAmountForMixOrMax & customerLinePercentForMixOrMax along the way. foreach (DiscountLine discountLineItem in salesLine.DiscountLines) { discountLineItem.FixInvalidAmountAndPercentage(); if (discountLineItem.DiscountLineType == DiscountLineType.CustomerDiscount) { // Customer Total if (discountLineItem.CustomerDiscountType == CustomerDiscountType.TotalDiscount) { totalDiscountItemList.Add(discountLineItem); } else { // Customer Line if (salesLine.LineMultilineDiscOnItem == LineMultilineDiscountOnItem.Both) { if (customerLineAmountForMixOrMax == decimal.Zero || (lineDiscountCalculationType == LineDiscountCalculationType.MaxLineMultiline && discountLineItem.Amount > customerLineAmountForMixOrMax) || (lineDiscountCalculationType == LineDiscountCalculationType.MinLineMultiline && discountLineItem.Amount > 0 && discountLineItem.Amount < customerLineAmountForMixOrMax)) { customerLineAmountForMixOrMax = discountLineItem.Amount; } if (customerLinePercentForMixOrMax == decimal.Zero || (lineDiscountCalculationType == LineDiscountCalculationType.MaxLineMultiline && discountLineItem.Percentage > customerLinePercentForMixOrMax) || (lineDiscountCalculationType == LineDiscountCalculationType.MinLineMultiline && discountLineItem.Percentage > 0 && discountLineItem.Percentage < customerLinePercentForMixOrMax)) { customerLinePercentForMixOrMax = discountLineItem.Percentage; } } customerLineDiscountItemList.Add(discountLineItem); } } else if (discountLineItem.DiscountLineType == DiscountLineType.PeriodicDiscount) { // Periodic a.k.a. Retail if (discountLineItem.PeriodicDiscountType == PeriodicDiscountOfferType.Threshold) { periodicThresholdDiscountItemList.Add(discountLineItem); } else { periodicDiscountItemList.Add(discountLineItem); } } else if (discountLineItem.DiscountLineType == DiscountLineType.ManualDiscount && (discountLineItem.ManualDiscountType == ManualDiscountType.LineDiscountAmount || discountLineItem.ManualDiscountType == ManualDiscountType.LineDiscountPercent)) { // Line Manual manualLineDiscountItem = discountLineItem; } else if (discountLineItem.DiscountLineType == DiscountLineType.ManualDiscount && (discountLineItem.ManualDiscountType == ManualDiscountType.TotalDiscountAmount || discountLineItem.ManualDiscountType == ManualDiscountType.TotalDiscountPercent)) { // Total manual totalDiscountItemList.Add(discountLineItem); } else if (discountLineItem.DiscountLineType == DiscountLineType.LoyaltyDiscount) { // Loyalty discount loyaltyDiscountLineList.Add(discountLineItem); } } salesLine.PeriodicDiscount = 0; salesLine.PeriodicPercentageDiscount = 0; // Step 2: allocate effective discount amount for periodic less threshold discount lines. AllocatePeriodicDiscountLines(salesLine, periodicDiscountItemList, roundingRule); // Step 3: allocate effective discount amount for periodic threshold discount lines. AllocatePeriodicDiscountLines(salesLine, periodicThresholdDiscountItemList, roundingRule); // Stpe 4: allocate effective discount amount for customer and manual line discounts. AllocateLineDiscountLines(salesLine, customerLineDiscountItemList, manualLineDiscountItem, lineDiscountCalculationType, customerLineAmountForMixOrMax, customerLinePercentForMixOrMax, roundingRule); // Stpe 4: allocate effective discount amount for total line discounts. AllocateTotalDiscountLines(salesLine, totalDiscountItemList, roundingRule); // Stpe 5: allocate effective discount amount for loyalty line discounts AllocateLoyaltyDiscountLines(salesLine, loyaltyDiscountLineList, roundingRule); }
/// <summary> /// This method will distribute the amountToDiscount across all the sale items in the transaction /// proportionally except for the line item with the largest amount. The remainder will be distributed /// to the line item with the largest amount to ensure the amount to discount is exactly applied. /// This method currently works for either the customer discount or when the total discount button is applied. /// </summary> /// <param name="transaction">The transaction receiving total discount lines.</param> /// <param name="discountType">Whether this discount is for a customer or for the total discount item.</param> /// <param name="amountToDiscount">The amount to discount the transaction.</param> private void AddTotalDiscAmountLines( SalesTransaction transaction, DiscountLineType discountType, decimal amountToDiscount) { decimal totalAmtAvailableForDiscount = decimal.Zero; // Build a list of the discountable items with the largest value item last. // Consider calculable lines only. Ignore voided or return-by-receipt lines. var discountableSaleItems = (from s in transaction.PriceCalculableSalesLines where s.IsEligibleForDiscount() && s.Quantity > 0 && PriceContextHelper.IsDiscountAllowed(this.priceContext, s.ItemId) orderby Math.Abs(s.NetAmount), s.LineId select s).ToList(); // Iterate through all non voided items whether we are going to discount or not so that they get added // back to the totals // Consider calculable lines only. Ignore voided or return-by-receipt lines. foreach (var saleItem in transaction.PriceCalculableSalesLines) { // We can clear the discount line for total discount because a total manual amount discount // will override a total manual percent discount, whereas customer discount can have both // amount and percentage applied simultaneously. if (discountType == DiscountLineType.ManualDiscount) { Discount.ClearManualDiscountLinesOfType(saleItem, ManualDiscountType.TotalDiscountAmount); Discount.ClearManualDiscountLinesOfType(saleItem, ManualDiscountType.TotalDiscountPercent); } SalesLineTotaller.CalculateLine(transaction, saleItem, d => this.priceContext.CurrencyAndRoundingHelper.Round(d)); if (saleItem.IsEligibleForDiscount() && saleItem.Quantity > 0) { // Calculate the total amount that is available for discount totalAmtAvailableForDiscount += Math.Abs(saleItem.NetAmountWithAllInclusiveTax); } } // Calculate the percentage (as a fraction) that we should attempt to discount each discountable item // to reach the total. decimal discountFactor = totalAmtAvailableForDiscount != decimal.Zero ? (amountToDiscount / totalAmtAvailableForDiscount) : decimal.Zero; decimal totalAmtDistributed = decimal.Zero; // Iterate through all discountable items. foreach (var saleItem in discountableSaleItems) { decimal amountToDiscountForThisItem = decimal.Zero; if (saleItem != discountableSaleItems.Last()) { // for every item except for the last in the list (which will have the largest value) // discount by the rounded amount that is closest to the percentage desired for the transaction decimal itemPrice = saleItem.NetAmount; amountToDiscountForThisItem = this.priceContext.CurrencyAndRoundingHelper.Round(discountFactor * Math.Abs(itemPrice)); totalAmtDistributed += amountToDiscountForThisItem; } else { // Discount the last item by the remainder to ensure that the exact desired discount is applied amountToDiscountForThisItem = amountToDiscount - totalAmtDistributed; } DiscountLine discountItem; if (amountToDiscountForThisItem != decimal.Zero) { if (discountType == DiscountLineType.ManualDiscount) { // Add a new total discount item discountItem = new DiscountLine(); discountItem.DiscountLineType = DiscountLineType.ManualDiscount; discountItem.ManualDiscountType = ManualDiscountType.TotalDiscountAmount; saleItem.DiscountLines.Add(discountItem); } else { // for customer discounts we need to either update the existing one, or add a new one. discountItem = GetCustomerDiscountItem(saleItem, CustomerDiscountType.TotalDiscount, DiscountLineType.CustomerDiscount); } discountItem.Amount = saleItem.Quantity != 0 ? amountToDiscountForThisItem / saleItem.Quantity : amountToDiscountForThisItem; } SalesLineTotaller.CalculateLine(transaction, saleItem, d => this.priceContext.CurrencyAndRoundingHelper.Round(d)); } }
private static bool CanCompoundTogether(IEnumerable <DiscountLine> existingDiscountLines, DiscountLine newDiscountLine) { bool canCompoundTogether = true; if (!newDiscountLine.IsCompoundable) { canCompoundTogether = false; } if (canCompoundTogether) { if (newDiscountLine.PeriodicDiscountType == PeriodicDiscountOfferType.Threshold) { // threshold compounded discounts: can NOT compound across priorities within threshold discounts. // can compound on top of non-theshold discounts. if (existingDiscountLines.Where(p => string.Equals(newDiscountLine.OfferId, p.OfferId, StringComparison.OrdinalIgnoreCase) || (p.PeriodicDiscountType == PeriodicDiscountOfferType.Threshold && newDiscountLine.PricingPriorityNumber != p.PricingPriorityNumber) || !p.IsCompoundable).Any()) { canCompoundTogether = false; } } else { // non-threshold compounded discounts: can NOT compound across priorities. if (existingDiscountLines.Where(p => string.Equals(newDiscountLine.OfferId, p.OfferId, StringComparison.OrdinalIgnoreCase) || newDiscountLine.PricingPriorityNumber != p.PricingPriorityNumber || !p.IsCompoundable).Any()) { canCompoundTogether = false; } } } return(canCompoundTogether); }
/// <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); }
/// <summary> /// Initializes a new instance of the <see cref="DiscountLineQuantity" /> class. /// </summary> /// <param name="discountLine">Discount line.</param> /// <param name="quantity">The quantity.</param> public DiscountLineQuantity(DiscountLine discountLine, decimal quantity) { this.DiscountLine = discountLine; this.Quantity = quantity; }
public void ApplyDiscountLines(SalesTransaction transaction, bool isReturn) { ThrowIf.Null(transaction, "transaction"); if (!this.salesLines.Any()) { return; } // Keep track of available line items that have non-compoundable discounts applied to them. HashSet <int> availableLines = new HashSet <int>(); // Get the lines in reverse order, so that BOGO scenarios apply to the last item by default. for (int x = this.salesLines.Count - 1; x >= 0; x--) { availableLines.Add(x); } // Finish up exclusive or best price first. foreach (DiscountLineQuantity discountLineQuantity in this.discountLineQuantitiesNonCompounded) { this.ApplyOneDiscountLine(discountLineQuantity, transaction, availableLines, isReturn); } //// Discounts will be in order of non-compoundable first, then compoundable, we should maintain that order. //// In addition, for each concurrency, threhold is the last. bool reallocateDiscountAmountForCompoundedThresholdDiscountAmount = false; // Prepare offer id to discount amount dictionary for compounded threshold discount with amount off. Dictionary <string, decimal> offerIdToDiscountAmountDictionary = new Dictionary <string, decimal>(StringComparer.OrdinalIgnoreCase); Dictionary <string, int> offerIdToPriorityDictionary = new Dictionary <string, int>(StringComparer.OrdinalIgnoreCase); foreach (DiscountLineQuantity discount in this.discountLineQuantitiesCompounded) { if (discount.DiscountLine.PeriodicDiscountType == PeriodicDiscountOfferType.MixAndMatch && discount.DiscountLine.IsCompoundable) { reallocateDiscountAmountForCompoundedThresholdDiscountAmount = true; } if (reallocateDiscountAmountForCompoundedThresholdDiscountAmount && discount.DiscountLine.PeriodicDiscountType == PeriodicDiscountOfferType.Threshold && discount.DiscountLine.IsCompoundable && discount.DiscountLine.Amount > decimal.Zero) { decimal thresholdDiscountAmount = decimal.Zero; offerIdToDiscountAmountDictionary.TryGetValue(discount.DiscountLine.OfferId, out thresholdDiscountAmount); // Effective amount was calculated earlier in ThresholdDiscount.cs for threshold discount with amount off. offerIdToDiscountAmountDictionary[discount.DiscountLine.OfferId] = thresholdDiscountAmount + this.priceContext.CurrencyAndRoundingHelper.Round(discount.DiscountLine.Amount * discount.Quantity); if (!offerIdToPriorityDictionary.ContainsKey(discount.DiscountLine.OfferId)) { offerIdToPriorityDictionary.Add(discount.DiscountLine.OfferId, discount.DiscountLine.PricingPriorityNumber); } } else { if (!discount.DiscountLine.IsCompoundable || discount.DiscountLine.PeriodicDiscountType == PeriodicDiscountOfferType.Threshold) { this.ApplyOneDiscountLine(discount, transaction, availableLines, isReturn); } else { // See comment for method ApplyOneDiscountLineToCompoundedOnly. decimal quantityNotApplied = this.ApplyOneDiscountLineToCompoundedOnly(discount, transaction, availableLines, isReturn); if (quantityNotApplied > decimal.Zero) { this.ApplyOneDiscountLine(discount, transaction, availableLines, isReturn, quantityNotApplied); } } } } //// We need to reallocate amount off for compounded threshold discount amount in some cases. //// Earlier we calculate discount amount right for the item group as a whole, but we weren't able to allocate it properly. //// E.g. compounded mix and match discount of bug one get one 1c, and compounded threshold discount of $10. //// Transaction has a item of quantity 2, one get 1c deal price, the other one gets nothing. //// Threshold discount amount would like $5 for both items, but the effective amount for the 1st one is just 1c. //// As such, we need to reallocate threshold discount $10 (total), 1c to 1st item, and $9.99 to the 2nd one. if (reallocateDiscountAmountForCompoundedThresholdDiscountAmount) { foreach (KeyValuePair <string, decimal> offerIdDiscountAmountPair in offerIdToDiscountAmountDictionary) { // First, build item index to discounted price dictionary, exclusing threshold percentage off. Dictionary <int, decimal> itemIndexToDiscountedPriceDictionary = new Dictionary <int, decimal>(); availableLines.Clear(); decimal totalPrice = decimal.Zero; for (int x = this.salesLines.Count - 1; x >= 0; x--) { IList <DiscountLine> existingDiscountLines = this.salesLines[x].DiscountLines; decimal salesLineQuantity = Math.Abs(this.salesLines[x].Quantity); // Ignore non-compounded sales lines. if (salesLineQuantity != decimal.Zero && !existingDiscountLines.Where(p => !p.IsCompoundable).Any()) { decimal discountedPrice = this.Price; availableLines.Add(x); // non-threshold discount $-off first foreach (DiscountLine discountLineAmountOff in existingDiscountLines) { if (discountLineAmountOff.Amount > decimal.Zero && discountLineAmountOff.PeriodicDiscountType != PeriodicDiscountOfferType.Threshold) { discountedPrice -= discountLineAmountOff.Amount; } } // non-threshold discount %-off foreach (DiscountLine discountLinePercentOff in existingDiscountLines) { if (discountLinePercentOff.Amount == decimal.Zero && discountLinePercentOff.PeriodicDiscountType != PeriodicDiscountOfferType.Threshold) { discountedPrice -= discountedPrice * (discountLinePercentOff.Percentage / 100m); } } // threshold discount $-off foreach (DiscountLine discountLineAmountOff in existingDiscountLines) { if (discountLineAmountOff.Amount > decimal.Zero && discountLineAmountOff.PeriodicDiscountType == PeriodicDiscountOfferType.Threshold) { discountedPrice -= discountLineAmountOff.Amount; } } discountedPrice = Math.Max(decimal.Zero, discountedPrice); itemIndexToDiscountedPriceDictionary[x] = discountedPrice; totalPrice += discountedPrice * salesLineQuantity; } } decimal offerDiscountAmount = offerIdDiscountAmountPair.Value; int priority = 0; offerIdToPriorityDictionary.TryGetValue(offerIdDiscountAmountPair.Key, out priority); int[] salesLineIndicesSorted = itemIndexToDiscountedPriceDictionary.Keys.ToArray(); SalesLineDiscountedPriceComparer comparer = new SalesLineDiscountedPriceComparer(itemIndexToDiscountedPriceDictionary, this.salesLines); Array.Sort(salesLineIndicesSorted, comparer.CompareNetPrice); for (int index = 0; index < salesLineIndicesSorted.Length; index++) { int salesLineIndex = salesLineIndicesSorted[index]; SalesLine salesLine = this.salesLines[salesLineIndex]; decimal salesLineQuantity = Math.Abs(salesLine.Quantity); decimal price = itemIndexToDiscountedPriceDictionary[salesLineIndex]; if (DiscountableItemGroup.IsFraction(salesLineQuantity)) { decimal myDiscountAmount = offerDiscountAmount * ((price * salesLineQuantity) / totalPrice); myDiscountAmount = this.priceContext.CurrencyAndRoundingHelper.Round(myDiscountAmount); decimal unitDiscountAmount = myDiscountAmount / salesLineQuantity; offerDiscountAmount -= myDiscountAmount; salesLine.DiscountLines.Add(NewDiscountLineCompoundedThreshold( offerIdDiscountAmountPair.Key, salesLine.LineNumber, unitDiscountAmount, priority)); } else { if (index == (salesLineIndicesSorted.Length - 1)) { // Last one. decimal smallestAmount = PriceContextHelper.GetSmallestNonNegativeAmount(this.priceContext, Math.Min(offerDiscountAmount, price)); if (smallestAmount > decimal.Zero && salesLineQuantity > decimal.Zero) { int totalDiscountAmountInSmallestAmount = (int)(offerDiscountAmount / smallestAmount); int averageDiscountAmountRoundingDownInSmallestAmount = totalDiscountAmountInSmallestAmount / (int)salesLineQuantity; decimal quantityForHigherDiscountAmount = totalDiscountAmountInSmallestAmount - (averageDiscountAmountRoundingDownInSmallestAmount * (int)salesLineQuantity); decimal quantityForLowerDiscountAmount = salesLineQuantity - quantityForHigherDiscountAmount; decimal unitDiscountAmountForLowerDiscountAmount = averageDiscountAmountRoundingDownInSmallestAmount * smallestAmount; if (quantityForHigherDiscountAmount > decimal.Zero) { SalesLine salesLineToAddDiscount = salesLine; if (quantityForLowerDiscountAmount > decimal.Zero) { SalesLine newLine = this.SplitLine(salesLine, quantityForHigherDiscountAmount); // Add the new line to the transaction and to the available lines for discounts. // Set the line number to the next available number. newLine.LineNumber = transaction.SalesLines.Max(p => p.LineNumber) + 1; transaction.SalesLines.Add(newLine); this.salesLines.Add(newLine); salesLineToAddDiscount = newLine; } DiscountLine discountLine = NewDiscountLineCompoundedThreshold( offerIdDiscountAmountPair.Key, salesLineToAddDiscount.LineNumber, unitDiscountAmountForLowerDiscountAmount + smallestAmount, priority); salesLineToAddDiscount.DiscountLines.Add(discountLine); } if (quantityForLowerDiscountAmount > decimal.Zero && unitDiscountAmountForLowerDiscountAmount > decimal.Zero) { DiscountLine discountLine = NewDiscountLineCompoundedThreshold( offerIdDiscountAmountPair.Key, salesLine.LineNumber, unitDiscountAmountForLowerDiscountAmount, priority); salesLine.DiscountLines.Add(discountLine); } } } else { decimal myDiscountAmount = offerDiscountAmount * ((price * salesLineQuantity) / totalPrice); decimal unitDiscountAmount = this.priceContext.CurrencyAndRoundingHelper.Round(myDiscountAmount / salesLineQuantity); if (unitDiscountAmount > decimal.Zero) { offerDiscountAmount -= unitDiscountAmount * salesLineQuantity; salesLine.DiscountLines.Add(NewDiscountLineCompoundedThreshold( offerIdDiscountAmountPair.Key, salesLine.LineNumber, unitDiscountAmount, priority)); } } } } } } }
/// <summary> /// Updates the sales transaction with the threshold promotion if applicable. /// </summary> /// <param name="existingTransaction">Existing transaction.</param> /// <param name="tempSalesTransaction">Copy of existing transaction.</param> /// <param name="context">The request context.</param> /// <param name="salesLineIndex">The sales line under consideration.</param> /// <param name="cartPromotionLines">The object with the cart promotion lines.</param> /// <param name="thresholdDiscount">The threshold discount line under consideration.</param> /// <param name="tiers">The tiers for the threshold discount.</param> private static void GetThresholdDiscounts( SalesTransaction existingTransaction, SalesTransaction tempSalesTransaction, RequestContext context, int salesLineIndex, Collection <string> cartPromotionLines, DiscountLine thresholdDiscount, IEnumerable <ThresholdDiscountTier> tiers) { // Find all the sales lines with the same offer. List <SalesLine> salesLinesWithOffer = tempSalesTransaction.SalesLines.Where(j => j.DiscountLines.Any(k => k.OfferId.Equals(thresholdDiscount.OfferId))).ToList(); decimal totalAmount = salesLinesWithOffer.Select(j => j.GrossAmount).Sum(); decimal currentQuantity = tempSalesTransaction.SalesLines[salesLineIndex].Quantity; // Find the minimum threshold amount required to hit a discount among all the tiers for this offer. IEnumerable <ThresholdDiscountTier> tiersForCurrentAmtOffer = tiers.Where(j => j.OfferId.Equals(thresholdDiscount.OfferId) && j.AmountThreshold > totalAmount).OrderBy(l => l.AmountThreshold); ThresholdDiscountTier tier = tiersForCurrentAmtOffer.Any() ? tiersForCurrentAmtOffer.First() : null; if (tier != null) { // Add that amount difference to the first item that has this offer in the cart by increasing its quantity and see if this discount applies after applying concurrency rules. existingTransaction.SalesLines[salesLineIndex].Quantity = Math.Ceiling(tempSalesTransaction.SalesLines[salesLineIndex].Quantity * (tier.AmountThreshold - totalAmount + tempSalesTransaction.SalesLines[salesLineIndex].GrossAmount) / (tempSalesTransaction.SalesLines[salesLineIndex].GrossAmount / tempSalesTransaction.SalesLines[salesLineIndex].Quantity)); CartWorkflowHelper.Calculate(context, existingTransaction, CalculationModes.All); DiscountLine isApplied = existingTransaction.SalesLines[salesLineIndex].DiscountLines.Where(j => j.OfferId.Equals(thresholdDiscount.OfferId)).SingleOrDefault(); if (isApplied != null) { var getItemsRequest = new GetItemsDataRequest(salesLinesWithOffer.Select(j => j.ItemId)) { QueryResultSettings = new QueryResultSettings(new ColumnSet("NAME"), PagingInfo.AllRecords) }; var getItemsResponse = context.Runtime.Execute <GetItemsDataResponse>(getItemsRequest, context); ReadOnlyCollection <Item> items = getItemsResponse.Items; StringBuilder buffer = new StringBuilder(); foreach (Item item in items.ToList()) { buffer.Append(item.Name).Append(", "); } buffer.Remove(buffer.Length - 2, 1); if (tier.DiscountMethod == ThresholdDiscountMethod.AmountOff) { thresholdDiscount.OfferName = string.Format(CultureInfo.CurrentUICulture, Resources.ThresholdDiscountPricePromotion, buffer, Math.Round(tier.AmountThreshold, 2), Math.Round(tier.DiscountValue, 2)); } else { thresholdDiscount.OfferName = string.Format(CultureInfo.CurrentUICulture, Resources.ThresholdDiscountPercentagePromotion, buffer, Math.Round(tier.AmountThreshold, 2), Math.Round(tier.DiscountValue, 2)); } cartPromotionLines.Add(thresholdDiscount.OfferName); } } existingTransaction.SalesLines[salesLineIndex].Quantity = currentQuantity; CartWorkflowHelper.Calculate(context, existingTransaction, CalculationModes.All); foreach (SalesLine salesLineWithOffer in salesLinesWithOffer) { salesLineWithOffer.DiscountLines.Remove(salesLineWithOffer.DiscountLines.Where(k => k.OfferId == thresholdDiscount.OfferId).SingleOrDefault()); } }
/// <summary> /// Calculate the manual line discount. /// </summary> /// <param name="transaction">The transaction receiving total discount lines.</param> /// <param name="saleItem">The sale item that contains the discount lines.</param> /// <param name="lineDiscountItem">The line discount amount to discount the transaction.</param> private void AddLineDiscount(SalesTransaction transaction, SalesLine saleItem, DiscountLine lineDiscountItem) { Item item = PriceContextHelper.GetItem(this.priceContext, saleItem.ItemId); bool isDiscountAllowed = item != null ? !item.NoDiscountAllowed : true; if (isDiscountAllowed) { saleItem.DiscountLines.Add(lineDiscountItem); SalesLineTotaller.CalculateLine(transaction, saleItem, d => this.priceContext.CurrencyAndRoundingHelper.Round(d)); } }
private static decimal AllocateLineDiscountLines( SalesLine salesLine, List <DiscountLine> customerLineDiscountItemList, DiscountLine manualLineDiscountItem, LineDiscountCalculationType lineDiscountCalculationType, decimal customerLineAmountForMixOrMax, decimal customerLinePercentForMixOrMax, RoundingRule roundingRule) { decimal lineDiscountEffectiveAmount = 0; decimal lineDiscountEffectivePercentage = 0; decimal grossAmountDiscountable = (salesLine.Price * salesLine.Quantity) - salesLine.PeriodicDiscount; if (grossAmountDiscountable != decimal.Zero) { // Round 1: amount off first for customer line discounts and manual line discount, in that order. if (customerLineDiscountItemList.Any()) { foreach (DiscountLine customerLine in customerLineDiscountItemList) { if (salesLine.LineMultilineDiscOnItem != LineMultilineDiscountOnItem.Both || lineDiscountCalculationType == LineDiscountCalculationType.LinePlusMultiline || lineDiscountCalculationType == LineDiscountCalculationType.LineMultiplyMultiline || (lineDiscountCalculationType == LineDiscountCalculationType.MaxLineMultiline && customerLine.Amount == customerLineAmountForMixOrMax) || (lineDiscountCalculationType == LineDiscountCalculationType.MinLineMultiline && customerLine.Amount == customerLineAmountForMixOrMax) || (lineDiscountCalculationType == LineDiscountCalculationType.Line && customerLine.CustomerDiscountType == CustomerDiscountType.LineDiscount) || (lineDiscountCalculationType == LineDiscountCalculationType.Multiline && customerLine.CustomerDiscountType == CustomerDiscountType.MultilineDiscount)) { customerLine.SetEffectiveAmountForAmountOff(grossAmountDiscountable - lineDiscountEffectiveAmount, salesLine.Quantity, roundingRule); lineDiscountEffectiveAmount += customerLine.EffectiveAmount; // In case of Min and Max, customerLineAmountForMixOrMax can only be applied once. customerLineAmountForMixOrMax = decimal.Zero; } else { customerLine.EffectiveAmount = decimal.Zero; } } } if (manualLineDiscountItem != null) { manualLineDiscountItem.SetEffectiveAmountForAmountOff(grossAmountDiscountable - lineDiscountEffectiveAmount, salesLine.Quantity, roundingRule); lineDiscountEffectiveAmount += manualLineDiscountItem.EffectiveAmount; } decimal grossAmountDiscountableLessAmountOff = grossAmountDiscountable - lineDiscountEffectiveAmount; decimal lineDiscountEffectiveAmountForPercentageOnly = 0; // Round 2: percentage off for customer line discounts and manual line discount, in that order. if (customerLineDiscountItemList.Any()) { foreach (DiscountLine customerLine in customerLineDiscountItemList) { if (salesLine.LineMultilineDiscOnItem != LineMultilineDiscountOnItem.Both || lineDiscountCalculationType == LineDiscountCalculationType.LinePlusMultiline || lineDiscountCalculationType == LineDiscountCalculationType.LineMultiplyMultiline || (lineDiscountCalculationType == LineDiscountCalculationType.MaxLineMultiline && customerLine.Percentage == customerLinePercentForMixOrMax) || (lineDiscountCalculationType == LineDiscountCalculationType.MinLineMultiline && customerLine.Percentage == customerLinePercentForMixOrMax) || (lineDiscountCalculationType == LineDiscountCalculationType.Line && customerLine.CustomerDiscountType == CustomerDiscountType.LineDiscount) || (lineDiscountCalculationType == LineDiscountCalculationType.Multiline && customerLine.CustomerDiscountType == CustomerDiscountType.MultilineDiscount)) { // Compound only when LineDiscountCalculationType.LineMultiplyMultiline. decimal grossAmountBase = grossAmountDiscountableLessAmountOff; if (lineDiscountCalculationType == LineDiscountCalculationType.LineMultiplyMultiline) { grossAmountBase = grossAmountDiscountableLessAmountOff - lineDiscountEffectiveAmountForPercentageOnly; } decimal maxDiscountAmount = grossAmountDiscountable - lineDiscountEffectiveAmount; decimal discountAmountAdded = customerLine.AddEffectiveAmountForPercentOff(grossAmountBase, maxDiscountAmount, roundingRule); lineDiscountEffectiveAmount += discountAmountAdded; lineDiscountEffectiveAmountForPercentageOnly += discountAmountAdded; // In case of Min and Max, customerLineAmountForMixOrMax can only be applied once. customerLineAmountForMixOrMax = decimal.Zero; } } } if (manualLineDiscountItem != null) { // Whether to add or to compound for manual discount follows the same rule for customer line and multiline discount. decimal grossAmountBase = grossAmountDiscountableLessAmountOff; if (lineDiscountCalculationType == LineDiscountCalculationType.LineMultiplyMultiline) { grossAmountBase = grossAmountDiscountableLessAmountOff - lineDiscountEffectiveAmountForPercentageOnly; } decimal maxDiscountAmount = grossAmountDiscountable - lineDiscountEffectiveAmount; decimal discountAmountAdded = manualLineDiscountItem.AddEffectiveAmountForPercentOff(grossAmountBase, maxDiscountAmount, roundingRule); lineDiscountEffectiveAmount += discountAmountAdded; lineDiscountEffectiveAmountForPercentageOnly += discountAmountAdded; } lineDiscountEffectivePercentage = (lineDiscountEffectiveAmount / grossAmountDiscountable) * 100m; } salesLine.LineDiscount = lineDiscountEffectiveAmount; salesLine.LinePercentageDiscount = Math.Round(lineDiscountEffectivePercentage, 2); return(lineDiscountEffectiveAmount); }
public override AppliedDiscountApplication GetAppliedDiscountApplication( DiscountableItemGroup[] discountableItemGroups, decimal[] remainingQuantities, IEnumerable <AppliedDiscountApplication> appliedDiscounts, DiscountApplication discountApplication, PriceContext priceContext) { if (discountApplication == null || !discountApplication.RetailDiscountLines.Any() || discountableItemGroups == null || remainingQuantities == null) { return(null); } decimal[] prices = new decimal[discountableItemGroups.Length]; Dictionary <int, IList <DiscountLineQuantity> > discountDictionary = this.GetExistingDiscountDictionaryAndDiscountedPrices( discountableItemGroups, remainingQuantities, appliedDiscounts, discountApplication, true, true, prices); RetailDiscountLineItem retailDiscountLineItem = discountApplication.RetailDiscountLines.ElementAt(0); DiscountOfferMethod discountMethod = (DiscountOfferMethod)retailDiscountLineItem.RetailDiscountLine.DiscountMethod; decimal dealPrice = decimal.Zero; decimal discountValue = decimal.Zero; decimal discountAmountForDiscountLine = decimal.Zero; switch (discountMethod) { case DiscountOfferMethod.DiscountAmount: discountValue = retailDiscountLineItem.RetailDiscountLine.DiscountAmount; discountAmountForDiscountLine = discountValue; break; case DiscountOfferMethod.DiscountPercent: discountValue = prices[retailDiscountLineItem.ItemIndex] * (retailDiscountLineItem.RetailDiscountLine.DiscountPercent / 100M); break; case DiscountOfferMethod.OfferPrice: dealPrice = retailDiscountLineItem.RetailDiscountLine.OfferPrice; decimal bestExistingDealPrice = 0m; bool hasExistingDealPrice = DiscountBase.TryGetBestExistingDealPrice(discountDictionary, retailDiscountLineItem.ItemIndex, out bestExistingDealPrice); // We don't use discounted price here. discountValue = DiscountBase.GetDiscountAmountFromDealPrice(discountableItemGroups[retailDiscountLineItem.ItemIndex].Price, hasExistingDealPrice, bestExistingDealPrice, dealPrice); discountAmountForDiscountLine = discountValue; break; default: break; } // When has no competing discounts or compounded, apply all remaining quantity. bool applyAllAvailableQuantity = (discountApplication.ApplyStandalone || this.CanCompound) && !discountApplication.HonorQuantity; decimal quantityToApply = applyAllAvailableQuantity ? remainingQuantities[retailDiscountLineItem.ItemIndex] : discountApplication.ItemQuantities[retailDiscountLineItem.ItemIndex]; decimal result = discountValue * quantityToApply; AppliedDiscountApplication newAppliedDiscountApplication = null; if (result > decimal.Zero) { Dictionary <int, decimal> itemQuantities; if (applyAllAvailableQuantity) { itemQuantities = new Dictionary <int, decimal>(); itemQuantities[retailDiscountLineItem.ItemIndex] = quantityToApply; } else { itemQuantities = discountApplication.ItemQuantities; } newAppliedDiscountApplication = new AppliedDiscountApplication(discountApplication, result, itemQuantities, isDiscountLineGenerated: true); DiscountLine discountLine = this.NewDiscountLine(discountApplication.DiscountCode, discountableItemGroups[retailDiscountLineItem.ItemIndex].ItemId); discountLine.PeriodicDiscountType = PeriodicDiscountOfferType.Offer; discountLine.DealPrice = dealPrice; discountLine.Amount = discountAmountForDiscountLine; discountLine.Percentage = retailDiscountLineItem.RetailDiscountLine.DiscountPercent; newAppliedDiscountApplication.AddDiscountLine(retailDiscountLineItem.ItemIndex, new DiscountLineQuantity(discountLine, itemQuantities[retailDiscountLineItem.ItemIndex])); if (discountApplication.RemoveItemsFromLookupsWhenApplied) { this.RemoveItemIndexGroupFromLookups(retailDiscountLineItem.ItemIndex); } } return(newAppliedDiscountApplication); }
/// <summary> /// This method will distribute the amountToDiscount across all the sale items in the transaction /// proportionally except for the line item with the largest amount. The remainder will be distributed /// to the line item with the largest amount to ensure the amount to discount is exactly applied. /// This method currently works when the redeem loyalty points button is applied. /// </summary> /// <param name="transaction">The transaction receiving loyalty discount lines.</param> /// <param name="amountToDiscount">The amount to discount the transaction.</param> private void AddLoyaltyDiscAmountLines( SalesTransaction transaction, decimal amountToDiscount) { decimal totalAmtAvailableForDiscount = decimal.Zero; // Build a list of the discountable items with the largest value item last var discountableSaleItems = (from s in transaction.SalesLines where ((s.IsEligibleForDiscount() && PriceContextHelper.IsDiscountAllowed(this.priceContext, s.ItemId)) || s.IsLoyaltyDiscountApplied) orderby Math.Abs(s.NetAmount), s.LineId select s).ToList(); // Iterate through all non voided items whether we are going to discount or not so that they get added // back to the totals foreach (SalesLine salesLine in transaction.SalesLines.Where(sl => !sl.IsVoided)) { Discount.ClearDiscountLinesOfType(salesLine, DiscountLineType.LoyaltyDiscount); SalesLineTotaller.CalculateLine(transaction, salesLine, d => this.priceContext.CurrencyAndRoundingHelper.Round(d)); if (salesLine.IsEligibleForDiscount() || salesLine.IsLoyaltyDiscountApplied) { // Calculate the total amount that is available for discount totalAmtAvailableForDiscount += Math.Abs(salesLine.NetAmountWithAllInclusiveTax); } } // Calculate the percentage (as a fraction) that we should attempt to discount each discountable item // to reach the total decimal discountFactor = totalAmtAvailableForDiscount != decimal.Zero ? (amountToDiscount / totalAmtAvailableForDiscount) : decimal.Zero; decimal totalAmtDistributed = decimal.Zero; // Iterate through all discountable items. foreach (SalesLine salesLine in discountableSaleItems) { decimal amountToDiscountForThisItem = decimal.Zero; if (salesLine != discountableSaleItems.Last()) { // for every item except for the last in the list (which will have the largest value) // discount by the rounded amount that is closest to the percentage desired for the transaction decimal itemPrice = salesLine.NetAmount; amountToDiscountForThisItem = this.priceContext.CurrencyAndRoundingHelper.Round(discountFactor * Math.Abs(itemPrice)); totalAmtDistributed += amountToDiscountForThisItem; } else { // Discount the last item by the remainder to ensure that the exact desired discount is applied amountToDiscountForThisItem = amountToDiscount - totalAmtDistributed; } DiscountLine discountLine; if (amountToDiscountForThisItem != decimal.Zero) { // Add a new loyalty points discount item discountLine = new DiscountLine(); discountLine.Amount = salesLine.Quantity != 0 ? amountToDiscountForThisItem / salesLine.Quantity : amountToDiscountForThisItem; discountLine.DiscountLineType = DiscountLineType.LoyaltyDiscount; salesLine.DiscountLines.Add(discountLine); salesLine.IsLoyaltyDiscountApplied = true; } SalesLineTotaller.CalculateLine(transaction, salesLine, d => this.priceContext.CurrencyAndRoundingHelper.Round(d)); } }
private static decimal AllocatePeriodicDiscountLines(SalesLine salesLine, List <DiscountLine> periodicDiscountItemList, RoundingRule roundingRule) { decimal periodicDiscountEffectiveAmount = 0; decimal periodicDiscountPercentage = salesLine.PeriodicPercentageDiscount; decimal quantityDiscounted = salesLine.Quantity; decimal grossAmountDiscountable = (salesLine.Price * quantityDiscounted) - salesLine.PeriodicDiscount; decimal totalPeriodicDiscountEffectiveAmount = salesLine.PeriodicDiscount; if (periodicDiscountItemList.Any() && grossAmountDiscountable != decimal.Zero) { // Round 0: figure out amount for deal price, in which best price wins. DiscountLine lineWithBestDealPrice = null; foreach (DiscountLine periodicLine in periodicDiscountItemList) { if (periodicLine.DealPrice > decimal.Zero) { if (lineWithBestDealPrice == null) { lineWithBestDealPrice = periodicLine; } else if (periodicLine.DealPrice < lineWithBestDealPrice.DealPrice) { // Sets the amount = 0 on existing lineWithBestDealPrice, and then resets lineWithBestDealPrice. lineWithBestDealPrice.Amount = 0; lineWithBestDealPrice = periodicLine; } else { periodicLine.Amount = decimal.Zero; } } } if (lineWithBestDealPrice != null) { lineWithBestDealPrice.SetAmountForDealPrice(grossAmountDiscountable - periodicDiscountEffectiveAmount, quantityDiscounted, roundingRule); } // Round 1: amount off second. foreach (DiscountLine periodicLine in periodicDiscountItemList) { periodicDiscountEffectiveAmount += periodicLine.SetEffectiveAmountForAmountOff(grossAmountDiscountable - periodicDiscountEffectiveAmount, quantityDiscounted, roundingRule); } // Round 2: percent off. If we have 2 periodic discounts applied to a single line, they much be compounded. foreach (DiscountLine periodicLine in periodicDiscountItemList) { if (grossAmountDiscountable != periodicDiscountEffectiveAmount) { decimal grossAmountBase = grossAmountDiscountable - periodicDiscountEffectiveAmount; decimal maxDiscountAmount = grossAmountBase; periodicDiscountEffectiveAmount += periodicLine.AddEffectiveAmountForPercentOff(grossAmountBase, maxDiscountAmount, roundingRule); } } totalPeriodicDiscountEffectiveAmount += periodicDiscountEffectiveAmount; periodicDiscountPercentage = (totalPeriodicDiscountEffectiveAmount / grossAmountDiscountable) * 100m; } salesLine.PeriodicDiscount = totalPeriodicDiscountEffectiveAmount; salesLine.PeriodicPercentageDiscount = Math.Round(periodicDiscountPercentage, 2); return(periodicDiscountEffectiveAmount); }