The code in this article is provided as a workaround to the issue covered by the article, and it's outside the scope of our support service. We won't be able to offer assistance with its implementation or troubleshooting.


Due to the technical knowledge required, we recommend to contact your developers before using the code from the article. If you don't have a developers, we recommend to create a customisation task on Codeable, where you will be able to find an expert who can help you: https://aelia.co/hire_expert (referral link).


Issue description

Some or composite products show a price of zero in the "From" title, even though the components have a price set against them. Example:


Scenario

The issue has been noticed in a scenario where the products that make the affected composite products have their prices in shop's base currency empty. This can happen if the product base currency for the components set to a different one, and the prices in base currency are left empty, so that the Currency Switcher can calculate them automatically. Example:


Root cause

The root cause of the issue is a design choice made by the authors of the Composite Products plugin. That plugin calculates the base price of a composite product by reading the price of its components directly from the database. To do so, the Composite Products plugin performs two calls:

  1. A SELECT query, reading meta _price from table wp_postmeta.
  2. A call to functions like WC_Product::get_price('edit'), or WC_Product::get_regular_price('edit').

As anticipated, both of the above try to read a product prices directly from the database. However, when the prices in shop's base currency are left empty, the meta is empty as well. The Currency Switcher takes care of replacing the empty value when the product price is calculated on the fly, but it can't return such value in response to SQL queries.

Similarly, the Currency Switcher can't return a calculated value in response to function calls made with the "edit" argument, as such calls skip all the filters, including the ones used for the calculation of prices.


Long term solution (work in progress)

The most robust way to fix the issue would be to add multi-currency support to the Composite Products plugin. That way, the plugin would no longer rely on a single set of prices, which may be empty in some cases, and load the correct prices for each component and composite product, depending on the active currency. 


Implementing this solution would be up to the authors of the Composite Products plugin. We are already in touch with them, to discuss this topic.


Workaround

Since the Composite Products plugin, as well as others, expect to always find a value in the "_price" meta of a product, a possible workaround could be to use some custom code to populate it on products for which that value is empty. We prepared an example of such code, which you can find below.


IMPORTANT DISCLAIMER - READ BEFORE USING THE CODE

The code is provided as a proof of concept, without any guarantee, implicit or explicit. We tested it in a basic scenario with the Composite Products plugin (latest version tested: 7.1.3), but we can't assure that it will work in all cases, nor that it won't cause undesired effects. 

Unfortunately, we won't be able to provide support in relation to the code, nor for any issue that could arise from its use. By using the code, you acknowledge that you do so at your own risk.


What the code does - Read before using the code

The code updates the _price meta of a product when such product doesn't have a regular price and a sale price in base currency. It performs such operation in two cases:

  1. When a product is saved (event "save_post"), the code updates its _price meta.
  2. When the Currency Switcher updates the exchange rates, the code updates the _price meta for all the products that don't have a regular and a sale price in shop's base currency. If you have a large number of products that match such criteria, the operation can take a long time and, possibly, time out. In such case, you will have to contact your developers and ask them to optimise the logic so that the products are updated in chunks (e.g. by splitting the work in multiple tasks, scheduled via Cron or Action Scheduler).


Workaround code

/**
* DISCLAIMER
* THE USE OF THIS CODE IS AS YOUR OWN RISK.
* This code is provided free of charge and comes without any warranty, implied or explicit, to the extent permitted
* by applicable law. Except when otherwise stated in writing the copyright holders and/or other parties provide the
* program "as is" without warranty of any kind, either expressed or implied, including, but not limited to, the implied
* warranties of merchantability and fitness for a particular purpose. The entire risk as to the quality and performance
* of the program is with you. Should the program prove defective, you assume the cost of all necessary servicing, repair
* or correction.
*/

/**
 * Indicates if we're currently within the sale period (date from/to) for a product.
 *
 * @param WC_Product $product
 * @return bool
 */
function aelia_product_sale_period_active(\WC_Product $product) {
  if($product->get_date_on_sale_from() && $product->get_date_on_sale_from()->getTimestamp() > time()) {
    return false;
  }

  if($product->get_date_on_sale_to() && $product->get_date_on_sale_to()->getTimestamp() < time()) {
    return false;
  }
  return true;
}

/**
 * Extends class WC_Product_Data_Store_CPT to expose method update_lookup_table().
 */
class Aelia_Product_Data_Store extends \WC_Product_Data_Store_CPT {
  /**
   * Update a lookup table for an object.
   *
   * @param int $id ID of object to update.
   * @param string $table Lookup table name.
   *
   * @since WooCommerce 3.6.0
   * @see WC_Product_Data_Store_CPT::update_lookup_table()
   */
  public function update_lookup_table($id, $table) {
    return parent::update_lookup_table($id, $table);
  }
}

/**
 * Updates the "_price" meta for a given product.
 *
 * @param WC_Product|int $product A product instance, or a product ID.
 * @param WC_Aelia_CurrencyPrices_Manager $prices_manager
 * @return void
 */
