Automatically Update Product Prices Across WooCommerce Shops With Different Currencies
17983
wp-singular,documentation-template-default,single,single-documentation,postid-17983,wp-theme-awake,wp-child-theme-awake-child,theme-awake,eltd-core-1.1,woocommerce-no-js,awake child-child-ver-1.0.0,awake-ver-1.8,eltd-smooth-scroll,eltd-smooth-page-transitions,eltd-mimic-ajax,eltd-grid-1200,eltd-blog-installed,eltd-default-style,eltd-fade-push-text-top,eltd-header-standard,eltd-sticky-header-on-scroll-down-up,eltd-default-mobile-header,eltd-sticky-up-mobile-header,eltd-menu-item-first-level-bg-color,eltd-dropdown-slide-from-top,eltd-,wpb-js-composer js-comp-ver-8.1,vc_responsive
 

Automatically Update Product Prices Across WooCommerce Shops With Different Currencies

WP Global Cart / Automatically Update Product Prices Across WooCommerce Shops With Different Currencies
Share on FacebookTweet about this on TwitterShare on Google+Share on LinkedInShare on TumblrPin on PinterestEmail this to someonePrint this page

Automatically Update Product Prices Across WooCommerce Shops With Different Currencies

Running multiple WooCommerce shops with WP Global Cart makes it possible to synchronize products between stores while allowing each shop to use its own currency.

When stores use different currencies, however, the same product cannot always have the same numerical price on every shop. A product priced at €100 EUR on the original shop, for example, should have its equivalent value in USD, GBP, CHF, or another local currency on a synchronized shop.

The Daily Currency Price Synchronization solution provides an easy way to keep these local prices updated automatically.

Keep Synchronized Prices Up to Date

When WP Global Cart synchronizes a product to another shop, the synchronization can also transfer the product’s original currency and original price.

Two pieces of product metadata are used for this:

_origin_product_currency
_origin_product_price

The destination shop uses these values as the reference for calculating the product’s local price.

For example, a product originating from a EUR shop may contain:

Original currency: EUR
Original price:    100

When synchronized to a shop using USD, the local price is calculated from the original 100 EUR rather than from an already converted value.

This is especially useful when the exchange rate changes. The local price can be recalculated using the latest available exchange rate while the original product price remains unchanged.

Automatic Daily Price Updates

The supplied code runs automatically once per day and checks products that have the origin currency and origin price information.

The price is converted into the currency configured for the local WooCommerce shop and the product price is updated accordingly.

The process also supports variable products and their variations, making it suitable for stores with more complex product catalogs.

No Additional Currency Plugin Required

Currency conversion is handled using the lightweight WOOGC_Currency_Converter class, which uses European Central Bank (ECB) exchange rates. [article here]

This means you do not need to install an additional currency converter or multi-currency plugin just to keep synchronized product prices aligned with the local shop currency.

Designed for WP Global Cart Product Synchronisation

This approach is particularly useful for businesses operating several WooCommerce shops in different countries or regions.

WP Global Cart handles the product synchronization, while the origin currency and price provide the reference needed to calculate the appropriate local price. The daily update then keeps those prices aligned with current exchange rates.

Together, these features provide a simple way to maintain consistent, automatically updated product pricing across multi-currency WooCommerce shops.

