Tuesday, January 29, 2013

Share cart between websites

I saw that this is a general issue among Magneto users/developers.
Here is how I was able to keep the cart between websites.
The solution is not fully tested yet but for me it seams to work.
[Update]
Warning: I seams that this solution doesn't work with multiple currencies installed. See comments from MichaƂ Burda
[/Update]
If someone tries it please let me know the result and eventual bugs that appear.
Preconditions:
  • 'Use SID on Frontend' must be st to 'Yes' (System->Configuration->Web->Session Validation Settings)
Approach:
Magneto already allows you to keep the quote (cart) between store views from the same website.
I tried to manipulate this feature into letting me keep the cart between all websites (store views).
How is the quote kept in $_SESSION?
Magento keeps the quote id in the $_SESSION like this:
$_SESSION['quote_id_5'] = 34;
In the code above, 34 represents the quote id and 5 represents the website it (not store view id).
So it's basically like this:
$_SESSION['quote_id_{WEBSITE_ID}'] = {QUOTE_ID};
This means that the quote is different for each website.
Now for the actual code:
I've created a new extension called Easylife_Sales.
The extension has a fail-safe, in case it doesn't work as expected, it's behavior can be disabled from the configuration area. but more on this later.
Extension files:
app/etc/modules/Easylife_Sales.xml - declaration file
<?xml version="1.0"?>
<config>
    <modules>
        <Easylife_Sales>
            <active>true</active>
            <codePool>local</codePool>
            <depends>
             <Mage_Sales /><!-- so it's loaded after Mage_Sales -->
             <Mage_Checkout /><!-- so it's loaded after Mage_Checkout -->
            </depends>
        </Easylife_Sales>
    </modules>
</config>
app/code/local/Easylife/Sales/etc/config.xml - configuration file
<?xml version="1.0"?>
<config>
 <modules>
  <Easylife_Sales>
   <version>0.0.1</version>
  </Easylife_Sales>
 </modules>
 <global>
  <models>
   <sales>
    <rewrite>
     <observer>Easylife_Sales_Model_Observer</observer>
     <quote>Easylife_Sales_Model_Quote</quote>
    </rewrite>
   </sales>
   <checkout>
    <rewrite>
     <session>Easylife_Sales_Model_Checkout_Session</session>
    </rewrite>
   </checkout>
  </models>
  <helpers>
   <sales>
    <rewrite>
     <data>Easylife_Sales_Helper_Data</data>
    </rewrite>
   </sales>
  </helpers>
 </global>
 <frontend>
  <events><!-- this section is not mandatory, explanations later -->
   <sales_quote_collect_totals_before>
    <observers>
     <easylife_sales>
      <class>sales/observer</class>
      <method>checkProductAvailability</method>
     </easylife_sales>
    </observers>
   </sales_quote_collect_totals_before>
  </events>
 </frontend>
 <default>
  <checkout>
   <options>
    <persistent_quote>1</persistent_quote><!-- this is for the fail-safe -->
   </options>
  </checkout>
 </default>
</config>
app/code/local/Easylife/Sales/Helper/Data.php - override the default sales helper to add the fail-save method
<?php 
class Easylife_Sales_Helper_Data extends Mage_Sales_Helper_Data{
    public function getIsQuotePersistent(){
  return Mage::getStoreConfigFlag('checkout/options/persistent_quote');
 }
}
app/code/local/Easylife/Sales/Model/Checkout/Session.php - override the default checkout session in order to change the session key for the quote
<?php 
class Easylife_Sales_Model_Checkout_Session extends Mage_Checkout_Model_Session{
 protected function _getQuoteIdKey()
    {
     if (Mage::helper('sales')->getIsQuotePersistent()){//if behavior is not disabled
         return 'quote_id';
     }
     return parent::_getQuoteIdKey();
    }
}
app/code/local/Easylife/Sales/Model/Quote.php - override the quote model to share between all websites
<?php 
class Easylife_Sales_Model_Quote extends Mage_Sales_Model_Quote{
 public function getSharedStoreIds(){
  if (Mage::helper('sales')->getIsQuotePersistent()){//if behavior is not diasabled
   $ids = Mage::getModel('core/store')->getCollection()->getAllIds();
   unset($ids[0]);//remove admin just in case
   return $ids;
  }
  return parent::getSharedStoreIds();
 }
}
app/code/local/Easylife/Sales/etc/system.xml - this allows you do disable the functionality in case something is wrong.
<?xml version="1.0"?>
<config>
 <sections>
  <checkout>
   <groups>
    <options>
     <fields>
      <persistent_quote translate="label" module="sales">
       <label>Keep cart between websites</label>
       <frontend_type>select</frontend_type>
       <source_model>adminhtml/system_config_source_yesno</source_model>
       <sort_order>100</sort_order>
       <show_in_default>1</show_in_default>
       <show_in_website>0</show_in_website>
       <show_in_store>0</show_in_store>
      </persistent_quote>
     </fields>
    </options>
   </groups>
  </checkout>
 </sections>
