diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php index c71ec520dfb71..9ce10623307d1 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php @@ -6,6 +6,7 @@ namespace Magento\Sales\Model\ResourceModel\Order\Handler; +use Magento\Catalog\Model\Product\Type; use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Invoice; @@ -60,13 +61,81 @@ public function check(Order $order) */ private function checkForCompleteState(Order $order, ?string $currentState): bool { - if ($currentState === Order::STATE_PROCESSING && !$order->canShip()) { + if ($currentState === Order::STATE_PROCESSING + && (!$order->canShip() || $this->isPartiallyRefundedOrderShipped($order)) + ) { return true; } return false; } + /** + * Check if all items are remaining items after partially refunded are shipped + * + * @param Order $order + * @return bool + */ + public function isPartiallyRefundedOrderShipped(Order $order): bool + { + $isPartiallyRefundedOrderShipped = false; + if ($this->getShippedItems($order) > 0 + && $this->getQtyItemsToShip($order) <= $this->getRefundedItems($order) + $this->getShippedItems($order)) { + $isPartiallyRefundedOrderShipped = true; + } + + return $isPartiallyRefundedOrderShipped; + } + + /** + * Get all refunded items number + * + * @param Order $order + * @return int + */ + private function getQtyItemsToShip(Order $order): int + { + $numOfItemsToShip = 0; + foreach ($order->getAllItems() as $item) { + if ($item->getProductType() == Type::TYPE_SIMPLE) { // only simple products are accountable for the order qty + $numOfItemsToShip += (int)$item->getQtyOrdered(); + } + } + return $numOfItemsToShip; + } + + /** + * Get all refunded items number + * + * @param Order $order + * @return int + */ + private function getRefundedItems(Order $order): int + { + $numOfRefundedItems = 0; + foreach ($order->getAllItems() as $item) { + if ($item->getProductType() == 'simple') { + $numOfRefundedItems += (int)$item->getQtyRefunded(); + } + } + return $numOfRefundedItems; + } + + /** + * Get all shipped items number + * + * @param Order $order + * @return int + */ + private function getShippedItems(Order $order): int + { + $numOfShippedItems = 0; + foreach ($order->getAllItems() as $item) { + $numOfShippedItems += (int)$item->getQtyShipped(); + } + return $numOfShippedItems; + } + /** * Check if order has unpaid invoices * diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/OrderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/OrderTest.php index 69242db7f5e7c..9666dabfa7462 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/OrderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/OrderTest.php @@ -8,20 +8,18 @@ namespace Magento\Sales\Model; use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Catalog\Test\Fixture\Virtual as ProductVirtualFixture; use Magento\Checkout\Test\Fixture\PlaceOrder as PlaceOrderFixture; use Magento\Checkout\Test\Fixture\SetBillingAddress as SetBillingAddressFixture; use Magento\Checkout\Test\Fixture\SetDeliveryMethod as SetDeliveryMethodFixture; use Magento\Checkout\Test\Fixture\SetGuestEmail as SetGuestEmailFixture; use Magento\Checkout\Test\Fixture\SetPaymentMethod as SetPaymentMethodFixture; use Magento\Checkout\Test\Fixture\SetShippingAddress as SetShippingAddressFixture; -use Magento\Framework\App\Config\MutableScopeConfigInterface; use Magento\Quote\Test\Fixture\AddProductToCart as AddProductToCartFixture; use Magento\Quote\Test\Fixture\GuestCart as GuestCartFixture; -use Magento\Sales\Model\Order\Email\Container\OrderIdentity; -use Magento\Sales\Model\Order\Email\Sender\OrderSender; -use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; use Magento\Sales\Test\Fixture\Creditmemo as CreditmemoFixture; use Magento\Sales\Test\Fixture\Invoice as InvoiceFixture; +use Magento\Sales\Test\Fixture\Shipment as ShipmentFixture; use Magento\SalesRule\Model\Rule; use Magento\SalesRule\Test\Fixture\Rule as RuleFixture; use Magento\TestFramework\Fixture\Config as Config; @@ -29,12 +27,8 @@ use Magento\TestFramework\Fixture\DataFixtureStorage; use Magento\TestFramework\Fixture\DataFixtureStorageManager; use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\Mail\Template\TransportBuilderMock; use PHPUnit\Framework\TestCase; -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ class OrderTest extends TestCase { /** @@ -42,52 +36,12 @@ class OrderTest extends TestCase */ private $fixtures; - /** - * @var TransportBuilderMock - */ - private $transportBuilderMock; - - /** - * @var MutableScopeConfigInterface - */ - private $mutableScopeConfig; - - /** - * @var CollectionFactory - */ - private $collectionFactory; - - /** - * @var EmailSenderHandler - */ - private $emailSenderHandler; - /** * Set up */ protected function setUp(): void { $this->fixtures = Bootstrap::getObjectManager()->get(DataFixtureStorageManager::class)->getStorage(); - $objectManager = Bootstrap::getObjectManager(); - $this->collectionFactory = $objectManager->get(CollectionFactory::class); - $this->transportBuilderMock = $objectManager->get(TransportBuilderMock::class); - $this->mutableScopeConfig = $objectManager->get(MutableScopeConfigInterface::class); - $this->emailSenderHandler = Bootstrap::getObjectManager()->create( - EmailSenderHandler::class, - [ - 'emailSender' => $objectManager->get(OrderSender::class), - 'entityResource' => $objectManager->get(\Magento\Sales\Model\ResourceModel\Order::class), - 'entityCollection' => $this->collectionFactory->create(), - 'identityContainer' => $objectManager->create(OrderIdentity::class), - ] - ); - $this->transportBuilderMock->clean(); - } - - protected function tearDown(): void - { - $this->transportBuilderMock->clean(); - parent::tearDown(); } /** @@ -139,43 +93,64 @@ public function testMultipleCreditmemosForZeroTotalOrder() ); } + /** + * Tests that an order with mixed product types in cart and with physical items either shipped or refunded cannot be shipped + */ #[ - Config('system/smtp/disable', '1', 'store', 'default'), - Config('sales_email/general/async_sending', '1'), + Config('carriers/freeshipping/active', '1', 'store', 'default'), + Config('payment/free/active', '1', 'store', 'default'), DataFixture(ProductFixture::class, as: 'product'), + DataFixture(ProductVirtualFixture::class, as: 'virtual'), DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture( + RuleFixture::class, + [ + 'simple_action' => Rule::BY_PERCENT_ACTION, + 'discount_amount' => 100, + 'apply_to_shipping' => 0, + 'stop_rules_processing' => 0, + 'sort_order' => 1, + ] + ), DataFixture( AddProductToCartFixture::class, - ['cart_id' => '$cart.id$', 'product_id' => '$product.id$'] + ['cart_id' => '$cart.id$', 'product_id' => '$product.id$', 'qty' => 2] + ), + DataFixture( + AddProductToCartFixture::class, + ['cart_id' => '$cart.id$', 'product_id' => '$virtual.id$', 'qty' => 2] ), DataFixture(SetBillingAddressFixture::class, ['cart_id' => '$cart.id$']), DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$']), DataFixture(SetGuestEmailFixture::class, ['cart_id' => '$cart.id$']), - DataFixture(SetDeliveryMethodFixture::class, ['cart_id' => '$cart.id$']), - DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$cart.id$']), - DataFixture(PlaceOrderFixture::class, ['cart_id' => '$cart.id$'], 'order') + DataFixture( + SetDeliveryMethodFixture::class, + ['cart_id' => '$cart.id$', 'carrier_code' => 'freeshipping', 'method_code' => 'freeshipping'] + ), + DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$cart.id$', 'method' => 'free']), + DataFixture(PlaceOrderFixture::class, ['cart_id' => '$cart.id$'], 'order'), + DataFixture(InvoiceFixture::class, ['order_id' => '$order.id$'], 'invoice'), + DataFixture( + CreditmemoFixture::class, + ['order_id' => '$order.id$', 'items' => [['qty' => 1, 'product_id' => '$product.id$']]], + 'creditmemo' + ), + DataFixture( + ShipmentFixture::class, + ['order_id' => '$order.id$', 'items' => [['qty' => 1, 'product_id' => '$product.id$']]], + 'shipment' + ) ] - public function testAsyncEmailForOrderCreatedWhenEmailSendingWasDisabled(): void + public function testOrderWithPartialShipmentAndPartialRefundAndMixedCartItems() { - $isEmailSent = false; - $this->transportBuilderMock->setOnMessageSentCallback( - function () use (&$isEmailSent) { - $isEmailSent = true; - } - ); $order = $this->fixtures->get('order'); - $this->assertEquals(0, $order->getSendEmail()); - $this->assertNull($order->getEmailSent()); - $this->mutableScopeConfig->setValue('system/smtp/disable', 0, 'store', 'default'); - $this->emailSenderHandler->sendEmails(); $this->assertFalse( - $isEmailSent, - 'Email is not expected to be sent' + $order->canShip(), + 'All items are shipped or refunded or virtual' + ); + $this->assertEquals( + Order::STATE_COMPLETE, + $order->getStatus() ); - $collection = $this->collectionFactory->create(); - $collection->addFieldToFilter('entity_id', $order->getId()); - $order = $collection->getFirstItem(); - $this->assertEquals(0, $order->getSendEmail()); - $this->assertNull($order->getEmailSent()); } }