CommonTaxCollector.php 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917
  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\AddressInterface as CustomerAddress;
  9. use Magento\Customer\Api\Data\RegionInterfaceFactory as CustomerAddressRegionFactory;
  10. use Magento\Framework\DataObject;
  11. use Magento\Quote\Model\Quote\Address as QuoteAddress;
  12. use Magento\Quote\Model\Quote\Address\Total\AbstractTotal;
  13. use Magento\Quote\Model\Quote\Item\AbstractItem;
  14. use Magento\Store\Model\Store;
  15. use Magento\Tax\Api\Data\QuoteDetailsInterfaceFactory;
  16. use Magento\Tax\Api\Data\QuoteDetailsItemInterface;
  17. use Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory;
  18. use Magento\Tax\Api\Data\TaxClassKeyInterface;
  19. use Magento\Tax\Api\Data\TaxDetailsInterface;
  20. use Magento\Tax\Api\Data\TaxDetailsItemInterface;
  21. use Magento\Tax\Api\Data\QuoteDetailsInterface;
  22. use Magento\Quote\Api\Data\ShippingAssignmentInterface;
  23. use Magento\Tax\Helper\Data as TaxHelper;
  24. use Magento\Framework\App\ObjectManager;
  25. use Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterface;
  26. use Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterfaceFactory;
  27. /**
  28. * Tax totals calculation model
  29. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  30. */
  31. class CommonTaxCollector extends AbstractTotal
  32. {
  33. /**#@+
  34. * Constants defined for type of items
  35. */
  36. const ITEM_TYPE_SHIPPING = 'shipping';
  37. const ITEM_TYPE_PRODUCT = 'product';
  38. /**#@-*/
  39. /**
  40. * Constant for shipping item code
  41. */
  42. const ITEM_CODE_SHIPPING = 'shipping';
  43. /**#@+
  44. * Constants for array keys
  45. */
  46. const KEY_ITEM = 'item';
  47. const KEY_BASE_ITEM = 'base_item';
  48. /**#@-*/
  49. /**#@+
  50. * Constants for fields in associated taxables array
  51. */
  52. const KEY_ASSOCIATED_TAXABLE_TYPE = 'type';
  53. const KEY_ASSOCIATED_TAXABLE_CODE = 'code';
  54. const KEY_ASSOCIATED_TAXABLE_UNIT_PRICE = 'unit_price';
  55. const KEY_ASSOCIATED_TAXABLE_BASE_UNIT_PRICE = 'base_unit_price';
  56. const KEY_ASSOCIATED_TAXABLE_QUANTITY = 'quantity';
  57. const KEY_ASSOCIATED_TAXABLE_TAX_CLASS_ID = 'tax_class_id';
  58. const KEY_ASSOCIATED_TAXABLE_PRICE_INCLUDES_TAX = 'price_includes_tax';
  59. const KEY_ASSOCIATED_TAXABLE_ASSOCIATION_ITEM_CODE = 'associated_item_code';
  60. /**#@-*/
  61. /**
  62. * When an extra taxable item is associated with quote and not with an item, this value
  63. * is used as associated item code
  64. */
  65. const ASSOCIATION_ITEM_CODE_FOR_QUOTE = 'quote';
  66. /**#@+
  67. * Constants for fields in tax details for associated taxable items
  68. */
  69. const KEY_TAX_DETAILS_TYPE = 'type';
  70. const KEY_TAX_DETAILS_CODE = 'code';
  71. const KEY_TAX_DETAILS_PRICE_EXCL_TAX = 'price_excl_tax';
  72. const KEY_TAX_DETAILS_BASE_PRICE_EXCL_TAX = 'base_price_excl_tax';
  73. const KEY_TAX_DETAILS_PRICE_INCL_TAX = 'price_incl_tax';
  74. const KEY_TAX_DETAILS_BASE_PRICE_INCL_TAX = 'base_price_incl_tax';
  75. const KEY_TAX_DETAILS_ROW_TOTAL = 'row_total_excl_tax';
  76. const KEY_TAX_DETAILS_BASE_ROW_TOTAL = 'base_row_total_excl_tax';
  77. const KEY_TAX_DETAILS_ROW_TOTAL_INCL_TAX = 'row_total_incl_tax';
  78. const KEY_TAX_DETAILS_BASE_ROW_TOTAL_INCL_TAX = 'base_row_total_incl_tax';
  79. const KEY_TAX_DETAILS_TAX_PERCENT = 'tax_percent';
  80. const KEY_TAX_DETAILS_ROW_TAX = 'row_tax';
  81. const KEY_TAX_DETAILS_BASE_ROW_TAX = 'base_row_tax';
  82. const KEY_TAX_DETAILS_APPLIED_TAXES = 'applied_taxes';
  83. /**#@-*/
  84. /**#@-*/
  85. protected $_config;
  86. /**
  87. * Counter that is used to construct temporary ids for taxable items
  88. *
  89. * @var int
  90. */
  91. protected $counter = 0;
  92. /**
  93. * Tax calculation service, the collector will call the service which performs the actual calculation
  94. *
  95. * @var \Magento\Tax\Api\TaxCalculationInterface
  96. */
  97. protected $taxCalculationService;
  98. /**
  99. * Factory to create QuoteDetails as input to tax calculation service
  100. *
  101. * @var \Magento\Tax\Api\Data\QuoteDetailsInterfaceFactory
  102. */
  103. protected $quoteDetailsDataObjectFactory;
  104. /**
  105. * @var CustomerAddressFactory
  106. */
  107. protected $customerAddressFactory;
  108. /**
  109. * @var CustomerAddressRegionFactory
  110. */
  111. protected $customerAddressRegionFactory;
  112. /**
  113. * @var \Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory
  114. */
  115. protected $taxClassKeyDataObjectFactory;
  116. /**
  117. * @var \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory
  118. */
  119. protected $quoteDetailsItemDataObjectFactory;
  120. /**
  121. * @var TaxHelper
  122. */
  123. private $taxHelper;
  124. /**
  125. * @var QuoteDetailsItemExtensionInterfaceFactory
  126. */
  127. private $quoteDetailsItemExtensionFactory;
  128. /**
  129. * Class constructor
  130. *
  131. * @param \Magento\Tax\Model\Config $taxConfig
  132. * @param \Magento\Tax\Api\TaxCalculationInterface $taxCalculationService
  133. * @param QuoteDetailsInterfaceFactory $quoteDetailsDataObjectFactory
  134. * @param \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $quoteDetailsItemDataObjectFactory
  135. * @param \Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory $taxClassKeyDataObjectFactory
  136. * @param CustomerAddressFactory $customerAddressFactory
  137. * @param CustomerAddressRegionFactory $customerAddressRegionFactory
  138. * @param TaxHelper|null $taxHelper
  139. * @param QuoteDetailsItemExtensionInterfaceFactory|null $quoteDetailsItemExtensionInterfaceFactory
  140. */
  141. public function __construct(
  142. \Magento\Tax\Model\Config $taxConfig,
  143. \Magento\Tax\Api\TaxCalculationInterface $taxCalculationService,
  144. \Magento\Tax\Api\Data\QuoteDetailsInterfaceFactory $quoteDetailsDataObjectFactory,
  145. \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $quoteDetailsItemDataObjectFactory,
  146. \Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory $taxClassKeyDataObjectFactory,
  147. CustomerAddressFactory $customerAddressFactory,
  148. CustomerAddressRegionFactory $customerAddressRegionFactory,
  149. TaxHelper $taxHelper = null,
  150. QuoteDetailsItemExtensionInterfaceFactory $quoteDetailsItemExtensionInterfaceFactory = null
  151. ) {
  152. $this->taxCalculationService = $taxCalculationService;
  153. $this->quoteDetailsDataObjectFactory = $quoteDetailsDataObjectFactory;
  154. $this->_config = $taxConfig;
  155. $this->taxClassKeyDataObjectFactory = $taxClassKeyDataObjectFactory;
  156. $this->quoteDetailsItemDataObjectFactory = $quoteDetailsItemDataObjectFactory;
  157. $this->customerAddressFactory = $customerAddressFactory;
  158. $this->customerAddressRegionFactory = $customerAddressRegionFactory;
  159. $this->taxHelper = $taxHelper ?: ObjectManager::getInstance()->get(TaxHelper::class);
  160. $this->quoteDetailsItemExtensionFactory = $quoteDetailsItemExtensionInterfaceFactory ?:
  161. ObjectManager::getInstance()->get(QuoteDetailsItemExtensionInterfaceFactory::class);
  162. }
  163. /**
  164. * Map quote address to customer address
  165. *
  166. * @param QuoteAddress $address
  167. * @return CustomerAddress
  168. */
  169. public function mapAddress(QuoteAddress $address)
  170. {
  171. $customerAddress = $this->customerAddressFactory->create();
  172. $customerAddress->setCountryId($address->getCountryId());
  173. $customerAddress->setRegion(
  174. $this->customerAddressRegionFactory->create()->setRegionId($address->getRegionId())
  175. );
  176. $customerAddress->setPostcode($address->getPostcode());
  177. $customerAddress->setCity($address->getCity());
  178. $customerAddress->setStreet($address->getStreet());
  179. return $customerAddress;
  180. }
  181. /**
  182. * Map an item to item data object
  183. *
  184. * @param \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $itemDataObjectFactory
  185. * @param AbstractItem $item
  186. * @param bool $priceIncludesTax
  187. * @param bool $useBaseCurrency
  188. * @param string $parentCode
  189. * @return QuoteDetailsItemInterface
  190. */
  191. public function mapItem(
  192. \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $itemDataObjectFactory,
  193. AbstractItem $item,
  194. $priceIncludesTax,
  195. $useBaseCurrency,
  196. $parentCode = null
  197. ) {
  198. if (!$item->getTaxCalculationItemId()) {
  199. $sequence = 'sequence-' . $this->getNextIncrement();
  200. $item->setTaxCalculationItemId($sequence);
  201. }
  202. /** @var QuoteDetailsItemInterface $itemDataObject */
  203. $itemDataObject = $itemDataObjectFactory->create();
  204. $itemDataObject->setCode($item->getTaxCalculationItemId())
  205. ->setQuantity($item->getQty())
  206. ->setTaxClassKey(
  207. $this->taxClassKeyDataObjectFactory->create()
  208. ->setType(TaxClassKeyInterface::TYPE_ID)
  209. ->setValue($item->getProduct()->getTaxClassId())
  210. )
  211. ->setIsTaxIncluded($priceIncludesTax)
  212. ->setType(self::ITEM_TYPE_PRODUCT);
  213. if ($useBaseCurrency) {
  214. if (!$item->getBaseTaxCalculationPrice()) {
  215. $item->setBaseTaxCalculationPrice($item->getBaseCalculationPriceOriginal());
  216. }
  217. if ($this->taxHelper->applyTaxOnOriginalPrice()) {
  218. $baseTaxCalculationPrice = $item->getBaseOriginalPrice();
  219. } else {
  220. $baseTaxCalculationPrice = $item->getBaseCalculationPriceOriginal();
  221. }
  222. $this->setPriceForTaxCalculation($itemDataObject, (float)$baseTaxCalculationPrice);
  223. $itemDataObject->setUnitPrice($item->getBaseTaxCalculationPrice())
  224. ->setDiscountAmount($item->getBaseDiscountAmount());
  225. } else {
  226. if (!$item->getTaxCalculationPrice()) {
  227. $item->setTaxCalculationPrice($item->getCalculationPriceOriginal());
  228. }
  229. if ($this->taxHelper->applyTaxOnOriginalPrice()) {
  230. $taxCalculationPrice = $item->getOriginalPrice();
  231. } else {
  232. $taxCalculationPrice = $item->getCalculationPriceOriginal();
  233. }
  234. $this->setPriceForTaxCalculation($itemDataObject, (float)$taxCalculationPrice);
  235. $itemDataObject->setUnitPrice($item->getTaxCalculationPrice())
  236. ->setDiscountAmount($item->getDiscountAmount());
  237. }
  238. $itemDataObject->setParentCode($parentCode);
  239. return $itemDataObject;
  240. }
  241. /**
  242. * Set price for tax calculation.
  243. *
  244. * @param QuoteDetailsItemInterface $quoteDetailsItem
  245. * @param float $taxCalculationPrice
  246. * @return void
  247. */
  248. private function setPriceForTaxCalculation(QuoteDetailsItemInterface $quoteDetailsItem, float $taxCalculationPrice)
  249. {
  250. $extensionAttributes = $quoteDetailsItem->getExtensionAttributes();
  251. if (!$extensionAttributes) {
  252. $extensionAttributes = $this->quoteDetailsItemExtensionFactory->create();
  253. }
  254. $extensionAttributes->setPriceForTaxCalculation($taxCalculationPrice);
  255. $quoteDetailsItem->setExtensionAttributes($extensionAttributes);
  256. }
  257. /**
  258. * Map item extra taxables
  259. *
  260. * @param \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $itemDataObjectFactory
  261. * @param AbstractItem $item
  262. * @param bool $priceIncludesTax
  263. * @param bool $useBaseCurrency
  264. * @return QuoteDetailsItemInterface[]
  265. */
  266. public function mapItemExtraTaxables(
  267. \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $itemDataObjectFactory,
  268. AbstractItem $item,
  269. $priceIncludesTax,
  270. $useBaseCurrency
  271. ) {
  272. $itemDataObjects = [];
  273. $extraTaxables = $item->getAssociatedTaxables();
  274. if (!$extraTaxables) {
  275. return [];
  276. }
  277. foreach ($extraTaxables as $extraTaxable) {
  278. $extraTaxableIncludesTax =
  279. isset($extraTaxable['price_includes_tax']) ? $extraTaxable['price_includes_tax'] : $priceIncludesTax;
  280. if ($useBaseCurrency) {
  281. $unitPrice = $extraTaxable[self::KEY_ASSOCIATED_TAXABLE_BASE_UNIT_PRICE];
  282. } else {
  283. $unitPrice = $extraTaxable[self::KEY_ASSOCIATED_TAXABLE_UNIT_PRICE];
  284. }
  285. /** @var QuoteDetailsItemInterface $itemDataObject */
  286. $itemDataObject = $itemDataObjectFactory->create();
  287. $itemDataObject->setCode($extraTaxable[self::KEY_ASSOCIATED_TAXABLE_CODE])
  288. ->setType($extraTaxable[self::KEY_ASSOCIATED_TAXABLE_TYPE])
  289. ->setQuantity($extraTaxable[self::KEY_ASSOCIATED_TAXABLE_QUANTITY])
  290. ->setTaxClassKey(
  291. $this->taxClassKeyDataObjectFactory->create()
  292. ->setType(TaxClassKeyInterface::TYPE_ID)
  293. ->setValue($extraTaxable[self::KEY_ASSOCIATED_TAXABLE_TAX_CLASS_ID])
  294. )
  295. ->setUnitPrice($unitPrice)
  296. ->setIsTaxIncluded($extraTaxableIncludesTax)
  297. ->setAssociatedItemCode($item->getTaxCalculationItemId());
  298. $itemDataObjects[] = $itemDataObject;
  299. }
  300. return $itemDataObjects;
  301. }
  302. /**
  303. * Add quote items
  304. *
  305. * @param ShippingAssignmentInterface $shippingAssignment
  306. * @param bool $priceIncludesTax
  307. * @param bool $useBaseCurrency
  308. * @return QuoteDetailsItemInterface[]
  309. */
  310. public function mapItems(
  311. ShippingAssignmentInterface $shippingAssignment,
  312. $priceIncludesTax,
  313. $useBaseCurrency
  314. ) {
  315. $items = $shippingAssignment->getItems();
  316. if (!count($items)) {
  317. return [];
  318. }
  319. //Populate with items
  320. $itemDataObjectFactory = $this->quoteDetailsItemDataObjectFactory;
  321. $itemDataObjects = [];
  322. foreach ($items as $item) {
  323. if ($item->getParentItem()) {
  324. continue;
  325. }
  326. if ($item->getHasChildren() && $item->isChildrenCalculated()) {
  327. $parentItemDataObject = $this->mapItem(
  328. $itemDataObjectFactory,
  329. $item,
  330. $priceIncludesTax,
  331. $useBaseCurrency
  332. );
  333. $itemDataObjects[] = $parentItemDataObject;
  334. foreach ($item->getChildren() as $child) {
  335. $childItemDataObject = $this->mapItem(
  336. $itemDataObjectFactory,
  337. $child,
  338. $priceIncludesTax,
  339. $useBaseCurrency,
  340. $parentItemDataObject->getCode()
  341. );
  342. $itemDataObjects[] = $childItemDataObject;
  343. $extraTaxableItems = $this->mapItemExtraTaxables(
  344. $itemDataObjectFactory,
  345. $item,
  346. $priceIncludesTax,
  347. $useBaseCurrency
  348. );
  349. $itemDataObjects = array_merge($itemDataObjects, $extraTaxableItems);
  350. }
  351. } else {
  352. $itemDataObject = $this->mapItem($itemDataObjectFactory, $item, $priceIncludesTax, $useBaseCurrency);
  353. $itemDataObjects[] = $itemDataObject;
  354. $extraTaxableItems = $this->mapItemExtraTaxables(
  355. $itemDataObjectFactory,
  356. $item,
  357. $priceIncludesTax,
  358. $useBaseCurrency
  359. );
  360. $itemDataObjects = array_merge($itemDataObjects, $extraTaxableItems);
  361. }
  362. }
  363. return $itemDataObjects;
  364. }
  365. /**
  366. * Populate the quote details with address information
  367. *
  368. * @param QuoteDetailsInterface $quoteDetails
  369. * @param QuoteAddress $address
  370. * @return QuoteDetailsInterface
  371. */
  372. public function populateAddressData(QuoteDetailsInterface $quoteDetails, QuoteAddress $address)
  373. {
  374. $quoteDetails->setBillingAddress($this->mapAddress($address->getQuote()->getBillingAddress()));
  375. $quoteDetails->setShippingAddress($this->mapAddress($address));
  376. return $quoteDetails;
  377. }
  378. /**
  379. * Get shipping data object.
  380. *
  381. * @param ShippingAssignmentInterface $shippingAssignment
  382. * @param QuoteAddress\Total $total
  383. * @param bool $useBaseCurrency
  384. * @return QuoteDetailsItemInterface
  385. */
  386. public function getShippingDataObject(
  387. ShippingAssignmentInterface $shippingAssignment,
  388. QuoteAddress\Total $total,
  389. $useBaseCurrency
  390. ) {
  391. $store = $shippingAssignment->getShipping()->getAddress()->getQuote()->getStore();
  392. if ($total->getShippingTaxCalculationAmount() === null) {
  393. //Save the original shipping amount because shipping amount will be overridden
  394. //with shipping amount excluding tax
  395. $total->setShippingTaxCalculationAmount($total->getShippingAmount());
  396. $total->setBaseShippingTaxCalculationAmount($total->getBaseShippingAmount());
  397. }
  398. if ($total->getShippingTaxCalculationAmount() !== null) {
  399. /** @var QuoteDetailsItemInterface $itemDataObject */
  400. $itemDataObject = $this->quoteDetailsItemDataObjectFactory->create()
  401. ->setType(self::ITEM_TYPE_SHIPPING)
  402. ->setCode(self::ITEM_CODE_SHIPPING)
  403. ->setQuantity(1);
  404. if ($useBaseCurrency) {
  405. $itemDataObject->setUnitPrice($total->getBaseShippingTaxCalculationAmount());
  406. } else {
  407. $itemDataObject->setUnitPrice($total->getShippingTaxCalculationAmount());
  408. }
  409. if ($total->getShippingDiscountAmount()) {
  410. if ($useBaseCurrency) {
  411. $itemDataObject->setDiscountAmount($total->getBaseShippingDiscountAmount());
  412. } else {
  413. $itemDataObject->setDiscountAmount($total->getShippingDiscountAmount());
  414. }
  415. }
  416. $itemDataObject->setTaxClassKey(
  417. $this->taxClassKeyDataObjectFactory->create()
  418. ->setType(TaxClassKeyInterface::TYPE_ID)
  419. ->setValue($this->_config->getShippingTaxClass($store))
  420. );
  421. $itemDataObject->setIsTaxIncluded(
  422. $this->_config->shippingPriceIncludesTax($store)
  423. );
  424. return $itemDataObject;
  425. }
  426. return null;
  427. }
  428. /**
  429. * Populate QuoteDetails object from quote address object
  430. *
  431. * @param ShippingAssignmentInterface $shippingAssignment
  432. * @param QuoteDetailsItemInterface[] $itemDataObjects
  433. * @return \Magento\Tax\Api\Data\QuoteDetailsInterface
  434. */
  435. protected function prepareQuoteDetails(ShippingAssignmentInterface $shippingAssignment, $itemDataObjects)
  436. {
  437. $items = $shippingAssignment->getItems();
  438. $address = $shippingAssignment->getShipping()->getAddress();
  439. if (!count($items)) {
  440. return $this->quoteDetailsDataObjectFactory->create();
  441. }
  442. $quoteDetails = $this->quoteDetailsDataObjectFactory->create();
  443. $this->populateAddressData($quoteDetails, $address);
  444. //Set customer tax class
  445. $quoteDetails->setCustomerTaxClassKey(
  446. $this->taxClassKeyDataObjectFactory->create()
  447. ->setType(TaxClassKeyInterface::TYPE_ID)
  448. ->setValue($address->getQuote()->getCustomerTaxClassId())
  449. );
  450. $quoteDetails->setItems($itemDataObjects);
  451. $quoteDetails->setCustomerId($address->getQuote()->getCustomerId());
  452. return $quoteDetails;
  453. }
  454. /**
  455. * Organize tax details by type and by item code
  456. *
  457. * @param TaxDetailsInterface $taxDetails
  458. * @param TaxDetailsInterface $baseTaxDetails
  459. * @return array
  460. */
  461. protected function organizeItemTaxDetailsByType(
  462. TaxDetailsInterface $taxDetails,
  463. TaxDetailsInterface $baseTaxDetails
  464. ) {
  465. /** @var \Magento\Tax\Api\Data\TaxDetailsItemInterface[] $keyedItems */
  466. $keyedItems = [];
  467. foreach ($taxDetails->getItems() as $item) {
  468. $keyedItems[$item->getCode()] = $item;
  469. }
  470. /** @var \Magento\Tax\Api\Data\TaxDetailsItemInterface[] $baseKeyedItems */
  471. $baseKeyedItems = [];
  472. foreach ($baseTaxDetails->getItems() as $item) {
  473. $baseKeyedItems[$item->getCode()] = $item;
  474. }
  475. $itemsByType = [];
  476. foreach ($keyedItems as $code => $item) {
  477. $baseItem = $baseKeyedItems[$code];
  478. $itemType = $item->getType();
  479. $itemsByType[$itemType][$code] = [self::KEY_ITEM => $item, self::KEY_BASE_ITEM => $baseItem];
  480. }
  481. return $itemsByType;
  482. }
  483. /**
  484. * Process product items in the quote.
  485. * Set the following aggregated values in the quote object:
  486. * subtotal, subtotalInclTax, tax, discount_tax_compensation,
  487. *
  488. * @param ShippingAssignmentInterface $shippingAssignment
  489. * @param array $itemTaxDetails
  490. * @param QuoteAddress\Total $total
  491. * @return $this
  492. */
  493. protected function processProductItems(
  494. ShippingAssignmentInterface $shippingAssignment,
  495. array $itemTaxDetails,
  496. QuoteAddress\Total $total
  497. ) {
  498. $store = $shippingAssignment->getShipping()->getAddress()->getQuote()->getStore();
  499. /** @var AbstractItem[] $keyedAddressItems */
  500. $keyedAddressItems = [];
  501. foreach ($shippingAssignment->getItems() as $addressItem) {
  502. $keyedAddressItems[$addressItem->getTaxCalculationItemId()] = $addressItem;
  503. }
  504. $subtotal = $baseSubtotal = 0;
  505. $discountTaxCompensation = $baseDiscountTaxCompensation = 0;
  506. $tax = $baseTax = 0;
  507. $subtotalInclTax = $baseSubtotalInclTax = 0;
  508. foreach ($itemTaxDetails as $code => $itemTaxDetail) {
  509. /** @var TaxDetailsItemInterface $taxDetail */
  510. $taxDetail = $itemTaxDetail[self::KEY_ITEM];
  511. /** @var TaxDetailsItemInterface $baseTaxDetail */
  512. $baseTaxDetail = $itemTaxDetail[self::KEY_BASE_ITEM];
  513. $quoteItem = $keyedAddressItems[$code];
  514. $this->updateItemTaxInfo($quoteItem, $taxDetail, $baseTaxDetail, $store);
  515. //Update aggregated values
  516. if ($quoteItem->getHasChildren() && $quoteItem->isChildrenCalculated()) {
  517. //avoid double counting
  518. continue;
  519. }
  520. $subtotal += $taxDetail->getRowTotal();
  521. $baseSubtotal += $baseTaxDetail->getRowTotal();
  522. $discountTaxCompensation += $taxDetail->getDiscountTaxCompensationAmount();
  523. $baseDiscountTaxCompensation += $baseTaxDetail->getDiscountTaxCompensationAmount();
  524. $tax += $taxDetail->getRowTax();
  525. $baseTax += $baseTaxDetail->getRowTax();
  526. $subtotalInclTax += $taxDetail->getRowTotalInclTax();
  527. $baseSubtotalInclTax += $baseTaxDetail->getRowTotalInclTax();
  528. }
  529. //Set aggregated values
  530. $total->setTotalAmount('subtotal', $subtotal);
  531. $total->setBaseTotalAmount('subtotal', $baseSubtotal);
  532. $total->setTotalAmount('tax', $tax);
  533. $total->setBaseTotalAmount('tax', $baseTax);
  534. $total->setTotalAmount('discount_tax_compensation', $discountTaxCompensation);
  535. $total->setBaseTotalAmount('discount_tax_compensation', $baseDiscountTaxCompensation);
  536. $total->setSubtotalInclTax($subtotalInclTax);
  537. $total->setBaseSubtotalTotalInclTax($baseSubtotalInclTax);
  538. $total->setBaseSubtotalInclTax($baseSubtotalInclTax);
  539. $shippingAssignment->getShipping()->getAddress()->setBaseSubtotalTotalInclTax($baseSubtotalInclTax);
  540. return $this;
  541. }
  542. /**
  543. * Process applied taxes for items and quote
  544. *
  545. * @param QuoteAddress\Total $total
  546. * @param ShippingAssignmentInterface $shippingAssignment
  547. * @param array $itemsByType
  548. * @return $this
  549. */
  550. protected function processAppliedTaxes(
  551. QuoteAddress\Total $total,
  552. ShippingAssignmentInterface $shippingAssignment,
  553. array $itemsByType
  554. ) {
  555. $total->setAppliedTaxes([]);
  556. $allAppliedTaxesArray = [];
  557. /** @var AbstractItem[] $keyedAddressItems */
  558. $keyedAddressItems = [];
  559. foreach ($shippingAssignment->getItems() as $addressItem) {
  560. $keyedAddressItems[$addressItem->getTaxCalculationItemId()] = $addressItem;
  561. }
  562. foreach ($itemsByType as $itemType => $items) {
  563. foreach ($items as $itemTaxCalculationId => $itemTaxDetails) {
  564. /** @var TaxDetailsItemInterface $taxDetails */
  565. $taxDetails = $itemTaxDetails[self::KEY_ITEM];
  566. $baseTaxDetails = $itemTaxDetails[self::KEY_BASE_ITEM];
  567. $appliedTaxes = $taxDetails->getAppliedTaxes();
  568. $baseAppliedTaxes = $baseTaxDetails->getAppliedTaxes();
  569. $itemType = $taxDetails->getType();
  570. $itemId = null;
  571. $associatedItemId = null;
  572. if ($itemType == self::ITEM_TYPE_PRODUCT) {
  573. //Use item id instead of tax calculation id
  574. $itemId = $keyedAddressItems[$itemTaxCalculationId]->getId();
  575. } else {
  576. if ($taxDetails->getAssociatedItemCode()
  577. && $taxDetails->getAssociatedItemCode() != self::ASSOCIATION_ITEM_CODE_FOR_QUOTE) {
  578. //This item is associated with a product item
  579. $associatedItemId = $keyedAddressItems[$taxDetails->getAssociatedItemCode()]->getId();
  580. } else {
  581. //This item is associated with an order, e.g., shipping, etc.
  582. $itemId = null;
  583. }
  584. }
  585. $extraInfo = [
  586. 'item_id' => $itemId,
  587. 'item_type' => $itemType,
  588. 'associated_item_id' => $associatedItemId,
  589. ];
  590. $appliedTaxesArray = $this->convertAppliedTaxes($appliedTaxes, $baseAppliedTaxes, $extraInfo);
  591. if ($itemType == self::ITEM_TYPE_PRODUCT) {
  592. $quoteItem = $keyedAddressItems[$itemTaxCalculationId];
  593. $quoteItem->setAppliedTaxes($appliedTaxesArray);
  594. }
  595. $allAppliedTaxesArray[$itemTaxCalculationId] = $appliedTaxesArray;
  596. foreach ($appliedTaxesArray as $appliedTaxArray) {
  597. $this->_saveAppliedTaxes(
  598. $total,
  599. [$appliedTaxArray],
  600. $appliedTaxArray['amount'],
  601. $appliedTaxArray['base_amount'],
  602. $appliedTaxArray['percent']
  603. );
  604. }
  605. }
  606. }
  607. $total->setItemsAppliedTaxes($allAppliedTaxesArray);
  608. return $this;
  609. }
  610. /**
  611. * Update tax related fields for quote item
  612. *
  613. * @param AbstractItem $quoteItem
  614. * @param TaxDetailsItemInterface $itemTaxDetails
  615. * @param TaxDetailsItemInterface $baseItemTaxDetails
  616. * @param Store $store
  617. * @return $this
  618. */
  619. public function updateItemTaxInfo($quoteItem, $itemTaxDetails, $baseItemTaxDetails, $store)
  620. {
  621. //The price should be base price
  622. $quoteItem->setPrice($baseItemTaxDetails->getPrice());
  623. $quoteItem->setConvertedPrice($itemTaxDetails->getPrice());
  624. $quoteItem->setPriceInclTax($itemTaxDetails->getPriceInclTax());
  625. $quoteItem->setRowTotal($itemTaxDetails->getRowTotal());
  626. $quoteItem->setRowTotalInclTax($itemTaxDetails->getRowTotalInclTax());
  627. $quoteItem->setTaxAmount($itemTaxDetails->getRowTax());
  628. $quoteItem->setTaxPercent($itemTaxDetails->getTaxPercent());
  629. $quoteItem->setDiscountTaxCompensationAmount($itemTaxDetails->getDiscountTaxCompensationAmount());
  630. $quoteItem->setBasePrice($baseItemTaxDetails->getPrice());
  631. $quoteItem->setBasePriceInclTax($baseItemTaxDetails->getPriceInclTax());
  632. $quoteItem->setBaseRowTotal($baseItemTaxDetails->getRowTotal());
  633. $quoteItem->setBaseRowTotalInclTax($baseItemTaxDetails->getRowTotalInclTax());
  634. $quoteItem->setBaseTaxAmount($baseItemTaxDetails->getRowTax());
  635. $quoteItem->setTaxPercent($baseItemTaxDetails->getTaxPercent());
  636. $quoteItem->setBaseDiscountTaxCompensationAmount($baseItemTaxDetails->getDiscountTaxCompensationAmount());
  637. //Set discount calculation price, this may be needed by discount collector
  638. if ($this->_config->discountTax($store)) {
  639. $quoteItem->setDiscountCalculationPrice($itemTaxDetails->getPriceInclTax());
  640. $quoteItem->setBaseDiscountCalculationPrice($baseItemTaxDetails->getPriceInclTax());
  641. } else {
  642. $quoteItem->setDiscountCalculationPrice($itemTaxDetails->getPrice());
  643. $quoteItem->setBaseDiscountCalculationPrice($baseItemTaxDetails->getPrice());
  644. }
  645. return $this;
  646. }
  647. /**
  648. * Update tax related fields for shipping
  649. *
  650. * @param ShippingAssignmentInterface $shippingAssignment
  651. * @param QuoteAddress\Total $total
  652. * @param TaxDetailsItemInterface $shippingTaxDetails
  653. * @param TaxDetailsItemInterface $baseShippingTaxDetails
  654. * @return $this
  655. */
  656. protected function processShippingTaxInfo(
  657. ShippingAssignmentInterface $shippingAssignment,
  658. QuoteAddress\Total $total,
  659. $shippingTaxDetails,
  660. $baseShippingTaxDetails
  661. ) {
  662. $total->setTotalAmount('shipping', $shippingTaxDetails->getRowTotal());
  663. $total->setBaseTotalAmount('shipping', $baseShippingTaxDetails->getRowTotal());
  664. $total->setTotalAmount(
  665. 'shipping_discount_tax_compensation',
  666. $shippingTaxDetails->getDiscountTaxCompensationAmount()
  667. );
  668. $total->setBaseTotalAmount(
  669. 'shipping_discount_tax_compensation',
  670. $baseShippingTaxDetails->getDiscountTaxCompensationAmount()
  671. );
  672. $total->setShippingInclTax($shippingTaxDetails->getRowTotalInclTax());
  673. $total->setBaseShippingInclTax($baseShippingTaxDetails->getRowTotalInclTax());
  674. $total->setShippingTaxAmount($shippingTaxDetails->getRowTax());
  675. $total->setBaseShippingTaxAmount($baseShippingTaxDetails->getRowTax());
  676. //Add the shipping tax to total tax amount
  677. $total->addTotalAmount('tax', $shippingTaxDetails->getRowTax());
  678. $total->addBaseTotalAmount('tax', $baseShippingTaxDetails->getRowTax());
  679. if ($this->_config->discountTax($shippingAssignment->getShipping()->getAddress()->getQuote()->getStore())) {
  680. $total->setShippingAmountForDiscount($shippingTaxDetails->getRowTotalInclTax());
  681. $total->setBaseShippingAmountForDiscount($baseShippingTaxDetails->getRowTotalInclTax());
  682. }
  683. return $this;
  684. }
  685. /**
  686. * Convert appliedTax data object from tax calculation service to internal array format
  687. *
  688. * @param \Magento\Tax\Api\Data\AppliedTaxInterface[] $appliedTaxes
  689. * @param \Magento\Tax\Api\Data\AppliedTaxInterface[] $baseAppliedTaxes
  690. * @param array $extraInfo
  691. * @return array
  692. */
  693. public function convertAppliedTaxes($appliedTaxes, $baseAppliedTaxes, $extraInfo = [])
  694. {
  695. $appliedTaxesArray = [];
  696. if (!$appliedTaxes || !$baseAppliedTaxes) {
  697. return $appliedTaxesArray;
  698. }
  699. foreach ($appliedTaxes as $taxId => $appliedTax) {
  700. $baseAppliedTax = $baseAppliedTaxes[$taxId];
  701. $rateDataObjects = $appliedTax->getRates();
  702. $rates = [];
  703. foreach ($rateDataObjects as $rateDataObject) {
  704. $rates[] = [
  705. 'percent' => $rateDataObject->getPercent(),
  706. 'code' => $rateDataObject->getCode(),
  707. 'title' => $rateDataObject->getTitle(),
  708. ];
  709. }
  710. $appliedTaxArray = [
  711. 'amount' => $appliedTax->getAmount(),
  712. 'base_amount' => $baseAppliedTax->getAmount(),
  713. 'percent' => $appliedTax->getPercent(),
  714. 'id' => $appliedTax->getTaxRateKey(),
  715. 'rates' => $rates,
  716. ];
  717. if (!empty($extraInfo)) {
  718. $appliedTaxArray = array_merge($appliedTaxArray, $extraInfo);
  719. }
  720. $appliedTaxesArray[] = $appliedTaxArray;
  721. }
  722. return $appliedTaxesArray;
  723. }
  724. /**
  725. * Collect applied tax rates information on address level
  726. *
  727. * @param QuoteAddress\Total $total
  728. * @param array $applied
  729. * @param float $amount
  730. * @param float $baseAmount
  731. * @param float $rate
  732. * @return void
  733. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  734. * @SuppressWarnings(PHPMD.NPathComplexity)
  735. */
  736. protected function _saveAppliedTaxes(
  737. QuoteAddress\Total $total,
  738. $applied,
  739. $amount,
  740. $baseAmount,
  741. $rate
  742. ) {
  743. $previouslyAppliedTaxes = $total->getAppliedTaxes();
  744. $process = count($previouslyAppliedTaxes);
  745. foreach ($applied as $row) {
  746. if ($row['percent'] == 0) {
  747. continue;
  748. }
  749. if (!isset($previouslyAppliedTaxes[$row['id']])) {
  750. $row['process'] = $process;
  751. $row['amount'] = 0;
  752. $row['base_amount'] = 0;
  753. $previouslyAppliedTaxes[$row['id']] = $row;
  754. }
  755. if ($row['percent'] !== null) {
  756. $row['percent'] = $row['percent'] ? $row['percent'] : 1;
  757. $rate = $rate ? $rate : 1;
  758. $appliedAmount = $amount / $rate * $row['percent'];
  759. $baseAppliedAmount = $baseAmount / $rate * $row['percent'];
  760. } else {
  761. $appliedAmount = 0;
  762. $baseAppliedAmount = 0;
  763. foreach ($row['rates'] as $rate) {
  764. $appliedAmount += $rate['amount'];
  765. $baseAppliedAmount += $rate['base_amount'];
  766. }
  767. }
  768. if ($appliedAmount || $previouslyAppliedTaxes[$row['id']]['amount']) {
  769. $previouslyAppliedTaxes[$row['id']]['amount'] += $appliedAmount;
  770. $previouslyAppliedTaxes[$row['id']]['base_amount'] += $baseAppliedAmount;
  771. } else {
  772. unset($previouslyAppliedTaxes[$row['id']]);
  773. }
  774. }
  775. $total->setAppliedTaxes($previouslyAppliedTaxes);
  776. }
  777. /**
  778. * Determine whether to include shipping in tax calculation
  779. *
  780. * @return bool
  781. */
  782. protected function includeShipping()
  783. {
  784. return false;
  785. }
  786. /**
  787. * Determine whether to include item in tax calculation
  788. *
  789. * @return bool
  790. */
  791. protected function includeItems()
  792. {
  793. return false;
  794. }
  795. /**
  796. * Determine whether to include item in tax calculation
  797. *
  798. * @return bool
  799. */
  800. protected function includeExtraTax()
  801. {
  802. return false;
  803. }
  804. /**
  805. * Determine whether to save applied tax in address
  806. *
  807. * @return bool
  808. */
  809. protected function saveAppliedTaxes()
  810. {
  811. return false;
  812. }
  813. /**
  814. * Increment and return counter.
  815. *
  816. * This function is intended to be used to generate temporary id for an item.
  817. *
  818. * @return int
  819. */
  820. protected function getNextIncrement()
  821. {
  822. return ++$this->counter;
  823. }
  824. }