Skip to content

Commit 6b088e9

Browse files
committed
Merge branch 'ACP2E-2756' of https://github.com/magento-l3/magento2ce into PR-03-12-2024-anna
2 parents ea79f7d + 5091255 commit 6b088e9

File tree

6 files changed

+166
-75
lines changed

6 files changed

+166
-75
lines changed

app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface;
99
use Magento\Backend\App\Action;
1010
use Magento\Sales\Helper\Data as SalesData;
11-
use Magento\Sales\Model\Order;
11+
use Magento\Sales\Model\Order\Creditmemo;
1212
use Magento\Sales\Model\Order\Email\Sender\CreditmemoSender;
1313

1414
class Save extends \Magento\Backend\App\Action implements HttpPostActionInterface
@@ -18,7 +18,7 @@ class Save extends \Magento\Backend\App\Action implements HttpPostActionInterfac
1818
*
1919
* @see _isAllowed()
2020
*/
21-
const ADMIN_RESOURCE = 'Magento_Sales::sales_creditmemo';
21+
public const ADMIN_RESOURCE = 'Magento_Sales::sales_creditmemo';
2222

2323
/**
2424
* @var \Magento\Sales\Controller\Adminhtml\Order\CreditmemoLoader
@@ -85,6 +85,7 @@ public function execute()
8585
$this->creditmemoLoader->setCreditmemo($this->getRequest()->getParam('creditmemo'));
8686
$this->creditmemoLoader->setInvoiceId($this->getRequest()->getParam('invoice_id'));
8787
$creditmemo = $this->creditmemoLoader->load();
88+
$this->adjustCreditMemoItemQuantities($creditmemo);
8889
if ($creditmemo) {
8990
if (!$creditmemo->isValidGrandTotal()) {
9091
throw new \Magento\Framework\Exception\LocalizedException(
@@ -141,4 +142,33 @@ public function execute()
141142
$resultRedirect->setPath('sales/*/new', ['_current' => true]);
142143
return $resultRedirect;
143144
}
145+
146+
/**
147+
* Adjust credit memo parent item quantities with children quantities
148+
*
149+
* @param Creditmemo $creditMemo
150+
* @return void
151+
*/
152+
private function adjustCreditMemoItemQuantities(Creditmemo $creditMemo): void
153+
{
154+
$items = $creditMemo->getAllItems();
155+
$parentQuantities = [];
156+
foreach ($items as $item) {
157+
if ($parentId = $item->getOrderItem()->getParentItemId()) {
158+
if (empty($parentQuantities[$parentId])) {
159+
$parentQuantities[$parentId] = $item->getQty();
160+
} else {
161+
$parentQuantities[$parentId] += $item->getQty();
162+
}
163+
}
164+
}
165+
166+
foreach ($parentQuantities as $parentId => $quantity) {
167+
foreach ($items as $item) {
168+
if ($item->getOrderItemId() == $parentId) {
169+
$item->setQty($quantity);
170+
}
171+
}
172+
}
173+
}
144174
}

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

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -861,7 +861,6 @@ public function canComment()
861861
* Retrieve order shipment availability
862862
*
863863
* @return bool
864-
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
865864
*/
866865
public function canShip()
867866
{
@@ -877,27 +876,26 @@ public function canShip()
877876
return false;
878877
}
879878

