13

Store products are supplied from different vendors. It is required to create multiple order for every vendor based on its products in the cart during one checkout. Is there any extension to achieve this task or should I start to develop custom checkout module. What about hot points for creating such extension vision of experienced developers of Magento? Can you explain me brief checkout flow architecture Magento friendly (as possible as code level)? Thanks much more!

mageUz
  • 6,234
  • 3
  • 29
  • 52
  • Have a look on the Multishipping which magento offers out of the box. And there is dropship but I have no idea whether it is good or what it is able to do. http://www.unirgy.com/products/udropship/ – Fabian Blechschmidt Jun 12 '13 at 13:52
  • @mageUz, have you worked below answer?. it's not worked for me.. Can post your code? – Manoj Kumar Dec 15 '14 at 11:46
  • @ManojKumar, yes I have already implement order splitting using multiaddress checkout logic, I am sure that the logic given below also works perfectly. – mageUz Dec 15 '14 at 13:48
  • @mageUz, when use this code Shopping Cart is Empty showing.. Any suggestions.. – Manoj Kumar Dec 17 '14 at 13:22
  • Hi that can be done using marketplace split cart module https://store.webkul.com/Magento-Marketplace-Split-Cart.html . Thanks – webkul Jun 01 '15 at 11:43

2 Answers2

11

It is doable quite easily with a rewrite of the checkout/type_onepage model.
In that class override the saveOrder() method as follows:

public function saveOrder()
{
    $quote = $this->getQuote();

    // First build an array with the items split by vendor
    $sortedItems = array();
    foreach ($quote->getAllItems() as $item) {
        $vendor = $item->getProduct()->getVendor(); // <- whatever you need
        if (! isset($sortedItems[$vendor])) {
            $sortedItems[$vendor] = $item;
        }
    }
    foreach ($sortedItems as $vendor => $items) {
        // Empty quote
        foreach ($quote->getAllItems() as $item) {
            $quote->getItemsCollection()->removeItemByKey($item->getId());
        }
        foreach ($items as $item) {
            $quote->addItem($item);
        }
        // Update totals for vendor
        $quote->setTotalsCollectedFlag(false)->collectTotals();

        // Delegate to parent method to place an order for each vendor
        parent::saveOrder();
    }
    return $this;
}

But be aware that in Magento a payment is associated with an invoice, and each invoice is associated with an order.

In consequence this means that as soon as you have multiple orders, you will also have split the payments. So this is only feasible if the payment method doesn't require user interaction during the payment.

UPDATE: The orginal answer delegated to parent::save() which had to be parent:saveOrder(). It is fixed in the example code now.

Vinai
  • 14,014
  • 2
  • 42
  • 83
  • Would appreciate if you could have a look at my similar question! http://magento.stackexchange.com/questions/6974/split-order-into-seperate-orders-and-calculate-shipping-seperately-with-ups-and – CaitlinHavener Aug 20 '13 at 21:22
  • were you able to split the order using above code, as i am trying to do the same but not without any luck – Deepak Mallah Feb 26 '14 at 05:41
  • Its been a long time since then, but yes, it did work. – Vinai Feb 26 '14 at 16:55
  • It doesn't work anymore, there is no parent so it can't be saved. Can you update your code so it work with recent versions of Magento? – Roy Jun 06 '14 at 09:22
  • 1
    Roy, of course you have to extend the original class for there to be a parent, but that is simple PHP, it has nothing to do with Magento. The method saveOrder still is present and active in Magento CE 1.9 just like it has always been. – Vinai Jun 07 '14 at 07:10
  • @Vinai, when i'm trying to use this code Error: Invalid method Mage_Sales_Model_Quote::removeItemByKey() error is came.. – Manoj Kumar Dec 07 '14 at 06:49
  • Updated the code for later Magento versions. Use $quote->getItemsCollection()->removeItemByKey($item->getId()) instead. – Vinai Dec 08 '14 at 19:07
  • @Vinai, when use this code Shopping Cart is Empty showing.. Any suggestions.. – Manoj Kumar Dec 17 '14 at 13:23
  • 3
    I had a problem with this snippet as the 2 order have the grandtotal and subtotal equal the entire order. After debugging i found out that even removing and re-adding the items, it would just use address cached items after collecting Total form the address... To solve it just clear for each address the items cache:

    $address->unsetData('cached_items_all'); $address->unsetData('cached_items_nominal'); $address->unsetData('cached_items_nonnominal');

    – Diogo Santiago Oct 22 '15 at 17:25
  • For multi-address checkout, yes. There you have to assign the quote address items, not the quote items, to each address. – Vinai Oct 23 '15 at 20:26
  • @Vinai . http://magento.stackexchange.com/questions/89429/split-order-by-product?noredirect=1#comment118892_89429. Can you please help me on this question. – Rahul Nov 10 '15 at 06:14
  • 1
    @Vinai $quote->addItem($item); this code not working. After adding item I echo $quote->getAllItems() with help of foreach loop. But there was no item. Can you help me on this please? – Amit Bera Mar 30 '16 at 05:59
  • @AmitBera works for me, sorry. Are you checking on the same quote instance? – Vinai Mar 30 '16 at 06:06
  • yes,checking on the same quote instance.I will share more details soon – Amit Bera Mar 30 '16 at 10:23
  • Hello @Vinai , I am trying to do the same but foreach ($items as $item) { $quote->addItem($item); } this loop is not working. I am using magento version 1.9.2.4 – vedu Aug 27 '16 at 05:34
  • @AmitBera can you please more information for this question it is use full for us – Pawankumar Jul 03 '18 at 09:26
  • Sorry, it is too old question, i don't remember it – Amit Bera Jul 03 '18 at 11:00
  • @AmitBera i got an error like

    Fatal error: Call to a member function getMethodInstance() on boolean in app/code/core/Mage/Payment/Model/Observer.php on line 46

    – Radha Krishna Oct 26 '18 at 15:27
  • will this code works for Magento 2.3? If so which plugin or event I have to use. Please suggest. – Ramesh KR May 19 '20 at 08:39
