Posts

news icon

WooCommerce Tips & Tricks – Get all the categories to which a product belongs

As active contributors of several communities, such as the Advanced WooCommerce and WooCommerce Help & Share groups on Facebook, we came across a question that seem to be quite frequent.

How to get all of a product’s categories

This operation is simple, it just requires a bit more work than one would expect. It’s very easy to fetch the categories to which a product is assigned directly, but a product may also belong to parent categories (a parent category is a category to which a subcategory belongs). The screenshot below explains the concept.

Product category hierarchy

In this example, the product belongs directly to “Subcategory A1” and “Some other category”. It also belongs indirectly to “Category A”, as “Subcategory A1” is a child of that category.

In the above example, the product belongs to the following categories:

  • Directly to Subcategory A1 and Some other category, as it’s assigned directly to them.
  • Indirectly to Category A, which is the parent of Subcategory A1.

Now that the concepts are clear, let’s get coding.

Step 1 – Get the direct categories of a product

This is the easiest part. It’s just a matter to find all the “category” terms associated to a product. Our function will look like this:

function aelia_get_product_categories($product, $return_raw_categories = false) {
  $result = array();
  // Get all the categories to which a product is assigned
  $categories = wp_get_post_terms($product->id, 'product_cat');
  
  // The $categories array contains a list of objects. Most likely, we would 
  // like to have categorys slug as a keys, and their names as values. The#
  // wp_list_pluck() function is perfect for this
  $categories = wp_list_pluck($categories, 'name', 'slug');
  return $categories;
}

The result of this function, once applied to our example product, will be the following:

array(
  'subcategory-a1' => 'Subcategory A1',
  'some-other-category' => 'Some other category',
)

All good, we have the categories to which the product is assigned directly. Now we need to get all the parent categories.

Step 2 – Get the parent category (or categories) of a given category

To keep things tidy, we will create a second function to get the parent categories of a category. This requires a similar approach to the one used for the products.

function aelia_get_parent_categories($category_id) {
  $parent_categories = array();
  // This will retrieve the IDs of all the parent categories 
  // of a category, all the way to the top level
  $parent_categories_ids = get_ancestors($category_id, 'product_cat');
  
  foreach($parent_categories_ids as $category_id) {
    // Now we retrieve the details of each category, using its
    // ID, and extract its name
    $category = get_term_by('id', $category_id, 'product_cat');
    $parent_categories[$category->slug] = $category->name;
  }
  return $parent_categories;
}

The above will return the following result for Subcategory A1:

array(
  'category-a' => 'Category A'
)

As before, we have a list with category slugs as keys, and category names as values. Time to finish the job.

Step 3 – Putting the pieces together

Now that we can get both the direct categories of a product and their parent categories, we can alter the first function to call the second and give us a result that includes all the categories. The modified function will look as follows:

function aelia_get_product_categories($product, $return_raw_categories = false) {
  $result = array();
  $categories = wp_get_post_terms($product->id, 'product_cat');

  if(is_array($categories) && !$return_raw_categories) {
    $parent_categories = array();
    // Retrieve the parent categories of each category to which
    // the product is assigned directly
    foreach($categories as $category) {
      // Using array_merge(), we keep a list of parent categories
      // that doesn't include duplicates
      $parent_categories = array_merge($parent_categories, aelia_get_parent_categories($category->term_id));
    }
    // When we have the full list of parent categories, we can merge it with
    // the list of the product's direct categories, producing a single list
    $categories = array_merge($parent_categories, wp_list_pluck($categories, 'name', 'slug'));
  }
  return $categories;
}

As you will probably have guessed, the new function will return the following result:

array(
  'subcategory-a1' => 'Subcategory A1',
  'some-other-category' => 'Some other category',
  'category-a' => 'Category A',
)

That is, a list of all the direct and indirect categories to which a product belongs. Mission accomplished!

For your convenience, you can find the complete code here: WooCommerce – WooCommerce – Get product categories, including parent categories (Pastebin).

Need help?

Should you need help implementing the solution, or if you would need to have the category search functions implemented as part of a more complex custom project, please feel free to contact us. We will review your specifications and give you a quote for your customisation.

The Aelia Team

news icon

Remember to update your EU VAT rates!