</config>
We are almost done. At this point the following happens. If you have a product in cart, you change the website and the product is not available in the new site the product is still in the cart. If you want this behavior then there is no problem. All you need to do is to remove these lines from config.xml
<observer>Easylife_Sales_Model_Observer</observer>
and
<events><!-- this section is not mandatory, explanations later -->
 <sales_quote_collect_totals_before>
  <observers>
   <easylife_sales>
    <class>sales/observer</class>
    <method>checkProductAvailability</method>
   </easylife_sales>
  </observers>
 </sales_quote_collect_totals_before>
</events>
In my case I wanted to remove from cart (permanently) the products that are not available in that website. if you want this behavior add the following file: app/code/local/Easylife/Sales/Model/Observer.php - this will remove from cart the products that are not valid on the current store.
<?php 
class Easylife_Sales_Model_Observer extends Mage_Sales_Model_Observer{
        public function checkProductAvailability($observer){
  if (!Mage::helper('sales')->getIsQuotePersistent()){
   return $this;
  }
  $quote = $observer->getEvent()->getQuote();
  $currentId = Mage::app()->getWebsite()->getId();
  
  $messages = array();
  
  foreach ($quote->getAllItems() as $item){   
   $product = $item->getProduct();
   if (!in_array($currentId, $product->getWebsiteIds())){
    $quote->removeItem($item->getId());
    $messages[] = Mage::helper('catalog')->__('Product %s is not available on website %s', $item->getName(), Mage::app()->getWebsite()->getName());
   }
  }
  foreach ($messages as $message){
   Mage::getSingleton('checkout/session')->addError($message);
  }
  return $this;
 }
}
Well that's about it. Enjoy and let me know how it turns out.

Marius.

