/** * {@inheritdoc} */ public function calculateTax(\Magento\Tax\Api\Data\QuoteDetailsInterface $quoteDetails, $storeId = null, $round = true) { if ($storeId === null) { $storeId = $this->storeManager->getStore()->getStoreId(); } // initial TaxDetails data $taxDetailsData = [TaxDetailsInterface::KEY_SUBTOTAL => 0.0, TaxDetailsInterface::KEY_TAX_AMOUNT => 0.0, TaxDetailsInterface::KEY_DISCOUNT_TAX_COMPENSATION_AMOUNT => 0.0, TaxDetailsInterface::KEY_APPLIED_TAXES => [], TaxDetailsInterface::KEY_ITEMS => []]; $items = $quoteDetails->getItems(); if (empty($items)) { return $this->taxDetailsDataObjectFactory->create()->setSubtotal(0.0)->setTaxAmount(0.0)->setDiscountTaxCompensationAmount(0.0)->setAppliedTaxes([])->setItems([]); } $this->computeRelationships($items); $calculator = $this->calculatorFactory->create($this->config->getAlgorithm($storeId), $storeId, $quoteDetails->getBillingAddress(), $quoteDetails->getShippingAddress(), $this->taxClassManagement->getTaxClassId($quoteDetails->getCustomerTaxClassKey(), 'customer'), $quoteDetails->getCustomerId()); $processedItems = []; /** @var QuoteDetailsItemInterface $item */ foreach ($this->keyedItems as $item) { if (isset($this->parentToChildren[$item->getCode()])) { $processedChildren = []; foreach ($this->parentToChildren[$item->getCode()] as $child) { $processedItem = $this->processItem($child, $calculator, $round); $taxDetailsData = $this->aggregateItemData($taxDetailsData, $processedItem); $processedItems[$processedItem->getCode()] = $processedItem; $processedChildren[] = $processedItem; } $processedItem = $this->calculateParent($processedChildren, $item->getQuantity()); $processedItem->setCode($item->getCode()); $processedItem->setType($item->getType()); } else { $processedItem = $this->processItem($item, $calculator, $round); $taxDetailsData = $this->aggregateItemData($taxDetailsData, $processedItem); } $processedItems[$processedItem->getCode()] = $processedItem; } $taxDetailsDataObject = $this->taxDetailsDataObjectFactory->create(); $this->dataObjectHelper->populateWithArray($taxDetailsDataObject, $taxDetailsData, '\\Magento\\Tax\\Api\\Data\\TaxDetailsInterface'); $taxDetailsDataObject->setItems($processedItems); return $taxDetailsDataObject; }
/** * Convert Tax Quote Details into data to be converted to a GetTax Request * * @param \Magento\Tax\Api\Data\QuoteDetailsInterface $taxQuoteDetails * @param \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment * @param \Magento\Quote\Api\Data\CartInterface $quote * @return array|null */ protected function convertTaxQuoteDetailsToData(\Magento\Tax\Api\Data\QuoteDetailsInterface $taxQuoteDetails, \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment, \Magento\Quote\Api\Data\CartInterface $quote) { $lines = []; $items = $taxQuoteDetails->getItems(); $keyedItems = $this->taxCalculation->getKeyedItems($items); $childrenItems = $this->taxCalculation->getChildrenItems($items); /** @var \Magento\Tax\Api\Data\QuoteDetailsItemInterface $item */ foreach ($keyedItems as $item) { /** * If a quote has children and they are calculated (e.g., Bundled products with dynamic pricing) * @see \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector::mapItems * then we only need to pass child items to AvaTax. Due to the logic in * @see \ClassyLlama\AvaTax\Framework\Interaction\TaxCalculation::calculateTaxDetails * the parent tax gets calculated based on children items */ // if (isset($childrenItems[$item->getCode()])) { /** @var \Magento\Tax\Api\Data\QuoteDetailsItemInterface $childItem */ foreach ($childrenItems[$item->getCode()] as $childItem) { $line = $this->interactionLine->getLine($childItem); if ($line) { $lines[] = $line; } } } else { $line = $this->interactionLine->getLine($item); if ($line) { $lines[] = $line; } } } // Shipping Address not documented in the interface for some reason // they do have a constant for it but not a method in the interface // // If quote is virtual, getShipping will return billing address, so no need to check if quote is virtual $shippingAddress = $shippingAssignment->getShipping()->getAddress(); $address = $this->address->getAddress($shippingAddress); $store = $this->storeRepository->getById($quote->getStoreId()); $currentDate = $this->getFormattedDate($store); // Quote created/updated date is not relevant, so just pass the current date $docDate = $currentDate; $customerUsageType = $quote->getCustomer() ? $this->taxClassHelper->getAvataxTaxCodeForCustomer($quote->getCustomer()) : null; return ['StoreId' => $store->getId(), 'Commit' => false, 'CurrencyCode' => $quote->getCurrency()->getQuoteCurrencyCode(), 'CustomerCode' => $this->getCustomerCode($quote), 'CustomerUsageType' => $customerUsageType, 'DestinationAddress' => $address, 'DocCode' => self::AVATAX_DOC_CODE_PREFIX . $quote->getId(), 'DocDate' => $docDate, 'DocType' => DocumentType::$SalesOrder, 'ExchangeRate' => $this->getExchangeRate($store, $quote->getCurrency()->getBaseCurrencyCode(), $quote->getCurrency()->getQuoteCurrencyCode()), 'ExchangeRateEffDate' => $currentDate, 'Lines' => $lines, 'DetailLevel' => DetailLevel::$Line, 'PurchaseOrderNo' => $quote->getReservedOrderId()]; }
/** * Convert quote/order/invoice/creditmemo to the AvaTax object and request tax from the Get Tax API * * @param \Magento\Quote\Model\Quote $quote * @param \Magento\Tax\Api\Data\QuoteDetailsInterface $taxQuoteDetails * @param \Magento\Tax\Api\Data\QuoteDetailsInterface $baseTaxQuoteDetails * @param \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment * @return \Magento\Tax\Api\Data\TaxDetailsInterface[] * @throws \ClassyLlama\AvaTax\Exception\TaxCalculationException * @throws \Exception */ public function getTaxDetailsForQuote(\Magento\Quote\Model\Quote $quote, \Magento\Tax\Api\Data\QuoteDetailsInterface $taxQuoteDetails, \Magento\Tax\Api\Data\QuoteDetailsInterface $baseTaxQuoteDetails, \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment) { $storeId = $quote->getStoreId(); $taxService = $this->taxService; try { // Total quantity of an item can be determined by multiplying parent * child quantity, so it's necessary // to calculate total quantities on a list of all items $this->taxCalculation->calculateTotalQuantities($taxQuoteDetails->getItems()); $this->taxCalculation->calculateTotalQuantities($baseTaxQuoteDetails->getItems()); // Taxes need to be calculated on the base prices/amounts, not the current currency prices. As a result of this, // only the $baseTaxQuoteDetails will have taxes calculated for it. The taxes for the current currency will be // calculated by multiplying the base tax rates * currency conversion rate. /** @var $getTaxRequest GetTaxRequest */ $getTaxRequest = $this->interactionTax->getGetTaxRequestForQuote($quote, $baseTaxQuoteDetails, $shippingAssignment); if (is_null($getTaxRequest)) { $message = __('$quote was empty or address was not valid so not running getTax request.'); throw new \ClassyLlama\AvaTax\Exception\TaxCalculationException($message); } $getTaxResult = $taxService->getTax($getTaxRequest, $storeId, true); if ($getTaxResult->getResultCode() == \AvaTax\SeverityLevel::$Success) { $store = $quote->getStore(); $baseTaxDetails = $this->taxCalculation->calculateTaxDetails($baseTaxQuoteDetails, $getTaxResult, true, $store); /** * If quote is using a currency other than the base currency, calculate tax details for both quote * currency and base currency. Otherwise use the same tax details object. */ if ($quote->getBaseCurrencyCode() != $quote->getQuoteCurrencyCode()) { $taxDetails = $this->taxCalculation->calculateTaxDetails($taxQuoteDetails, $getTaxResult, false, $store); } else { $taxDetails = $baseTaxDetails; } return [self::KEY_TAX_DETAILS => $taxDetails, self::KEY_BASE_TAX_DETAILS => $baseTaxDetails]; } else { $message = __('Bad result code: %1', $getTaxResult->getResultCode()); $this->avaTaxLogger->warning($message, ['request' => var_export($getTaxRequest, true), 'result' => var_export($getTaxResult, true)]); throw new \ClassyLlama\AvaTax\Exception\TaxCalculationException($message); } } catch (\SoapFault $exception) { $message = "Exception: \n"; if ($exception) { $message .= $exception->faultstring; } $message .= $taxService->__getLastRequest() . "\n"; $message .= $taxService->__getLastResponse() . "\n"; $this->avaTaxLogger->error("Exception: \n" . $exception ? $exception->faultstring : "", ['request' => var_export($taxService->__getLastRequest(), true), 'result' => var_export($taxService->__getLastResponse(), true)]); throw new \ClassyLlama\AvaTax\Exception\TaxCalculationException($message); } catch (\Exception $exception) { $message = $exception->getMessage(); $this->avaTaxLogger->error($message); throw new \ClassyLlama\AvaTax\Exception\TaxCalculationException($message); } }
/** * Calculates tax for each of the items in a quote/order/invoice/credit memo * * This code is heavily influenced by this method: * @see Magento\Tax\Model\TaxCalculation::calculateTax() * * @param \Magento\Tax\Api\Data\QuoteDetailsInterface $taxQuoteDetails * @param GetTaxResult $getTaxResult * @param bool $useBaseCurrency * @param \Magento\Framework\App\ScopeInterface $scope * @return \Magento\Tax\Api\Data\TaxDetailsInterface */ public function calculateTaxDetails(\Magento\Tax\Api\Data\QuoteDetailsInterface $taxQuoteDetails, GetTaxResult $getTaxResult, $useBaseCurrency, $scope) { // initial TaxDetails data $taxDetailsData = [TaxDetails::KEY_SUBTOTAL => 0.0, TaxDetails::KEY_TAX_AMOUNT => 0.0, TaxDetails::KEY_DISCOUNT_TAX_COMPENSATION_AMOUNT => 0.0, TaxDetails::KEY_APPLIED_TAXES => [], TaxDetails::KEY_ITEMS => []]; $items = $taxQuoteDetails->getItems(); $keyedItems = $this->getKeyedItems($items); $childrenItems = $this->getChildrenItems($items); $processedItems = []; /** @var \Magento\Tax\Api\Data\QuoteDetailsItemInterface $item */ foreach ($keyedItems as $item) { if (isset($childrenItems[$item->getCode()])) { $processedChildren = []; foreach ($childrenItems[$item->getCode()] as $child) { $processedItem = $this->getTaxDetailsItem($child, $getTaxResult, $useBaseCurrency, $scope); if ($processedItem) { $taxDetailsData = $this->aggregateItemData($taxDetailsData, $processedItem); $processedItems[$processedItem->getCode()] = $processedItem; $processedChildren[] = $processedItem; } } $processedItem = $this->calculateParent($processedChildren, $item->getQuantity()); $processedItem->setCode($item->getCode()); $processedItem->setType($item->getType()); } else { $processedItem = $this->getTaxDetailsItem($item, $getTaxResult, $useBaseCurrency, $scope); $taxDetailsData = $this->aggregateItemData($taxDetailsData, $processedItem); if ($processedItem) { $processedItems[$processedItem->getCode()] = $processedItem; } } $processedItems[$processedItem->getCode()] = $processedItem; } $taxDetailsDataObject = $this->taxDetailsDataObjectFactory->create(); $this->dataObjectHelper->populateWithArray($taxDetailsDataObject, $taxDetailsData, '\\Magento\\Tax\\Api\\Data\\TaxDetailsInterface'); $taxDetailsDataObject->setItems($processedItems); return $taxDetailsDataObject; }