AbstractCalculator.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Tax\Model\Calculation;
  7. use Magento\Customer\Api\Data\AddressInterface as CustomerAddress;
  8. use Magento\Tax\Api\Data\AppliedTaxInterfaceFactory;
  9. use Magento\Tax\Api\Data\AppliedTaxRateInterfaceFactory;
  10. use Magento\Tax\Api\Data\QuoteDetailsItemInterface;
  11. use Magento\Tax\Api\Data\TaxDetailsItemInterface;
  12. use Magento\Tax\Api\Data\TaxDetailsItemInterfaceFactory;
  13. use Magento\Tax\Api\TaxClassManagementInterface;
  14. use Magento\Tax\Model\Calculation;
  15. /**
  16. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  17. */
  18. abstract class AbstractCalculator
  19. {
  20. /**#@+
  21. * Constants for delta rounding key
  22. */
  23. const KEY_REGULAR_DELTA_ROUNDING = 'regular';
  24. const KEY_APPLIED_TAX_DELTA_ROUNDING = 'applied_tax_amount';
  25. const KEY_TAX_BEFORE_DISCOUNT_DELTA_ROUNDING = 'tax_before_discount';
  26. /**#@-*/
  27. /**#@-*/
  28. protected $taxDetailsItemDataObjectFactory;
  29. /**
  30. * Tax calculation tool
  31. *
  32. * @var Calculation
  33. */
  34. protected $calculationTool;
  35. /**
  36. * Store id
  37. *
  38. * @var int
  39. */
  40. protected $storeId;
  41. /**
  42. * Customer tax class id
  43. *
  44. * @var int
  45. */
  46. protected $customerTaxClassId;
  47. /**
  48. * Customer id
  49. *
  50. * @var int
  51. */
  52. protected $customerId;
  53. /**
  54. * Shipping Address
  55. *
  56. * @var CustomerAddress
  57. */
  58. protected $shippingAddress;
  59. /**
  60. * Billing Address
  61. *
  62. * @var CustomerAddress
  63. */
  64. protected $billingAddress;
  65. /**
  66. * Tax configuration object
  67. *
  68. * @var \Magento\Tax\Model\Config
  69. */
  70. protected $config;
  71. /**
  72. * Address rate request
  73. *
  74. * Request object contain:
  75. * country_id (->getCountryId())
  76. * region_id (->getRegionId())
  77. * postcode (->getPostcode())
  78. * customer_class_id (->getCustomerClassId())
  79. * store (->getStore())
  80. *
  81. * @var \Magento\Framework\DataObject
  82. */
  83. private $addressRateRequest = null;
  84. /**
  85. * Rounding deltas for prices
  86. *
  87. * @var string[]
  88. * example:
  89. * [
  90. * 'type' => [
  91. * 'rate' => 'rounding delta',
  92. * ],
  93. * ]
  94. */
  95. protected $roundingDeltas;
  96. /**
  97. * Tax Class Service
  98. *
  99. * @var TaxClassManagementInterface
  100. */
  101. protected $taxClassManagement;
  102. /**
  103. * @var AppliedTaxInterfaceFactory
  104. */
  105. protected $appliedTaxDataObjectFactory;
  106. /**
  107. * @var AppliedTaxRateInterfaceFactory
  108. */
  109. protected $appliedTaxRateDataObjectFactory;
  110. /**
  111. * Constructor
  112. *
  113. * @param TaxClassManagementInterface $taxClassService
  114. * @param TaxDetailsItemInterfaceFactory $taxDetailsItemDataObjectFactory
  115. * @param AppliedTaxInterfaceFactory $appliedTaxDataObjectFactory
  116. * @param AppliedTaxRateInterfaceFactory $appliedTaxRateDataObjectFactory
  117. * @param Calculation $calculationTool
  118. * @param \Magento\Tax\Model\Config $config
  119. * @param int $storeId
  120. * @param \Magento\Framework\DataObject $addressRateRequest
  121. */
  122. public function __construct(
  123. TaxClassManagementInterface $taxClassService,
  124. TaxDetailsItemInterfaceFactory $taxDetailsItemDataObjectFactory,
  125. AppliedTaxInterfaceFactory $appliedTaxDataObjectFactory,
  126. AppliedTaxRateInterfaceFactory $appliedTaxRateDataObjectFactory,
  127. Calculation $calculationTool,
  128. \Magento\Tax\Model\Config $config,
  129. $storeId,
  130. \Magento\Framework\DataObject $addressRateRequest = null
  131. ) {
  132. $this->taxClassManagement = $taxClassService;
  133. $this->taxDetailsItemDataObjectFactory = $taxDetailsItemDataObjectFactory;
  134. $this->appliedTaxDataObjectFactory = $appliedTaxDataObjectFactory;
  135. $this->appliedTaxRateDataObjectFactory = $appliedTaxRateDataObjectFactory;
  136. $this->calculationTool = $calculationTool;
  137. $this->config = $config;
  138. $this->storeId = $storeId;
  139. $this->addressRateRequest = $addressRateRequest;
  140. }
  141. /**
  142. * Set billing address
  143. *
  144. * @codeCoverageIgnoreStart
  145. * @param CustomerAddress $billingAddress
  146. * @return void
  147. */
  148. public function setBillingAddress(CustomerAddress $billingAddress)
  149. {
  150. $this->billingAddress = $billingAddress;
  151. }
  152. /**
  153. * Set shipping address
  154. *
  155. * @param CustomerAddress $shippingAddress
  156. * @return void
  157. */
  158. public function setShippingAddress(CustomerAddress $shippingAddress)
  159. {
  160. $this->shippingAddress = $shippingAddress;
  161. }
  162. /**
  163. * Set customer tax class id
  164. *
  165. * @param int $customerTaxClassId
  166. * @return void
  167. */
  168. public function setCustomerTaxClassId($customerTaxClassId)
  169. {
  170. $this->customerTaxClassId = $customerTaxClassId;
  171. }
  172. /**
  173. * Set customer id
  174. *
  175. * @param int $customerId
  176. * @return void
  177. */
  178. public function setCustomerId($customerId)
  179. {
  180. $this->customerId = $customerId;
  181. }
  182. // @codeCoverageIgnoreEnd
  183. /**
  184. * Calculate tax details for quote item with given quantity
  185. *
  186. * @param QuoteDetailsItemInterface $item
  187. * @param int $quantity
  188. * @param bool $round
  189. * @return TaxDetailsItemInterface
  190. */
  191. public function calculate(QuoteDetailsItemInterface $item, $quantity, $round = true)
  192. {
  193. if ($item->getIsTaxIncluded()) {
  194. return $this->calculateWithTaxInPrice($item, $quantity, $round);
  195. } else {
  196. return $this->calculateWithTaxNotInPrice($item, $quantity, $round);
  197. }
  198. }
  199. /**
  200. * Calculate tax details for quote item with tax in price with given quantity
  201. *
  202. * @param QuoteDetailsItemInterface $item
  203. * @param int $quantity
  204. * @param bool $round
  205. * @return TaxDetailsItemInterface
  206. */
  207. abstract protected function calculateWithTaxInPrice(QuoteDetailsItemInterface $item, $quantity, $round = true);
  208. /**
  209. * Calculate tax details for quote item with tax not in price with given quantity
  210. *
  211. * @param QuoteDetailsItemInterface $item
  212. * @param int $quantity
  213. * @param bool $round
  214. * @return TaxDetailsItemInterface
  215. */
  216. abstract protected function calculateWithTaxNotInPrice(QuoteDetailsItemInterface $item, $quantity, $round = true);
  217. /**
  218. * Get address rate request
  219. *
  220. * Request object contain:
  221. * country_id (->getCountryId())
  222. * region_id (->getRegionId())
  223. * postcode (->getPostcode())
  224. * customer_class_id (->getCustomerClassId())
  225. * store (->getStore())
  226. *
  227. * @return \Magento\Framework\DataObject
  228. */
  229. protected function getAddressRateRequest()
  230. {
  231. if (null == $this->addressRateRequest) {
  232. $this->addressRateRequest = $this->calculationTool->getRateRequest(
  233. $this->shippingAddress,
  234. $this->billingAddress,
  235. $this->customerTaxClassId,
  236. $this->storeId,
  237. $this->customerId
  238. );
  239. }
  240. return $this->addressRateRequest;
  241. }
  242. /**
  243. * Check if tax rate is same as store tax rate
  244. *
  245. * @param float $rate
  246. * @param float $storeRate
  247. * @return bool
  248. */
  249. protected function isSameRateAsStore($rate, $storeRate)
  250. {
  251. if ((bool)$this->config->crossBorderTradeEnabled($this->storeId)) {
  252. return true;
  253. } else {
  254. return (abs($rate - $storeRate) < 0.00001);
  255. }
  256. }
  257. /**
  258. * Create AppliedTax data object based applied tax rates and tax amount
  259. *
  260. * @param float $rowTax
  261. * @param array $appliedRate
  262. * example:
  263. * [
  264. * 'id' => 'id',
  265. * 'percent' => 7.5,
  266. * 'rates' => [
  267. * 'code' => 'code',
  268. * 'title' => 'title',
  269. * 'percent' => 5.3,
  270. * ],
  271. * ]
  272. * @return \Magento\Tax\Api\Data\AppliedTaxInterface
  273. */
  274. protected function getAppliedTax($rowTax, $appliedRate)
  275. {
  276. $appliedTaxDataObject = $this->appliedTaxDataObjectFactory->create();
  277. $appliedTaxDataObject->setAmount($rowTax);
  278. $appliedTaxDataObject->setPercent($appliedRate['percent']);
  279. $appliedTaxDataObject->setTaxRateKey($appliedRate['id']);
  280. /** @var \Magento\Tax\Api\Data\AppliedTaxRateInterface[] $rateDataObjects */
  281. $rateDataObjects = [];
  282. foreach ($appliedRate['rates'] as $rate) {
  283. //Skipped position, priority and rule_id
  284. $rateDataObjects[$rate['code']] = $this->appliedTaxRateDataObjectFactory->create()
  285. ->setPercent($rate['percent'])
  286. ->setCode($rate['code'])
  287. ->setTitle($rate['title']);
  288. }
  289. $appliedTaxDataObject->setRates($rateDataObjects);
  290. return $appliedTaxDataObject;
  291. }
  292. /**
  293. * Create AppliedTax data object based on applied tax rates and tax amount
  294. *
  295. * @param float $rowTax
  296. * @param float $totalTaxRate
  297. * @param array $appliedRates May contain multiple tax rates when catalog price includes tax
  298. * example:
  299. * [
  300. * [
  301. * 'id' => 'id1',
  302. * 'percent' => 7.5,
  303. * 'rates' => [
  304. * 'code' => 'code1',
  305. * 'title' => 'title1',
  306. * 'percent' => 5.3,
  307. * ],
  308. * ],
  309. * [
  310. * 'id' => 'id2',
  311. * 'percent' => 8.5,
  312. * 'rates' => [
  313. * 'code' => 'code2',
  314. * 'title' => 'title2',
  315. * 'percent' => 7.3,
  316. * ],
  317. * ],
  318. * ]
  319. * @return \Magento\Tax\Api\Data\AppliedTaxInterface[]
  320. */
  321. protected function getAppliedTaxes($rowTax, $totalTaxRate, $appliedRates)
  322. {
  323. /** @var \Magento\Tax\Api\Data\AppliedTaxInterface[] $appliedTaxes */
  324. $appliedTaxes = [];
  325. $totalAppliedAmount = 0;
  326. foreach ($appliedRates as $appliedRate) {
  327. if ($appliedRate['percent'] == 0) {
  328. continue;
  329. }
  330. $appliedAmount = $rowTax / $totalTaxRate * $appliedRate['percent'];
  331. //Use delta rounding to split tax amounts for each tax rates between items
  332. $appliedAmount = $this->deltaRound(
  333. $appliedAmount,
  334. $appliedRate['id'],
  335. true,
  336. self::KEY_APPLIED_TAX_DELTA_ROUNDING
  337. );
  338. if ($totalAppliedAmount + $appliedAmount > $rowTax) {
  339. $appliedAmount = $rowTax - $totalAppliedAmount;
  340. }
  341. $totalAppliedAmount += $appliedAmount;
  342. $appliedTaxDataObject = $this->appliedTaxDataObjectFactory->create();
  343. $appliedTaxDataObject->setAmount($appliedAmount);
  344. $appliedTaxDataObject->setPercent($appliedRate['percent']);
  345. $appliedTaxDataObject->setTaxRateKey($appliedRate['id']);
  346. /** @var \Magento\Tax\Api\Data\AppliedTaxRateInterface[] $rateDataObjects */
  347. $rateDataObjects = [];
  348. foreach ($appliedRate['rates'] as $rate) {
  349. //Skipped position, priority and rule_id
  350. $rateDataObjects[$rate['code']] = $this->appliedTaxRateDataObjectFactory->create()
  351. ->setPercent($rate['percent'])
  352. ->setCode($rate['code'])
  353. ->setTitle($rate['title']);
  354. }
  355. $appliedTaxDataObject->setRates($rateDataObjects);
  356. $appliedTaxes[$appliedTaxDataObject->getTaxRateKey()] = $appliedTaxDataObject;
  357. }
  358. return $appliedTaxes;
  359. }
  360. /**
  361. * Round price based on previous rounding operation delta
  362. *
  363. * @param float $price
  364. * @param string $rate
  365. * @param bool $direction
  366. * @param string $type
  367. * @param bool $round
  368. * @return float
  369. */
  370. protected function deltaRound($price, $rate, $direction, $type = self::KEY_REGULAR_DELTA_ROUNDING, $round = true)
  371. {
  372. if ($price) {
  373. $rate = (string)$rate;
  374. $type = $type . $direction;
  375. // initialize the delta to a small number to avoid non-deterministic behavior with rounding of 0.5
  376. $delta = isset($this->roundingDeltas[$type][$rate]) ?
  377. $this->roundingDeltas[$type][$rate] :
  378. 0.000001;
  379. $price += $delta;
  380. $roundPrice = $price;
  381. if ($round) {
  382. $roundPrice = $this->calculationTool->round($roundPrice);
  383. }
  384. $this->roundingDeltas[$type][$rate] = $price - $roundPrice;
  385. $price = $roundPrice;
  386. }
  387. return $price;
  388. }
  389. /**
  390. * Given a store price that includes tax at the store rate, this function will back out the store's tax, and add in
  391. * the customer's tax. Returns this new price which is the customer's price including tax.
  392. *
  393. * @param float $storePriceInclTax
  394. * @param float $storeRate
  395. * @param float $customerRate
  396. * @param boolean $round
  397. * @return float
  398. */
  399. protected function calculatePriceInclTax($storePriceInclTax, $storeRate, $customerRate, $round = true)
  400. {
  401. $storeTax = $this->calculationTool->calcTaxAmount($storePriceInclTax, $storeRate, true, false);
  402. $priceExclTax = $storePriceInclTax - $storeTax;
  403. $customerTax = $this->calculationTool->calcTaxAmount($priceExclTax, $customerRate, false, false);
  404. $customerPriceInclTax = $priceExclTax + $customerTax;
  405. if ($round) {
  406. $customerPriceInclTax = $this->calculationTool->round($customerPriceInclTax);
  407. }
  408. return $customerPriceInclTax;
  409. }
  410. }