Tax.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Tax\Model\Sales\Total\Quote;
  7. use Magento\Customer\Api\Data\AddressInterfaceFactory as CustomerAddressFactory;
  8. use Magento\Customer\Api\Data\RegionInterfaceFactory as CustomerAddressRegionFactory;
  9. use Magento\Framework\App\ObjectManager;
  10. use Magento\Framework\Serialize\Serializer\Json;
  11. use Magento\Quote\Api\Data\ShippingAssignmentInterface;
  12. use Magento\Quote\Model\Quote\Address;
  13. use Magento\Tax\Api\Data\TaxClassKeyInterface;
  14. use Magento\Tax\Model\Calculation;
  15. /**
  16. * Tax totals calculation model
  17. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  18. */
  19. class Tax extends CommonTaxCollector
  20. {
  21. /**
  22. * Counter
  23. *
  24. * @var int
  25. */
  26. protected $counter = 0;
  27. /**
  28. * Tax module helper
  29. *
  30. * @var \Magento\Tax\Helper\Data
  31. */
  32. protected $_taxData;
  33. /**
  34. * Tax configuration object
  35. *
  36. * @var \Magento\Tax\Model\Config
  37. */
  38. protected $_config;
  39. /**
  40. * Discount tax compensationes array
  41. *
  42. * @var array
  43. */
  44. protected $_discountTaxCompensationes = [];
  45. /**
  46. * @var Json
  47. */
  48. private $serializer;
  49. /**
  50. * Class constructor
  51. *
  52. * @param \Magento\Tax\Model\Config $taxConfig
  53. * @param \Magento\Tax\Api\TaxCalculationInterface $taxCalculationService
  54. * @param \Magento\Tax\Api\Data\QuoteDetailsInterfaceFactory $quoteDetailsDataObjectFactory
  55. * @param \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $quoteDetailsItemDataObjectFactory
  56. * @param \Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory $taxClassKeyDataObjectFactory
  57. * @param CustomerAddressFactory $customerAddressFactory
  58. * @param CustomerAddressRegionFactory $customerAddressRegionFactory
  59. * @param \Magento\Tax\Helper\Data $taxData
  60. * @param Json $serializer
  61. */
  62. public function __construct(
  63. \Magento\Tax\Model\Config $taxConfig,
  64. \Magento\Tax\Api\TaxCalculationInterface $taxCalculationService,
  65. \Magento\Tax\Api\Data\QuoteDetailsInterfaceFactory $quoteDetailsDataObjectFactory,
  66. \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $quoteDetailsItemDataObjectFactory,
  67. \Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory $taxClassKeyDataObjectFactory,
  68. CustomerAddressFactory $customerAddressFactory,
  69. CustomerAddressRegionFactory $customerAddressRegionFactory,
  70. \Magento\Tax\Helper\Data $taxData,
  71. Json $serializer = null
  72. ) {
  73. $this->setCode('tax');
  74. $this->_taxData = $taxData;
  75. $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
  76. parent::__construct(
  77. $taxConfig,
  78. $taxCalculationService,
  79. $quoteDetailsDataObjectFactory,
  80. $quoteDetailsItemDataObjectFactory,
  81. $taxClassKeyDataObjectFactory,
  82. $customerAddressFactory,
  83. $customerAddressRegionFactory
  84. );
  85. }
  86. /**
  87. * Collect tax totals for quote address
  88. *
  89. * @param \Magento\Quote\Model\Quote $quote
  90. * @param ShippingAssignmentInterface $shippingAssignment
  91. * @param Address\Total $total
  92. * @return $this
  93. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  94. */
  95. public function collect(
  96. \Magento\Quote\Model\Quote $quote,
  97. \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment,
  98. \Magento\Quote\Model\Quote\Address\Total $total
  99. ) {
  100. $this->clearValues($total);
  101. if (!$shippingAssignment->getItems()) {
  102. return $this;
  103. }
  104. $baseTaxDetails = $this->getQuoteTaxDetails($shippingAssignment, $total, true);
  105. $taxDetails = $this->getQuoteTaxDetails($shippingAssignment, $total, false);
  106. //Populate address and items with tax calculation results
  107. $itemsByType = $this->organizeItemTaxDetailsByType($taxDetails, $baseTaxDetails);
  108. if (isset($itemsByType[self::ITEM_TYPE_PRODUCT])) {
  109. $this->processProductItems($shippingAssignment, $itemsByType[self::ITEM_TYPE_PRODUCT], $total);
  110. }
  111. if (isset($itemsByType[self::ITEM_TYPE_SHIPPING])) {
  112. $shippingTaxDetails = $itemsByType[self::ITEM_TYPE_SHIPPING][self::ITEM_CODE_SHIPPING][self::KEY_ITEM];
  113. $baseShippingTaxDetails =
  114. $itemsByType[self::ITEM_TYPE_SHIPPING][self::ITEM_CODE_SHIPPING][self::KEY_BASE_ITEM];
  115. $this->processShippingTaxInfo($shippingAssignment, $total, $shippingTaxDetails, $baseShippingTaxDetails);
  116. }
  117. //Process taxable items that are not product or shipping
  118. $this->processExtraTaxables($total, $itemsByType);
  119. //Save applied taxes for each item and the quote in aggregation
  120. $this->processAppliedTaxes($total, $shippingAssignment, $itemsByType);
  121. if ($this->includeExtraTax()) {
  122. $total->addTotalAmount('extra_tax', $total->getExtraTaxAmount());
  123. $total->addBaseTotalAmount('extra_tax', $total->getBaseExtraTaxAmount());
  124. }
  125. return $this;
  126. }
  127. /**
  128. * Clear tax related total values in address
  129. *
  130. * @param Address\Total $total
  131. * @return void
  132. */
  133. protected function clearValues(Address\Total $total)
  134. {
  135. $total->setTotalAmount('subtotal', 0);
  136. $total->setBaseTotalAmount('subtotal', 0);
  137. $total->setTotalAmount('tax', 0);
  138. $total->setBaseTotalAmount('tax', 0);
  139. $total->setTotalAmount('shipping', 0);
  140. $total->setBaseTotalAmount('shipping', 0);
  141. $total->setTotalAmount('discount_tax_compensation', 0);
  142. $total->setBaseTotalAmount('discount_tax_compensation', 0);
  143. $total->setTotalAmount('shipping_discount_tax_compensation', 0);
  144. $total->setBaseTotalAmount('shipping_discount_tax_compensation', 0);
  145. $total->setSubtotalInclTax(0);
  146. $total->setBaseSubtotalInclTax(0);
  147. $total->setShippingInclTax(0);
  148. $total->setBaseShippingInclTax(0);
  149. $total->setShippingTaxAmount(0);
  150. $total->setBaseShippingTaxAmount(0);
  151. $total->setShippingAmountForDiscount(0);
  152. $total->setBaseShippingAmountForDiscount(0);
  153. $total->setBaseShippingAmountForDiscount(0);
  154. $total->setTotalAmount('extra_tax', 0);
  155. $total->setBaseTotalAmount('extra_tax', 0);
  156. }
  157. /**
  158. * Call tax calculation service to get tax details on the quote and items
  159. *
  160. * @param ShippingAssignmentInterface $shippingAssignment
  161. * @param Address\Total $total
  162. * @param bool $useBaseCurrency
  163. * @return \Magento\Tax\Api\Data\TaxDetailsInterface
  164. */
  165. protected function getQuoteTaxDetails($shippingAssignment, $total, $useBaseCurrency)
  166. {
  167. $address = $shippingAssignment->getShipping()->getAddress();
  168. //Setup taxable items
  169. $priceIncludesTax = $this->_config->priceIncludesTax($address->getQuote()->getStore());
  170. $itemDataObjects = $this->mapItems($shippingAssignment, $priceIncludesTax, $useBaseCurrency);
  171. //Add shipping
  172. $shippingDataObject = $this->getShippingDataObject($shippingAssignment, $total, $useBaseCurrency);
  173. if ($shippingDataObject != null) {
  174. $itemDataObjects[] = $shippingDataObject;
  175. }
  176. //process extra taxable items associated only with quote
  177. $quoteExtraTaxables = $this->mapQuoteExtraTaxables(
  178. $this->quoteDetailsItemDataObjectFactory,
  179. $address,
  180. $useBaseCurrency
  181. );
  182. if (!empty($quoteExtraTaxables)) {
  183. $itemDataObjects = array_merge($itemDataObjects, $quoteExtraTaxables);
  184. }
  185. //Preparation for calling taxCalculationService
  186. $quoteDetails = $this->prepareQuoteDetails($shippingAssignment, $itemDataObjects);
  187. $taxDetails = $this->taxCalculationService
  188. ->calculateTax($quoteDetails, $address->getQuote()->getStore()->getStoreId());
  189. return $taxDetails;
  190. }
  191. /**
  192. * Map extra taxables associated with quote
  193. *
  194. * @param \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $itemDataObjectFactory
  195. * @param Address $address
  196. * @param bool $useBaseCurrency
  197. * @return \Magento\Tax\Api\Data\QuoteDetailsItemInterface[]
  198. */
  199. public function mapQuoteExtraTaxables(
  200. \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $itemDataObjectFactory,
  201. Address $address,
  202. $useBaseCurrency
  203. ) {
  204. $itemDataObjects = [];
  205. $extraTaxables = $address->getAssociatedTaxables();
  206. if (!$extraTaxables) {
  207. return [];
  208. }
  209. foreach ($extraTaxables as $extraTaxable) {
  210. if ($useBaseCurrency) {
  211. $unitPrice = $extraTaxable[self::KEY_ASSOCIATED_TAXABLE_BASE_UNIT_PRICE];
  212. } else {
  213. $unitPrice = $extraTaxable[self::KEY_ASSOCIATED_TAXABLE_UNIT_PRICE];
  214. }
  215. $itemDataObjects[] = $itemDataObjectFactory->create()
  216. ->setCode($extraTaxable[self::KEY_ASSOCIATED_TAXABLE_CODE])
  217. ->setType($extraTaxable[self::KEY_ASSOCIATED_TAXABLE_TYPE])
  218. ->setQuantity($extraTaxable[self::KEY_ASSOCIATED_TAXABLE_QUANTITY])
  219. ->setTaxClassKey(
  220. $this->taxClassKeyDataObjectFactory->create()
  221. ->setType(TaxClassKeyInterface::TYPE_ID)
  222. ->setValue($extraTaxable[self::KEY_ASSOCIATED_TAXABLE_TAX_CLASS_ID])
  223. )
  224. ->setUnitPrice($unitPrice)
  225. ->setIsTaxIncluded($extraTaxable[self::KEY_ASSOCIATED_TAXABLE_PRICE_INCLUDES_TAX])
  226. ->setAssociatedItemCode($extraTaxable[self::KEY_ASSOCIATED_TAXABLE_ASSOCIATION_ITEM_CODE]);
  227. }
  228. return $itemDataObjects;
  229. }
  230. /**
  231. * Process everything other than product or shipping, save the result in quote
  232. *
  233. * @param Address\Total $total
  234. * @param array $itemsByType
  235. * @return $this
  236. * @SuppressWarnings(PHPMD.UnusedLocalVariable)
  237. */
  238. protected function processExtraTaxables(Address\Total $total, array $itemsByType)
  239. {
  240. $extraTaxableDetails = [];
  241. foreach ($itemsByType as $itemType => $itemTaxDetails) {
  242. if ($itemType != self::ITEM_TYPE_PRODUCT && $itemType != self::ITEM_TYPE_SHIPPING) {
  243. foreach ($itemTaxDetails as $itemCode => $itemTaxDetail) {
  244. /** @var \Magento\Tax\Api\Data\TaxDetailsInterface $taxDetails */
  245. $taxDetails = $itemTaxDetail[self::KEY_ITEM];
  246. /** @var \Magento\Tax\Api\Data\TaxDetailsInterface $baseTaxDetails */
  247. $baseTaxDetails = $itemTaxDetail[self::KEY_BASE_ITEM];
  248. $appliedTaxes = $taxDetails->getAppliedTaxes();
  249. $baseAppliedTaxes = $baseTaxDetails->getAppliedTaxes();
  250. $associatedItemCode = $taxDetails->getAssociatedItemCode();
  251. $appliedTaxesArray = $this->convertAppliedTaxes($appliedTaxes, $baseAppliedTaxes);
  252. $extraTaxableDetails[$itemType][$associatedItemCode][] = [
  253. self::KEY_TAX_DETAILS_TYPE => $taxDetails->getType(),
  254. self::KEY_TAX_DETAILS_CODE => $taxDetails->getCode(),
  255. self::KEY_TAX_DETAILS_PRICE_EXCL_TAX => $taxDetails->getPrice(),
  256. self::KEY_TAX_DETAILS_PRICE_INCL_TAX => $taxDetails->getPriceInclTax(),
  257. self::KEY_TAX_DETAILS_BASE_PRICE_EXCL_TAX => $baseTaxDetails->getPrice(),
  258. self::KEY_TAX_DETAILS_BASE_PRICE_INCL_TAX => $baseTaxDetails->getPriceInclTax(),
  259. self::KEY_TAX_DETAILS_ROW_TOTAL => $taxDetails->getRowTotal(),
  260. self::KEY_TAX_DETAILS_ROW_TOTAL_INCL_TAX => $taxDetails->getRowTotalInclTax(),
  261. self::KEY_TAX_DETAILS_BASE_ROW_TOTAL => $baseTaxDetails->getRowTotal(),
  262. self::KEY_TAX_DETAILS_BASE_ROW_TOTAL_INCL_TAX => $baseTaxDetails->getRowTotalInclTax(),
  263. self::KEY_TAX_DETAILS_TAX_PERCENT => $taxDetails->getTaxPercent(),
  264. self::KEY_TAX_DETAILS_ROW_TAX => $taxDetails->getRowTax(),
  265. self::KEY_TAX_DETAILS_BASE_ROW_TAX => $baseTaxDetails->getRowTax(),
  266. self::KEY_TAX_DETAILS_APPLIED_TAXES => $appliedTaxesArray,
  267. ];
  268. $total->addTotalAmount('tax', $taxDetails->getRowTax());
  269. $total->addBaseTotalAmount('tax', $baseTaxDetails->getRowTax());
  270. //TODO: save applied taxes for the item
  271. }
  272. }
  273. }
  274. $total->setExtraTaxableDetails($extraTaxableDetails);
  275. return $this;
  276. }
  277. /**
  278. * Add tax totals information to address object
  279. *
  280. * @param \Magento\Quote\Model\Quote $quote
  281. * @param Address\Total $total
  282. * @return array|null
  283. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  284. * @SuppressWarnings(PHPMD.NPathComplexity)
  285. */
  286. public function fetch(\Magento\Quote\Model\Quote $quote, \Magento\Quote\Model\Quote\Address\Total $total)
  287. {
  288. $totals = [];
  289. $store = $quote->getStore();
  290. $applied = $total->getAppliedTaxes();
  291. if (is_string($applied)) {
  292. $applied = $this->serializer->unserialize($applied);
  293. }
  294. $amount = $total->getTaxAmount();
  295. if ($amount === null) {
  296. $this->enhanceTotalData($quote, $total);
  297. $amount = $total->getTaxAmount();
  298. }
  299. $taxAmount = $amount + $total->getTotalAmount('discount_tax_compensation');
  300. $area = null;
  301. if ($this->_config->displayCartTaxWithGrandTotal($store) && $total->getGrandTotal()) {
  302. $area = 'taxes';
  303. }
  304. $totals[] = [
  305. 'code' => $this->getCode(),
  306. 'title' => __('Tax'),
  307. 'full_info' => $applied ? $applied : [],
  308. 'value' => $amount,
  309. 'area' => $area,
  310. ];
  311. /**
  312. * Modify subtotal
  313. */
  314. if ($this->_config->displayCartSubtotalBoth($store) || $this->_config->displayCartSubtotalInclTax($store)) {
  315. if ($total->getSubtotalInclTax() > 0) {
  316. $subtotalInclTax = $total->getSubtotalInclTax();
  317. } else {
  318. $subtotalInclTax = $total->getSubtotal() + $taxAmount - $total->getShippingTaxAmount();
  319. }
  320. $totals[] = [
  321. 'code' => 'subtotal',
  322. 'title' => __('Subtotal'),
  323. 'value' => $subtotalInclTax,
  324. 'value_incl_tax' => $subtotalInclTax,
  325. 'value_excl_tax' => $total->getSubtotal(),
  326. ];
  327. }
  328. if (empty($totals)) {
  329. return null;
  330. }
  331. return $totals;
  332. }
  333. /**
  334. * Adds minimal tax information to the "total" data structure
  335. *
  336. * @param \Magento\Quote\Model\Quote $quote
  337. * @param Address\Total $total
  338. * @return null
  339. */
  340. protected function enhanceTotalData(
  341. \Magento\Quote\Model\Quote $quote,
  342. \Magento\Quote\Model\Quote\Address\Total $total
  343. ) {
  344. $taxAmount = 0;
  345. $shippingTaxAmount = 0;
  346. $discountTaxCompensation = 0;
  347. $subtotalInclTax = $total->getSubtotalInclTax();
  348. $computeSubtotalInclTax = true;
  349. if ($total->getSubtotalInclTax() > 0) {
  350. $computeSubtotalInclTax = false;
  351. }
  352. /** @var \Magento\Quote\Model\Quote\Address $address */
  353. foreach ($quote->getAllAddresses() as $address) {
  354. $taxAmount += $address->getTaxAmount();
  355. $shippingTaxAmount += $address->getShippingTaxAmount();
  356. $discountTaxCompensation += $address->getDiscountTaxCompensationAmount();
  357. if ($computeSubtotalInclTax) {
  358. $subtotalInclTax += $address->getSubtotalInclTax();
  359. }
  360. }
  361. $total->setTaxAmount($taxAmount);
  362. $total->setShippingTaxAmount($shippingTaxAmount);
  363. $total->setDiscountTaxCompensationAmount($discountTaxCompensation); // accessed via 'discount_tax_compensation'
  364. $total->setSubtotalInclTax($subtotalInclTax);
  365. return;
  366. }
  367. /**
  368. * Process model configuration array.
  369. *
  370. * This method can be used for changing totals collect sort order
  371. *
  372. * @param array $config
  373. * @param store $store
  374. * @return array
  375. */
  376. public function processConfigArray($config, $store)
  377. {
  378. $calculationSequence = $this->_taxData->getCalculationSequence($store);
  379. switch ($calculationSequence) {
  380. case Calculation::CALC_TAX_BEFORE_DISCOUNT_ON_INCL:
  381. $config['before'][] = 'discount';
  382. break;
  383. default:
  384. $config['after'][] = 'discount';
  385. break;
  386. }
  387. return $config;
  388. }
  389. /**
  390. * Get Tax label
  391. *
  392. * @return \Magento\Framework\Phrase
  393. */
  394. public function getLabel()
  395. {
  396. return __('Tax');
  397. }
  398. }