diff --git a/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php b/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php index 8138e193e7978..1278d156ba869 100644 --- a/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php +++ b/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php @@ -158,7 +158,7 @@ protected function canRefundItem($item, $qtys = [], $invoiceQtysRefundLimits = [ if ($item->isDummy()) { if ($item->getHasChildren()) { foreach ($item->getChildrenItems() as $child) { - if (empty($qtys)) { + if (empty($qtys) || (count(array_unique($qtys)) === 1 && (int)end($qtys) === 0)) { if ($this->canRefundNoDummyItem($child, $invoiceQtysRefundLimits)) { return true; } diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOpenAndFillCreditMemoRefundBundleWithQtyActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOpenAndFillCreditMemoRefundBundleWithQtyActionGroup.xml new file mode 100644 index 0000000000000..65a64bdf9eb90 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOpenAndFillCreditMemoRefundBundleWithQtyActionGroup.xml @@ -0,0 +1,45 @@ + + + + + + + Clicks on the 'Credit Memos' section on the Admin Orders edit page. Fills in the provided Refund details (child item qty, Shipping Refund, Adjustment Refund, Adjustment Fee and Row number). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoItemsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoItemsSection.xml index 74b773b0830d9..fa98843b0199a 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoItemsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoItemsSection.xml @@ -24,5 +24,6 @@ + \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditmemoWithBundleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditmemoWithBundleProductTest.xml new file mode 100644 index 0000000000000..3a9b252a95a3d --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditmemoWithBundleProductTest.xml @@ -0,0 +1,104 @@ + + + + + + + + <stories value="Github issue: #23440 fix Refund for bundle product without receiving product back"/> + <description value="Create Creditmemo for bundle product with without receiving product back(all child item qty = 0)"/> + <features value="Sales"/> + <severity value="AVERAGE"/> + <group value="Sales"/> + </annotations> + + <before> + <createData entity="FlatRateShippingMethodDefault" stepKey="setDefaultFlatRateShippingMethod"/> + <createData entity="Simple_US_Customer_CA" stepKey="simpleCustomer"/> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiBundleProduct" stepKey="product"/> + <createData entity="CheckboxOption" stepKey="checkboxBundleOption"> + <requiredEntity createDataKey="product"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="checkboxBundleOption"/> + <requiredEntity createDataKey="simple1"/> + <field key="qty">2</field> + <field key="is_default">1</field> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink2"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="checkboxBundleOption"/> + <requiredEntity createDataKey="simple2"/> + <field key="qty">2</field> + <field key="is_default">1</field> + </createData> + <createData entity="DropDownBundleOption" stepKey="dropDownBundleOption"> + <requiredEntity createDataKey="product"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink3"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="dropDownBundleOption"/> + <requiredEntity createDataKey="simple1"/> + <field key="qty">2</field> + <field key="is_default">1</field> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink4"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="dropDownBundleOption"/> + <requiredEntity createDataKey="simple2"/> + <field key="qty">2</field> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <deleteData createDataKey="product" stepKey="delete"/> + <deleteData createDataKey="simpleCustomer" stepKey="deleteSimpleCustomer"/> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + + <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$simpleCustomer$$"/> + </actionGroup> + <actionGroup ref="AddBundleProductToOrderAndCheckPriceInGridActionGroup" stepKey="addBundleProductToOrder"> + <argument name="product" value="$$product$$"/> + <argument name="quantity" value="1"/> + <argument name="price" value="$738.00"/> + </actionGroup> + <actionGroup ref="OrderSelectFlatRateShippingActionGroup" stepKey="orderSelectFlatRateShippingMethod"/> + <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> + <actionGroup ref="StartCreateInvoiceFromOrderPageActionGroup" stepKey="startInvoice"/> + <actionGroup ref="SubmitInvoiceActionGroup" stepKey="submitInvoice"/> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="openOrder"> + <argument name="orderId" value="$grabOrderId"/> + </actionGroup> + <actionGroup ref="AdminOpenAndFillCreditMemoRefundBundleWithQtyActionGroup" stepKey="fillCreditMemoRefund"> + <argument name="itemQtyToRefund" value="0"/> + <argument name="rowNumberItemOne" value="3"/> + <argument name="rowNumberItemTwo" value="5"/> + <argument name="rowNumberItemThree" value="6"/> + <argument name="adjustmentRefund" value="10"/> + </actionGroup> + <actionGroup ref="SubmitCreditMemoActionGroup" stepKey="submitCreditMemo" /> + + <actionGroup ref="AdminOpenCreditMemoFromOrderPageActionGroup" stepKey="openCreditMemo" /> + <scrollTo selector="{{AdminCreditMemoViewTotalSection.subtotal}}" stepKey="scrollToTotal"/> + <actionGroup ref="AssertAdminCreditMemoViewPageTotalsActionGroup" stepKey="assertCreditMemoViewPageTotals"> + <argument name="subtotal" value="$0.00"/> + <argument name="adjustmentRefund" value="$10.00"/> + <argument name="adjustmentFee" value="$0.00"/> + <argument name="grandTotal" value="$10.00"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoFactoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoFactoryTest.php new file mode 100644 index 0000000000000..4cf571d3b6108 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoFactoryTest.php @@ -0,0 +1,123 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Model\Order; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Sales\Model\Order\CreditmemoFactory; +use Magento\Sales\Model\Order\Item; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use ReflectionMethod; + +/** + * Unit test for creditmemo factory class. + */ +class CreditmemoFactoryTest extends TestCase +{ + /** + * @var CreditmemoFactory + */ + protected $subject; + + /** + * @var ReflectionMethod + */ + protected $testMethod; + + /** + * @var Item|MockObject + */ + protected $orderItemMock; + + /** + * @var Item|MockObject + */ + protected $orderChildItemOneMock; + + /** + * @var Item|MockObject + */ + protected $orderChildItemTwoMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->orderItemMock = $this->createPartialMock( + Item::class, + ['getChildrenItems', 'isDummy', 'getHasChildren', 'getId', 'getParentItemId'] + ); + $this->orderChildItemOneMock = $this->createPartialMock( + Item::class, + ['getQtyToRefund', 'getId'] + ); + $this->orderChildItemTwoMock = $this->createPartialMock( + Item::class, + ['getQtyToRefund', 'getId'] + ); + $this->testMethod = new ReflectionMethod(CreditmemoFactory::class, 'canRefundItem'); + + $objectManagerHelper = new ObjectManagerHelper($this); + $this->subject = $objectManagerHelper->getObject(CreditmemoFactory::class, []); + } + + /** + * Check if order item can be refunded + * @return void + */ + public function testCanRefundItem(): void + { + $orderItemQtys = [ + 2 => 0, + 3 => 0 + ]; + $invoiceQtysRefundLimits = []; + + $this->orderItemMock->expects($this->any()) + ->method('getId') + ->willReturn(1); + $this->orderItemMock->expects($this->any()) + ->method('getParentItemId') + ->willReturn(false); + $this->orderItemMock->expects($this->any()) + ->method('isDummy') + ->willReturn(true); + $this->orderItemMock->expects($this->any()) + ->method('getHasChildren') + ->willReturn(true); + + $this->orderChildItemOneMock->expects($this->any()) + ->method('getQtyToRefund') + ->willReturn(1); + $this->orderChildItemOneMock->expects($this->any()) + ->method('getId') + ->willReturn(2); + + $this->orderChildItemTwoMock->expects($this->any()) + ->method('getQtyToRefund') + ->willReturn(1); + $this->orderChildItemTwoMock->expects($this->any()) + ->method('getId') + ->willReturn(3); + $this->orderItemMock->expects($this->any()) + ->method('getChildrenItems') + ->willReturn([$this->orderChildItemOneMock, $this->orderChildItemTwoMock]); + + $this->testMethod->setAccessible(true); + + $this->assertTrue( + $this->testMethod->invoke( + $this->subject, + $this->orderItemMock, + $orderItemQtys, + $invoiceQtysRefundLimits + ) + ); + } +}