Create Order and Quote Programmatically in Magento 2

Posted by

In this article, we delve into the process of seamlessly transitioning a quote into a fully-fledged order in Magento. Key steps include initializing an empty quote, skilfully populating it with products, meticulously configuring billing and shipping methods, and integrating customer addresses. Ultimately, we culminate this journey by seamlessly transforming the quote into a definitive order. Additionally, we explore the optional realm of generating an invoice upon order completion, offering a comprehensive understanding of the entire transaction lifecycle.

Uses of Order generation

One of the main reasons you will need this code is you are developing an application which will involve order generation. Applications like creating an order from One click order, Order with specific shipping and payment methods which are not for retail customers, Developing bulk order systems and the mostly used order generation from API feeds. One such application is Amazon Integration from Amasty. Amasty is one of the well-known Magento/Adobe commerce extension providers. They provide a very good support to their customers.

Required Details to Create order

To create order programmatically there are some details needed.

  • Product details:- These are important details needed to generate an order. The product details consist of a SKU and Qty. However, you can also specify other details if you don’t want to use system-generated ones like the price of the product.
  • Customer details:- For customer details an email address is needed in case the customer is already registered with Magento. However, it would be better to assume the customer is new (Guest user). In this case, you will need an email along with the first name and last name. The middle name is optional.
  • Store:- If you are running a multistore then a store id is needed.
  • Shipping Address:- When providing a shipping address, it’s essential to include the receiver’s full name, particularly if it differs from the person who placed the order. The complete address should consist of the street, city, region (region_id is generated from this and is required for Magento systems), country_id, postcode, and telephone number. In our example, we’ll illustrate how to obtain the region_id from the region field.
  • Billing Address:- The billing address format is the same as the shipping address. This is only needed if your billing address is different from the shipping address.
  • Payment method:- A payment method needs to be specified while creating an order. In this case, the payment method shouldn’t have bank details or card details. Like a custom payment method or cash-on-delivery method.
  • Shipping method:- The last thing which is needed to create an order is a shipping method. You can either use a custom developed shipping method or use any existing Magento shipping methods.

Create a Quote

A quote is created when a customer adds their first item to the cart and is used throughout the checkout process to manage and finalize the order. In our workflow, we begin by creating a quote, incorporating all suitable details from customer information to shipping preferences. Once the quote is fully processed, it is converted into an order, culminating in the generation of a finalized transaction.

$cartId = $this->cartManagementInterface->createEmptyCart();
$quote= $this->cartRepositoryInterface->get($cartId);

The above two lines create an empty quote and assign it to a variable name quote. The interface classes responsible for the above are.

\Magento\Quote\Api\CartManagementInterface 
\Magento\Quote\Api\CartRepositoryInterface 

Development

We will create a custom module for demonstration of order creation. To create a simple module follow our article Create Magento 2 Custom Module Development. For our example to show how we can create an order programmatically, we will use one more module which will call our module main function which internally will create an order.

system.xml

This file we have created holds some default values. In our case we used to get store value, You can also do the same for others like shipping costs, adding extra fees, increasing product price before an order or reducing the price of the product. If want me to make any changes then please raise an issue on Git Hub for this code base. https://github.com/aveshnaik007/magento_order_generation.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <tab id="learningmagento" translate="label" sortOrder="15">
            <label>learning Magento</label>
        </tab>
        <section id="learning_magento_order" translate="label" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
            <label>Order Generation via API</label>
            <tab>learningmagento</tab>
            <resource>Learningmagento_Ordergeneration::default_config</resource>
            <group id="general_settings" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>General Settings</label>
                <field id="default_store" translate="label" type="select" sortOrder="15" showInDefault="1" showInWebsite="0" showInStore="0">
                    <label>Default Store</label>
                    <source_model>Learningmagento\Ordergeneration\Model\Config\Stores</source_model>
                </field>
            </group>
        </section>
    </system>
</config>

Model class for configuration dropdown

<?php

/**
 *
 * @category  Custom Development
 * @email     contactus@learningmagento.com
 * @author    Learning Magento
 * @website   learningmagento.com
 * @Date      06-04-2024
 */
namespace Learningmagento\Ordergeneration\Model\Config;

use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource;

class Stores extends AbstractSource
{
    protected $storeRepository;

    public function __construct(
        \Magento\Store\Api\StoreRepositoryInterface $storeRepository
    ) {
        $this->storeRepository = $storeRepository;
    }

