Shipping.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Shipping\Model;
  7. use Magento\Framework\App\ObjectManager;
  8. use Magento\Quote\Model\Quote\Address\RateCollectorInterface;
  9. use Magento\Quote\Model\Quote\Address\RateRequestFactory;
  10. use Magento\Sales\Model\Order\Shipment;
  11. /**
  12. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  13. */
  14. class Shipping implements RateCollectorInterface
  15. {
  16. /**
  17. * Default shipping orig for requests
  18. *
  19. * @var array
  20. */
  21. protected $_orig = null;
  22. /**
  23. * Cached result
  24. *
  25. * @var \Magento\Shipping\Model\Rate\Result
  26. */
  27. protected $_result = null;
  28. /**
  29. * Part of carrier xml config path
  30. *
  31. * @var string
  32. */
  33. protected $_availabilityConfigField = 'active';
  34. /**
  35. * Core store config
  36. *
  37. * @var \Magento\Framework\App\Config\ScopeConfigInterface
  38. */
  39. protected $_scopeConfig;
  40. /**
  41. * @var \Magento\Store\Model\StoreManagerInterface
  42. */
  43. protected $_storeManager;
  44. /**
  45. * @var \Magento\Shipping\Model\Config
  46. */
  47. protected $_shippingConfig;
  48. /**
  49. * @var \Magento\Shipping\Model\CarrierFactory
  50. */
  51. protected $_carrierFactory;
  52. /**
  53. * @var \Magento\Shipping\Model\Rate\ResultFactory
  54. */
  55. protected $_rateResultFactory;
  56. /**
  57. * @var \Magento\Quote\Model\Quote\Address\RateRequestFactory
  58. */
  59. protected $_shipmentRequestFactory;
  60. /**
  61. * @var \Magento\Directory\Model\RegionFactory
  62. */
  63. protected $_regionFactory;
  64. /**
  65. * @var \Magento\Framework\Math\Division
  66. */
  67. protected $mathDivision;
  68. /**
  69. * @var \Magento\CatalogInventory\Api\StockRegistryInterface
  70. */
  71. protected $stockRegistry;
  72. /**
  73. * @var RateRequestFactory
  74. */
  75. private $rateRequestFactory;
  76. /**
  77. * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
  78. * @param \Magento\Shipping\Model\Config $shippingConfig
  79. * @param \Magento\Store\Model\StoreManagerInterface $storeManager
  80. * @param \Magento\Shipping\Model\CarrierFactory $carrierFactory
  81. * @param \Magento\Shipping\Model\Rate\ResultFactory $rateResultFactory
  82. * @param \Magento\Shipping\Model\Shipment\RequestFactory $shipmentRequestFactory
  83. * @param \Magento\Directory\Model\RegionFactory $regionFactory
  84. * @param \Magento\Framework\Math\Division $mathDivision
  85. * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry
  86. * @param RateRequestFactory $rateRequestFactory
  87. *
  88. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  89. */
  90. public function __construct(
  91. \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
  92. \Magento\Shipping\Model\Config $shippingConfig,
  93. \Magento\Store\Model\StoreManagerInterface $storeManager,
  94. \Magento\Shipping\Model\CarrierFactory $carrierFactory,
  95. \Magento\Shipping\Model\Rate\ResultFactory $rateResultFactory,
  96. \Magento\Shipping\Model\Shipment\RequestFactory $shipmentRequestFactory,
  97. \Magento\Directory\Model\RegionFactory $regionFactory,
  98. \Magento\Framework\Math\Division $mathDivision,
  99. \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry,
  100. RateRequestFactory $rateRequestFactory = null
  101. ) {
  102. $this->_scopeConfig = $scopeConfig;
  103. $this->_shippingConfig = $shippingConfig;
  104. $this->_storeManager = $storeManager;
  105. $this->_carrierFactory = $carrierFactory;
  106. $this->_rateResultFactory = $rateResultFactory;
  107. $this->_shipmentRequestFactory = $shipmentRequestFactory;
  108. $this->_regionFactory = $regionFactory;
  109. $this->mathDivision = $mathDivision;
  110. $this->stockRegistry = $stockRegistry;
  111. $this->rateRequestFactory = $rateRequestFactory ?: ObjectManager::getInstance()->get(RateRequestFactory::class);
  112. }
  113. /**
  114. * Get shipping rate result model
  115. *
  116. * @return \Magento\Shipping\Model\Rate\Result
  117. */
  118. public function getResult()
  119. {
  120. if (empty($this->_result)) {
  121. $this->_result = $this->_rateResultFactory->create();
  122. }
  123. return $this->_result;
  124. }
  125. /**
  126. * Set shipping orig data
  127. *
  128. * @param array $data
  129. * @return void
  130. */
  131. public function setOrigData($data)
  132. {
  133. $this->_orig = $data;
  134. }
  135. /**
  136. * Reset cached result
  137. *
  138. * @return $this
  139. */
  140. public function resetResult()
  141. {
  142. $this->getResult()->reset();
  143. return $this;
  144. }
  145. /**
  146. * Retrieve configuration model
  147. *
  148. * @return \Magento\Shipping\Model\Config
  149. */
  150. public function getConfig()
  151. {
  152. return $this->_shippingConfig;
  153. }
  154. /**
  155. * Retrieve all methods for supplied shipping data
  156. *
  157. * @param \Magento\Quote\Model\Quote\Address\RateRequest $request
  158. * @return $this
  159. * @todo make it ordered
  160. */
  161. public function collectRates(\Magento\Quote\Model\Quote\Address\RateRequest $request)
  162. {
  163. $storeId = $request->getStoreId();
  164. if (!$request->getOrig()) {
  165. $request->setCountryId(
  166. $this->_scopeConfig->getValue(
  167. Shipment::XML_PATH_STORE_COUNTRY_ID,
  168. \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
  169. $request->getStore()
  170. )
  171. )->setRegionId(
  172. $this->_scopeConfig->getValue(
  173. Shipment::XML_PATH_STORE_REGION_ID,
  174. \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
  175. $request->getStore()
  176. )
  177. )->setCity(
  178. $this->_scopeConfig->getValue(
  179. Shipment::XML_PATH_STORE_CITY,
  180. \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
  181. $request->getStore()
  182. )
  183. )->setPostcode(
  184. $this->_scopeConfig->getValue(
  185. Shipment::XML_PATH_STORE_ZIP,
  186. \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
  187. $request->getStore()
  188. )
  189. );
  190. }
  191. $limitCarrier = $request->getLimitCarrier();
  192. if (!$limitCarrier) {
  193. $carriers = $this->_scopeConfig->getValue(
  194. 'carriers',
  195. \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
  196. $storeId
  197. );
  198. foreach ($carriers as $carrierCode => $carrierConfig) {
  199. $this->collectCarrierRates($carrierCode, $request);
  200. }
  201. } else {
  202. if (!is_array($limitCarrier)) {
  203. $limitCarrier = [$limitCarrier];
  204. }
  205. foreach ($limitCarrier as $carrierCode) {
  206. $carrierConfig = $this->_scopeConfig->getValue(
  207. 'carriers/' . $carrierCode,
  208. \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
  209. $storeId
  210. );
  211. if (!$carrierConfig) {
  212. continue;
  213. }
  214. $this->collectCarrierRates($carrierCode, $request);
  215. }
  216. }
  217. return $this;
  218. }
  219. /**
  220. * Collect rates of given carrier
  221. *
  222. * @param string $carrierCode
  223. * @param \Magento\Quote\Model\Quote\Address\RateRequest $request
  224. * @return $this
  225. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  226. * @SuppressWarnings(PHPMD.NPathComplexity)
  227. */
  228. public function collectCarrierRates($carrierCode, $request)
  229. {
  230. /* @var $carrier \Magento\Shipping\Model\Carrier\AbstractCarrier */
  231. $carrier = $this->_carrierFactory->createIfActive($carrierCode, $request->getStoreId());
  232. if (!$carrier) {
  233. return $this;
  234. }
  235. $carrier->setActiveFlag($this->_availabilityConfigField);
  236. $result = $carrier->checkAvailableShipCountries($request);
  237. if (false !== $result && !$result instanceof \Magento\Quote\Model\Quote\Address\RateResult\Error) {
  238. $result = $carrier->processAdditionalValidation($request);
  239. }
  240. /*
  241. * Result will be false if the admin set not to show the shipping module
  242. * if the delivery country is not within specific countries
  243. */
  244. if (false !== $result) {
  245. if (!$result instanceof \Magento\Quote\Model\Quote\Address\RateResult\Error) {
  246. if ($carrier->getConfigData('shipment_requesttype')) {
  247. $packages = $this->composePackagesForCarrier($carrier, $request);
  248. if (!empty($packages)) {
  249. $sumResults = [];
  250. foreach ($packages as $weight => $packageCount) {
  251. $request->setPackageWeight($weight);
  252. $result = $carrier->collectRates($request);
  253. if (!$result) {
  254. return $this;
  255. } else {
  256. $result->updateRatePrice($packageCount);
  257. }
  258. $sumResults[] = $result;
  259. }
  260. if (!empty($sumResults) && count($sumResults) > 1) {
  261. $result = [];
  262. foreach ($sumResults as $res) {
  263. if (empty($result)) {
  264. $result = $res;
  265. continue;
  266. }
  267. foreach ($res->getAllRates() as $method) {
  268. foreach ($result->getAllRates() as $resultMethod) {
  269. if ($method->getMethod() == $resultMethod->getMethod()) {
  270. $resultMethod->setPrice($method->getPrice() + $resultMethod->getPrice());
  271. continue;
  272. }
  273. }
  274. }
  275. }
  276. }
  277. } else {
  278. $result = $carrier->collectRates($request);
  279. }
  280. } else {
  281. $result = $carrier->collectRates($request);
  282. }
  283. if (!$result) {
  284. return $this;
  285. }
  286. }
  287. if ($carrier->getConfigData('showmethod') == 0 && $result->getError()) {
  288. return $this;
  289. }
  290. // sort rates by price
  291. if (method_exists($result, 'sortRatesByPrice') && is_callable([$result, 'sortRatesByPrice'])) {
  292. $result->sortRatesByPrice();
  293. }
  294. $this->getResult()->append($result);
  295. }
  296. return $this;
  297. }
  298. /**
  299. * Compose Packages For Carrier.
  300. * Divides order into items and items into parts if it's necessary
  301. *
  302. * @param \Magento\Shipping\Model\Carrier\AbstractCarrier $carrier
  303. * @param \Magento\Quote\Model\Quote\Address\RateRequest $request
  304. * @return array [int, float]
  305. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  306. * @SuppressWarnings(PHPMD.NPathComplexity)
  307. */
  308. public function composePackagesForCarrier($carrier, $request)
  309. {
  310. $allItems = $request->getAllItems();
  311. $fullItems = [];
  312. $maxWeight = (double)$carrier->getConfigData('max_package_weight');
  313. /** @var $item \Magento\Quote\Model\Quote\Item */
  314. foreach ($allItems as $item) {
  315. if ($item->getProductType() == \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE
  316. && $item->getProduct()->getShipmentType()
  317. ) {
  318. continue;
  319. }
  320. $qty = $item->getQty();
  321. $changeQty = true;
  322. $checkWeight = true;
  323. $decimalItems = [];
  324. if ($item->getParentItem()) {
  325. if (!$item->getParentItem()->getProduct()->getShipmentType()) {
  326. continue;
  327. }
  328. $qty = $item->getIsQtyDecimal()
  329. ? $item->getParentItem()->getQty()
  330. : $item->getParentItem()->getQty() * $item->getQty();
  331. }
  332. $itemWeight = $item->getWeight();
  333. if ($item->getIsQtyDecimal()
  334. && $item->getProductType() != \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE
  335. ) {
  336. $productId = $item->getProduct()->getId();
  337. $stockItem = $this->stockRegistry->getStockItem($productId, $item->getStore()->getWebsiteId());
  338. if ($stockItem->getIsDecimalDivided()) {
  339. if ($stockItem->getEnableQtyIncrements() && $stockItem->getQtyIncrements()) {
  340. $itemWeight = $itemWeight * $stockItem->getQtyIncrements();
  341. $qty = round($item->getWeight() / $itemWeight * $qty);
  342. $changeQty = false;
  343. } else {
  344. $itemWeight = $itemWeight * $item->getQty();
  345. if ($itemWeight > $maxWeight) {
  346. $qtyItem = floor($itemWeight / $maxWeight);
  347. $decimalItems[] = ['weight' => $maxWeight, 'qty' => $qtyItem];
  348. $weightItem = $this->mathDivision->getExactDivision($itemWeight, $maxWeight);
  349. if ($weightItem) {
  350. $decimalItems[] = ['weight' => $weightItem, 'qty' => 1];
  351. }
  352. $checkWeight = false;
  353. } else {
  354. $itemWeight = $itemWeight * $item->getQty();
  355. }
  356. }
  357. } else {
  358. $itemWeight = $itemWeight * $item->getQty();
  359. }
  360. }
  361. if ($checkWeight && $maxWeight && $itemWeight > $maxWeight) {
  362. return [];
  363. }
  364. if ($changeQty
  365. && !$item->getParentItem()
  366. && $item->getIsQtyDecimal()
  367. && $item->getProductType() != \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE
  368. ) {
  369. $qty = 1;
  370. }
  371. if (!empty($decimalItems)) {
  372. foreach ($decimalItems as $decimalItem) {
  373. $fullItems = array_merge(
  374. $fullItems,
  375. array_fill(0, $decimalItem['qty'] * $qty, $decimalItem['weight'])
  376. );
  377. }
  378. } else {
  379. $fullItems = array_merge($fullItems, array_fill(0, $qty, $itemWeight));
  380. }
  381. }
  382. sort($fullItems);
  383. return $this->_makePieces($fullItems, $maxWeight);
  384. }
  385. /**
  386. * Make pieces
  387. * Compose packages list based on given items, so that each package is as heavy as possible
  388. *
  389. * @param array $items
  390. * @param float $maxWeight
  391. * @return array
  392. */
  393. protected function _makePieces($items, $maxWeight)
  394. {
  395. $pieces = [];
  396. if (!empty($items)) {
  397. $sumWeight = 0;
  398. $reverseOrderItems = $items;
  399. arsort($reverseOrderItems);
  400. foreach ($reverseOrderItems as $key => $weight) {
  401. if (!isset($items[$key])) {
  402. continue;
  403. }
  404. unset($items[$key]);
  405. $sumWeight = $weight;
  406. foreach ($items as $key => $weight) {
  407. if ($sumWeight + $weight < $maxWeight) {
  408. unset($items[$key]);
  409. $sumWeight += $weight;
  410. } elseif ($sumWeight + $weight > $maxWeight) {
  411. $pieces[] = (string)(double)$sumWeight;
  412. break;
  413. } else {
  414. unset($items[$key]);
  415. $pieces[] = (string)(double)($sumWeight + $weight);
  416. $sumWeight = 0;
  417. break;
  418. }
  419. }
  420. }
  421. if ($sumWeight > 0) {
  422. $pieces[] = (string)(double)$sumWeight;
  423. }
  424. $pieces = array_count_values($pieces);
  425. }
  426. return $pieces;
  427. }
  428. /**
  429. * Collect rates by address
  430. *
  431. * @param \Magento\Framework\DataObject $address
  432. * @param null|bool|array $limitCarrier
  433. * @return $this
  434. */
  435. public function collectRatesByAddress(\Magento\Framework\DataObject $address, $limitCarrier = null)
  436. {
  437. /** @var $request \Magento\Quote\Model\Quote\Address\RateRequest */
  438. $request = $this->rateRequestFactory->create();
  439. $request->setAllItems($address->getAllItems());
  440. $request->setDestCountryId($address->getCountryId());
  441. $request->setDestRegionId($address->getRegionId());
  442. $request->setDestPostcode($address->getPostcode());
  443. $request->setPackageValue($address->getBaseSubtotal());
  444. $request->setPackageValueWithDiscount($address->getBaseSubtotalWithDiscount());
  445. $request->setPackageWeight($address->getWeight());
  446. $request->setFreeMethodWeight($address->getFreeMethodWeight());
  447. $request->setPackageQty($address->getItemQty());
  448. /** @var \Magento\Store\Api\Data\StoreInterface $store */
  449. $store = $this->_storeManager->getStore();
  450. $request->setStoreId($store->getId());
  451. $request->setWebsiteId($store->getWebsiteId());
  452. $request->setBaseCurrency($store->getBaseCurrency());
  453. $request->setPackageCurrency($store->getCurrentCurrency());
  454. $request->setLimitCarrier($limitCarrier);
  455. $request->setBaseSubtotalInclTax($address->getBaseSubtotalInclTax());
  456. return $this->collectRates($request);
  457. }
  458. /**
  459. * Set part of carrier xml config path
  460. *
  461. * @param string $code
  462. * @return $this
  463. */
  464. public function setCarrierAvailabilityConfigField($code = 'active')
  465. {
  466. $this->_availabilityConfigField = $code;
  467. return $this;
  468. }
  469. }