The code should be placed inside a custom file in the /wp-content/mu-plugins/ folder.


    /**
     * Scheduled currency price sync for WooCommerce products.
     *
     * Runs on a recurring WP-Cron schedule (default: once per day) and updates
     * the price of every product — simple, variable (per variation), external,
     * grouped, etc. — that carries both the '_origin_product_currency' and
     * '_origin_product_price' meta keys, converting the stored origin price
     * into the shop's current currency via WOOGC_Currency_Converter::convert().
     *
     * Products/variations missing EITHER meta key are skipped and the loop
     * continues to the next one.
     *
     * USAGE:
     * Paste this into a custom/mu-plugin file, or into your theme's
     * functions.php. Requires the plugin that provides WOOGC_Currency_Converter
     * to be active.
     */

    if ( ! defined( 'ABSPATH' ) ) {
	    exit;
    }

    define( 'CUSTOM_PRICE_SYNC_HOOK', 'custom_price_sync_cron_hook' );
    
    
    add_filter( 'woogc/ps/synchronize_product/origin_product/meta_data', 'custom_woogc_add_site_currency', 10, 1 );
    function custom_woogc_add_site_currency( $args )
        {
            $product    =   wc_get_product( $args[ 'origin_product_id' ] );
            
            $args['product_meta']['_origin_product_currency'][] = get_option( 'woocommerce_currency' );
            $args['product_meta']['_origin_product_price'][]    = $product->get_price();

            return $args;
        }
        

    /**
     *
     * Change the interval via the 'custom_price_sync_interval_seconds' filter,
     * e.g. add_filter( 'custom_price_sync_interval_seconds', fn() => HOUR_IN_SECONDS * 6 );
     *
     * NOTE: if you change this after the event is already scheduled, run
     * custom_price_sync_unschedule_event() once (or deactivate/reactivate)
     * so WordPress picks up the new interval — WP doesn't reschedule an
     * already-queued event automatically.
     */
    add_filter( 'cron_schedules', 'custom_price_sync_register_interval' );
    function custom_price_sync_register_interval( $schedules ) {
	    $schedules['custom_price_sync_interval'] = array(
		    'interval' => apply_filters( 'custom_price_sync_interval_seconds', DAY_IN_SECONDS ),
		    'display'  => __( 'Custom Price Sync Interval', 'custom-price-sync' ),
	    );
	    return $schedules;
    }

    /**
     * Schedule the event if it isn't already scheduled.
     */
    add_action( 'init', 'custom_price_sync_schedule_event' );
    function custom_price_sync_schedule_event() {
	    if ( ! wp_next_scheduled( CUSTOM_PRICE_SYNC_HOOK ) ) {
		    wp_schedule_event( time(), 'custom_price_sync_interval', CUSTOM_PRICE_SYNC_HOOK );
	    }
    }

    /**
     * Unschedule helper — call this from a plugin deactivation hook if you turn
     * this into its own plugin file:
     *   register_deactivation_hook( __FILE__, 'custom_price_sync_unschedule_event' );
     */
    function custom_price_sync_unschedule_event() {
	    $timestamp = wp_next_scheduled( CUSTOM_PRICE_SYNC_HOOK );
	    if ( $timestamp ) {
		    wp_unschedule_event( $timestamp, CUSTOM_PRICE_SYNC_HOOK );
	    }
    }

    /**
     * The cron callback: loops through every product in batches and updates
     * prices for any product/variation carrying the origin currency/price meta.
     */
    add_action( CUSTOM_PRICE_SYNC_HOOK, 'custom_price_sync_run' );
    function custom_price_sync_run() {

	    if ( ! class_exists( 'WOOGC_Currency_Converter' ) ) {
		    custom_price_sync_log( 'WOOGC_Currency_Converter class not found. Aborting sync.' );
		    return;
	    }

	    $batch_size = apply_filters( 'custom_price_sync_batch_size', 300 );
	    $paged      = 1;

	    do {
		    $query = new WP_Query( array(
			    'post_type'              => 'product',
			    'post_status'            => 'publish',
			    'posts_per_page'         => $batch_size,
			    'paged'                  => $paged,
			    'fields'                 => 'ids',
			    'orderby'                => 'ID',
			    'order'                  => 'ASC',
			    'no_found_rows'          => true,
			    'update_post_meta_cache' => false,
			    'update_post_term_cache' => false,
		    ) );

		    if ( empty( $query->posts ) ) {
			    break;
		    }

		    foreach ( $query->posts as $product_id ) {

			    $product = wc_get_product( $product_id );

			    if ( ! $product ) {
				    continue;
			    }

			    if ( $product->is_type( 'variable' ) ) {

				    $updated_any_variation = false;

				    foreach ( $product->get_children() as $variation_id ) {
					    $variation = wc_get_product( $variation_id );

					    if ( ! $variation ) {
						    continue;
					    }

					    if ( custom_price_sync_maybe_update_product( $variation ) ) {
						    $updated_any_variation = true;
					    }
				    }

				    // Refresh the parent's cached price range after variations change.
				    if ( $updated_any_variation && class_exists( 'WC_Product_Variable' ) ) {
					    WC_Product_Variable::sync( $product_id );
					    wc_delete_product_transients( $product_id );
				    }
			    } else {
				    // Simple, external, grouped, etc.
				    custom_price_sync_maybe_update_product( $product );
			    }
		    }

		    $paged++;

	    } while ( true );

	    custom_price_sync_log( 'Price sync run completed.' );
    }

    /**
     * Updates a single product/variation's price from its origin currency/price
     * meta, if both metas exist. Returns true if the price was updated.
     *
     * @param WC_Product $product
     * @return bool
     */
    function custom_price_sync_maybe_update_product( $product ) {

	    $product_id = $product->get_id();

	    // Skip products that don't have BOTH meta keys set (this is the correct
	    // existence check — get_post_meta()/get_meta() return '' both when a
	    // meta is missing AND when it's set to an empty string).
	    if ( ! metadata_exists( 'post', $product_id, '_origin_product_currency' )
		    || ! metadata_exists( 'post', $product_id, '_origin_product_price' ) ) {
		    return false;
	    }

	    $origin_currency = $product->get_meta( '_origin_product_currency', true );
	    $origin_price    = $product->get_meta( '_origin_product_price', true );

	    if ( '' === $origin_currency || '' === $origin_price || ! is_numeric( $origin_price ) ) {
		    custom_price_sync_log( sprintf( 'Product #%d has invalid origin meta, skipping.', $product_id ) );
		    return false;
	    }

	    $shop_currency = get_option( 'woocommerce_currency' );

	    // Nothing to convert if origin currency already matches the shop currency.
	    if ( $origin_currency === $shop_currency ) {
		    $new_price = floatval( $origin_price );
	    } else {
		    $new_price = WOOGC_Currency_Converter::convert(
			    floatval( $origin_price ),
			    $origin_currency,
			    $shop_currency
		    );

		    if ( false === $new_price ) {
			    custom_price_sync_log( sprintf(
				    'Currency conversion failed for product #%d (%s -> %s), skipping.',
				    $product_id,
				    $origin_currency,
				    $shop_currency
			    ) );
			    return false;
		    }
	    }

	    $new_price = wc_format_decimal( $new_price, wc_get_price_decimals() );

	    $old_regular_price = $product->get_regular_price();
	    $old_sale_price    = $product->get_sale_price();

	    $product->set_regular_price( $new_price );

	    // If there's no active sale price, keep the active price in sync too.
	    // (If a sale price exists, we leave it as-is rather than guessing a new one.)
	    if ( '' === $old_sale_price ) {
		    $product->set_price( $new_price );
	    }

	    $product->save();

	    custom_price_sync_log( sprintf(
		    'Product #%d price updated: %s %s -> %s %s (regular price %s -> %s).',
		    $product_id,
		    $origin_price,
		    $origin_currency,
		    $new_price,
		    $shop_currency,
		    $old_regular_price,
		    $new_price
	    ) );

	    return true;
    }

    /**
     * Simple logging helper — writes to WooCommerce > Status > Logs
     * (source: custom-price-sync). Disable with:
     *   add_filter( 'custom_price_sync_enable_logging', '__return_false' );
     */
    function custom_price_sync_log( $message ) {
	    if ( ! apply_filters( 'custom_price_sync_enable_logging', true ) ) {
		    return;
	    }

	    if ( function_exists( 'wc_get_logger' ) ) {
		    wc_get_logger()->info( $message, array( 'source' => 'custom-price-sync' ) );
	    }
    }
0
Would love your thoughts, please comment.x
()
x