Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
*
Expand Down
119 changes: 47 additions & 72 deletions dev/tests/integration/testsuite/Magento/Sales/Model/OrderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,86 +8,40 @@
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;
use Magento\TestFramework\Fixture\DataFixture;
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
{
/**
* @var DataFixtureStorage
*/
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();
}

/**
Expand Down Expand Up @@ -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());
}
}