879+
return $this->checkItemShipping();
880+
}
881+
882+
/**
883+
* Check if at least one of the order items can be shipped
884+
*
885+
* @return bool
886+
*/
887+
private function checkItemShipping(): bool
888+
{
880889
foreach ($this->getAllItems() as $item) {
881890
$qtyToShip = !$item->getParentItem() || $item->getParentItem()->getProductType() !== Type::TYPE_BUNDLE ?
882891
$item->getQtyToShip() : $item->getSimpleQtyToShip();
883892

884-
if ($qtyToShip > 0 && !$item->getIsVirtual() &&
885-
!$item->getLockedDoShip() && !$this->isRefunded($item)) {
893+
if ($qtyToShip > 0 && !$item->getIsVirtual() && !$item->getLockedDoShip()) {
886894
return true;
887895
}
888896
}
889-
return false;
890-
}
891897

892-
/**
893-
* Check if item is refunded.
894-
*
895-
* @param OrderItemInterface $item
896-
* @return bool
897-
*/
898-
private function isRefunded(OrderItemInterface $item)
899-
{
900-
return $item->getQtyRefunded() == $item->getQtyOrdered();
898+
return false;
901899
}
902900

903901
/**

app/code/Magento/Sales/Model/Order/Item.php

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,36 +24,38 @@
2424
*/
2525
class Item extends AbstractModel implements OrderItemInterface
2626
{
27-
const STATUS_PENDING = 1;
27+
public const STATUS_PENDING = 1;
2828

2929
// No items shipped, invoiced, canceled, refunded nor backordered
30-
const STATUS_SHIPPED = 2;
30+
public const STATUS_SHIPPED = 2;
3131

3232
// When qty ordered - [qty canceled + qty returned] = qty shipped
33-
const STATUS_INVOICED = 9;
33+
public const STATUS_INVOICED = 9;
3434

3535
// When qty ordered - [qty canceled + qty returned] = qty invoiced
36-
const STATUS_BACKORDERED = 3;
36+
public const STATUS_BACKORDERED = 3;
3737

3838
// When qty ordered - [qty canceled + qty returned] = qty backordered
39-
const STATUS_CANCELED = 5;
39+
public const STATUS_CANCELED = 5;
4040

4141
// When qty ordered = qty canceled
42-
const STATUS_PARTIAL = 6;
42+
public const STATUS_PARTIAL = 6;
4343

4444
// If [qty shipped or(max of two) qty invoiced + qty canceled + qty returned]
4545
// < qty ordered
46-
const STATUS_MIXED = 7;
46+
public const STATUS_MIXED = 7;
4747

4848
// All other combinations
49-
const STATUS_REFUNDED = 8;
49+
public const STATUS_REFUNDED = 8;
5050

5151
// When qty ordered = qty refunded
52-
const STATUS_RETURNED = 4;
52+
public const STATUS_RETURNED = 4;
5353

54-
// When qty ordered = qty returned // not used at the moment
55-
56-
// When qty ordered = qty returned // not used at the moment
54+
/**
55+
* When qty ordered = qty returned // not used at the moment
56+
*
57+
* @var string
58+
*/
5759
protected $_eventPrefix = 'sales_order_item';
5860

5961
/**

app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php

Lines changed: 45 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -18,84 +18,87 @@ class State
1818
*
1919
* @param Order $order
2020
* @return $this
21-
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
22-
* @SuppressWarnings(PHPMD.NPathComplexity)
2321
*/
2422
public function check(Order $order)
2523
{
2624
$currentState = $order->getState();
27-
if ($currentState == Order::STATE_NEW && $order->getIsInProcess()) {
25+
if ($this->checkForProcessingState($order, $currentState)) {
2826
$order->setState(Order::STATE_PROCESSING)
2927
->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING));
3028
$currentState = Order::STATE_PROCESSING;
3129
}
30+
if ($order->isCanceled() || $order->canUnhold() || $order->canInvoice()) {
31+
return $this;
32+
}
33+
34+
if ($this->checkForClosedState($order, $currentState)) {
35+
$order->setState(Order::STATE_CLOSED)
36+
->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_CLOSED));
37+
return $this;
38+
}
3239

33-
if (!$order->isCanceled() && !$order->canUnhold() && !$order->canInvoice()) {
34-
if (in_array($currentState, [Order::STATE_PROCESSING, Order::STATE_COMPLETE])
35-
&& !$order->canCreditmemo()
36-
&& !$order->canShip()
37-
&& $order->getIsNotVirtual()
38-
) {
39-
$order->setState(Order::STATE_CLOSED)
40-
->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_CLOSED));
41-
} elseif ($currentState === Order::STATE_PROCESSING
42-
&& (!$order->canShip() || $this->isPartiallyRefundedOrderShipped($order))
43-
) {
44-
$order->setState(Order::STATE_COMPLETE)
45-
->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_COMPLETE));
46-
} elseif ($order->getIsVirtual() && $order->getStatus() === Order::STATE_CLOSED) {
47-
$order->setState(Order::STATE_CLOSED);
48-
}
40+
if ($this->checkForCompleteState($order, $currentState)) {
41+
$order->setState(Order::STATE_COMPLETE)
42+
->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_COMPLETE));
43+
return $this;
4944
}
45+
5046
return $this;
5147
}
5248