    /**
     * Retrieve All options
     *
     * @return array
     */    public function getAllOptions()
    {
        $stores = $this->storeRepository ->getList();
        $storeList = [];
        foreach ($stores as $store) {
            $storeList[] = [
                'label' => $store->getName(),
                'value' => $store->getId()
            ];
        }
        return $storeList;
    }
}

Helper class

A helper class to retrieve the configurational value. In our case the store id.

<?php

/**
 *
 * @category  Custom Development
 * @email     contactus@learningmagento.com
 * @author    Learning Magento
 * @website   learningmagento.com
 * @Date      11-04-2024
 */
namespace Learningmagento\Ordergeneration\Helper;

use Magento\Framework\App\Helper\AbstractHelper;
use Magento\Store\Model\ScopeInterface;

class Configuration extends AbstractHelper
{
    private function getConfigValue($field, $storeId = null)
    {
        return $this->scopeConfig->getValue(
            $field, ScopeInterface::SCOPE_STORE, $storeId
        );
    }

    public function getOrderGenerationConfig($field)
    {
        return $this->getConfigValue('learning_magento_order/general_settings/'.$field);
    }
}

Api Interface

The interface class shows how the proxy design pattern works. We can directly define a model class and call the function from the model class. However, if you want to use the code for creating a custom API for order generation then you can use this way. The writing of Interface, di and model is the oops concept encapsulation.

<?php

/**
 *
 * @category  Custom Development
 * @email     contactus@learningmagento.com
 * @author    Learning Magento
 * @website   learningmagento.com
 * @Date      03-04-2024
 */
namespace Learningmagento\Ordergeneration\Api;

/**
 * Interface to generate an order
 */interface CreateOrderInterface
{
    /**
     * This method generate a fresh order
     *
     * @param array $orderData
     * @return mixed
     */    public function generateOrder($orderData);
}

di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="Learningmagento\Ordergeneration\Api\CreateOrderInterface" type="Learningmagento\Ordergeneration\Model\CreateOrder" />
</config>

The design pattern which follows this concept is the Proxy Design Pattern.

Model class to create quote and order

The model class CreateOrder is an important class here as the order generation programmatically is within this class from generating an empty cart to invoicing an order. First, we are creating an empty cart (quote). Then we are adding customer details to it. We are looping into an array of products to get product SKU and qty. If the product is not found or if qty doesn’t meet then we can store it into an array and stop the process by throwing an exception by passing the respective reason.

<?php

/**
 *
 * @category  Custom Development
 * @email     contactus@learningmagento.com
 * @author    Learning Magento
 * @website   learningmagento.com
 * @Date      03-04-2024
 */
namespace Learningmagento\Ordergeneration\Model;

use Learningmagento\Ordergeneration\Api\CreateOrderInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\InventorySalesAdminUi\Model\GetSalableQuantityDataBySku;

class CreateOrder implements CreateOrderInterface
{
    protected $cartManagementInterface;
    protected $cartRepositoryInterface;

    protected $storeManager;

    protected $productFactory;

    protected $salableQty;

    protected $regionCollection;

    protected $configuration;

    protected $messager;

    protected $invoiceService;

    protected $transaction;

    public function __construct(
        \Magento\Quote\Api\CartManagementInterface $cartManagementInterface,
        \Magento\Quote\Api\CartRepositoryInterface $cartRepositoryInterface,
        \Magento\Store\Model\StoreManagerInterface $storeManager,
        \Magento\Catalog\Model\ProductFactory $productFactory,
        \Magento\Directory\Model\ResourceModel\Region\CollectionFactory $regionCollection,
        \Learningmagento\Ordergeneration\Helper\Configuration $configuration,
        \Magento\Framework\Message\ManagerInterface $messager,
        \Magento\Sales\Model\Service\InvoiceService $invoiceService,
        \Magento\Framework\DB\Transaction $transaction,
        GetSalableQuantityDataBySku  $salableQty
    ) {
        $this->cartManagementInterface = $cartManagementInterface;
        $this->cartRepositoryInterface = $cartRepositoryInterface;
        $this->storeManager = $storeManager;
        $this->productFactory = $productFactory;
        $this->regionCollection = $regionCollection;
        $this->configuration = $configuration;
        $this->messager = $messager;
        $this->invoiceService = $invoiceService;
        $this->transection = $transaction;
        $this->salableQty = $salableQty;
    }