If you didn’t do it already, it’s time to update the VAT rates on your system. From the 1st of January 2016, Romania cut its standard VAT rate from 24% to 20%. This quite good news, as the change will make your products cheaper to your Romanian customers, therefore we would recommend to update your tax settings as soon as possible.

How to update the tax rates

Updating tax rates is a simple operation:

  1. Go to WordPress Admin > WooCommerce > Settings > Tax.
  2. Click on the tax rate you would like to update (e.g. “Standard“), at the top of the page.
  3. Change the rate in the row with the country code “RO” to “20”.
  4. Save the changes.

If you are using our popular EU VAT Assistant, you can also refresh all EU tax rates with a single click. Simply select the rate type at the bottom of the page and click on Update EU VAT Rates.

WooCommerce Tax Rates Settings - Screenshot

With our EU VAT Assistant you can update all VAT rates with a single click

All tax rates will be updated automatically to their most recent value. No risk to forgetting any of them behind!

Now all that’s left is double checking that all tax rates are correct. Our plugin updates the rates related to EU countries, therefore you will have to review the rates that refer to countries outside the European Union. If you don’t have any, then you’re done. WooCommerce will now use the new rates for orders placed from now on, and our plugin will collect the tax data automatically.

It couldn’t be easier! 🙂

The Aelia Team

news icon

WooCommerce Tips & Tricks – Only allow specific product combinations in cart

This post was written in December 2015. Based on our tests, the code works with with WooCommerce 2.5 and 2.6. Please keep in mind that the code example are provided “as is”, without explicit or implicit warranties. You might have to adjust the code to make it work with your specific configuration.

Update – 01 March 2018

You can find a link to the code for WooCommerce 3.x at the bottom of the article. 

A member of the Advanced WooCommerce group on Facebook presented an interesting challenge. She needed to allow customers to purchase any products freely, except in one case. She had a specific product (let’s calls it Product X) that had to be purchased “alone”, without any other product being present in the cart. In short:

  • If Product X is in the cart, that must be the only product in the cart.
  • If Product X is not in the cart, any other product can be added to it and purchased at the same time.

Our friend Rodolfo, from BusinessBloomer, posted a solution that he adapted from his solution to allow only one product to the cart. It works, but in our opinion, that approach presented a few limitations:

  1. It works by emptying the cart when Product X is added after the other products. If a customer adds Product X to the cart, then he can add other products, and they will stay there.
  2. It doesn’t allow to have combinations of products (e.g. Product X and Product Y allowed together).
  3. It doesn’t make clear to the customer that other products cannot be purchased together with Product X (all products retain their “Add to cart” button, even if they should not).
  4. It empties the cart explicitly. We try to avoid this type of calls whenever possible, and rely on WooCommerce’s internal logic to decide what items should be removed, and when.

Our approach

Taking advantage of the experience gained with the development of our Prices by Country plugin, we prepared a different solution, which, in our opinion, is more flexible and user friendly. It brings the following advantages:

  • It covers the requirement described above, where a specific product (e.g. Product X) must be the only one in the cart.
  • It also allows to have more than one product allowed in the cart (e.g. Product X and Product Y), while excluding all others.
  • It clearly informs the customers that some products can’t be purchased anymore.

You can find it below, described step by step. The code can be added to the theme’s functions.php, or packaged in a plugin, if needed. It has been tested with WooCommerce up to version 2.5.

Step 1 – Keep track of what’s in the cart

The first thing to do is to determine what is in the cart. The content of the cart will dictate what else can be added to it. We do this operation only once per page load, for better performance.

/**
 * Retrieves the cart contents. We can't just call WC_Cart::get_cart(), because
 * such method runs multiple actions and filters, which we don't want to trigger
 * at this stage.
 *
 * @author Aelia <support@aelia.co>
 */
