ShipmentFactory.php 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Sales\Model\Order;
  7. use Magento\Framework\Exception\LocalizedException;
  8. use Magento\Framework\Serialize\Serializer\Json;
  9. /**
  10. * Factory class for @see \Magento\Sales\Api\Data\ShipmentInterface
  11. *
  12. * @api
  13. * @since 100.0.2
  14. */
  15. class ShipmentFactory
  16. {
  17. /**
  18. * Order converter.
  19. *
  20. * @var \Magento\Sales\Model\Convert\Order
  21. */
  22. protected $converter;
  23. /**
  24. * Shipment track factory.
  25. *
  26. * @var \Magento\Sales\Model\Order\Shipment\TrackFactory
  27. */
  28. protected $trackFactory;
  29. /**
  30. * Instance name to create.
  31. *
  32. * @var string
  33. */
  34. protected $instanceName;
  35. /**
  36. * Serializer
  37. *
  38. * @var Json
  39. */
  40. private $serializer;
  41. /**
  42. * Factory constructor.
  43. *
  44. * @param \Magento\Sales\Model\Convert\OrderFactory $convertOrderFactory
  45. * @param \Magento\Sales\Model\Order\Shipment\TrackFactory $trackFactory
  46. * @param \Magento\Framework\Serialize\Serializer\Json $serializer
  47. */
  48. public function __construct(
  49. \Magento\Sales\Model\Convert\OrderFactory $convertOrderFactory,
  50. \Magento\Sales\Model\Order\Shipment\TrackFactory $trackFactory,
  51. Json $serializer = null
  52. ) {
  53. $this->converter = $convertOrderFactory->create();
  54. $this->trackFactory = $trackFactory;
  55. $this->instanceName = \Magento\Sales\Api\Data\ShipmentInterface::class;
  56. $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()
  57. ->get(Json::class);
  58. }
  59. /**
  60. * Creates shipment instance with specified parameters.
  61. *
  62. * @param \Magento\Sales\Model\Order $order
  63. * @param array $items
  64. * @param array|null $tracks
  65. * @return \Magento\Sales\Api\Data\ShipmentInterface
  66. */
  67. public function create(\Magento\Sales\Model\Order $order, array $items = [], $tracks = null)
  68. {
  69. $shipment = $this->prepareItems($this->converter->toShipment($order), $order, $items);
  70. if ($tracks) {
  71. $shipment = $this->prepareTracks($shipment, $tracks);
  72. }
  73. return $shipment;
  74. }
  75. /**
  76. * Adds items to the shipment.
  77. *
  78. * @param \Magento\Sales\Api\Data\ShipmentInterface $shipment
  79. * @param \Magento\Sales\Model\Order $order
  80. * @param array $items
  81. * @return \Magento\Sales\Api\Data\ShipmentInterface
  82. * @throws LocalizedException
  83. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  84. */
  85. protected function prepareItems(
  86. \Magento\Sales\Api\Data\ShipmentInterface $shipment,
  87. \Magento\Sales\Model\Order $order,
  88. array $items = []
  89. ) {
  90. $shipmentItems = [];
  91. foreach ($order->getAllItems() as $orderItem) {
  92. if ($this->validateItem($orderItem, $items) === false) {
  93. continue;
  94. }
  95. /** @var \Magento\Sales\Model\Order\Shipment\Item $item */
  96. $item = $this->converter->itemToShipmentItem($orderItem);
  97. if ($orderItem->getIsVirtual() || ($orderItem->getParentItemId() && !$orderItem->isShipSeparately())) {
  98. $item->isDeleted(true);
  99. }
  100. if ($orderItem->isDummy(true)) {
  101. $qty = 0;
  102. if (isset($items[$orderItem->getParentItemId()])) {
  103. $productOptions = $orderItem->getProductOptions();
  104. if (isset($productOptions['bundle_selection_attributes'])) {
  105. $bundleSelectionAttributes = $this->serializer->unserialize(
  106. $productOptions['bundle_selection_attributes']
  107. );
  108. if ($bundleSelectionAttributes) {
  109. $qty = $bundleSelectionAttributes['qty'] * $items[$orderItem->getParentItemId()];
  110. $qty = min($qty, $orderItem->getSimpleQtyToShip());
  111. $item->setQty($this->castQty($orderItem, $qty));
  112. $shipmentItems[] = $item;
  113. continue;
  114. } else {
  115. $qty = 1;
  116. }
  117. }
  118. } else {
  119. $qty = 1;
  120. }
  121. } else {
  122. if (isset($items[$orderItem->getId()])) {
  123. $qty = min($items[$orderItem->getId()], $orderItem->getQtyToShip());
  124. } elseif (!count($items)) {
  125. $qty = $orderItem->getQtyToShip();
  126. } else {
  127. continue;
  128. }
  129. }
  130. $item->setQty($this->castQty($orderItem, $qty));
  131. $shipmentItems[] = $item;
  132. }
  133. return $this->setItemsToShipment($shipment, $shipmentItems);
  134. }
  135. /**
  136. * Validate order item before shipment
  137. *
  138. * @param Item $orderItem
  139. * @param array $items
  140. * @return bool
  141. */
  142. private function validateItem(\Magento\Sales\Model\Order\Item $orderItem, array $items)
  143. {
  144. if (!$this->canShipItem($orderItem, $items)) {
  145. return false;
  146. }
  147. // Remove from shipment items without qty or with qty=0
  148. if (!$orderItem->isDummy(true)
  149. && (!isset($items[$orderItem->getId()]) || $items[$orderItem->getId()] <= 0)
  150. ) {
  151. return false;
  152. }
  153. return true;
  154. }
  155. /**
  156. * Set prepared items to shipment document
  157. *
  158. * @param \Magento\Sales\Api\Data\ShipmentInterface $shipment
  159. * @param array $shipmentItems
  160. * @return \Magento\Sales\Api\Data\ShipmentInterface
  161. */
  162. private function setItemsToShipment(\Magento\Sales\Api\Data\ShipmentInterface $shipment, $shipmentItems)
  163. {
  164. $totalQty = 0;
  165. /**
  166. * Verify that composite products shipped separately has children, if not -> remove from collection
  167. */
  168. /** @var \Magento\Sales\Model\Order\Shipment\Item $shipmentItem */
  169. foreach ($shipmentItems as $key => $shipmentItem) {
  170. if ($shipmentItem->getOrderItem()->getHasChildren()
  171. && $shipmentItem->getOrderItem()->isShipSeparately()
  172. ) {
  173. $containerId = $shipmentItem->getOrderItem()->getId();
  174. $childItems = array_filter($shipmentItems, function ($item) use ($containerId) {
  175. return $containerId == $item->getOrderItem()->getParentItemId();
  176. });
  177. if (count($childItems) <= 0) {
  178. unset($shipmentItems[$key]);
  179. continue;
  180. }
  181. }
  182. $totalQty += $shipmentItem->getQty();
  183. $shipment->addItem($shipmentItem);
  184. }
  185. return $shipment->setTotalQty($totalQty);
  186. }
  187. /**
  188. * Adds tracks to the shipment.
  189. *
  190. * @param \Magento\Sales\Api\Data\ShipmentInterface $shipment
  191. * @param array $tracks
  192. * @throws \Magento\Framework\Exception\LocalizedException
  193. * @return \Magento\Sales\Api\Data\ShipmentInterface
  194. */
  195. protected function prepareTracks(\Magento\Sales\Api\Data\ShipmentInterface $shipment, array $tracks)
  196. {
  197. foreach ($tracks as $data) {
  198. if (empty($data['number'])) {
  199. throw new \Magento\Framework\Exception\LocalizedException(
  200. __('Please enter a tracking number.')
  201. );
  202. }
  203. $shipment->addTrack(
  204. $this->trackFactory->create()->addData($data)
  205. );
  206. }
  207. return $shipment;
  208. }
  209. /**
  210. * Checks if order item can be shipped.
  211. *
  212. * Dummy item can be shipped or with his children or
  213. * with parent item which is included to shipment.
  214. *
  215. * @param \Magento\Sales\Model\Order\Item $item
  216. * @param array $items
  217. * @return bool
  218. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  219. */
  220. protected function canShipItem($item, array $items = [])
  221. {
  222. if ($item->getIsVirtual() || $item->getLockedDoShip()) {
  223. return false;
  224. }
  225. if ($item->isDummy(true)) {
  226. if ($item->getHasChildren()) {
  227. if ($item->isShipSeparately()) {
  228. return true;
  229. }
  230. foreach ($item->getChildrenItems() as $child) {
  231. if ($child->getIsVirtual()) {
  232. continue;
  233. }
  234. if (empty($items)) {
  235. if ($child->getQtyToShip() > 0) {
  236. return true;
  237. }
  238. } else {
  239. if (isset($items[$child->getId()]) && $items[$child->getId()] > 0) {
  240. return true;
  241. }
  242. }
  243. }
  244. return false;
  245. } elseif ($item->getParentItem()) {
  246. $parent = $item->getParentItem();
  247. if (empty($items)) {
  248. return $parent->getQtyToShip() > 0;
  249. } else {
  250. return isset($items[$parent->getId()]) && $items[$parent->getId()] > 0;
  251. }
  252. }
  253. } else {
  254. return $item->getQtyToShip() > 0;
  255. }
  256. }
  257. /**
  258. * @param Item $item
  259. * @param string|int|float $qty
  260. * @return float|int
  261. */
  262. private function castQty(\Magento\Sales\Model\Order\Item $item, $qty)
  263. {
  264. if ($item->getIsQtyDecimal()) {
  265. $qty = (double)$qty;
  266. } else {
  267. $qty = (int)$qty;
  268. }
  269. return $qty > 0 ? $qty : 0;
  270. }
  271. }