2

The following is tested in CE ver 1.9.0.x

/**
 * Overwrite core
 */
class Vendor_Module_Model_Checkout_Type_Onepage extends Mage_Checkout_Model_Type_Onepage
{
    protected $_oriAddresses = array();

    /**
     * Prepare order from quote_items  
     *
     * @param   array of Mage_Sales_Model_Quote_Item 
     * @return  Mage_Sales_Model_Order
     * @throws  Mage_Checkout_Exception
     */
    protected function _prepareOrder2($quoteItems)
    {
        $quote = $this->getQuote();
        $quote->unsReservedOrderId();
        $quote->reserveOrderId();

        // new instance of quote address
        $quote->setIsMultiShipping(true); // required for new instance of Mage_Sales_Model_Quote_Address
        $address = Mage::getModel('sales/quote_address');
        $weight = 0;
        $addressType = 'billing';
        foreach ($quoteItems as $quoteItem) {
            $address->addItem($quoteItem, $quoteItem->getQty());
            $weight += $quoteItem->getWeight();
            if (!$quoteItem->getIsVirtual()) {
                $addressType = 'shipping';
            }
        }
        // get original shipping address that contains multiple quote_items
        if (!isset($this->_oriAddresses[$addressType])) {
            $this->_oriAddresses[$addressType] = Mage::getResourceModel('sales/quote_address_collection')
                ->setQuoteFilter($quote->getId())
                ->addFieldToFilter('address_type', $addressType)
                ->getFirstItem();
        }
        Mage::helper('core')->copyFieldset('sales_convert_quote_address', 'to_customer_address', $this->_oriAddresses[$addressType], $address);
        Mage::helper('core')->copyFieldset('sales_convert_quote_address', 'to_order', $this->_oriAddresses[$addressType], $address);
        $address->setQuote($quote)
            ->setWeight($weight)
            ->setSubtotal(0)
            ->setBaseSubtotal(0)
            ->setGrandTotal(0)
            ->setBaseGrandTotal(0)
            ->setCollectShippingRates(true)
            ->collectTotals()
            ->collectShippingRates()        
            ;

        $convertQuote = Mage::getSingleton('sales/convert_quote');
        $order = $convertQuote->addressToOrder($address);
        $order->setBillingAddress(
            $convertQuote->addressToOrderAddress($quote->getBillingAddress())
        );

        if ($address->getAddressType() == 'billing') {
            $order->setIsVirtual(1);
        } else {
            $order->setShippingAddress($convertQuote->addressToOrderAddress($address));
        }

        $order->setPayment($convertQuote->paymentToOrderPayment($quote->getPayment()));
        if (Mage::app()->getStore()->roundPrice($address->getGrandTotal()) == 0) {
            $order->getPayment()->setMethod('free');
        }

        foreach ($quoteItems as $quoteItem) {
            $orderItem = $convertQuote->itemToOrderItem($quoteItem);  // use quote_item to transfer is_qty_decimal
            if ($quoteItem->getParentItem()) {
                $orderItem->setParentItem($order->getItemByQuoteItemId($quoteItem->getParentItem()->getId()));
            }
            $order->addItem($orderItem);
        }

        return $order;
    }