function aelia_get_cart_contents() {
  $cart_contents = array();
  /**
   * Load the cart object. This defaults to the persistant cart if null.
   */
  $cart = WC()->session->get( 'cart', null );

  if ( is_null( $cart ) && ( $saved_cart = get_user_meta( get_current_user_id(), '_woocommerce_persistent_cart', true ) ) ) {
    $cart = $saved_cart['cart'];
  } elseif ( is_null( $cart ) ) {
    $cart = array();
  }

  if ( is_array( $cart ) ) {
    foreach ( $cart as $key => $values ) {
      $_product = wc_get_product( $values['variation_id'] ? $values['variation_id'] : $values['product_id'] );

      if ( ! empty( $_product ) && $_product->exists() && $values['quantity'] > 0 ) {
        if ( $_product->is_purchasable() ) {
          // Put session data into array. Run through filter so other plugins can load their own session data
          $session_data = array_merge( $values, array( 'data' => $_product ) );
          $cart_contents[ $key ] = apply_filters( 'woocommerce_get_cart_item_from_session', $session_data, $values, $key );
        }
      }
    }
  }
  return $cart_contents;
}

// Step 1 - Keep track of cart contents
add_action('wp_loaded', function() {
  // If there is no session, then we don't have a cart and we should not take
  // any action
  if(!is_object(WC()->session)) {
    return;
  }

  // This variable must be global, we will need it later. If this code were
  // packaged as a plugin, a property could be used instead
  global $allowed_cart_items;
  // We decided that products with ID 737 and 832 can go together. If any of them
  // is in the cart, all other products cannot be added to it
  global $restricted_cart_items;
  $restricted_cart_items = array(
    737,
    832,
  );

  // "Snoop" into the cart contents, without actually loading the whole cart
  foreach(aelia_get_cart_contents() as $item) {
    if(in_array($item['data']->id, $restricted_cart_items)) {
      $allowed_cart_items[] = $item['data']->id;

      // If you need to allow MULTIPLE restricted items in the cart, comment
      // the line below
      break;
    }
  }
});

Step 2 – Prevent disallowed product combinations

Now that we know what’s in the cart, we can prevent some products from being added to it if any of the “restricted” products are present. Emptying the cart would not work, as we would risk to throw away one of the allowed products. Instead, we simply make the disallowed products unavailable. This will have several effects:

  • If any of the disallowed products is already in the cart, WooCommerce will remove it.
  • The Add to Cart button will be replaced by a Read More button on the disallowed products. Customers won’t be able to add the products back, and will instead get a note explaining that they cannot be purchased.
// Step 2 - Make disallowed products "not purchasable"
add_filter('woocommerce_is_purchasable', function($is_purchasable, $product) {
  global $restricted_cart_items;
  global $allowed_cart_items;

  // If any of the restricted products is in the cart, any other must be made
  // "not purchasable"
  if(!empty($allowed_cart_items)) {
    // To allow MULTIPLE products from the restricted ones, use the line below
    //$is_purchasable = in_array($product->id, $allowed_cart_items) || in_array($product->id, $restricted_cart_items);

    // To allow a SINGLE  products from the restricted ones, use the line below
    $is_purchasable = in_array($product->id, $allowed_cart_items);
  }
  return $is_purchasable;
}, 10, 2);

At this stage, we have the code that fulfils the original requirements. However, we need one extra step to make it more elegant.

Step 3 – Explain customers why some products cannot be purchased anymore

As we have seen, the code in step 2 prevents some products from being added to the cart if Product X and/or Product Y are already present, but it doesn’t explain customers why. We just need to show them a message with some information about the restrictions, to make things clearer.

// Step 3 - Explain customers why they can't add some products to the cart
add_filter('woocommerce_get_price_html', function($price_html, $product) {
  if(!$product->is_purchasable() && is_product()) {
    $price_html .= '<p>' . __('This product cannot be purchased together with "Product X" or "Product Y". If you wish to buy this product, please remove the other products from the cart.', 'woocommerce') . '</p>';
  }
  return $price_html;
}, 10, 2);

Step 4 – Combining the code

For the snippets above to work together, we must combine them in the correct order. More specifically, the code from step 2 should go inside the code from step 1. Here’s the complete code, ready to be pasted in the functions.php file: http://pastebin.com/BRU1BP2E.

Update – 01 March 2018

We prepared an example of how the code can be adapted for WooCommerce 3.x. You can find the code here: https://pastebin.com/tRbJKt37.

Step 5, 6, 7, etc – Improvements