44 comments:

  1. Hi !

    I've tried your extension, but when I flushed the cash it showed me a fatal error for "getQuoteId() on a non-object" ... I'm working on Magento 1.4.1.0

    Thanks for helping, hope it'll help you back ;)

    ReplyDelete
    Replies
    1. I'm not sure this works on 1.4. I only tested and managed to make it work only on 1.7 (and ee 1.12)

      Delete
  2. Hi
    nice extension.

    Is there trouble printing the invoice
    The invoice is generated without problems, but when printing stays on a blank page...

    i hope found the solution ;)

    thanks

    ReplyDelete
    Replies
    1. To be honest, I didn't test the invoice printing. I did this for a website that managed the invoices shipments and credit notes outside Magento so I didn't bother to check. I will test and come back with a solution. Until then make sure that this code generated the error. Disable the extension and try to print an invoice. See if the error reproduces.

      Delete
  3. hi
    I have two website in magento 1.7.0.2.
    I used this module but its not working in checkout cart in magento 1.7.0.2.
    When I add Product in website1 and then when I check website2 it shows empty cart.

    ReplyDelete
    Replies
    1. Did you set 'Use SID on Frontend' to 'Yes'? (System->Configuration->Web->Session Validation Settings). If you did you can try skipping the last part (the observer that removes the products from the quote). If that doesn't work either...I'm out of ideas. Sorry.

      Delete
  4. thanks for your rply..
    but i want if one product is not availble in one website but shown this products in both website cart...

    ReplyDelete
  5. hi
    plz reply me as early as possible.
    I want to show cart same for two website if product is not availble in any website.

    ReplyDelete
    Replies
    1. Like I said...this code worked for me, I didn't test it very much on different Magento versions and different configurations. I really have no idea why it doesn't work for you. Maybe this helps you debug the issue: http://magento.stackexchange.com/questions/428/fundamentals-for-debugging-a-magento-store

      Delete
  6. Hi, can you tell me if this works with different currencies. I'll be immensely grateful. I've been trying to find the best solution. As far I managed to share a cart between websites - but... on one shop I must have all the products (that's bot very convenient, but bearable - the ones from second store are added to a category that is not visible on store), however the second thing is currency. I would like to operate on 2 currencies, unfortunately when I allow different currency it completely messes with the mechanism. Once again I'll be very grateful if you could tell me if it works with 2 currencies :))

    ReplyDelete
    Replies
    1. You might be on to something here. I didn't test with multiple currencies. Can you give more details about "completely messes with the mechanism"? What exactly happens when you change the currency? Please give a scenario step by step and describe the result you are getting.

      Delete
  7. with my solutions (not yours, to be clear):

    I have 2 available currencies: PLN and Euro
    When you change to PLN everything is still added in the cart with euro prices (because alphabetically euro is before Zloty(PLN), so when you want to buy things in PLN you can't check out with this currency but only with euro.
    I have 2 different websites with 2 different Store View (polish, english) - right now I was forced to turn off second currency (euro) for english store views

    ReplyDelete
    Replies
    1. Thanks for the scenario. I will try to reproduce it and if/when I come up with a solution I will post it.

      Delete
  8. Marius I've just tested your solution and everything works like a charm, including currencies ! BIG THANKS
    http://gold.yasmeen.pl
    http://nowysklep.yasmeen.pl
    You can check it here

    ReplyDelete
  9. Works perfect Maurius. You are a real saviour.

    One of my clients have a store with three different websites under same domain but with different sub-directories (http://www.domain/ => USD store , http://www.domain/ represents EURO store). They have different prices in different currency (No conversion). This makes sure if i change the websites , the right price with the right currency adds for the products in the cart.

    Thanks a lot for this simple module ;-)

    You really deserve a beer for this simple module, your answers on stackexchange and all the above ModuleCreator.

    ReplyDelete
  10. Where can we download this extension?

    Best,
    Miha

    ReplyDelete
    Replies
    1. I didn't make this an extension, because I didn't get to test much. I've only developed it for a specific project I was working on and it turned out to solve the issue on that project. But you can find the full code in this post. Maybe it works for you also.

      Delete
  11. Hi,
    Thanks for hard work, actually i want the opposite of everyone is looking for, i am looking to restrict cart sharing in store level in same website, i mean if user add to cart 1 product from store A and the used store switcher (magento slandered) to move to store B, (both A & B under same website) here exactly i want to keep him logged in (if registered) and clear his cart so when he is in store B the cart is empty
    Note: setting a warning popup about loosing cart items is easy to set

    So please if there is anyone has an idea about that to help us ASAP

    Thanks

    ReplyDelete
    Replies
    1. I didn't try what you are asking for, but I think it can be achieved following the same code as above. What you need to change is `Easylife_Sales_Model_Quote::getSharedStoreIds` to make it return only the current store id instead of all and maybe the session key for the quote `Easylife_Sales_Model_Checkout_Session::_getQuoteIdKey` to make it return `quote_id_{CURRENT STORE ID}`. Give it a try and let me know if it works or if at least gets you in the right dirrection

      Delete
  12. Hi friend,Your extension works beautifully! however, I could see something and do not know if it has to do with its length. Products with more than 1 quantity it multiplies the value by the amount of shipping product, when to go to intermediate payment. Could you help me?

    ReplyDelete
    Replies
    1. Your issue started after you implemented the code In this post? To make sure, just disable the "extension" and see if the issue reproduces. This never happened to me. I honestly have no idea where to start.

      Delete
    2. really is not an error in your code. the error persists even disabling the sharing cart. Thank you for your attention, the extension is wonderful!

      Delete
  13. hi Marius,
    thanks for your work. I used it in 1.8.0 and works fine!

    ReplyDelete
  14. Hi,

    this extension works great, thanks! However it doesn't seem to be working for users that are NOT logged in?

    Are you able to help me configure this so it does?

    ReplyDelete
  15. Does this work for 1.8 ... ?

    ReplyDelete
    Replies
    1. I can't tell you for sure. I haven't tested on 1.8. If you try it please post here the results.

      Delete
  16. hi,

    I'm trying to use this features and I can't get it work while using FLAT category and FLAT products.
    If the products is not available at the website the cart is empty, if the product is available, it works great.

    ReplyDelete
  17. Can some send met his extension?

    ReplyDelete
    Replies
    1. Sorry but there is no extension. It is just the code shown above. I didn't make it an extension because it was not fully tested with all the possible site/cart configurations. It may not work on certain conditions, but it may serve you as a starting point.

      Delete
  18. In version 1.8.1 works perfectly as long as Use Flat Catalog Product & Use Flat Catalog Category are both set to No. Otherwise cart is empty if product is not available on the website you are changing to.

    ReplyDelete
    Replies
    1. I didn't try it, but I thing you are right. And I see why. When using the flat catalog, the products that are not available in a certain store view are not indexed in the flat tables. So when switching websites, if a product in cart is not found in the new store it can remain in the cart. I have no idea how you can overcome this other than disabling flat catalog.

      Delete
    2. That does sound correct. I went with disabling the flat catelog at this stage.

      My only problem now is if you click on a product in the cart that belong's to a different website it try's to locate the product on the current website instead of linking back to the original website.

      Delete
    3. Yeah...I think this is a totally different problem. The product links from the cart always link to the product in the current website. I have no idea how to overcome this. Maybe keep a reference to the store view it was added on in a separate field and try to build the url in the cart based on that reference. But I'm not sure this will work.

      Delete
    4. Managed to get it sorted out. Probably not the cleanest way, but it will do for now. If anyone else need's the solution just follow below.

      Edit template/checkout/cart/item/default.phtml and add the following code:

      $productStoreId=implode("",$_item->getProduct()->getStoreIds());

      And then change $this->getProductUrl()

      to

      $this->getProduct()->setStoreId($productStoreId)->getProductUrl()

      Delete
    5. Hey... that actually looks nice and simple.

      Delete
  19. thank you,it working!Can you separate the order order of goods corresponding to their stores . that will be perfect

    ReplyDelete
  20. I have tried to replicate your extension however I am getting
    Mage registry key "_singleton/sales/observer" already exists

    Any ideas on how to fix?

    ReplyDelete
    Replies
    1. Did you clear the cache? check if the file names are properly upercased or lowercased.

      Delete
    2. I'm having the same issue. Triple checked the files names (copy pasted every file/folder name from your blog)
      I keep getting: Mage registry key "_singleton/sales/observer" already exists

      It's fixed when I remove Easylife_Sales_Model_Observer and the other block of code but then the extension does nothing for me.

      I've got a deadline for this Monday and I really don't know how to fix this. Hope you can help me out.

      Delete
  21. Hi, I get the same error, everything is ok with file names? Any ideas?

    ReplyDelete
  22. I am getting this error by using your code when i try to add a product in cart Fatal error: Call to a member function setStoreId() on a non-object in /home/bmessg/public_html/teamwear1/app/code/core/Mage/Checkout/Model/Session.php on line 122

    ReplyDelete
  23. I' getting the same error as the Post of November 18, 2015 3:48:00 PM.
    Mage registry key "_singleton/sales/observer" already exists
    Cache has been cleared multiple times and filenames have been copy&pasted... Only /code/core/Mage/Sales/etc/config.xml has the same sales/observer class registered.
    Magento v1.9.2.1

    Thanks for any idea!

    ReplyDelete
    Replies
    1. Ok when removing Easylife_Sales_Model_Observer.

      Delete
  24. Thanks for this.

    In our setup, we set all products to belong to all stores. Both stores share one root catalog. Using the code given here allowed the cross cart functionality to work 100%. However, and issue with having products assigned to both sites is search results. The products from the other store appear in the current store search results.

    I overcame this by limiting the search collection to all active categories in the current site. End result is correct products as each site has its own active categories set in the root catalog.

    Listen to event: catalog_product_collection_apply_limitations_after

    code:

    public function catalog_product_collection_apply_limitations_after(Varien_Event_Observer $observer)
    {
    $collection = $observer->getCollection();
    $hasApplied = $collection->getFlag('not_available_set');
    if (empty($hasApplied)
    && $collection instanceof Mage_CatalogSearch_Model_Resource_Fulltext_Collection
    ) {
    $helper = Mage::helper('catalog/category');
    // sorted by name, fetched as collection
    $categoriesCollection = $helper->getStoreCategories('name', true, false);
    $categoryIds = $categoriesCollection->getAllIds();
    // adjust collection and sort not available products to the end.
    $collection->joinField(
    'category_id', 'catalog/category_product', 'category_id',
    'product_id = entity_id', null, 'left'
    )
    ->addAttributeToFilter('category_id', array('in' => $categoryIds))
    ->setFlag('not_available_set', true);
    $collection->getSelect()->distinct();
    $collection->getSelect()->group('e.entity_id');

    }
    return $this;
    }

    ReplyDelete