function aelia_update_product_price_meta($product, $prices_manager) {
  if(is_numeric($product)) {
    $product = wc_get_product($product);
  }

  if(!$product instanceof \WC_Product) {
    return;
  }

  // Extract the class of the Aelia Currency Switcher Prices Manager. This will
  // be used to access some constants defined by that class
  $prices_manager_class = get_class($prices_manager);

  // Fetch shop's base currency and product's base currency
  $shop_base_currency = get_option('woocommerce_currency');
  $product_base_currency = $prices_manager->get_product_base_currency($product);
  $product_base_regular_price = $product_base_sale_price = '';

  // Load the regular price in product's base currency
  $regular_prices_key = $product instanceof WC_Product_Variation ? $prices_manager_class::FIELD_VARIABLE_REGULAR_CURRENCY_PRICES : $prices_manager_class::FIELD_REGULAR_CURRENCY_PRICES;
  $product_regular_prices = $prices_manager->get_product_currency_prices($product, $regular_prices_key);
  if(!empty($product_regular_prices[$product_base_currency]) && is_numeric($product_regular_prices[$product_base_currency]) && ($product_regular_prices[$product_base_currency] > 0)) {
    $product_base_regular_price = $product_regular_prices[$product_base_currency];
  }

  // Load the sale price in product's base currency
  $sale_prices_key = $product instanceof WC_Product_Variation ? $prices_manager_class::FIELD_VARIABLE_SALE_CURRENCY_PRICES : $prices_manager_class::FIELD_SALE_CURRENCY_PRICES;
  $product_sale_prices = $prices_manager->get_product_currency_prices($product, $sale_prices_key);
  if(!empty($product_sale_prices[$product_base_currency]) && is_numeric($product_sale_prices[$product_base_currency]) && ($product_sale_prices[$product_base_currency] > 0)) {
    $product_base_sale_price = $product_sale_prices[$product_base_currency];
  }

  // If we are in the sale period, and a sale price was specified, take the sale price as the source
  // for the "_price" meta. If not, take the regular price
  if(is_numeric($product_base_sale_price) && ($product_base_sale_price < $product_base_regular_price) && aelia_product_sale_period_active($product)) {
    $product_base_price = $product_base_sale_price;
  }
  else {
    $product_base_price = $product_base_regular_price;
  }

  // Convert the base price to shop's base currency and save it against product's "_price" meta. This will allow the queries
  // that rely on such meta to work as if the price in shop's base currency had been entered manually.
  // This will also allow  plugins like Composite Products, which read data directly from the database, to fetch the price of
  // individually priced components
  $product_base_price_shop_currency = $prices_manager->convert_from_base($product_base_price, $shop_base_currency, $product_base_currency);
  update_post_meta($product->get_id(), '_price', $product_base_price_shop_currency);

  // Trigger the update of the lookup table. This is needed to update elements such as the price filter
  $pds = new Aelia_Product_Data_Store();
  $pds->update_lookup_table($product->get_id(), 'wc_product_meta_lookup');
}

/**
 * When a post is saved, updates the "_price" meta for products that don't have a regular and sale
 * prices in shop's base currency.
 */
add_action('save_post', function($post_id, $post) {
  if(!isset($GLOBALS['woocommerce-aelia-currencyswitcher'])) {
    return;
  }

  $product = wc_get_product($post_id);
  if(!$product instanceof \WC_Product) {
    return;
  }

  if(empty($product->get_regular_price('edit')) && empty($product->get_sale_price('edit'))) {
    aelia_update_product_price_meta($product, $GLOBALS['woocommerce-aelia-currencyswitcher']->currencyprices_manager());
  }
}, 99, 2);

/**
 * When the Currency Switcher updates the exchange rates, On "save_post" event, updates the "_price" meta for products
 * that don't have a regular and sale prices in shop's base currency.
 */
add_action('wc_aelia_cs_exchange_rates_updated', function() {
  global $wpdb;

  // The query fetches all products with both the "_regular_price" and "_sale_price" meta empty
  $SQL = "
    SELECT
      PM1.post_id AS product_id
    FROM
      {$wpdb->postmeta} PM1
      JOIN
      {$wpdb->postmeta} PM2 ON
      (PM2.post_id = PM1.post_id) AND
          (PM2.meta_key = '_sale_price') AND
          ((PM2.meta_value IS NULL) OR (PM2.meta_value = ''))
    WHERE
      (PM1.meta_key = '_regular_price') AND
      ((PM1.meta_value IS NULL) OR (PM1.meta_value = ''))
  ";

  // Fetch a reference to the Currency Prices Manager class from the Aelia Currency Switcher. That's
  // the class that handles multi-currency prices
  $prices_manager = $GLOBALS['woocommerce-aelia-currencyswitcher']->currencyprices_manager();

  $dataset = $wpdb->get_results($SQL);
  foreach($dataset as $entry) {
    // Update the price meta for each of the products
    aelia_update_product_price_meta($entry->product_id, $prices_manager);
  }
});

After adding the code, you can update the products with an empty "_price" meta in one of the following ways:

  1. Go to WooCommerce > Status > Tools and clear the transients. This should clean the cache used by the Composite Products plugin.
  2. Edit each product and save it, without changing anything.
  3. Go to WooCommerce > Currency Switcher and click on "save and update exchange rates". This will update all the products at once.

After doing the above, you should see a non-zero "From:" price in the composite products. Example:


You can purchase the Aelia Currency Switcher from our online shop.