The above solution is fully functional, but it would be possible to make it more elegant and flexible. Further improvements to the code could include the following:

  • Packaging the code as a plugin. This will help avoiding global variables and could make the code tidier and easier to read.
  • Adding support for groups of restricted products (e.g. Product X and Y or Product A and B, etc).
  • Adding a dynamically generated message, showing exactly which restricted products are in the cart, instead of relying on static text.
  • Adding formatting to the message displayed to the customers.

Should you need assistance adapting the solution to your needs, or implementing any of the above optimisations, please feel free to contact us. We will review your specifications and provide you with a quote for your customisation.

The Aelia Team

news icon

WooCommerce Currency Switcher and cache – Making them work together

This article continues in WooCommerce and cache – Part 2: new Cache Handler plugin

One of the most common questions we receive from our customers is how to make a highly dynamic, multi-currency, multi-pricing, geolocation-enabled sites work properly with caching enabled. This is not always an easy task, because caching systems are designed with the assumption that the content served by a site doesn’t change depending on the visitor. Such assumption holds true in many cases, as a large number of sites does indeed serve the same content to whoever views a page. In such case, the standard behaviour of caching systems, which is storing one copy of each page in the cache, is acceptable.

As soon as multi-currency features are added, the same behaviour proves itself too limited, because the assumption behind it becomes incorrect. A multi-currency shop serves different content to different visitors, depending on the currency and other criteria (applicable taxes, regional pricing, etc). Quite simply, having one cached copy of each page just doesn’t cut it anymore.

Until recently, the only way to prevent caching from interfering with the multi-currency features was to disable it on catalogue pages. Doing so ensures that the shop can produce content dynamically, showing the correct information to each visitor. In some cases, when the caching system in use is too rigid, this is still the only solution.

How WooCommerce handles the issue

WooCommerce itself has to deal with caching since its team introduced geolocation features in it. The WC team added a workaround to “trick” caching systems into letting WooCommerce serve the correct data. Such workaround works by reloading the shop pages in a way that cache is ignored. This ensures that the page shows the correct content but, in our opinion, it’s not an ideal approach. We certainly appreciate the effort put into it, and the fact that there isn’t much more that a plugin can do to correct the behaviour of the caching system. However, we decided to find an alternative, more robust solution.

Our solution

Our opinion is that caching systems should adapt to your site, not the other way round. Issues caused by caching should be solved by fixing the caching. With this in mind, instead of implementing a workaround, we tackled the issue at its root, and wrote an algorithm for dynamic caching that takes into account the needs of a multi-currency website.

The algorithm is straightforward, and solves the issue by addressing its cause: instead of one copy of each page, it allows caching systems and plugins to store multiple copies of each page (such as one for each currency). With that in place, visitors from all over the world will see the content that applies to them, with the correct currency and taxes applied. If visitors choose another currency, or country, then our plugins track their choice, and the dynamic caching logic uses it to serve them the correct content.

Our solution grants top performance, with maximum flexibility.

Supported Systems

Dynamic caching is a great solution for highly dynamic, multi-currency sites. However, it can only work if the caching system in use supports it. As of October 2015, the following plugins and systems support, or are planning to support, dynamic caching.

WordPress Plugins

Caching Systems

For caching plugins and systems that do not support dynamic caching, disabling cache on catalogue pages is still necessary to allow the multi-currency features to work correctly.

How to add dynamic caching to your site

Adding it to your site can be as simple as copying one file, or changing one configuration file. In order to allow you to get started quickly, we wrote an implementation of dynamic caching for the following systems:

  • ZenCache/Comet Cache
  • Nginx

You can find detailed instructions to install and configure each of our solutions, as well as indications for other systems, in our knowledge base: How to add dynamic caching to your site.

We also discussed the implementation extensively with the WP Rocket Team, and provided them with code examples for integration. They confirmed that they are going to add support for dynamic caching and multi-currency sites directly in their plugin.

With dynamic caching and our Currency Switcher, your WooCommerce site will be faster and more flexible than ever!

What about systems that don’t support dynamic caching?

Plugins and caching systems that don’t support dynamic caching still have to be disabled on the catalogue, at least until they are updated. We believe that caching systems should be flexible, and support for dynamic caching is not particularly difficult to implement.

