Calculation.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. /**
  7. * Tax Calculation Resource Model
  8. */
  9. namespace Magento\Tax\Model\ResourceModel;
  10. class Calculation extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
  11. {
  12. /**
  13. * Store ISO 3166-1 alpha-2 USA country code
  14. */
  15. const USA_COUNTRY_CODE = 'US';
  16. /**
  17. * Rates cache
  18. *
  19. * @var array
  20. */
  21. protected $_ratesCache = [];
  22. /**
  23. * Tax data
  24. *
  25. * @var \Magento\Tax\Helper\Data
  26. */
  27. protected $_taxData;
  28. /**
  29. * @var \Magento\Store\Model\StoreManagerInterface
  30. */
  31. protected $_storeManager;
  32. /**
  33. * @param \Magento\Framework\Model\ResourceModel\Db\Context $context
  34. * @param \Magento\Tax\Helper\Data $taxData
  35. * @param \Magento\Store\Model\StoreManagerInterface $storeManager
  36. * @param string $connectionName
  37. */
  38. public function __construct(
  39. \Magento\Framework\Model\ResourceModel\Db\Context $context,
  40. \Magento\Tax\Helper\Data $taxData,
  41. \Magento\Store\Model\StoreManagerInterface $storeManager,
  42. $connectionName = null
  43. ) {
  44. $this->_taxData = $taxData;
  45. $this->_storeManager = $storeManager;
  46. parent::__construct($context, $connectionName);
  47. }
  48. /**
  49. * Resource initialization
  50. *
  51. * @return void
  52. */
  53. protected function _construct()
  54. {
  55. $this->_setMainTable('tax_calculation');
  56. }
  57. /**
  58. * Delete calculation settings by rule id
  59. *
  60. * @param int $ruleId
  61. * @return $this
  62. */
  63. public function deleteByRuleId($ruleId)
  64. {
  65. $conn = $this->getConnection();
  66. $where = $conn->quoteInto('tax_calculation_rule_id = ?', (int)$ruleId);
  67. $conn->delete($this->getMainTable(), $where);
  68. return $this;
  69. }
  70. /**
  71. * Retrieve distinct calculation
  72. *
  73. * @param string $field
  74. * @param int $ruleId
  75. * @return array
  76. */
  77. public function getCalculationsById($field, $ruleId)
  78. {
  79. $select = $this->getConnection()->select();
  80. $select->from($this->getMainTable(), $field)->where('tax_calculation_rule_id = ?', (int)$ruleId);
  81. return $this->getConnection()->fetchCol($select);
  82. }
  83. /**
  84. * Get tax rate information: calculation process data and tax rate
  85. *
  86. * @param \Magento\Framework\DataObject $request
  87. * @return array
  88. */
  89. public function getRateInfo($request)
  90. {
  91. $rates = $this->_getRates($request);
  92. return [
  93. 'process' => $this->getCalculationProcess($request, $rates),
  94. 'value' => $this->_calculateRate($rates)
  95. ];
  96. }
  97. /**
  98. * Get tax rate for specific tax rate request
  99. *
  100. * @param \Magento\Framework\DataObject $request
  101. * @return int
  102. */
  103. public function getRate($request)
  104. {
  105. return $this->_calculateRate($this->_getRates($request));
  106. }
  107. /**
  108. * Retrieve Calculation Process
  109. *
  110. * @param \Magento\Framework\DataObject $request
  111. * @param array|null $rates
  112. * @return array
  113. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  114. * @SuppressWarnings(PHPMD.NPathComplexity)
  115. */
  116. public function getCalculationProcess($request, $rates = null)
  117. {
  118. if ($rates === null) {
  119. $rates = $this->_getRates($request);
  120. }
  121. $result = [];
  122. $row = [];
  123. $ids = [];
  124. $currentRate = 0;
  125. $totalPercent = 0;
  126. $countedRates = count($rates);
  127. for ($i = 0; $i < $countedRates; $i++) {
  128. $rate = $rates[$i];
  129. $value = (isset($rate['value']) ? $rate['value'] : $rate['percent']) * 1;
  130. $oneRate = [
  131. 'code' => $rate['code'],
  132. 'title' => $rate['title'],
  133. 'percent' => $value,
  134. 'position' => $rate['position'],
  135. 'priority' => $rate['priority']
  136. ];
  137. if (isset($rate['tax_calculation_rule_id'])) {
  138. $oneRate['rule_id'] = $rate['tax_calculation_rule_id'];
  139. }
  140. if (isset($rate['hidden'])) {
  141. $row['hidden'] = $rate['hidden'];
  142. }
  143. if (isset($rate['amount'])) {
  144. $row['amount'] = $rate['amount'];
  145. }
  146. if (isset($rate['base_amount'])) {
  147. $row['base_amount'] = $rate['base_amount'];
  148. }
  149. if (isset($rate['base_real_amount'])) {
  150. $row['base_real_amount'] = $rate['base_real_amount'];
  151. }
  152. $row['rates'][] = $oneRate;
  153. $ruleId = null;
  154. if (isset($rates[$i + 1]['tax_calculation_rule_id'])) {
  155. $ruleId = $rate['tax_calculation_rule_id'];
  156. }
  157. $priority = $rate['priority'];
  158. $ids[] = $rate['code'];
  159. if (isset($rates[$i + 1]['tax_calculation_rule_id'])) {
  160. while (isset($rates[$i + 1]) && $rates[$i + 1]['tax_calculation_rule_id'] == $ruleId) {
  161. $i++;
  162. }
  163. }
  164. $currentRate += $value;
  165. if (!isset(
  166. $rates[$i + 1]
  167. ) || $rates[$i + 1]['priority'] != $priority || isset(
  168. $rates[$i + 1]['process']
  169. ) && $rates[$i + 1]['process'] != $rate['process']
  170. ) {
  171. if (!empty($rates[$i]['calculate_subtotal'])) {
  172. $row['percent'] = $currentRate;
  173. $totalPercent += $currentRate;
  174. } else {
  175. $row['percent'] = $this->_collectPercent($totalPercent, $currentRate);
  176. $totalPercent += $row['percent'];
  177. }
  178. $row['id'] = implode('', $ids);
  179. $result[] = $row;
  180. $row = [];
  181. $ids = [];
  182. $currentRate = 0;
  183. }
  184. }
  185. return $result;
  186. }
  187. /**
  188. * Return combined percent value
  189. *
  190. * @param float|int $percent
  191. * @param float|int $rate
  192. * @return float
  193. */
  194. protected function _collectPercent($percent, $rate)
  195. {
  196. return (100 + $percent) * ($rate / 100);
  197. }
  198. /**
  199. * Create search templates for postcode
  200. *
  201. * @param string $postcode
  202. * @param string|null $exactPostcode
  203. * @return string[]
  204. */
  205. protected function _createSearchPostCodeTemplates($postcode, $exactPostcode = null)
  206. {
  207. // as needed, reduce the postcode to the correct length
  208. $len = $this->_taxData->getPostCodeSubStringLength();
  209. $postcode = substr($postcode, 0, $len);
  210. // begin creating the search template array
  211. $strArr = [$postcode, $postcode . '*'];
  212. // if supplied, use the exact postcode as the basis for the search templates
  213. if ($exactPostcode) {
  214. $postcode = substr($exactPostcode, 0, $len);
  215. $strArr[] = $postcode;
  216. }
  217. // finish building out the search template array
  218. $strlen = strlen($postcode);
  219. for ($i = 1; $i < $strlen; $i++) {
  220. $strArr[] = sprintf('%s*', substr($postcode, 0, -$i));
  221. }
  222. return $strArr;
  223. }
  224. /**
  225. * Returns tax rates for request - either pereforms SELECT from DB, or returns already cached result
  226. * Notice that productClassId due to optimization can be array of ids
  227. *
  228. * @param \Magento\Framework\DataObject $request
  229. * @return array
  230. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  231. * @SuppressWarnings(PHPMD.NPathComplexity)
  232. * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
  233. */
  234. protected function _getRates($request)
  235. {
  236. // Extract params that influence our SELECT statement and use them to create cache key
  237. $storeId = $this->_storeManager->getStore($request->getStore())->getId();
  238. $customerClassId = $request->getCustomerClassId();
  239. $countryId = $request->getCountryId();
  240. $regionId = $request->getRegionId();
  241. $postcode = $request->getPostcode();
  242. // Process productClassId as it can be array or usual value. Form best key for cache.
  243. $productClassId = $request->getProductClassId();
  244. $ids = is_array($productClassId) ? $productClassId : [$productClassId];
  245. foreach ($ids as $key => $val) {
  246. $ids[$key] = (int)$val; // Make it integer for equal cache keys even in case of null/false/0 values
  247. }
  248. $ids = array_unique($ids);
  249. sort($ids);
  250. $productClassKey = implode(',', $ids);
  251. // Form cache key and either get data from cache or from DB
  252. $cacheKey = implode(
  253. '|',
  254. [$storeId, $customerClassId, $productClassKey, $countryId, $regionId, $postcode]
  255. );
  256. if (!isset($this->_ratesCache[$cacheKey])) {
  257. // Make SELECT and get data
  258. $select = $this->getConnection()->select();
  259. $select->from(
  260. ['main_table' => $this->getMainTable()],
  261. [
  262. 'tax_calculation_rate_id',
  263. 'tax_calculation_rule_id',
  264. 'customer_tax_class_id',
  265. 'product_tax_class_id'
  266. ]
  267. )->where(
  268. 'customer_tax_class_id = ?',
  269. (int)$customerClassId
  270. );
  271. if ($productClassId) {
  272. $select->where('product_tax_class_id IN (?)', $productClassId);
  273. }
  274. $ifnullTitleValue = $this->getConnection()->getCheckSql(
  275. 'title_table.value IS NULL',
  276. 'rate.code',
  277. 'title_table.value'
  278. );
  279. $ruleTableAliasName = $this->getConnection()->quoteIdentifier('rule.tax_calculation_rule_id');
  280. $select->join(
  281. ['rule' => $this->getTable('tax_calculation_rule')],
  282. $ruleTableAliasName . ' = main_table.tax_calculation_rule_id',
  283. ['rule.priority', 'rule.position', 'rule.calculate_subtotal']
  284. )->join(
  285. ['rate' => $this->getTable('tax_calculation_rate')],
  286. 'rate.tax_calculation_rate_id = main_table.tax_calculation_rate_id',
  287. [
  288. 'value' => 'rate.rate',
  289. 'rate.tax_country_id',
  290. 'rate.tax_region_id',
  291. 'rate.tax_postcode',
  292. 'rate.tax_calculation_rate_id',
  293. 'rate.code'
  294. ]
  295. )->joinLeft(
  296. ['title_table' => $this->getTable('tax_calculation_rate_title')],
  297. "rate.tax_calculation_rate_id = title_table.tax_calculation_rate_id " .
  298. "AND title_table.store_id = '{$storeId}'",
  299. ['title' => $ifnullTitleValue]
  300. )->where(
  301. 'rate.tax_country_id = ?',
  302. $countryId
  303. )->where(
  304. "rate.tax_region_id IN(?)",
  305. [0, (int)$regionId]
  306. );
  307. $postcodeIsNumeric = is_numeric($postcode);
  308. $postcodeIsRange = false;
  309. $originalPostcode = null;
  310. if (is_string($postcode) && preg_match('/^(.+)-(.+)$/', $postcode, $matches)) {
  311. if ($countryId == self::USA_COUNTRY_CODE && is_numeric($matches[2]) && strlen($matches[2]) == 4) {
  312. $postcodeIsNumeric = true;
  313. $originalPostcode = $postcode;
  314. $postcode = $matches[1];
  315. } else {
  316. $postcodeIsRange = true;
  317. $zipFrom = $matches[1];
  318. $zipTo = $matches[2];
  319. }
  320. }
  321. if ($postcodeIsNumeric || $postcodeIsRange) {
  322. $selectClone = clone $select;
  323. $selectClone->where('rate.zip_is_range IS NOT NULL');
  324. }
  325. $select->where('rate.zip_is_range IS NULL');
  326. if ($postcode != '*' || $postcodeIsRange) {
  327. $select->where(
  328. "rate.tax_postcode IS NULL OR rate.tax_postcode IN('*', '', ?)",
  329. $postcodeIsRange ? $postcode : $this->_createSearchPostCodeTemplates($postcode, $originalPostcode)
  330. );
  331. if ($postcodeIsNumeric) {
  332. $selectClone->where('? BETWEEN rate.zip_from AND rate.zip_to', $postcode);
  333. } elseif ($postcodeIsRange) {
  334. $selectClone->where('rate.zip_from >= ?', $zipFrom)
  335. ->where('rate.zip_to <= ?', $zipTo);
  336. }
  337. }
  338. /**
  339. * @see ZF-7592 issue http://framework.zend.com/issues/browse/ZF-7592
  340. */
  341. if ($postcodeIsNumeric || $postcodeIsRange) {
  342. $select = $this->getConnection()->select()->union(
  343. ['(' . $select . ')', '(' . $selectClone . ')']
  344. );
  345. }
  346. $select->order(
  347. 'priority ' . \Magento\Framework\DB\Select::SQL_ASC
  348. )->order(
  349. 'tax_calculation_rule_id ' . \Magento\Framework\DB\Select::SQL_ASC
  350. )->order(
  351. 'tax_country_id ' . \Magento\Framework\DB\Select::SQL_DESC
  352. )->order(
  353. 'tax_region_id ' . \Magento\Framework\DB\Select::SQL_DESC
  354. )->order(
  355. 'tax_postcode ' . \Magento\Framework\DB\Select::SQL_DESC
  356. )->order(
  357. 'value ' . \Magento\Framework\DB\Select::SQL_DESC
  358. );
  359. $fetchResult = $this->getConnection()->fetchAll($select);
  360. $filteredRates = [];
  361. if ($fetchResult) {
  362. foreach ($fetchResult as $rate) {
  363. if (!isset($filteredRates[$rate['tax_calculation_rate_id']])) {
  364. $filteredRates[$rate['tax_calculation_rate_id']] = $rate;
  365. }
  366. }
  367. }
  368. $this->_ratesCache[$cacheKey] = array_values($filteredRates);
  369. }
  370. return $this->_ratesCache[$cacheKey];
  371. }
  372. /**
  373. * Calculate rate
  374. *
  375. * @param array $rates
  376. * @return int
  377. */
  378. protected function _calculateRate($rates)
  379. {
  380. $result = 0;
  381. $currentRate = 0;
  382. $countedRates = count($rates);
  383. for ($i = 0; $i < $countedRates; $i++) {
  384. $rate = $rates[$i];
  385. $rule = $rate['tax_calculation_rule_id'];
  386. $value = $rate['value'];
  387. $priority = $rate['priority'];
  388. while (isset($rates[$i + 1]) && $rates[$i + 1]['tax_calculation_rule_id'] == $rule) {
  389. $i++;
  390. }
  391. $currentRate += $value;
  392. if (!isset($rates[$i + 1]) || $rates[$i + 1]['priority'] != $priority) {
  393. if (!empty($rates[$i]['calculate_subtotal'])) {
  394. $result += $currentRate;
  395. } else {
  396. $result += $this->_collectPercent($result, $currentRate);
  397. }
  398. $currentRate = 0;
  399. }
  400. }
  401. return $result;
  402. }
  403. /**
  404. * Retrieve rate ids
  405. *
  406. * @param \Magento\Framework\DataObject $request
  407. * @return array
  408. */
  409. public function getRateIds($request)
  410. {
  411. $result = [];
  412. $rates = $this->_getRates($request);
  413. $countedRates = count($rates);
  414. for ($i = 0; $i < $countedRates; $i++) {
  415. $rate = $rates[$i];
  416. $rule = $rate['tax_calculation_rule_id'];
  417. $result[] = $rate['tax_calculation_rate_id'];
  418. while (isset($rates[$i + 1]) && $rates[$i + 1]['tax_calculation_rule_id'] == $rule) {
  419. $i++;
  420. }
  421. }
  422. return $result;
  423. }
  424. }