    /**
     * This method generate a fresh order
     *
     * @param $orderData
     * @return mixed
     */    public function generateOrder($order)
    {
        try {
            $cart = $this->emptyCart();
            $storeId = $this->getSelectedStore();
            $store = $this->storeManager->getStore($storeId);
            $cart->setStore($store);
            $cart->setCurrency();
            // Setting a customer data
            $cart->setCustomerId(null);
            $cart->setCustomerEmail($order['email']);
            $cart->setCustomerFirstname($order['name']['firstname']);
            $cart->setCustomerLastname($order['name']['lastname']);
            $cart->setCustomerIsGuest(true);
            $cart->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID);
            if (!isset($order['order_items']) && empty($order['order_items'])) {
                throw new LocalizedException(__("No items added for generating an order"));
            }
            $reason = [];
            foreach ($order['order_items'] as $item) {
                if (isset($item['sku']) && !empty($item['sku'])) {
                    $qty = $item['qty'];
                    $product = $this->productFactory->create()->loadByAttribute('sku', $item['sku']);
                    if (empty($product)) {
                        $reason[] = $item['sku'] . " SKU not exist on store";
                        break;
                    }
                    if (isset($product) && !empty($product)) {
                        if ($product->getStatus() == '1') {
                            $stock = $this->salableQty->execute($item['sku']);
                            $stockStatus = ($stock[0]['qty'] > 0 && $stock[0]['qty'] >= $qty) ? true : false;
                            if ($stockStatus) {
                                $product->setSkipSaleableCheck(true);
                                $product->setData('is_salable', true);
                                $cart->setIsSuperMode(true);
                                $cart->addProduct($product, (int)$qty);
                            } else {
                                if (!isset($cancelItems[$item['sku']])) {
                                    $reason[] = $item['sku'] . "SKU out of stock";
                                }
                            }
                        } else {
                            $reason[] = $item['sku'] . " SKU not enabled on store";
                        }
                    } else {
                        $reason[] = $item['sku'] . " SKU not exist on store";
                    }
                } else {
                    $reason[] = $item['sku'] ." SKU key not exist in payload";
                }
            }

            if(count($reason)>0) {
                $txt = "";
                foreach ($reason as $rr) {
                    $txt.= " ".$rr;
                }
                throw new LocalizedException(__($txt));
            }


            $shippingRegion = $this->regionCollection->create()
                ->addFieldToFilter("code", ["eq" => $order['shipping']['state']])
                ->getFirstItem();



            $shipAddress = [
                'firstname' => $order['shipping']['firstname'],
                'lastname' => $order['shipping']['lastname'],
                'street' => $order['shipping']['street'],
                'city' => $order['shipping']['city'],
                'region_id' => $shippingRegion->getId(),
                'country_id' => $order['shipping']['country_code'],
                'region' => $shippingRegion->getData("default_name"),
                'postcode' => $order['shipping']['postcode'],
                'telephone' => $order['shipping']['telephone'],
                'fax' => '',
                'save_in_address_book' => 1
            ];

            $billingRegion = $this->regionCollection->create()
                ->addFieldToFilter("code", ["eq" => $order['billing']['state']])
                ->getFirstItem();


            $billAddress = [
                'firstname' => $order['billing']['firstname'],
                'lastname' => $order['billing']['lastname'],
                'street' => $order['billing']['street'],
                'city' => $order['billing']['city'],
                'country_id' => $order['billing']['country_code'],
                'region_id' => $billingRegion->getId(),
                'region' => $billingRegion->getData("default_name"),
                'postcode' => $order['billing']['postcode'],
                'telephone' => $order['billing']['telephone'],
                'fax' => '',
                'save_in_address_book' => 1
            ];
            $cart->getBillingAddress()->addData($billAddress);
            $cart->getShippingAddress()->addData($shipAddress);
            $shippingAddress = $cart->getShippingAddress();

            $shippingAddress->setCollectShippingRates(true)
                ->collectShippingRates()
                ->setShippingMethod(\Learningmagento\CustomShipping\Model\Carrier\Storeship::ORDER_CODE);

            $cart->setPaymentMethod(\Learningmagento\CustomPayment\Model\PaymentMethod::CODE);

            $cart->setInventoryProcessed(false);
            $cart->save();
            $cart->getPayment()->importData(
                [
                    'method' => \Learningmagento\CustomPayment\Model\PaymentMethod::CODE
                ]
            );

            $cart->collectTotals()->save();
            foreach ($cart->getAllItems() as $item) {
                $item->setDiscountAmount(0);
                $item->setBaseDiscountAmount(0);
                $item->setOriginalCustomPrice($item->getPrice())
                    ->setOriginalPrice($item->getPrice())
                    ->save();
            }
            try {
                /** @var \Magento\Sales\Model\Order $magentoOrder */                $magentoOrder = $this->cartManagementInterface->submit($cart);
            } catch (\Exception $e) {
                $this->messager->addErrorMessage($e->getMessage());
            }
            if (isset($magentoOrder) && !empty($magentoOrder)) {

                $magentoOrder->setStatus(\Magento\Sales\Model\Order::STATE_PROCESSING)
                    ->setState(\Magento\Sales\Model\Order::STATE_PROCESSING);
                $magentoOrder->save();

                $this->generateInvoice($magentoOrder);
            }

        }
        catch (\Exception $exception) {
            $this->messager->addErrorMessage($exception->getMessage());
        }
    }

    protected function emptyCart()
    {
        $cartId = $this->cartManagementInterface->createEmptyCart();
        return $this->cartRepositoryInterface->get($cartId);
    }

    /**
     * Invoice generation of an order
     *
     * @param $order
     */    protected function generateInvoice($order)
    {
        try {
            $invoice = $this->invoiceService->prepareInvoice($order);
            $invoice->register();
            $invoice->save();
            $transactionSave = $this->transection->addObject($invoice)->addObject($invoice->getOrder());
            $transactionSave->save();
            $order->addStatusHistoryComment(__('Notified customer about invoice #%1.', $invoice->getId()))
                ->setIsCustomerNotified(true)->save();
            $order->setStatus('processing')->save();
        } catch (\Exception $exception) {
            $this->messager->addErrorMessage($exception->getMessage());
        }
    }

    protected function getSelectedStore()
    {
        return $this->configuration->getOrderGenerationConfig("default_store");
    }
}