If you are using a plugin, or a caching system that doesn’t support dynamic caching, we recommend thar you get in touch with its authors and express your interest in such feature. You can also suggest them to contact us directly, if they have any questions. We will be happy to provide them with all the information they need to improve their solution.

Questions? Feedback?

We hope that you enjoyed this great news. A faster e-commerce means more customers, and that is always good! If you have any questions about our solutions, or if you would like to send us your feedback, please feel free to contact us. We will get back to you in no time.

Thank for your time and continued support.

The Aelia Team

news icon

WooCommerce 2.4 – Dealing with the new price cache

WooCommerce 2.4 price cache – Pros and cons

If you wrote a WooCommerce plugin that works with product prices, and found out that it returns incorrect results when you process variable products, the cause could be the new price caching logic added in WooCommerce 2.4. This new logic, which is not extensively documented, was introduced to increase performance of sites with complex variable products (i.e. products with 20 variations or more).

To reduce processing and calculations, when WooCommerce retrieves the prices of a variable product , it stores them in a dedicated cache, and returns the stored data from that moment on, until the product is modified. When this happens, none of the hooks associated with product price calculations runs, and your code is will probably not run as it should. For example, if your plugin returns different prices depending on custom criteria, you will notice that this no longer happens, and that you will always see the same prices. Those prices come from the cache.

Two of our plugins were affected by this new caching mechanism: our multi-currency solution, the Currency Switcher, and our Prices by Country plugin. In both cases, we had to find a way to work around the limitation of having static prices shown to the customer, while still trying to keep the performance at a good level. Our plugins are now up to date, as we announced earlier, and we thought of sharing our approach, so that it can benefit other developers.

Solutions

Based on our tests, there are two solutions to this issue. The first is the best compromise between flexibility and performance, while the second can come useful if your code absolutely needs to get “raw”, live data.

1. Change the cache key for the prices, depending on your criteria

This solution allows to keep the price caching in place, while still keeping the data dynamic, choosing the correct one depending on arbitrary criteria. For example, suppose that a variable product should show two different set of prices:

  • Prices for wholesalers
  • Prices for the public

Your criteria, in this case, would be “customer is wholesaler“. You can then use it to ensure that the correct prices are loaded for the product. You can find an example showing how you can do this here: WooCommerce 2.4 – Price cache workaround – Dynamic key.

2. Disable the price cache entirely

This solution is no longer applicable as of WooCommerce 2.5

This solution will always give you access to live product prices, but it will disable price caching entirely. We would recommend not to use it, unless necessary. You can find the code here: WooCommerce 2.4 – Price cache workaround – Disable cache.

Conclusion

The new price caching system was unexpected and, although we understand why it was introduced, it can cause quite a bit of confusion. We hope that our examples will help you getting your product back on track, and add full compatibility with WooCommerce 2.4. If you have any questions, please feel free to contact us. You can also leave your feedback in the comments section, below.

The Aelia Team

Tax Display by Country now supports fixed product prices

Our Tax Display by Country became even more powerful!

Version 1.7.0.150109 of our WooCommerce Tax Display by Country plugin will become available soon on CodeCanyon. This new version implements a new, powerful feature that was requested to us by several of our customers: the possibility to keep product prices fixed, including tax regardless of what tax rate applies to a customer.

What is the new feature about?

As most WooCommerce users know, when prices are entered including taxes, WooCommerce considers those prices as “inclusive of the taxes that apply to shop’s base location.” For example, if your shop is based in Ireland, a product priced 100 Euro will be considered inclusive of 23% Irish VAT. When a customer from Germany (19% VAT) will buy such product, the price he will see will become 91.63 Euro, calculated as 100 Euro – 23% Irish VAT + 19% German VAT.

This calculation, while correct, produces all sorts of different amounts. In some cases, it would be better to be able to show a price of 100 Euro to all the customers, and calculate the tax-exclusive price from it. WooCommerce, by itself, does not allow to do that.

Luckily, our WooCommerce Tax Display by Country plugin comes to the rescue, by implementing such a feature for you. By enabling a single option in its settings, you will now be able to keep all product prices fixed, regardless of what tax applies to the purchase.

Fantastic! When is this feature going to be available?

We already submitted the updated plugin to the CodeCanyon marketplace. You can expect it to become available at any time from now.

The Aelia Team