/** * 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); }