5349
/**
54-
* Check if all items are remaining items after partially refunded are shipped
50+
* Check if order can be automatically switched to complete state
5551
*
5652
* @param Order $order
53+
* @param string|null $currentState
5754
* @return bool
5855
*/
59-
public function isPartiallyRefundedOrderShipped(Order $order): bool
56+
private function checkForCompleteState(Order $order, ?string $currentState): bool
6057
{
61-
$isPartiallyRefundedOrderShipped = false;
62-
if ($this->getShippedItems($order) > 0
63-
&& $order->getTotalQtyOrdered() <= $this->getRefundedItems($order) + $this->getShippedItems($order)) {
64-
$isPartiallyRefundedOrderShipped = true;
58+
if ($currentState === Order::STATE_PROCESSING && !$order->canShip()) {
59+
return true;
6560
}
6661

67-
return $isPartiallyRefundedOrderShipped;
62+
return false;
6863
}
6964

7065
/**
71-
* Get all refunded items number
66+
* Check if order can be automatically switched to closed state
7267
*
7368
* @param Order $order
74-
* @return int
69+
* @param string|null $currentState
70+
* @return bool
7571
*/
76-
private function getRefundedItems(Order $order): int
72+
private function checkForClosedState(Order $order, ?string $currentState): bool
7773
{
78-
$numOfRefundedItems = 0;
79-
foreach ($order->getAllItems() as $item) {
80-
if ($item->getProductType() == 'simple') {
81-
$numOfRefundedItems += (int)$item->getQtyRefunded();
82-
}
74+
if (in_array($currentState, [Order::STATE_PROCESSING, Order::STATE_COMPLETE])
75+
&& !$order->canCreditmemo()
76+
&& !$order->canShip()
77+
&& $order->getIsNotVirtual()
78+
) {
79+
return true;
8380
}
84-
return $numOfRefundedItems;
81+
82+
if ($order->getIsVirtual() && $order->getStatus() === Order::STATE_CLOSED) {
83+
return true;
84+
}
85+
86+
return false;
8587
}
8688

8789
/**
88-
* Get all shipped items number
90+
* Check if order can be automatically switched to processing state
8991
*
9092
* @param Order $order
91-
* @return int
93+
* @param string|null $currentState
94+
* @return bool
9295
*/
93-
private function getShippedItems(Order $order): int
96+
private function checkForProcessingState(Order $order, ?string $currentState): bool
9497
{
95-
$numOfShippedItems = 0;
96-
foreach ($order->getAllItems() as $item) {
97-
$numOfShippedItems += (int)$item->getQtyShipped();
98+
if ($currentState == Order::STATE_NEW && $order->getIsInProcess()) {
99+
return true;
98100
}
99-
return $numOfShippedItems;
101+
102+
return false;
100103
}
101104
}

app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use Magento\Sales\Helper\Data as SalesData;
2828
use Magento\Sales\Model\Order;
2929
use Magento\Sales\Model\Order\Creditmemo;
30+
use Magento\Sales\Model\Order\Creditmemo\Item;
3031
use Magento\Sales\Model\Order\Email\Sender\CreditmemoSender;
3132
use PHPUnit\Framework\MockObject\MockObject;
3233
use PHPUnit\Framework\TestCase;
@@ -210,9 +211,19 @@ public function testSaveActionOnlineRefundToStoreCredit()
210211

211212
$creditmemoMock = $this->createPartialMock(
212213
Creditmemo::class,
213-
['load', 'getGrandTotal']
214+
['load', 'getGrandTotal', 'getAllItems']
214215
);
215216
$creditmemoMock->expects($this->once())->method('getGrandTotal')->willReturn('1');
217+
$orderItem = $this->createMock(Order\Item::class);
218+
$orderItem->expects($this->once())
219+
->method('getParentItemId');
220+
$creditMemoItem = $this->createMock(Item::class);
221+
$creditMemoItem->expects($this->once())
222+
->method('getOrderItem')
223+
->willReturn($orderItem);
224+
$creditmemoMock->expects($this->once())
225+
->method('getAllItems')
226+
->willReturn([$creditMemoItem]);
216227
$this->memoLoaderMock->expects(
217228
$this->once()
218229
)->method(
@@ -258,9 +269,19 @@ public function testSaveActionWithNegativeCreditmemo()
258269

259270
$creditmemoMock = $this->createPartialMock(
260271
Creditmemo::class,
261-
['load', 'isValidGrandTotal']
272+
['load', 'isValidGrandTotal', 'getAllItems']
262273
);
263274
$creditmemoMock->expects($this->once())->method('isValidGrandTotal')->willReturn(false);
275+
$orderItem = $this->createMock(Order\Item::class);
276+
$orderItem->expects($this->once())
277+
->method('getParentItemId');
278+
$creditMemoItem = $this->createMock(Item::class);
279+
$creditMemoItem->expects($this->once())
280+
->method('getOrderItem')
281+
->willReturn($orderItem);
282+
$creditmemoMock->expects($this->once())
283+
->method('getAllItems')
284+
->willReturn([$creditMemoItem]);
264285
$this->memoLoaderMock->expects(
265286
$this->once()
266287
)->method(
@@ -342,7 +363,7 @@ public function testExecuteEmails(
342363

343364
$creditmemo = $this->createPartialMock(
344365
Creditmemo::class,
345-
['isValidGrandTotal', 'getOrder', 'getOrderId']
366+
['isValidGrandTotal', 'getOrder', 'getOrderId', 'getAllItems']
346367
);
347368
$creditmemo->expects($this->once())
348369
->method('isValidGrandTotal')
@@ -353,6 +374,16 @@ public function testExecuteEmails(
353374
$creditmemo->expects($this->once())
354375
->method('getOrderId')
355376
->willReturn($orderId);
377+
$orderItem = $this->createMock(Order\Item::class);
378+
$orderItem->expects($this->once())
379+
->method('getParentItemId');
380+
$creditMemoItem = $this->createMock(Item::class);
381+
$creditMemoItem->expects($this->once())
382+
->method('getOrderItem')
383+
->willReturn($orderItem);
384+
$creditmemo->expects($this->once())
385+
->method('getAllItems')
386+
->willReturn([$creditMemoItem]);
356387

357388
$this->_requestMock->expects($this->any())
358389
->method('getParam')

0 commit comments

Comments
 (0)