Skip to content

Commit c415874

Browse files
authored
Merge pull request #6020 from magento-mpi/MC-35633
[MPI] MC-35633: POST /rest/V1/shipment does not change OrderItems qtyShipped
2 parents b06b839 + 9bc7404 commit c415874

File tree

7 files changed

+547
-67
lines changed

7 files changed

+547
-67
lines changed

app/code/Magento/Sales/Model/ShipOrder.php

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,32 @@
55
*/
66
namespace Magento\Sales\Model;
77

8+
use DomainException;
89
use Magento\Framework\App\ResourceConnection;
10+
use Magento\Sales\Api\Data\ShipmentCommentCreationInterface;
11+
use Magento\Sales\Api\Data\ShipmentCreationArgumentsInterface;
12+
use Magento\Sales\Api\Data\ShipmentItemCreationInterface;
13+
use Magento\Sales\Api\Data\ShipmentPackageCreationInterface;
14+
use Magento\Sales\Api\Data\ShipmentTrackCreationInterface;
15+
use Magento\Sales\Api\Exception\CouldNotShipExceptionInterface;
16+
use Magento\Sales\Api\Exception\DocumentValidationExceptionInterface;
917
use Magento\Sales\Api\OrderRepositoryInterface;
1018
use Magento\Sales\Api\ShipmentRepositoryInterface;
1119
use Magento\Sales\Api\ShipOrderInterface;
20+
use Magento\Sales\Exception\CouldNotShipException;
21+
use Magento\Sales\Exception\DocumentValidationException;
1222
use Magento\Sales\Model\Order\Config as OrderConfig;
1323
use Magento\Sales\Model\Order\OrderStateResolverInterface;
14-
use Magento\Sales\Model\Order\ShipmentDocumentFactory;
1524
use Magento\Sales\Model\Order\Shipment\NotifierInterface;
1625
use Magento\Sales\Model\Order\Shipment\OrderRegistrarInterface;
26+
use Magento\Sales\Model\Order\ShipmentDocumentFactory;
1727
use Magento\Sales\Model\Order\Validation\ShipOrderInterface as ShipOrderValidator;
1828
use Psr\Log\LoggerInterface;
1929

2030
/**
2131
* Class ShipOrder
32+
*
33+
* Save shipment and order data
2234
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
2335
*/
2436
class ShipOrder implements ShipOrderInterface
@@ -111,30 +123,30 @@ public function __construct(
111123
}
112124

