/// <summary> /// High-level calculation method to get charges from the calculation logic /// and attach them to the transaction and it's lines. /// </summary> /// <param name="request">The request containing context and transaction to update.</param> /// <returns> /// Response with updated transaction. /// </returns> private static GetChargesServiceResponse CalculateCharges(GetChargesServiceRequest request) { // extract transaction we'll be populating var transaction = request.Transaction; if (request.RequestContext.GetChannelConfiguration().ChannelType == RetailChannelType.OnlineStore || request.RequestContext.GetChannelConfiguration().ChannelType == RetailChannelType.SharePointOnlineStore) { // clear all the non-manual charges ClearNonManualCharges(transaction); } // total transaction so that we have order totals and line net amounts for percentage charges and tiered fixed charges SalesTransactionTotaler.CalculateTotals(request.RequestContext, transaction); // put charges on the transaction lines // Consider calculable lines only. Ignore voided or return-by-receipt lines. foreach (var salesLine in transaction.ChargeCalculableSalesLines) { var priceChargesPerLine = CalculatePriceCharges(salesLine, request); if (priceChargesPerLine != null) { salesLine.ChargeLines.Add(priceChargesPerLine); } } // Auto-Charges are only supported for Online stores. if (request.RequestContext.GetChannelConfiguration().ChannelType == RetailChannelType.OnlineStore || request.RequestContext.GetChannelConfiguration().ChannelType == RetailChannelType.SharePointOnlineStore) { CalculateAutoCharges(request, transaction); } return(new GetChargesServiceResponse(transaction)); }
/// <summary> /// Static entry point to calculate amount paid and due. /// </summary> /// <param name="context">The request context.</param> /// <param name="salesTransaction">The sales transaction.</param> public static void CalculateAmountPaidAndDue(RequestContext context, SalesTransaction salesTransaction) { ThrowIf.Null(context, "context"); ThrowIf.Null(salesTransaction, "salesTransaction"); decimal paymentRequiredAmount; salesTransaction.AmountPaid = SalesTransactionTotaler.GetPaymentsSum(salesTransaction.TenderLines); // decides what is expected to be paid for this transaction switch (salesTransaction.CartType) { case CartType.CustomerOrder: paymentRequiredAmount = SalesTransactionTotaler.CalculateRequiredPaymentAmount(context, salesTransaction); break; case CartType.Shopping: case CartType.Checkout: case CartType.AccountDeposit: paymentRequiredAmount = salesTransaction.TotalAmount; break; case CartType.IncomeExpense: paymentRequiredAmount = salesTransaction.IncomeExpenseTotalAmount; break; default: throw new DataValidationException( DataValidationErrors.Microsoft_Dynamics_Commerce_Runtime_InvalidRequest, string.Format("SalesTransactionTotaler::CalculateAmountPaidAndDue: CartType '{0}' not supported.", salesTransaction.CartType)); } salesTransaction.SalesPaymentDifference = paymentRequiredAmount - salesTransaction.AmountPaid; salesTransaction.AmountDue = paymentRequiredAmount - salesTransaction.AmountPaid; TenderLine lastTenderLine = null; if (!salesTransaction.TenderLines.IsNullOrEmpty()) { lastTenderLine = salesTransaction.ActiveTenderLines.LastOrDefault(); } if (lastTenderLine != null) { // Calculate the expected (rounded) amount due for last payment. decimal amountDueBeforeLastPayment = paymentRequiredAmount - salesTransaction.ActiveTenderLines.Take(salesTransaction.ActiveTenderLines.Count - 1).Sum(t => t.Amount); GetPaymentRoundedValueServiceRequest roundAmountDueBeforeLastPaymentRequest = new GetPaymentRoundedValueServiceRequest(amountDueBeforeLastPayment, lastTenderLine.TenderTypeId, isChange: false); GetRoundedValueServiceResponse roundAmountDueBeforeLastPaymentResponse = context.Execute <GetRoundedValueServiceResponse>(roundAmountDueBeforeLastPaymentRequest); // Set amont due to zero if payment amount equals to expected rounded payment amount. Otherwise another payment should be required (that could use different rounding settings). if (roundAmountDueBeforeLastPaymentResponse.RoundedValue == lastTenderLine.Amount) { salesTransaction.AmountDue = decimal.Zero; } } // When required amount is positive, amount due must be zero or negative (overtender), otherwise (e.g. for refunds or exchanges) exact amount has to refunded (zero balance). salesTransaction.IsRequiredAmountPaid = (paymentRequiredAmount > 0 && salesTransaction.AmountDue <= 0) || (paymentRequiredAmount <= 0 && salesTransaction.AmountDue == 0); }
/// <summary> /// Calculates the tax for the last item. /// </summary> /// <param name="request">The request.</param> /// <returns>The response.</returns> private static CalculateTaxServiceResponse CalculateTax(CalculateTaxServiceRequest request) { ThrowIf.Null(request, "request"); TaxHelpers.SetSalesTaxGroup(request.RequestContext, request.Transaction); SalesTaxOverrideHelper.CalculateTaxOverrides(request.RequestContext, request.Transaction); // Consider active (non-void) lines for tax. // Need to recalculate tax on return-by-receipt lines because we cannot reconstruct tax lines from return transaction lines alone. // A few key information like IsExempt, IsTaxInclusive, TaxCode are not available on return transaction line. foreach (var saleItem in request.Transaction.ActiveSalesLines) { saleItem.TaxRatePercent = 0; saleItem.TaxLines.Clear(); } var totaler = new SalesTransactionTotaler(request.Transaction); totaler.CalculateTotals(request.RequestContext); ClearChargeTaxLines(request.Transaction); TaxContext taxContext = new TaxContext(request.RequestContext); TaxCodeProvider defaultProvider = GetTaxProvider(request.RequestContext, taxContext); defaultProvider.CalculateTax(request.RequestContext, request.Transaction); return(new CalculateTaxServiceResponse(request.Transaction)); }
/// <summary> /// Calculates the deposit and updates the sales transaction deposit amount. /// </summary> /// <param name="context">The request context.</param> /// <param name="salesTransaction">The sales transaction.</param> public static void CalculateDeposit(RequestContext context, SalesTransaction salesTransaction) { ThrowIf.Null(context, "context"); ThrowIf.Null(salesTransaction, "salesTransaction"); salesTransaction.CalculatedDepositAmount = SalesTransactionTotaler.CalculateDepositAmount(context, salesTransaction); salesTransaction.RequiredDepositAmount = salesTransaction.IsDepositOverridden ? RoundCurrencyAmount(context, salesTransaction.OverriddenDepositAmount.Value) : salesTransaction.CalculatedDepositAmount; // the deposit available on the pickup operation is the DepositAvailableAmount // but we cannot use more deposit than the DepositRequiredAmount for the transaction if (salesTransaction.CustomerOrderMode == CustomerOrderMode.Pickup) { bool hasAnyQuantityRemainingForPickup = salesTransaction.SalesLines.Any(line => (line.QuantityInvoiced + line.Quantity) < line.QuantityOrdered); if (hasAnyQuantityRemainingForPickup) { salesTransaction.PrepaymentAmountAppliedOnPickup = Math.Min(salesTransaction.AvailableDepositAmount, salesTransaction.RequiredDepositAmount); } else { salesTransaction.PrepaymentAmountAppliedOnPickup = salesTransaction.AvailableDepositAmount; } } else { salesTransaction.PrepaymentAmountAppliedOnPickup = decimal.Zero; } }
/// <summary> /// Gets the calculated deposit amount. /// </summary> /// <param name="context">The request context.</param> /// <param name="salesTransaction">The sales transaction.</param> /// <returns>Calculated deposit amount.</returns> private static decimal CalculateDepositAmount(RequestContext context, SalesTransaction salesTransaction) { ThrowIf.Null(context, "context"); ThrowIf.Null(salesTransaction, "salesTransaction"); decimal calculatedDepositAmount = decimal.Zero; if (salesTransaction.CartType == CartType.CustomerOrder && (salesTransaction.CustomerOrderMode == CustomerOrderMode.CustomerOrderCreateOrEdit || salesTransaction.CustomerOrderMode == CustomerOrderMode.Pickup)) { decimal minimumDepositMultiplier = context.GetChannelConfiguration().MinimumDepositPercentage / 100M; if (minimumDepositMultiplier > decimal.Zero) { // get cancellation charge total amount decimal cancellationChargeAmount = SalesTransactionTotaler.GetCancellationChargeAmount(context, salesTransaction, context.GetChannelConfiguration()); // take the transaction total minus the cancellation charges decimal transactionTotalAmountWithoutCancellationCharges = salesTransaction.TotalAmount - cancellationChargeAmount; // charges are added before deposit calculation - we need to make sure we do not calculate the deposit over cancellation charges // deposit required = (order total - any cancellation charges) * minimum deposit multiplier calculatedDepositAmount = transactionTotalAmountWithoutCancellationCharges * minimumDepositMultiplier; } } return(RoundCurrencyAmount(context, calculatedDepositAmount)); }
/// <summary> /// Calculates all of the discounts for the transactions. /// </summary> /// <param name="context">The request context.</param> /// <param name="transaction">The sales transaction.</param> /// <param name="discountCalculationMode">Discount calculation mode.</param> public void CalculateDiscount(RequestContext context, SalesTransaction transaction, DiscountCalculationMode discountCalculationMode) { if (transaction == null) { throw new ArgumentNullException("transaction"); } // take a snapshot of discount amount coming from previous computations, if there is any Dictionary <string, decimal> previousLineDiscounts = new Dictionary <string, decimal>(StringComparer.OrdinalIgnoreCase); // Consider calculable lines only. Ignore voided or return-by-receipt lines. if (transaction.PriceCalculableSalesLines.Any()) { // Consider calculable lines only. Ignore voided or return-by-receipt lines. foreach (var line in transaction.PriceCalculableSalesLines) { // if it is zero, means this is the first time we are calculating, there is no previous state if (line.DiscountAmount != 0) { previousLineDiscounts.Add(line.LineId, line.DiscountAmount); } } } var channelConfiguration = context.GetChannelConfiguration(); var customer = GetCustomer(context, transaction.CustomerId); PE.PricingEngine.CalculateDiscountsForLines( this.pricingDataManager, transaction, new ChannelCurrencyOperations(context), channelConfiguration.Currency, customer.LineDiscountGroup, customer.MultilineDiscountGroup, customer.TotalDiscountGroup, false, discountCalculationMode, context.GetNowInChannelTimeZone()); // check whether any discount discrepancy occurred after calculation if (transaction.PriceCalculableSalesLines.Any()) { // Consider calculable lines only. Ignore voided or return-by-receipt lines. foreach (var line in transaction.PriceCalculableSalesLines) { // calculate on a clone so that we don't modify the actual sales line var clonedLine = line.Clone <SalesLine>(); SalesTransactionTotaler.CalculateLine(context, transaction, clonedLine); Discount.RaiseNotificationIfDiscountWasInvalidated(context, previousLineDiscounts, clonedLine); } } transaction.IsDiscountFullyCalculated = discountCalculationMode.HasFlag(DiscountCalculationMode.CalculateAll); }
/// <summary> /// Gets the cancellation total charge amount (include taxes) on a sales transaction. /// </summary> /// <param name="context">The request context.</param> /// <param name="salesTransaction">The sales transaction to get the cancellation charge amount from.</param> /// <param name="channelConfiguration">The channel configuration object.</param> /// <returns>The cancellation total charge amount.</returns> private static decimal GetCancellationChargeAmount(RequestContext context, SalesTransaction salesTransaction, ChannelConfiguration channelConfiguration) { ThrowIf.Null(salesTransaction, "salesTransaction"); ThrowIf.Null(channelConfiguration, "channelConfiguration"); decimal cancellationChargeAmount = decimal.Zero; // sum up all cancellation charges in the order foreach (ChargeLine chargeLine in salesTransaction.ChargeLines) { if (chargeLine.ChargeCode.Equals(channelConfiguration.CancellationChargeCode, StringComparison.OrdinalIgnoreCase)) { cancellationChargeAmount += chargeLine.CalculatedAmount + chargeLine.TaxAmountExclusive; } } return(SalesTransactionTotaler.RoundCurrencyAmount(context, cancellationChargeAmount)); }
/// <summary> /// Calculates the required amount that must be paid for a customer order transaction. /// </summary> /// <param name="context">The request context.</param> /// <param name="salesTransaction">The transaction that must be used for the calculation.</param> /// <returns>The amount that must be paid for this transaction.</returns> public static decimal CalculateRequiredPaymentAmount(RequestContext context, SalesTransaction salesTransaction) { ThrowIf.Null(context, "context"); ThrowIf.Null(salesTransaction, "salesTransaction"); if (salesTransaction.CartType != CartType.CustomerOrder) { throw new InvalidOperationException("Transaction must be a customer order."); } switch (salesTransaction.CustomerOrderMode) { case CustomerOrderMode.CustomerOrderCreateOrEdit: return(salesTransaction.RequiredDepositAmount - salesTransaction.PrepaymentAmountPaid); case CustomerOrderMode.QuoteCreateOrEdit: return(decimal.Zero); case CustomerOrderMode.Cancellation: decimal cancellationChargeAmount = SalesTransactionTotaler.GetCancellationChargeAmount( context, salesTransaction, context.GetChannelConfiguration()); // We need to refund the amount paid for the order minus cancellation charges // For refunding, we use a negative amount due return(cancellationChargeAmount - salesTransaction.PrepaymentAmountPaid); case CustomerOrderMode.Return: return(salesTransaction.TotalAmount); case CustomerOrderMode.Pickup: // customer needs to pay the total for the transaction minus the credit he has due to prepayments // in case he has more credit than what the transaction is worth, we need to refund him return(salesTransaction.TotalAmount - salesTransaction.PrepaymentAmountAppliedOnPickup); case CustomerOrderMode.None: case CustomerOrderMode.OrderRecalled: return(decimal.Zero); default: throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Customer order mode '{0}' not supported.", salesTransaction.CustomerOrderMode)); } }
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); }
/// <summary> /// Calculates the amount paid and due on the sales transaction. /// </summary> /// <param name="calculateAmountPaidAndDueRequest">The request.</param> /// <returns>The service response.</returns> private static Response CalculateAmountPaidAndDue(CalculateAmountPaidAndDueServiceRequest calculateAmountPaidAndDueRequest) { SalesTransactionTotaler.CalculateAmountPaidAndDue(calculateAmountPaidAndDueRequest.RequestContext, calculateAmountPaidAndDueRequest.Transaction); return(new CalculateAmountPaidAndDueServiceResponse(calculateAmountPaidAndDueRequest.Transaction)); }
/// <summary> /// Calculates the totals on the sales transaction. /// </summary> /// <param name="calculateTotalsRequest">The request.</param> /// <returns>The service response.</returns> private static Response CalculateTotals(CalculateTotalsServiceRequest calculateTotalsRequest) { SalesTransactionTotaler.CalculateTotals(calculateTotalsRequest.RequestContext, calculateTotalsRequest.Transaction); return(new CalculateTotalsServiceResponse(calculateTotalsRequest.Transaction)); }