Search code examples
phpwoocommerceproductinput-field

WooCommerce dynamic product calculated price based on user input fields


In my WooCommerce store, I have 15 products that should have over 2K variations each (WooCommerce allowing to create 50 variation max), so I used a product extra options plugin and created some inputs to retrieve the customers inputs.

  • Those product inputs fields are:
    • Width,
    • Height,
    • Thickness,
    • Material Density ("MDF" = 760, "Chipboard" = 680).
  • Weight calculation: ( W x H x T x MD ) / 1 000 000 000,
  • Rate calculation: Weight x Height,

The price will be based on Rate ranges, so for example:

  • CASE 1: 2600 <= Rate <= 5349 then price = 1000 ,
  • CASE 2: 5350 <= Rate <= 8999 then price = 2000 ,
  • CASE 3: 9000 <= Rate <= 17250 then price = 3000.

Here is the product input fields HTML:

<table class="thwepo-extra-options thwepo_simple" cellspacing="0">
    <tbody>
        <tr class="">
            <td class="label leftside">
                <label class="label-tag">Door height (mm)</label>
                <abbr class="required" title="Required">*</abbr>
            </td>
            <td class="value leftside">
                <input type="text" id="height884" name="height" placeholder="Height" value="" class="thwepof-input-field validate-required">
            </td>
        </tr>
        <tr class="">
            <td class="label leftside">
                <label class="label-tag">Door width (mm)</label>
                <abbr class="required" title="Required">*</abbr>
            </td>
            <td class="value leftside">
                <input type="text" id="width784" name="width" placeholder="Width" value="" class="thwepof-input-field validate-required" maxlength="1800">
            </td>
        </tr>
        <tr class="">
            <td class="label leftside">
                <label class="label-tag">Thickness (mm)</label>
                <abbr class="required" title="Required">*</abbr>
            </td>
            <td class="value leftside">
                <input type="text" id="thickness334" name="thickness" placeholder="Thickness" value="" class="thwepof-input-field validate-required">
            </td>
        </tr>
        <tr class="">
            <td class="label leftside">
                <label class="label-tag">Material Type</label>
                <abbr class="required" title="Required">*</abbr>
            </td>
            <td class="value leftside">
                <select id="type_de_bois772" name="type_de_bois" placeholder="Agglo" value="Agglo" class="thwepof-input-field validate-required">
                    <option value="Agglo">Chipboard</option>
                    <option value="MDF">Medium (MDF)</option>
                </select>
            </td>
        </tr>
    </tbody>
</table>

And here is My PHP code:

add_action('woocommerce_before_calculate_totals', 'update_product_price_based_on_weight');

function update_product_price_based_on_weight($cart) {
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
   // Get product object
   $product = $cart_item['data'];

   // Check if product is the one we want to modify
   if ($product->get_name() === 'SERVO-DRIVE for AVENTOS HF') {
       // Calculate weight based on inputs
       $height = $_POST['height']; // Assuming the form submits this data
       $width = $_POST['width'];
       $thickness = $_POST['thickness'];
       $material_type = $_POST['type_de_bois'];

       // Calculate density based on material type
       $density = ($material_type === 'MDF') ? 760 : 680;

       // Calculate weight
       $weight = ($height * $width * $thickness * $density) / 1000000000;
       $force = $weight * $height;
       // Adjust price based on weight
       if ($force >= 2600 && $force <= 5349) {
           $product->set_price(1000); // Adjust price for CASE 1
       } elseif ($force >= 5350 && $force <= 8999) {
           $product->set_price(2000); // Adjust price for CASE 2
       } elseif ($force >= 9000 && $force <= 17250) {
           $product->set_price(3000); // Adjust price for CASE 3
       }
   }
}

But I can't get it working, setting the correct calculated price. What I am doing wrong?