    /**
     * Overwrite core function
     */
    public function saveOrder()
    {
        $quote = $this->getQuote();
        if ($quote->getItemsCount() > 1) {
            $items = $quote->getAllVisibleItems();
            $group = array();
            $split = array();
            foreach ($items as $item) {
                if (Mage::helper('vendor')->checkSku($item->getSku())) {
                    $split[] = array($item); // one item per order
                } else {
                    $group[] = $item; // all other items in one order
                }
            }
            if (count($split)) {
                if (count($group)) {
                    $split[] = $group;
                }
                return $this->_splitQuote($split);
            }
        }
        return parent::saveOrder();
    }

    /**
     * Split quote to multiple orders
     * 
     * @param array of Mage_Sales_Model_Quote_Item
     * @return Mage_Checkout_Model_Type_Onepage
     */
    protected function _splitQuote($split)
    {
        $this->validate();
        $isNewCustomer = false;
        switch ($this->getCheckoutMethod()) {
            case self::METHOD_GUEST:
                $this->_prepareGuestQuote();
                break;
            case self::METHOD_REGISTER:
                $this->_prepareNewCustomerQuote();
                $isNewCustomer = true;
                break;
            default:
                $this->_prepareCustomerQuote();
                break;
        }
        if ($isNewCustomer) {
            try {
                $this->_involveNewCustomer();
            } catch (Exception $e) {
                Mage::logException($e);
            }
        }

        $quote = $this->getQuote()->save();
        $orderIds = array();
        Mage::getSingleton('core/session')->unsOrderIds();
        $this->_checkoutSession->clearHelperData();

        /**
         * a flag to set that there will be redirect to third party after confirmation
         * eg: paypal standard ipn
         */
        $redirectUrl = $quote->getPayment()->getOrderPlaceRedirectUrl();

        foreach ($split as $quoteItems) {
            $order = $this->_prepareOrder2($quoteItems);
            $order->place();
            $order->save();
            Mage::dispatchEvent('checkout_type_onepage_save_order_after',
                array('order'=>$order, 'quote'=>$quote));
            /**
             * we only want to send to customer about new order when there is no redirect to third party
             */
            if (!$redirectUrl && $order->getCanSendNewEmailFlag()) {
                $order->sendNewOrderEmail();
            }
            $orderIds[$order->getId()] = $order->getIncrementId();
        }

        Mage::getSingleton('core/session')->setOrderIds($orderIds);

        // add order information to the session
        $this->_checkoutSession
            ->setLastQuoteId($quote->getId())
            ->setLastSuccessQuoteId($quote->getId())
            ->setLastOrderId($order->getId())
            ->setRedirectUrl($redirectUrl)
            ->setLastRealOrderId($order->getIncrementId());

        // as well a billing agreement can be created
        $agreement = $order->getPayment()->getBillingAgreement();
        if ($agreement) {
            $this->_checkoutSession->setLastBillingAgreementId($agreement->getId());
        }

        // add recurring profiles information to the session
        $service = Mage::getModel('sales/service_quote', $quote);
        $profiles = $service->getRecurringPaymentProfiles();
        if ($profiles) {
            $ids = array();
            foreach ($profiles as $profile) {
                $ids[] = $profile->getId();
            }
            $this->_checkoutSession->setLastRecurringProfileIds($ids);
            // TODO: send recurring profile emails
        }

        Mage::dispatchEvent(
            'checkout_submit_all_after',
            array('order' => $order, 'quote' => $quote, 'recurring_profiles' => $profiles)
        );

        return $this;
    }
}

IMPORTANT You need to customize your payment methods to retrieve the grand total from the quote.

kiatng
  • 694
  • 3
  • 16
  • It is working. Thanks. How to group cart quote by vendor? other then "// one item per order".. – Syed Ibrahim Sep 27 '18 at 14:37
  • 1
    $group[] = $item; // all other items in one order can hold multiple items per order, you can easily modify such that each vendor has its own $group. – kiatng Sep 28 '18 at 02:28
  • Updated and it is working. But Payment is captured only for the last order(If we split multiple order). How we need to update this? I need to collect combined payment total. – Syed Ibrahim Sep 28 '18 at 09:46
  • The total amount is in the quote object, you can load it with $quote->Mage::getModel('sales/quote')->load($lastOrder->getQuoteId()). Then there are many totals you can retrieve from $quote, one of which is $quote->getGrandTotal() – kiatng Oct 01 '18 at 08:46