/// <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)); } }
/// <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> /// 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 void CalculateAmountDue(RequestContext context, SalesTransaction salesTransaction) { if (salesTransaction == null) { throw new ArgumentNullException("salesTransaction"); } ChannelConfiguration channelConfiguration = context.GetChannelConfiguration(); string cancellationcode = channelConfiguration.CancellationChargeCode; string currencyCode = channelConfiguration.Currency; RoundingRule roundingRule = amountToRound => RoundWithPricesRounding(context, amountToRound, currencyCode); SalesTransactionTotaler.ClearTotalAmounts(salesTransaction); salesTransaction.NumberOfItems = 0m; // initialize with header-level charges list var charges = new List <ChargeLine>(); if (salesTransaction.ChargeLines.Any()) { charges.AddRange(salesTransaction.ChargeLines); } // Calculate totals for sale items , which might also include line-level misc charge in it. foreach (SalesLine saleLineItem in salesTransaction.SalesLines) { if (saleLineItem.IsVoided == false) { // Calculate the sum of items salesTransaction.NumberOfItems += Math.Abs(saleLineItem.Quantity); // calculate the line item cost excluding charges and tax on charges. SalesLineTotaller.CalculateLine(salesTransaction, saleLineItem, roundingRule); UpdateTotalAmounts(salesTransaction, saleLineItem); } else { saleLineItem.PeriodicDiscountPossibilities.Clear(); SalesLineTotaller.CalculateLine(salesTransaction, saleLineItem, roundingRule); } saleLineItem.WasChanged = false; } // Add eligible charges on sales lines foreach (SalesLine salesLine in salesTransaction.ChargeCalculableSalesLines) { charges.AddRange(salesLine.ChargeLines); } decimal incomeExpenseTotalAmount = 0m; foreach (IncomeExpenseLine incomeExpense in salesTransaction.IncomeExpenseLines) { if (incomeExpense.AccountType != IncomeExpenseAccountType.None) { salesTransaction.NetAmountWithTax += incomeExpense.Amount; salesTransaction.NetAmountWithNoTax += incomeExpense.Amount; salesTransaction.GrossAmount += incomeExpense.Amount; salesTransaction.IncomeExpenseTotalAmount += incomeExpense.Amount; // The total is done to calculate the payment amount. incomeExpenseTotalAmount += incomeExpense.Amount; } } foreach (ChargeLine charge in charges) { AddToTaxItems(salesTransaction, charge); // Calculate tax on the charge item. CalculateTaxForCharge(charge); if (charge.ChargeCode.Equals(cancellationcode, StringComparison.OrdinalIgnoreCase) && IsSeparateTaxInCancellationCharge(context)) { salesTransaction.TaxOnCancellationCharge += charge.TaxAmount; } else { salesTransaction.TaxAmount += charge.TaxAmount; } // Later there is "TotalAmount = NetAmountWithTax + ChargeAmount", so we should add TaxAmountExclusive here salesTransaction.NetAmountWithTax += charge.TaxAmountExclusive; } salesTransaction.DiscountAmount = salesTransaction.PeriodicDiscountAmount + salesTransaction.LineDiscount + salesTransaction.TotalDiscount; salesTransaction.ChargeAmount = salesTransaction.ChargesTotal(); // Subtotal is the net amount for the transaction (which includes the discounts) and optionally the tax amount if tax inclusive salesTransaction.SubtotalAmount = roundingRule(salesTransaction.NetAmountWithNoTax + salesTransaction.TaxAmountInclusive); salesTransaction.SubtotalAmountWithoutTax = context.GetChannelConfiguration().PriceIncludesSalesTax ? salesTransaction.SubtotalAmount - salesTransaction.TaxAmount : salesTransaction.SubtotalAmount; // Net amount when saved should include charges, it should be done after Subtotal amount calc because Subtotal does not include charge amount. salesTransaction.NetAmountWithNoTax = roundingRule(salesTransaction.NetAmountWithNoTax + salesTransaction.ChargeAmount); if (salesTransaction.IncomeExpenseLines.Any()) { // Setting the total amount sames as Payment amount for Income/ expense accounts. salesTransaction.TotalAmount = incomeExpenseTotalAmount; } else if (salesTransaction.TransactionType == SalesTransactionType.CustomerAccountDeposit && salesTransaction.CustomerAccountDepositLines.Any()) { CustomerAccountDepositLine customerAccountDepositLine = salesTransaction.CustomerAccountDepositLines.SingleOrDefault(); salesTransaction.SubtotalAmountWithoutTax = customerAccountDepositLine.Amount; salesTransaction.TotalAmount = customerAccountDepositLine.Amount; } else { // NetAmountWithTax already includes the discounts salesTransaction.TotalAmount = roundingRule(salesTransaction.NetAmountWithTax + salesTransaction.ChargeAmount); } }
/// <summary> /// Populates the fields for total amount, total discount, and total taxes on the sales line. /// </summary> /// <param name="context">The request context.</param> /// <param name="salesTransaction">The sales transaction.</param> /// <param name="salesLine">The sales line.</param> public static void CalculateLine(RequestContext context, SalesTransaction salesTransaction, SalesLine salesLine) { RoundingRule roundingRule = SalesTransactionTotaler.GetRoundingRule(context); SalesLineTotaller.CalculateLine(salesTransaction, salesLine, roundingRule); }
private static void CalculateLine(DateTimeOffset transactionBeginDateTime, LineDiscountCalculationType lineDiscountCalculationType, SalesLine salesLine, RoundingRule salesRoundingRule, bool compareDiscounts) { if (salesLine == null) { throw new ArgumentNullException("salesLine"); } if (salesRoundingRule == null) { throw new ArgumentNullException("salesRoundingRule"); } decimal discountAmount = decimal.Zero; SalesLineTotaller.CalculateTax(salesLine); if ((salesLine.Blocked == false) && (salesLine.DateToActivateItem <= transactionBeginDateTime)) { salesLine.GrossAmount = salesRoundingRule(salesLine.Price * salesLine.Quantity); if (!salesLine.IsPriceLocked || salesLine.QuantityOrdered != salesLine.Quantity) { salesLine.LineDiscount = 0; salesLine.LinePercentageDiscount = 0; salesLine.PeriodicDiscount = 0; salesLine.PeriodicPercentageDiscount = 0; salesLine.TotalDiscount = decimal.Zero; salesLine.TotalPercentageDiscount = decimal.Zero; salesLine.LoyaltyDiscountAmount = decimal.Zero; salesLine.LoyaltyPercentageDiscount = decimal.Zero; if (salesLine.IsVoided || salesLine.IsPriceOverridden) { salesLine.DiscountLines.Clear(); } if (compareDiscounts) { ComparingDiscounts(transactionBeginDateTime, lineDiscountCalculationType, salesLine, salesRoundingRule); } AllocateDiscountAmountToDiscountLines(salesLine, lineDiscountCalculationType, salesRoundingRule); } else { FixDiscountAmountsOnSalesLine(salesLine, salesRoundingRule); } discountAmount = salesLine.PeriodicDiscount + salesLine.LineDiscount + salesLine.TotalDiscount + salesLine.LoyaltyDiscountAmount; salesLine.NetAmountWithAllInclusiveTax = salesLine.GrossAmount - discountAmount; // Removing exempt inclusive taxes. // Update - Bug 938614, - not deducting anymore. If PM's later decide to deduct this inclusive tax then uncomment line below. salesLine.NetAmount = salesLine.NetAmountWithAllInclusiveTax; // -salesLine.TaxAmountExemptInclusive; if (salesLine.Quantity != 0) { salesLine.NetAmountPerUnit = salesLine.NetAmountWithNoTax() / salesLine.Quantity; } salesLine.UnitQuantity = salesLine.UnitOfMeasureConversion.Convert(salesLine.Quantity); salesLine.DiscountAmount = discountAmount; salesLine.TotalAmount = salesLine.NetAmountWithTax(); salesLine.NetAmountWithoutTax = salesLine.NetAmount - salesLine.TaxAmountInclusive; } }
/// <summary> /// Calculates all of the discount lines for the transactions. /// </summary> /// <param name="pricingDataManager">Provides data access to the calculation.</param> /// <param name="transaction">The sales transaction.</param> /// <param name="shouldTotalLines">True if discount lines should be totaled for each line. False if they should be left as raw discount lines.</param> /// <param name="priceContext">Price context.</param> /// <remarks>Each sales line will have a collection of DiscountLines and a net discount total in DiscountAmount property (if totaling is enabled).</remarks> public static void CalculateDiscountsForLines( IPricingDataAccessor pricingDataManager, SalesTransaction transaction, bool shouldTotalLines, PriceContext priceContext) { if (transaction == null) { throw new ArgumentNullException("transaction"); } List <SalesLine> existingSalesLines = new List <SalesLine>(); List <SalesLine> newSalesLines = new List <SalesLine>(); if (priceContext.CalculateForNewSalesLinesOnly) { foreach (SalesLine salesLine in transaction.SalesLines) { if (priceContext.NewSalesLineIdSet.Contains(salesLine.LineId)) { newSalesLines.Add(salesLine); } else { existingSalesLines.Add(salesLine); } } // Calculate for new sales lines only. transaction.SalesLines.Clear(); transaction.SalesLines.AddRange(newSalesLines); } Discount discountEngine = InitializeDiscountEngine(pricingDataManager); discountEngine.CalculateDiscount(pricingDataManager, transaction, priceContext); if (priceContext.CalculateForNewSalesLinesOnly) { // Add existing sales lines back after calculating for new sales lines only. List <SalesLine> newSalesLinesFromCalculation = new List <SalesLine>(); foreach (SalesLine salesLine in transaction.SalesLines) { if (!priceContext.NewSalesLineIdSet.Contains(salesLine.LineId)) { newSalesLinesFromCalculation.Add(salesLine); } } transaction.SalesLines.Clear(); transaction.SalesLines.AddRange(existingSalesLines); transaction.SalesLines.AddRange(newSalesLines); transaction.SalesLines.AddRange(newSalesLinesFromCalculation); } if (shouldTotalLines) { // Consider calculable lines only. Ignore voided or return-by-receipt lines. foreach (var salesLine in transaction.PriceCalculableSalesLines) { SalesLineTotaller.CalculateLine(transaction, salesLine, d => priceContext.CurrencyAndRoundingHelper.Round(d)); // technically rounding rule should be "sales rounding" rule } } }
/// <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> /// 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)); } }