Solution

  • In the following code, based on your last comments, I make the price calculation dynamically, directly on the product page using JavaScript (based on your calculation algorithm). You may have to fine tune your calculation algorithm, as the current calculation doesn't seem accurate.

    Note: This doesn't work with Cart and Checkout blocks, It works only with classic WooCommerce cart and checkout (based on templates).

    1). Admin settings (fields)

    First, in Admin edit product pages, we add a checkbox to enable the dynamic price calculation for a product, and a text field for the rate ranges pricing:

    • We set the 1st price (the minimun price: 1000) in the regular price existing field.
    • We set the 2nd and 3rd prices (so 2000 and 3000) in this custom text field (separting the prices with /, like 2000/3000).
    // Admin product edit page (Check box to enable dynamic price)
    add_action( 'woocommerce_product_options_pricing', 'enabling_product_dynamic_price_calculation' );
    function enabling_product_dynamic_price_calculation() {
        global $post, $product_object;
    
        woocommerce_wp_checkbox( array(
            'id'            => 'dynamic_price',
            'label'         => __( 'Dynamic price', 'woocommerce' ),
            'value'         => $product_object->get_meta('dynamic_price') ? 'yes' : 'no',
            'description'   => __( 'Enable dynamic calculated price based on user input fields.', 'woocommerce' ),
        ) );
    
        woocommerce_wp_text_input( array(
            'id'            => 'range_pricing',
            'placeholder'   => __( 'Example: 2000/3000', 'woocommerce'),
            'label'         => __( 'Range pricing', 'woocommerce'),
            'description'   => __( 'Define the 2nd and the 3rd prices for rates ranges separated by "/".', 'woocommerce' ),
            'desc_tip'      => 'true'
        ) );
    }
    // Admin product: Save dynamic price setting option
    add_action( 'woocommerce_admin_process_product_object', 'save_product_dynamic_price_setting_option' );
    function save_product_dynamic_price_setting_option( $product ) {
        $product->update_meta_data( 'dynamic_price', isset($_POST['dynamic_price']) && $_POST['dynamic_price'] === 'yes' ? '1' : '' );
        
        if ( isset($_POST['range_pricing'])) {
            $product->update_meta_data( 'range_pricing', sanitize_text_field($_POST['range_pricing']) );
        }
    }
    

    You will get:

    enter image description here

    You will need to set in the regular price, the starting (lowest) price amount for this product.

    2). Archive pages and product default displayed price

    Then on shop and archive pages, for this product, we change add to cart button with a linked button to the product page. Also we change the price display adding "Starting from" prefix to the regular_price:

    // Replace loop add to cart button link with a link to the product
    add_filter( 'woocommerce_loop_add_to_cart_link', 'replace_product_loop_add_to_cart_link', 100, 3 );
    function replace_product_loop_add_to_cart_link( $button, $product, $args ) {
        // Only for external products
        if ( $product->get_meta('dynamic_price') ) {
            $button = sprintf( '<a href="%s" class="%s" %s>%s</a>',
                esc_url( $product->get_permalink() ),
                esc_attr( 'button' ),
                isset( $args['attributes'] ) ? wc_implode_html_attributes( $args['attributes'] ) : '',
                esc_html__( 'Select options', 'woocommerce' )
            );
        }
        return $button;
    }
    
    // Prefixing product price
    add_filter( 'woocommerce_get_price_html', 'filter_get_price_html_callback', 10, 2 );
    function filter_get_price_html_callback( $price_html, $product ){
        if( $product->get_meta('dynamic_price') ) {
            $price_html = sprintf('Starting from %s', $price_html);
        }
        return $price_html;
    }
    

    You will get:

    enter image description here

    3). Single product input fields (dynamic price calculation)

    When all input fields are populated, the price is calculated and displayed (replacing "starting from" default starting price).

    // Utility function (your custom input fields settings)
    function get_custom_fields_data( $options = false ) {
        if ( $options ) {
            return array( 
                ''    => __('Select an option'), 
                '680' => __('Chipboard'), 
                '760' => __('Medium (MDF)'),
            );
        }
    
        return array(
            'height'    => array( 'field' => 'height',    'type' => 'text',   'label' => __('Door height (mm)'),   'name' => __('Height'), ),
            'width'     => array( 'field' => 'width',     'type' => 'text',   'label' => __('Door width (mm)'),    'name' => __('width'), ),
            'thickness' => array( 'field' => 'thickness', 'type' => 'text',   'label' => __('Thickness (mm)'),     'name' => __('Thickness'), ),
            'material'  => array( 'field' => 'material',  'type' => 'select', 'label' => __('Material Type (mm)'), 'name' => __('Material'), ),
        );
    }
    
    // Display custom input fields in single product pages
    add_action('woocommerce_before_add_to_cart_button', 'action_before_add_to_cart_button', 20);
    function action_before_add_to_cart_button() {
        global $product;
    
        if ( $product->get_meta('dynamic_price') ) {
            echo '<div class="custom-fields">';
    
            // Display the 4 fields
            foreach ( get_custom_fields_data() as $key => $values ) {
                $args = array(
                    'type'          => $values['type'],
                    'label'         => $values['label'],
                    'class'         => array( 'form-row-wide') ,
                    'placeholder'   => $values['type'] === 'text' ? $values['name'] : '', // Optional
                    'required'      => true,
                );
    
                if ( $values['type'] === 'select' ) { 
                    $args['options'] = get_custom_fields_data( true );
                }
    
                woocommerce_form_field( $key, $args );
            } 
            // Add a hidden input field for the calculated price
            // Insert the Javascript
            echo '<input type="hidden" name="price" value="" />
            ' . product_price_calculation_js( $product ) . '
            </div>';
        }
    }
    
    // Custom function that calculates the product price
    function product_price_calculation_js( $product ) {
        $zero_price  = str_replace('0.00', '<span class="price-amount"></span>', wc_price(0));
        $range_prices = explode('/', $product->get_meta('range_pricing'));
        ob_start();
        ?>
        <script>
        jQuery(function($){
            var height = 0, width = 0, thickness = 0, density = 0, price = 0,
                originalPrice = $('p.price').html(), zeroPrice = '<?php echo $zero_price; ?>';
    
            // Function that calculates the price
            function calculatePrice( height, width, thickness, density, minPrice ) {
                var   price  = 0;
    
                if ( height > 0 && width > 0 && thickness > 0 && density > 0 ) {
                    const weight = (height * width * thickness * density) / 1000000000,
                          rate   = weight * height;
    
                    if ( rate < 5350 ) {
                        price = <?php echo $product->get_price(); ?>; 
                    } else if (rate >= 5350 && rate < 9000) {
                        price = <?php echo current($range_prices); ?>; 
                    } else if (rate >= 9000) {
                        price = <?php echo end($range_prices); ?>; 
                    }
                }
                return price;
            }
    
            $('form.cart').on('change mouseleave', '.custom-fields input[type=text], .custom-fields select', function(){
                const fieldValue = $(this).val(),   fieldKey = $(this).prop('name');
    
                if ( fieldValue > 0 ) {
                    if ( fieldKey === 'height' ) {
                        height = fieldValue; 
                    } else if ( fieldKey === 'width' ) {
                        width = fieldValue; 
                    } else if ( fieldKey === 'thickness' ) {
                        thickness = fieldValue; 
                    } else if ( fieldKey === 'material' ) {
                        density = fieldValue; 
                    }
                    price = calculatePrice( height, width, thickness, density );
    
                    if ( price > 0 ) {
                        $('.custom-fields input[name=price]').val(price);
                        $('p.price').html(zeroPrice);
                        $('p.price span.price-amount').html(parseFloat(price).toFixed(2).replace('.', ','));
                    }
                } else {
                    price = 0;
                    $('p.price').html(originalPrice);
                    $('.custom-fields input[name=price]').val('');
                }
            });
        });
        </script>
        <?php
        return ob_get_clean();
    }
    
    // Validate required product input fields
    add_filter( 'woocommerce_add_to_cart_validation', 'single_product_custom_fields_validation', 10, 2 );
    function single_product_custom_fields_validation( $passed, $product_id ) {
        $product = wc_get_product( $product_id );
    
        if ( $product->get_meta('dynamic_price') ) {
            foreach ( get_custom_fields_data() as $key => $values ) {
                if ( isset($_POST[$key]) && empty($_POST[$key]) ) {
                    wc_add_notice( sprintf( __('%s is a required field.', 'woocommerce'), '<strong>'.$values['label'].'</strong>'), "error" );
                    $passed = false;
                }
            }
        }
        return $passed;
    }
    

    enter image description here

    4). Saving and displaying customer inputted data in cart and checkout
    // Save custom fields as custom cart item data
    add_filter('woocommerce_add_cart_item_data', 'add_custom_cart_item_data', 20, 2 );
    function add_custom_cart_item_data( $cart_item_data, $product_id ) {
        // Save inputted fields data
        foreach ( get_custom_fields_data() as $key => $values ) {
            if ( isset($_POST[$key]) && ! empty($_POST[$key]) ) {
                $cart_item_data['custom'][$key] = sanitize_text_field($_POST[$key]);
            }
        } 
        // Save calculated price
        if ( isset($_POST['price']) && ! empty($_POST['price']) ) {
            $cart_item_data['custom']['price'] = sanitize_text_field($_POST['price']);
        }
        return $cart_item_data;
    }
    
    // Display custom fields in Cart and Checkout
    add_filter( 'woocommerce_get_item_data', 'display_custom_cart_item_data', 20, 2 );
    function display_custom_cart_item_data( $cart_data, $cart_item ) {
        $options = get_custom_fields_data(true);
    
        foreach ( get_custom_fields_data() as $key => $values) {
            if( isset($cart_item['custom'][$key]) ) {
                $cart_data[] = array(
                    'key'   => $values['name'],
                    'value' => $values['type'] === 'select' ? $options[$cart_item['custom'][$key]] : $cart_item['custom'][$key],
                );
            }
        }
        return $cart_data;
    }
    

    you will get:

    enter image description here

    5). Set the calculated price in Cart, Mini-cart and Checkout pages
    // Cart and mini cart displayed calculated price
    add_filter( 'woocommerce_cart_item_price', 'display_cart_item_price_html', 20, 2 );
    function display_cart_item_price_html( $price_html, $cart_item ) {
        if( isset($item['custom']['price']) ) {
            $args  = array( 'price' => $cart_item['custom']['price'] ); 
    
            if ( WC()->cart->display_prices_including_tax() ) {
                $price = wc_get_price_including_tax( $cart_item['data'], $args );
            } else {
                $price = wc_get_price_excluding_tax( $cart_item['data'], $args );
            }
            return wc_price( $price );
        }
        return $price_html;
    }
    
    // Change and set cart item custom calculated price
    add_action('woocommerce_before_calculate_totals', 'set_custom_cart_item_price');
    function set_custom_cart_item_price( $cart ) {
        if ( is_admin() && !defined('DOING_AJAX') )
            return;
    
        foreach ( $cart->get_cart() as $item_key => $item ) {
            if( isset($item['custom']['price']) ) {
                $item['data']->set_price($item['custom']['price']);
            }
        }
    }
    
    6). Save customer inputted data and display it in the order and emails
    // Save and display custom fields (custom order item metadata)
    add_action( 'woocommerce_checkout_create_order_line_item', 'save_order_item_custom_meta_data', 10, 4 );
    function save_order_item_custom_meta_data( $item, $cart_item_key, $values, $order ) {
        foreach ( get_custom_fields_data() as $key => $data ) {
            $options = get_custom_fields_data(true);
    
            if( isset($values['custom'][$key]) ) {
                $meta_value = $data['type'] === 'select' ? $options[$values['custom'][$key]] : $values['custom'][$key];
                $item->update_meta_data($key, $meta_value); 
            }
        }
    }
    
    // Add readable "meta key" label name replacement
    add_filter('woocommerce_order_item_display_meta_key', 'filter_wc_order_item_display_meta_key', 10, 3 );
    function filter_wc_order_item_display_meta_key( $display_key, $meta, $item ) {
        if( $item->get_type() === 'line_item' ) {
            foreach ( get_custom_fields_data() as $key => $values ) {
                if( $meta->key === $key ) {
                    $display_key = $values['name'];
                }
            }
        }
        return $display_key;
    }
    

    Code goes in functions.php file of your child theme (or in a plugin). Tested and works.