We then set the shipping and billing address to our quote. We are using our custom shipping and payment method. You can refer to the custom method generation code or you can design your own or can use Magento’s default COD and Check payment. At last, we create an order from the cart (quote). In addition, we are also creating an invoice for an order. You can either set this setting in the configuration or you can remove the code for invoice generation.

External Model to Create Order

The code below is a controller class which is used as a test class to call our interface method which will create order based on the values passed into the function generateOrder. We are passing an array containing customer details, products to be ordered, and shipping and billing addresses.

<?php

/**
 *
 * @category  Custom Development
 * @email     contactus@learningmagento.com
 * @author    Learning Magento
 * @website   learningmagento.com
 * @Date      11-04-2024
 */
namespace Learningmagento\TestOrder\Controller\Order;

use Magento\Framework\App\ResponseInterface;

class GenerateOrder implements \Magento\Framework\App\ActionInterface
{
    protected $createOrder;

    public function __construct
    (
        \Learningmagento\Ordergeneration\Api\CreateOrderInterface $createOrder
    ) {
        $this->createOrder = $createOrder;
    }

    /**
     * Execute action based on request and return result
     *
     * @return \Magento\Framework\Controller\ResultInterface|ResponseInterface
     * @throws \Magento\Framework\Exception\NotFoundException
     */    public function execute()
    {
        $order = [
            "email" => "contactus@learningmagento.com",
            "name" => [
                "firstname" => "Learn",
                "lastname" => "magento"
            ],
            "order_items" => [
                [
                    "sku" => "129AB",
                    "qty" => 1
                ],
                [
                    "sku" => "11536",
                    "qty" => 2
                ]
            ],
            "shipping" => [
                "firstname" => "Learn",
                "lastname" => "magento",
                'street' => '3094 Shobe Lane',
                'city' => 'Centennial',
                'country_code' => 'US',
                'state' => 'Co',
                'postcode' => '80112',
                'telephone' => '970-470-1279',
            ],
            "billing" => [
                "firstname" => "Learn",
                "lastname" => "magento",
                'street' => '3094 Shobe Lane',
                'city' => 'Centennial',
                'country_code' => 'US',
                'state' => 'Co',
                'postcode' => '80112',
                'telephone' => '970-470-1279',
            ]
        ];

        $this->createOrder->generateOrder($order);
    }
}

Conclusion

The above example we used will create an order. You can use the above code to customize it according to your needs. If you need any help do reach out to me over the GitHub repo.