/** * {@inheritdoc} */ protected function calculateWithTaxNotInPrice(QuoteDetailsItemInterface $item, $quantity, $round = true) { $taxRateRequest = $this->getAddressRateRequest()->setProductClassId($this->taxClassManagement->getTaxClassId($item->getTaxClassKey())); $rate = $this->calculationTool->getRate($taxRateRequest); $appliedRates = $this->calculationTool->getAppliedRates($taxRateRequest); $applyTaxAfterDiscount = $this->config->applyTaxAfterDiscount($this->storeId); $discountAmount = $item->getDiscountAmount(); $discountTaxCompensationAmount = 0; // Calculate $price $price = $this->calculationTool->round($item->getUnitPrice()); $unitTaxes = []; $unitTaxesBeforeDiscount = []; $appliedTaxes = []; //Apply each tax rate separately foreach ($appliedRates as $appliedRate) { $taxId = $appliedRate['id']; $taxRate = $appliedRate['percent']; $unitTaxPerRate = $this->calculationTool->calcTaxAmount($price, $taxRate, false); $unitTaxAfterDiscount = $unitTaxPerRate; //Handle discount if ($discountAmount && $applyTaxAfterDiscount) { //TODO: handle originalDiscountAmount $unitDiscountAmount = $discountAmount / $quantity; $taxableAmount = max($price - $unitDiscountAmount, 0); $unitTaxAfterDiscount = $this->calculationTool->calcTaxAmount($taxableAmount, $taxRate, false, true); } $appliedTaxes[$taxId] = $this->getAppliedTax($unitTaxAfterDiscount * $quantity, $appliedRate); $unitTaxes[] = $unitTaxAfterDiscount; $unitTaxesBeforeDiscount[] = $unitTaxPerRate; } $unitTax = array_sum($unitTaxes); $unitTaxBeforeDiscount = array_sum($unitTaxesBeforeDiscount); $rowTax = $unitTax * $quantity; $priceInclTax = $price + $unitTaxBeforeDiscount; return $this->taxDetailsItemDataObjectFactory->create()->setCode($item->getCode())->setType($item->getType())->setRowTax($rowTax)->setPrice($price)->setPriceInclTax($priceInclTax)->setRowTotal($price * $quantity)->setRowTotalInclTax($priceInclTax * $quantity)->setDiscountTaxCompensationAmount($discountTaxCompensationAmount)->setAssociatedItemCode($item->getAssociatedItemCode())->setTaxPercent($rate)->setAppliedTaxes($appliedTaxes); }
/** * {@inheritdoc} */ protected function calculateWithTaxNotInPrice(QuoteDetailsItemInterface $item, $quantity, $round = true) { $taxRateRequest = $this->getAddressRateRequest()->setProductClassId($this->taxClassManagement->getTaxClassId($item->getTaxClassKey())); $rate = $this->calculationTool->getRate($taxRateRequest); $appliedRates = $this->calculationTool->getAppliedRates($taxRateRequest); $applyTaxAfterDiscount = $this->config->applyTaxAfterDiscount($this->storeId); $discountAmount = $item->getDiscountAmount(); $discountTaxCompensationAmount = 0; // Calculate $rowTotal $price = $this->calculationTool->round($item->getUnitPrice()); $rowTotal = $price * $quantity; $rowTaxes = []; $rowTaxesBeforeDiscount = []; $appliedTaxes = []; //Apply each tax rate separately foreach ($appliedRates as $appliedRate) { $taxId = $appliedRate['id']; $taxRate = $appliedRate['percent']; $rowTaxPerRate = $this->calculationTool->calcTaxAmount($rowTotal, $taxRate, false, false); $deltaRoundingType = self::KEY_REGULAR_DELTA_ROUNDING; if ($applyTaxAfterDiscount) { $deltaRoundingType = self::KEY_TAX_BEFORE_DISCOUNT_DELTA_ROUNDING; } $rowTaxPerRate = $this->roundAmount($rowTaxPerRate, $taxId, false, $deltaRoundingType, $round); $rowTaxAfterDiscount = $rowTaxPerRate; //Handle discount if ($applyTaxAfterDiscount) { //TODO: handle originalDiscountAmount $taxableAmount = max($rowTotal - $discountAmount, 0); $rowTaxAfterDiscount = $this->calculationTool->calcTaxAmount($taxableAmount, $taxRate, false, false); $rowTaxAfterDiscount = $this->roundAmount($rowTaxAfterDiscount, $taxId, false, self::KEY_REGULAR_DELTA_ROUNDING, $round); } $appliedTaxes[$taxId] = $this->getAppliedTax($rowTaxAfterDiscount, $appliedRate); $rowTaxes[] = $rowTaxAfterDiscount; $rowTaxesBeforeDiscount[] = $rowTaxPerRate; } $rowTax = array_sum($rowTaxes); $rowTaxBeforeDiscount = array_sum($rowTaxesBeforeDiscount); $rowTotalInclTax = $rowTotal + $rowTaxBeforeDiscount; $priceInclTax = $rowTotalInclTax / $quantity; if ($round) { $priceInclTax = $this->calculationTool->round($priceInclTax); } return $this->taxDetailsItemDataObjectFactory->create()->setCode($item->getCode())->setType($item->getType())->setRowTax($rowTax)->setPrice($price)->setPriceInclTax($priceInclTax)->setRowTotal($rowTotal)->setRowTotalInclTax($rowTotalInclTax)->setDiscountTaxCompensationAmount($discountTaxCompensationAmount)->setAssociatedItemCode($item->getAssociatedItemCode())->setTaxPercent($rate)->setAppliedTaxes($appliedTaxes); }
/** * Convert a quote/order/invoice/credit memo item to a tax details item objects * * This includes tax for the item as well as any additional line item tax information like Gift Wrapping * * @param QuoteDetailsItemInterface $item * @param GetTaxResult $getTaxResult * @param bool $useBaseCurrency * @param \Magento\Framework\App\ScopeInterface $scope * @return \Magento\Tax\Api\Data\TaxDetailsItemInterface */ protected function getTaxDetailsItem(QuoteDetailsItemInterface $item, GetTaxResult $getTaxResult, $useBaseCurrency, $scope) { $price = $item->getUnitPrice(); /* @var $taxLine \AvaTax\TaxLine */ $taxLine = $getTaxResult->getTaxLine($item->getCode()); // Items that are children of other items won't have lines in the response if (!$taxLine instanceof \AvaTax\TaxLine) { return false; } $rate = (double) ($taxLine->getRate() * Tax::RATE_MULTIPLIER); $tax = (double) $taxLine->getTax(); /** * Magento uses base rates for determining what to charge a customer, not the currency rate (i.e., the non-base * rate). Because of this, the base amounts are what is being sent to AvaTax for rate calculation. When we get * the base tax amounts back from AvaTax, we have to convert those to the current store's currency using the * \Magento\Framework\Pricing\PriceCurrencyInterface::convert() method. However if we simply convert the AvaTax * base tax amount * currency multiplier, we may run into issues due to rounding. * * For example, a $9.90 USD base price * a 6% tax rate equals a tax amount of $0.59 (.594 rounded). Assume the * current currency has a conversion rate of 2x. The price will display to the user as $19.80. There are two * ways we can calculate the tax amount: * 1. Multiply the tax amount received back from AvaTax, which would be $1.18 ($0.59 * 2). * 2. Multiply using this formula (base price * currency rate) * tax rate) ((9.99 * 2) * .06) * which would be $1.19 (1.188 rounded) * * The second approach is more accurate and is what we are doing here. */ if (!$useBaseCurrency) { /** * We could recalculate the amount using the same logic found in this class: * @see \ClassyLlama\AvaTax\Framework\Interaction\Line::convertTaxQuoteDetailsItemToData, * but using the taxable amount returned back from AvaTax is the only way to get an accurate amount as * some items sent to AvaTax may be tax exempt */ $taxableAmount = (double) $taxLine->getTaxable(); $amount = $this->priceCurrency->convert($taxableAmount, $scope); $tax = $amount * $taxLine->getRate(); $tax = $this->calculationTool->round($tax); } $rowTax = $tax; /** * In native Magento, the "row_total_incl_tax" and "base_row_total_incl_tax" fields contain the tax before * discount. The AvaTax 15 API doesn't have the concept of before/after discount tax, so in order to determine * the "before discount tax amount", we need to multiply the discount by the rate returned by AvaTax. * @see \Magento\Tax\Model\Calculation\AbstractAggregateCalculator::calculateWithTaxNotInPrice * * If the rate is 0, then this product doesn't have taxes applied and tax on discount shouldn't be calculated. * If tax is 0, then item was tax-exempt for some reason and tax on discount shouldn't be calculated */ if ($taxLine->getRate() > 0 && $tax > 0) { /** * Accurately calculating what AvaTax would have charged before discount requires checking to see if any * of the tax amount is tax exempt. If so, we need to find out what percentage of the total amount AvaTax * deemed as taxable and then use that percentage when calculating the discount amount. This partially * taxable scenario can arise in a situation like this: * @see https://help.avalara.com/kb/001/Why_is_freight_taxed_partially_on_my_sale * * To test this functionality, you can create a "Base Override" Tax Rule in the AvaTax admin to mark certain * jurisdictions as partially taxable. */ $taxableAmountPercentage = 1; if ($taxLine->getExemption() > 0) { // This value is the total amount sent to AvaTax for tax calculation, before AvaTax determined what // portion of the amount is taxable $totalAmount = $taxLine->getTaxable() + $taxLine->getExemption(); // Avoid division by 0 if ($totalAmount != 0) { $taxableAmountPercentage = $taxLine->getTaxable() / $totalAmount; } } $effectiveDiscountAmount = $taxableAmountPercentage * $item->getDiscountAmount(); $taxOnDiscountAmount = $effectiveDiscountAmount * $taxLine->getRate(); $taxOnDiscountAmount = $this->calculationTool->round($taxOnDiscountAmount); $rowTaxBeforeDiscount = $rowTax + $taxOnDiscountAmount; } else { $rowTaxBeforeDiscount = 0; } $extensionAttributes = $item->getExtensionAttributes(); if ($extensionAttributes) { $quantity = $extensionAttributes->getTotalQuantity() !== null ? $extensionAttributes->getTotalQuantity() : $item->getQuantity(); } else { $quantity = $item->getQuantity(); } $rowTotal = $price * $quantity; $rowTotalInclTax = $rowTotal + $rowTaxBeforeDiscount; $priceInclTax = $rowTotalInclTax / $quantity; /** * Since the AvaTax extension does not support merchants adding products with tax already factored into the * price, we don't need to do any calculations for this number. The only time this value would be something * other than 0 is when this method runs: * @see \Magento\Tax\Model\Calculation\AbstractAggregateCalculator::calculateWithTaxInPrice */ $discountTaxCompensationAmount = 0; /** * The \Magento\Tax\Model\Calculation\AbstractAggregateCalculator::calculateWithTaxNotInPrice method that this * method is patterned off of has $round as a variable, but any time that method is used in the context of a * collect totals on a quote, rounding is always used. */ $round = true; if ($round) { $priceInclTax = $this->calculationTool->round($priceInclTax); } $appliedTax = $this->getAppliedTax($getTaxResult, $rowTax); $appliedTaxes = [$appliedTax->getTaxRateKey() => $appliedTax]; return $this->taxDetailsItemDataObjectFactory->create()->setCode($item->getCode())->setType($item->getType())->setRowTax($rowTax)->setPrice($price)->setPriceInclTax($priceInclTax)->setRowTotal($rowTotal)->setRowTotalInclTax($rowTotalInclTax)->setDiscountTaxCompensationAmount($discountTaxCompensationAmount)->setAssociatedItemCode($item->getAssociatedItemCode())->setTaxPercent($rate)->setAppliedTaxes($appliedTaxes); }