113125
/**
126+
* Process the shipment and save shipment and order data
127+
*
114128
* @param int $orderId
115-
* @param \Magento\Sales\Api\Data\ShipmentItemCreationInterface[] $items
129+
* @param ShipmentItemCreationInterface[] $items
116130
* @param bool $notify
117131
* @param bool $appendComment
118-
* @param \Magento\Sales\Api\Data\ShipmentCommentCreationInterface|null $comment
119-
* @param \Magento\Sales\Api\Data\ShipmentTrackCreationInterface[] $tracks
120-
* @param \Magento\Sales\Api\Data\ShipmentPackageCreationInterface[] $packages
121-
* @param \Magento\Sales\Api\Data\ShipmentCreationArgumentsInterface|null $arguments
132+
* @param ShipmentCommentCreationInterface|null $comment
133+
* @param ShipmentTrackCreationInterface[] $tracks
134+
* @param ShipmentPackageCreationInterface[] $packages
135+
* @param ShipmentCreationArgumentsInterface|null $arguments
122136
* @return int
123-
* @throws \Magento\Sales\Api\Exception\DocumentValidationExceptionInterface
124-
* @throws \Magento\Sales\Api\Exception\CouldNotShipExceptionInterface
125-
* @throws \Magento\Framework\Exception\InputException
126-
* @throws \Magento\Framework\Exception\NoSuchEntityException
127-
* @throws \DomainException
137+
* @throws DocumentValidationExceptionInterface
138+
* @throws CouldNotShipExceptionInterface
139+
* @throws DomainException
128140
*/
129141
public function execute(
130142
$orderId,
131143
array $items = [],
132144
$notify = false,
133145
$appendComment = false,
134-
\Magento\Sales\Api\Data\ShipmentCommentCreationInterface $comment = null,
146+
ShipmentCommentCreationInterface $comment = null,
135147
array $tracks = [],
136148
array $packages = [],
137-
\Magento\Sales\Api\Data\ShipmentCreationArgumentsInterface $arguments = null
149+
ShipmentCreationArgumentsInterface $arguments = null
138150
) {
139151
$connection = $this->resourceConnection->getConnection('sales');
140152
$order = $this->orderRepository->get($orderId);
@@ -158,7 +170,7 @@ public function execute(
158170
$packages
159171
);
160172
if ($validationMessages->hasMessages()) {
161-
throw new \Magento\Sales\Exception\DocumentValidationException(
173+
throw new DocumentValidationException(
162174
__("Shipment Document Validation Error(s):\n" . implode("\n", $validationMessages->getMessages()))
163175
);
164176
}
@@ -169,16 +181,19 @@ public function execute(
169181
$this->orderStateResolver->getStateForOrder($order, [OrderStateResolverInterface::IN_PROGRESS])
170182
);
171183
$order->setStatus($this->config->getStateDefaultStatus($order->getState()));
172-
$this->shipmentRepository->save($shipment);
184+
$shippingData = $this->shipmentRepository->save($shipment);
173185
$this->orderRepository->save($order);
174186
$connection->commit();
175187
} catch (\Exception $e) {
176188
$this->logger->critical($e);
177189
$connection->rollBack();
178-
throw new \Magento\Sales\Exception\CouldNotShipException(
190+
throw new CouldNotShipException(
179191
__('Could not save a shipment, see error log for details')
180192
);
181193
}
194+
if ($shipment && empty($shipment->getEntityId())) {
195+
$shipment->setEntityId($shippingData->getEntityId());
196+
}
182197
if ($notify) {
183198
if (!$appendComment) {
184199
$comment = null;
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Sales\Plugin;
9+
10+
use Exception;
11+
use Magento\Framework\DB\Transaction;
12+
use Magento\Framework\Exception\LocalizedException;
13+
use Magento\Sales\Api\Data\ShipmentInterface;
14+
use Magento\Sales\Model\Order;
15+
use Magento\Sales\Model\Order\Shipment\Item;
16+
use Magento\Sales\Model\Order\ShipmentRepository;
17+
use Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader;
18+
19+
/**
20+
* Plugin to update order data before and after saving shipment via API
21+
*/
22+
class ProcessOrderAndShipmentViaAPI
23+
{
24+
/**
25+
* @var ShipmentLoader
26+
*/
27+
private $shipmentLoader;
28+
29+
/**
30+
* @var Transaction
31+
*/
32+
private $transaction;
33+
34+
/**
35+
* Init plugin
36+
*
37+
* @param ShipmentLoader $shipmentLoader
38+
* @param Transaction $transaction
39+
*/
40+
public function __construct(
41+
ShipmentLoader $shipmentLoader,
42+
Transaction $transaction
43+
) {
44+
$this->shipmentLoader = $shipmentLoader;
45+
$this->transaction = $transaction;
46+
}
47+
48+
/**
49+
* Process shipping details before saving shipment via API
50+
*
51+
* @param ShipmentRepository $shipmentRepository
52+
* @param ShipmentInterface $shipmentData
53+
* @return array
54+
* @throws LocalizedException
55+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
56+
* @SuppressWarnings(PHPMD.NPathComplexity)
57+
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
58+
*/
59+
public function beforeSave(
60+
ShipmentRepository $shipmentRepository,
61+
ShipmentInterface $shipmentData
62+
): array {
63+
$this->shipmentLoader->setOrderId($shipmentData->getOrderId());
64+
$trackData = !empty($shipmentData->getTracks()) ?
65+
$this->getShipmentTracking($shipmentData) : [];
66+
$this->shipmentLoader->setTracking($trackData);
67+
$shipmentItems = !empty($shipmentData) ?
68+
$this->getShipmentItems($shipmentData) : [];
69+
$orderItems = [];
70+
if (!empty($shipmentData)) {
71+
$order = $shipmentData->getOrder();
72+
$orderItems = $order ? $this->getOrderItems($order) : [];
73+
}
74+
$data = (!empty($shipmentItems) && !empty($orderItems)) ?
75+
$this->getShippingData($shipmentItems, $orderItems) : [];
76+
$this->shipmentLoader->setShipment($data);
77+
$shipment = $this->shipmentLoader->load();
78+
$shipment = empty($shipment) ? $shipmentData
79+
: $this->processShippingDetails($shipmentData, $shipment);
80+
return [$shipment];
81+
}
82+
83+
/**
84+
* Save order data after saving shipment via API
85+
*
86+
* @param ShipmentRepository $shipmentRepository
87+
* @param ShipmentInterface $shipment
88+
* @return ShipmentInterface
89+
* @throws Exception
90+
*/
91+
public function afterSave(
92+
ShipmentRepository $shipmentRepository,
93+
ShipmentInterface $shipment
94+
): ShipmentInterface {
95+
$shipmentDetails = $shipmentRepository->get($shipment->getEntityId());
96+
$order = $shipmentDetails->getOrder();
97+
$shipmentItems = !empty($shipment) ?
98+
$this->getShipmentItems($shipment) : [];
99+
$this->processOrderItems($order, $shipmentItems);
100+
$order->setIsInProcess(true);
101+
$this->transaction
102+
->addObject($order)
103+
->save();
104+
return $shipment;
105+
}
106+
107+
/**
108+
* Process shipment items
109+
*
110+
* @param ShipmentInterface $shipment
111+
* @return array
112+
* @throws LocalizedException
113+
*/
114+
private function getShipmentItems(ShipmentInterface $shipment): array
115+
{
116+
$shipmentItems = [];
117+
foreach ($shipment->getItems() as $item) {
118+
$sku = $item->getSku();
119+
if (isset($sku)) {
120+
$shipmentItems[$sku]['qty'] = $item->getQty();
121+
}
122+
}
123+
return $shipmentItems;
124+
}
125+
126+
/**
127+
* Get shipment tracking data from the shipment array
128+
*
129+
* @param ShipmentInterface $shipment
130+
* @return array
131+
*/
132+
private function getShipmentTracking(ShipmentInterface $shipment): array
133+
{
134+
$trackData = [];
135+
foreach ($shipment->getTracks() as $key => $track) {
136+
$trackData[$key]['number'] = $track->getTrackNumber();
137+
$trackData[$key]['title'] = $track->getTitle();
138+
$trackData[$key]['carrier_code'] = $track->getCarrierCode();
139+
}
140+
return $trackData;
141+
}
142+
143+
/**
144+
* Get orderItems from shipment order
145+
*
146+
* @param Order $order
147+
* @return array
148+
*/
149+
private function getOrderItems(Order $order): array
150+
{
151+
$orderItems = [];
152+
foreach ($order->getItems() as $item) {
153+
$orderItems[$item->getSku()] = $item->getItemId();
154+
}
155+
return $orderItems;
156+
}
157+
158+
/**
159+
* Get available shipping data from shippingItems and orderItems
160+
*
161+
* @param array $shipmentItems
162+
* @param array $orderItems
163+
* @return array
164+
* @throws LocalizedException
165+
*/
166+
private function getShippingData(array $shipmentItems, array $orderItems): array
167+
{
168+
$data = [];
169+
foreach ($shipmentItems as $shippingItemSku => $shipmentItem) {
170+
if (isset($orderItems[$shippingItemSku])) {
171+
$itemId = (int) $orderItems[$shippingItemSku];
172+
$data['items'][$itemId] = $shipmentItem['qty'];
173+
}
174+
}
175+
return $data;
176+
}
177+
178+
/**
179+
* Process shipping comments if available
180+
*
181+
* @param ShipmentInterface $shipmentData
182+
* @param ShipmentInterface $shipment
183+
* @return void
184+
*/
185+
private function processShippingComments(ShipmentInterface $shipmentData, ShipmentInterface $shipment): void
186+
{
187+
foreach ($shipmentData->getComments() as $comment) {
188+
$shipment->addComment(
189+
$comment->getComment(),
190+
$comment->getIsCustomerNotified(),
191+
$comment->getIsVisibleOnFront()
192+
);
193+
$shipment->setCustomerNote($comment->getComment());
194+
$shipment->setCustomerNoteNotify((bool) $comment->getIsCustomerNotified());
195+
}
196+
}
197+
198+
/**
199+
* Process shipping details
200+
*
201+
* @param ShipmentInterface $shipmentData
202+
* @param ShipmentInterface $shipment
203+
* @return ShipmentInterface
204+
*/
205+
private function processShippingDetails(
206+
ShipmentInterface $shipmentData,
207+
ShipmentInterface $shipment
208+
): ShipmentInterface {
209+
if (empty($shipment->getItems())) {
210+
$shipment->setItems($shipmentData->getItems());
211+
}
212+
if (!empty($shipmentData->getComments())) {
213+
$this->processShippingComments($shipmentData, $shipment);
214+
}
215+
if ((int) $shipment->getTotalQty() < 1) {
216+
$shipment->setTotalQty($shipmentData->getTotalQty());
217+
}
218+
return $shipment;
219+
}
220+
221+
/**
222+
* Process order items data and set the proper item qty
223+
*
224+
* @param Order $order
225+
* @param array $shipmentItems
226+
* @throws LocalizedException
227+
*/
228+
private function processOrderItems(Order $order, array $shipmentItems): void
229+
{
230+
/** @var Item $item */
231+
foreach ($order->getAllItems() as $item) {
232+
if (isset($shipmentItems[$item->getSku()])) {
233+
$qty = (float)$shipmentItems[$item->getSku()]['qty'];
234+
$item->setQty($qty);
235+
if ((float)$item->getQtyToShip() > 0) {
236+
$item->setQtyShipped((float)$item->getQtyToShip());
237+
}
238+
}
239+
}
240+
}
241+
}

app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
use Psr\Log\LoggerInterface;
3535

3636
/**
37+
* Class ShipOrderTest
38+
*
39+
* Test Save shipment and order data
3740
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
3841
* @SuppressWarnings(PHPMD.TooManyFields)
3942
*/
@@ -186,7 +189,7 @@ protected function setUp(): void
186189
->getMockForAbstractClass();
187190
$this->shipOrderValidatorMock = $this->getMockBuilder(ShipOrderInterface::class)
188191
->disableOriginalConstructor()
189-
->getMockForAbstractClass();
192+
->getMock();
190193
$this->validationMessagesMock = $this->getMockBuilder(ValidatorResultInterface::class)
191194
->disableOriginalConstructor()
192195
->setMethods(['hasMessages', 'getMessages', 'addMessage'])
@@ -291,7 +294,7 @@ public function testExecute($orderId, $items, $notify, $appendComment)
291294
->method('notify')
292295
->with($this->orderMock, $this->shipmentMock, $this->shipmentCommentCreationMock);
293296
}
294-
$this->shipmentMock->expects($this->once())
297+
$this->shipmentMock->expects($this->exactly(2))
295298
->method('getEntityId')
296299
->willReturn(2);
297300
$this->assertEquals(

app/code/Magento/Sales/etc/webapi_rest/di.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,7 @@
1919
</argument>
2020
</arguments>
2121
</type>
22+
<type name="Magento\Sales\Model\Order\ShipmentRepository">
23+
<plugin name="process_order_and_shipment_via_api" type="Magento\Sales\Plugin\ProcessOrderAndShipmentViaAPI" />
24+
</type>
2225
</config>

app/code/Magento/Sales/etc/webapi_soap/di.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,7 @@
1919
</argument>
2020
</arguments>
2121
</type>
22+
<type name="Magento\Sales\Model\Order\ShipmentRepository">
23+
<plugin name="process_order_and_shipment_via_api" type="Magento\Sales\Plugin\ProcessOrderAndShipmentViaAPI" />
24+
</type>
2225
</config>

0 commit comments

Comments
 (0)