VAT Based on User Role

No VAT If User Role is Company

Often you need to charge VAT or Dutch BTW for customers only and not for your vendors or companies shopping at your website. So how do you make sure that users with the role business see no VAT and customers or private persons do see VAT?

User Role Check

To check for specific role and apply a rule to it you can use this code:

## The (repetitive) conditional function based on your "special" user roles 
function is_special_user_role(){
  if ( ! is_user_logged_in() ) return false;

  $user = wp_get_current_user(); // current user
  $user_roles = $user->roles; // It's always an array (as a user can have many roles)

  // HERE your defined user roles
  $defined_user_roles = array( 'company' );

  if ( count( array_intersect( $user_roles, $defined_user_roles ) ) > 0 ) 
  return true;
  else return false; // ==> Added update here
}

VAT Exemption Check

Now, to check if a user role is tax exempt and then turn off VAT/BTW for that user role we can add this action hook:

## Function that will check for user role and turn off VAT/tax for that role
add_action( 'template_redirect', 'wc_diff_rate_for_user', 1 );
function wc_diff_rate_for_user() {
    // check for the user role and set the customer object to have no VAT
    if ( is_special_user_role() )
        WC()->customer->set_is_vat_exempt( true ); // Updated HERE
}

NB If you just need a display excl. vat and do not really need the user to be tax exempt this is not the setup you are looking for.

VAT Price Suffix

Function that removes the price suffix based on role. So with it you could hide the including tax title for certain user roles:

Function that removes the price suffix (inc. Tax) from variable products based on role
 add_filter( 'woocommerce_get_price_suffix', 'wc_get_price_suffix_filter', 10, 2 );
 function wc_get_price_suffix_filter( $price_display_suffix, $item ) {
     // check for the user role return blank if it matches
     if ( is_special_user_role() )
         $price_display_suffix = '';
 return $price_display_suffix;
 }

NB https://stackoverflow.com/a/46678453/460885

NBB Great resource https://gist.github.com/jasperf/0568154ca7b4f8a8551b41d5d0a26b0e

Display Product Excluding VAT

So what if your products are all inclusive VAT? How do you then display the price for a certain user role excluding VAT? Well, you can add a price suffix at wp-admin/admin.php?page=wc-settings&tab=tax

Prijs Excl BTW {price_excluding_tax}

And then you work out a why to hide the price including VAT with CSS or otherwise. With CSS is is kind of hard as there are no unique classes for the basic price display nor for the discounted price display. For the discounted price this could work:

ins {

    visibility: hidden;
}

The same for del for the price itself, but would not remove the divs.

NB https://docs.woocommerce.com/document/setting-up-taxes-in-woocommerce/#section-10

Price Display Incl. & Excl. VAT at the same time

Another option would be code to load it separately in the product single template or with a shortcode. Here is a way to show the price with and without taxes and that at the same time.

function edit_price_display() {
     global $product;
     $price = $product->price;
     $price_incl_tax = $price + round($price * ( 21 / 100 ), 2);
     $price_incl_tax = number_format($price_incl_tax, 2, ",", ".");
     $price = number_format($price, 2, ",", ".");
     $display_price = '';     $display_price .= '€ ' . $price_incl_tax .' incl BTW';
     $display_price .= '
';
     $display_price .= '€ ' . $price .' excl BTW';
     $display_price .= '';
     echo $display_price;
 }
 add_filter('woocommerce_get_price_html', 'edit_price_display', 10, 2);

And to only show in on the product single page we could use

function edit_price_display($price, $instance) {
     global $product;
  if(is_singular('product')) {     
  $price = $product->price;
  $price_incl_tax = $price + round($price * ( 21 / 100 ), 2);
  $price_incl_tax = number_format($price_incl_tax, 2, ",", ".");     
  $price = number_format($price, 2, ",", ".");     
  $display_price = '<span class="price">';     
  $display_price .= '<span class="amount">€ ' . $price_incl_tax .'<small class="woocommerce-price-suffix"> incl BTW</small></span>';     
  $display_price .= '<br>';     $display_price .= '<span class="amount">€ ' . $price .'<small class="woocommerce-price-suffix"> excl BTW</small></span>';     
  $display_price .= '</span>';     
  echo $display_price; } else {     echo $price;     }
 }
 add_filter('woocommerce_get_price_html', 'edit_price_display', 10, 2);

NB Code may be malformatted. Check source for code with proper html tags!

NBB We could also then filter with another conditional by role. See further down for an example using just that.

Source Thomas Schmidt

WooCommerce Different Tax Rate by Role

As suggested by WooCommerce themselves we can use the woocommerce_product_tax_class filter to use a different tax rate for a user:

<?php
 /**
 Apply a different tax rate based on the user role.
 */
 function wc_diff_rate_for_user( $tax_class, $product ) {
 if ( is_user_logged_in() && current_user_can( 'administrator' ) ) {
     $tax_class = 'Zero Rate';
 }
 return $tax_class;
 }
 add_filter( 'woocommerce_product_tax_class', 'wc_diff_rate_for_user', 1, 2 ); 

The administrator role used here as you can see and $tax_class is set to zero.

NB https://docs.woocommerce.com/document/setting-up-taxes-in-woocommerce/#section-19

Product Page Display Excl. VAT by Role

If you would like to only show the price excluding VAT or a user role and still have the user pay VAT in the end you can use the following code

Custom Fields in WooCommerce

One way or another working with WooCommerce you wind up needing custom fields and preferably Advanced Custom Fields or ACF fields. This tutorial will talk about (advanced) custom fields in WooCommerce and the many way these can be used. This might wind up being a long post and lots or resources will be used and referred to so bear with me.

Table of Contents

Basic Single Product Page Display

What if you want to add an ACF field with your own code to the theme? Well that is just like adding any other advanced custom field type to any template really. As shown at the beginning you just need to focus on the correct product type and field type and then load the data you entered in the backend properly there.

To add an ACF field to the single product page in WooCommerce after the summary you can use a basic setup like this:

// Add ACF Info to Product display page 
add_action( 'woocommerce_after_single_product_summary', "ACF_product_content", 10 ); 
function ACF_product_content(){      
echo '<h2> ACF Content </h2>';      
if (function_exists('the_field')){     
echo '<p>Woohoo, the_field function exists! </p>';     
the_field('test_field');   
}    
}

It uses the WooCommerce woocommerce_after_single_product_summary hook to add information after the product description. And there are more hooks in WooCommerce for other locations of course.

NB The actual code to load the field in the backend is not mentioned here. This as that is quite easily done using the ACF GUI.

See https://support.advancedcustomfields.com/forums/topic/acf-with-woocommerce/

ACF & Custom Tabs

You can also use advanced custom fields to add custom tabs to WooCommerce. This is actually often needed. Many products tend to require extra information in a custom tab. Liquid Web wrote adding custom tabs with ACF. What they do is create a repeater field using a custom file added to the theme. Here is a forked snippet for the repeater field:

<?php
 if( function_exists('acf_add_local_field_group') ):
 acf_add_local_field_group(array (
     'key' => 'acf_product_options',
     'title' => 'Product Options',
     'fields' => array (
         array (
             'key' => 'acf_product_options_tabbedcontent_label',
             'label' => 'Tabbed Content',
             'name' => '',
             'type' => 'tab',
             'instructions' => '',
             'required' => 0,
             'conditional_logic' => 0,
             'wrapper' => array (
                 'width' => '',
                 'class' => '',
                 'id' => '',
             ),
             'placement' => 'top',
             'endpoint' => 0,
         ),
         array (
             'key' => 'acf_product_options_tabbedcontent_tabs',
             'label' => 'Tabs',
             'name' => 'tabs',
             'type' => 'repeater',
             'instructions' => '',
             'required' => 0,
             'conditional_logic' => 0,
             'wrapper' => array (
                 'width' => '',
                 'class' => '',
                 'id' => '',
             ),
             'min' => '',
             'max' => '',
             'layout' => 'row',
             'button_label' => 'Add Tab',
             'sub_fields' => array (
                 array (
                     'key' => 'acf_product_options_tabbedcontent_tab_title',
                     'label' => 'Tab Title',
                     'name' => 'tab_title',
                     'type' => 'text',
                     'instructions' => '',
                     'required' => 0,
                     'conditional_logic' => 0,
                     'wrapper' => array (
                         'width' => '',
                         'class' => '',
                         'id' => '',
                     ),
                     'default_value' => '',
                     'placeholder' => '',
                     'prepend' => '',
                     'append' => '',
                     'maxlength' => '',
                     'readonly' => 0,
                     'disabled' => 0,
                 ),
                 array (
                     'key' => 'acf_product_options_tabbedcontent_tab_content',
                     'label' => 'Tab Content',
                     'name' => 'tab_content',
                     'type' => 'wysiwyg',
                     'instructions' => '',
                     'required' => 0,
                     'conditional_logic' => 0,
                     'wrapper' => array (
                         'width' => '',
                         'class' => '',
                         'id' => '',
                     ),
                     'default_value' => '',
                     'tabs' => 'all',
                     'toolbar' => 'full',
                     'media_upload' => 1,
                 ),
             ),
         ),
     ),
     'location' => array (
         array (
             array (
                 'param' => 'post_type',
                 'operator' => '==',
                 'value' => 'product',
             ),
         ),
     ),
     'menu_order' => 0,
     'position' => 'normal',
     'style' => 'default',
     'label_placement' => 'top',
     'instruction_placement' => 'label',
     'hide_on_screen' => '',
 ));
 endif;

Then they load the custom tab with another function in functions.php. This code adds the extra WooCommerce product tab and then loads the repeater field inside the tab. Here is a forked snippet for this WooCommerce product tab addition.

<?php
function hwid_load_custom_tab( $tab_key, $tab_info ) {
	echo apply_filters( 'the_content', $tab_info['tabContent'] );
}

function hwid_add_content_tabs( $tabs ) {

	global $post;

	$custom_tabs = get_field( 'tabs', $post->ID );

	foreach( $custom_tabs as $index => $tab ) {
		$tabs['customTab-' . $index] = array(
			'title' => $tab['tab_title'],
			'priority' => 20 + $index,
			'tabContent' => $tab['tab_content'],
			'callback' => 'hwid_load_custom_tab'
		);
	}

	return $tabs;
}

add_filter( 'woocommerce_product_tabs', 'hwid_add_content_tabs' );

NB Snippets made by AJ Morrris https://gist.github.com/ajmorris for Liquid Web

Booster also allows you to add custom tabs. See https://booster.io/features/woocommerce-custom-product-tabs/ . You can choose to add tabs for all product pages or for specific ones.

ACF WooCommerce Order Form

If you would like to display additional information about your products in checkout / at the order form you can read about at WP Major. Major focusses on getting ACF fields in the order form and they use the WooCommerce Product Table plugin. But it does explain how to create them for WooCommerce products so that helps

Remember, for this tutorial we’re mainly focused on taking that custom field data and getting it into a WooCommerce order form ..

Divi & Advanced Custom Fields

The Elegant Themes Divi theme as an ACF module these days. It does however not work with all the custom field types yet. It just works with single fields, tables and repeater fields.

However, since the end of 2018 Divi Builder has dynamic content:

Not only does Divi support that use of standard dynamic WordPress content, it also supports the use of custom field data. Whether you have created your own custom fields, or registered a new custom field with a plugin like Advanced Custom Fields, that dynamic data can now be used within the Divi Builder and connected to any module content area.

Advanced Custom Fields to WooCommerce Attributes

Sometimes you need to add a custom field to a product attribute.

A third and important way to group products is to use attributes. There are two uses of this data type that are relevant for WooCommerce: WooCommerce widgets and variable products

Jordan Smith came up with some nice code for that. Snippet forked and added below. This code you can add to your child theme or basic theme’s functions.php. It ads an ACF custom rule type, rule values and then ads it to product attributes.

<?php 
 // Adds a custom rule type.
 add_filter( 'acf/location/rule_types', function( $choices ){
     $choices[ __("Other",'acf') ]['wc_prod_attr'] = 'WC Product Attribute';
     return $choices;
 } );
 // Adds custom rule values.
 add_filter( 'acf/location/rule_values/wc_prod_attr', function( $choices ){
     foreach ( wc_get_attribute_taxonomies() as $attr ) {
         $pa_name = wc_attribute_taxonomy_name( $attr->attribute_name );
         $choices[ $pa_name ] = $attr->attribute_label;
     }
     return $choices;
 } );
 // Matching the custom rule.
 add_filter( 'acf/location/rule_match/wc_prod_attr', function( $match, $rule, $options ){
     if ( isset( $options['taxonomy'] ) ) {
         if ( '==' === $rule['operator'] ) {
             $match = $rule['value'] === $options['taxonomy'];
         } elseif ( '!=' === $rule['operator'] ) {
             $match = $rule['value'] !== $options['taxonomy'];
         }
     }
     return $match;
 }, 10, 3 );

ACF Field WooCommerce Category

Adding an advanced custom field to a WooCommerce category is similar in ways to adding one to an attribute:

// step 1 add a location rule type
    add_filter('acf/location/rule_types', 'acf_wc_product_type_rule_type');
    function acf_wc_product_type_rule_type($choices) {
      // first add the "Product" Category if it does not exist
      // this will be a place to put all custom rules assocaited with woocommerce
      // the reason for checking to see if it exists or not first
      // is just in case another custom rule is added
      if (!isset($choices['Product'])) {
        $choices['Product'] = array();
      }
      // now add the 'Category' rule to it
      if (!isset($choices['Product']['product_cat'])) {
        // product_cat is the taxonomy name for woocommerce products
        $choices['Product']['product_cat_term'] = 'Product Category Term';
      }
      return $choices;
    }
    
    // step 2 skip custom rule operators, not needed
    
    
    // step 3 add custom rule values
    add_filter('acf/location/rule_values/product_cat_term', 'acf_wc_product_type_rule_values');
    function acf_wc_product_type_rule_values($choices) {
      // basically we need to get an list of all product categories
      // and put the into an array for choices
      $args = array(
        'taxonomy' => 'product_cat',
        'hide_empty' => false
      );
      $terms = get_terms($args);
      foreach ($terms as $term) {
        $choices[$term->term_id] = $term->name;
      }
      return $choices;
    }
    
    // step 4, rule match
    add_filter('acf/location/rule_match/product_cat_term', 'acf_wc_product_type_rule_match', 10, 3);
    function acf_wc_product_type_rule_match($match, $rule, $options) {
      if (!isset($_GET['tag_ID'])) {
        // tag id is not set
        return $match;
      }
      if ($rule['operator'] == '==') {
        $match = ($rule['value'] == $_GET['tag_ID']);
      } else {
        $match = !($rule['value'] == $_GET['tag_ID']);
      }
      return $match;
    }

NB Code course John Huebner ACF

ACF Below Product Image

To display an advanced custom field below the product image can be done with relative ease. We found a snippet at Business Bloomer by Rodolfo Melogi as a nice example:

/**
* @snippet       Display Advanced Custom Fields @ Single Product - WooCommerce
* @how-to        Get CustomizeWoo.com FREE
* @sourcecode    https://businessbloomer.com/?p=22015
* @author        Rodolfo Melogli
* @compatible    WooCommerce 3.5.7
* @donate $9     https://businessbloomer.com/bloomer-armada/
*/
 
add_action( 'woocommerce_product_thumbnails', 'bbloomer_display_acf_field_under_images', 30 );
 
function bbloomer_display_acf_field_under_images() {
echo '<b>Trade Price:</b> ' . get_field('trade');
// Note: 'trade' is the slug of the ACF
}

It uses the woocommerce_product_thumbnails hook to add elements below the product thumbnails.

Product Page & Product Table Plugin

To display an ACF field on a product page you can also use ACF to create the fields and the WooCommerce Product Table Plugin. You can read all about it at Barn2 . You can use this plugin to show a lot of product data on top of standard ones.

  • Product image, name, price
  • Short or long description
  • Categories and tags
  • Attributes and variations
  • Star rating from reviews
  • Embedded audio and video

Product Page Admin Only Note

Sometimes the end user wants to add notes in the backend for his use only. A field that is only shown in the admin area. In the admin area for a specific product. How would you go about this? Well, you do of course not have to print / display fields in the frontend so if you do not load them there you can just create them using the advanced field interface. Another way, if somehow your theme autoloads all ACF fields, is to hide them frontend with CSS.

I would use a text area or a field as ACF field type if the note is very short. That would suffice to add a note in the backend.

WC Register Form ACF Fields

To add fields to the WooCommerce Account registration form you could use https://wordpress.org/plugins/acf-woocommerce-account-fields/ . For us it did not work well though. Seems to be outdated somewhat.

You can also add custom ones using your own code. See https://stackoverflow.com/a/49054519/460885 where the awesome LoicTheAztec use the WooCommerce hook woocommerce_register_form to add fields and where he does validation as well.

And to save the form custom fields as well we use the following below as shown in SO thread:

// To save WooCommerce registration form custom fields.
 add_action( 'woocommerce_created_customer', 'wc_save_registration_form_fields' );
 function wc_save_registration_form_fields( $customer_id ) {
     if ( isset($_POST['role']) ) {
         if( $_POST['role'] == 'reseller' ){
             $user = new WP_User($customer_id);
             $user->set_role('reseller');
         }
     }
 }

NB This is similar to https://wpvilla.in/assign-specific-role-on-registration/

WooCommerce Checkout Fields

Here is another example showing a field at the end of the WooCommerce registration form’s notes or on checkout:

/**
 Add custom fields to user / checkout
 */
 add_action( 'woocommerce_after_order_notes', 'my_custom_checkout_field' ); 
 function my_custom_checkout_field( $checkout ) {
 echo '<div id="bv_custom_checkout_field"><h2>Measurements</h2>'; /* Weight */ woocommerce_form_field( 'weight_customer', array(     'type'          => 'text',     'class'         => array('my-class form-row-wide'),     'label'         => __('Your weight'),     'placeholder'   => __('Your weight'), ), get_user_meta(  get_current_user_id(),'weight_customer' , true  ) ); echo '</div>';
 }
 /**
 Verification 
 */
 add_action('woocommerce_checkout_process', 'my_custom_checkout_field_process'); 
 function my_custom_checkout_field_process() {
     // Check 
     if ( ! $_POST['weight_customer'] )
         wc_add_notice( __( 'Do not forget weight.' ), 'error' );
 }
 Update field
 */
 add_action( 'woocommerce_checkout_update_order_meta', 'my_custom_checkout_field_update_order_meta' ); 
 function my_custom_checkout_field_update_order_meta( $order_id ) {
     if ( ! empty( $_POST['weight_customer'] ) ) {
         update_user_meta( get_current_user_id(), 'weight_customer', sanitize_text_field( $_POST['weight_customer'], '' ));
     }
 }

Here woocommerce_after_order_notes and woocommerce_checkout_update_order_meta are the hooks used. So data is added after the order notes and the woocommerce checkout update order meta hook is used to store the extra fields. For other location see a good visual guide at https://businessbloomer.com/woocommerce-visual-hook-guide-checkout-page/ .

Snippet source Max @ ACF Forum

Custom Checkout Fields

If you need to make tweak to fields at /checkout you can use stuff discussed at https://docs.woocommerce.com/document/tutorial-customising-checkout-fields-using-actions-and-filters/ . To for example change the order comment placeholder you use:

// Hook in
add_filter( 'woocommerce_checkout_fields' , 'custom_override_checkout_fields' );

// Our hooked in function - $fields is passed via the filter!
function custom_override_checkout_fields( $fields ) {
     $fields['order']['order_comments']['placeholder'] = 'My new placeholder';
     return $fields;
}

Now if you want to add more billing fields and or add them before or after exisitng you can use something like

 /**
 * Simple checkout field addition example.
 * 
 * @param  array $fields List of existing billing fields.
 * @return array         List of modified billing fields.
 */
function jeroensormani_add_checkout_fields( $fields ) {
  $fields['billing_FIELD_ID'] = array(
      'label'        => __( 'FIELD LABEL' ),
      'type'        => 'text',
      'class'        => array( 'form-row-wide' ),
      'priority'     => 35,
      'required'     => true,
  );

  return $fields;
}
add_filter( 'woocommerce_billing_fields', 'jeroensormani_add_checkout_fields' );

If you want to position them you need to know the priorities of the current ones:

  • First name – 10
  • Last name – 20
  • Company name – 30
  • Country – 40
  • Street address – 50
  • Apartment, suite, unit etc. (optional) – 60
  • Town / City – 70
  • State – 80
  • Postcode / ZIP – 90
  • Phone – 100
  • Email – 110

Also, do not forget validation. Here a basic one for checking if a real number is entered. Good for a VAT number for example

**
 * Add custom field validation for BTW or KvK Number
 */
function js_custom_checkout_field_validation( $data, $errors ) {
  foreach ( WC()->checkout()->get_checkout_fields() as $fieldset_key => $fieldset ) {
      foreach ( $fieldset as $key => $field ) {

          if ( isset( $field['validate'] ) && in_array( 'btw-number', $field['validate'] ) ) {
              if ( ! empty( $data[ $key ] ) && ! preg_match( '/[a-z0-9]{10}/', $data[ $key ] ) ) {
                  $errors->add( 'validation', 'Looks like your club number is invalid.' );
              }
          }
      }
  }

}
add_action( 'woocommerce_after_checkout_validation', 'js_custom_checkout_field_validation', 10, 2 );

NB source Jeroen Sormani

NBB Checkout Manager Plugin is very useful too although paid if in need of conditionals

NBBB There is Also WooCommerce Booster https://booster.io/features/woocommerce-checkout-customization/ , but no conditionals offered.

My Account / WooCommerce Registration

Like any WooCommerce page there are multiple hooks to adjust or add things to /my-account. See https://businessbloomer.com/woocommerce-visual-hook-guide-account-pages/.

To adjust or add more fields things are more complicated like for the checkout, but this is also possible. Business Bloomer has a snippet to add first and last name for example:

/**
 @snippet       Add First & Last Name to My Account Register Form - WooCommerce
 @how-to        Get CustomizeWoo.com FREE
 @sourcecode    https://businessbloomer.com/?p=21974
 @author        Rodolfo Melogli
 @credits       Claudio SM Web
 @compatible    WC 3.5.2
 @donate $9     https://businessbloomer.com/bloomer-armada/
 */ 
 ///////////////////////////////
 // 1. ADD FIELDS
 add_action( 'woocommerce_register_form_start', 'bbloomer_add_name_woo_account_registration' );
 function bbloomer_add_name_woo_account_registration() {
     ?>
 <p class="form-row form-row-first"> <label for="reg_billing_first_name"><?php _e( 'First name', 'woocommerce' ); ?> <span class="required">*</span></label> <input type="text" class="input-text" name="billing_first_name" id="reg_billing_first_name" value="<?php if ( ! empty( $_POST['billing_first_name'] ) ) esc_attr_e( $_POST['billing_first_name'] ); ?>" /> </p> <p class="form-row form-row-last"> <label for="reg_billing_last_name"><?php _e( 'Last name', 'woocommerce' ); ?> <span class="required">*</span></label> <input type="text" class="input-text" name="billing_last_name" id="reg_billing_last_name" value="<?php if ( ! empty( $_POST['billing_last_name'] ) ) esc_attr_e( $_POST['billing_last_name'] ); ?>" /> </p> <div class="clear"></div> <?php
 }
 ///////////////////////////////
 // 2. VALIDATE FIELDS
 add_filter( 'woocommerce_registration_errors', 'bbloomer_validate_name_fields', 10, 3 );
 function bbloomer_validate_name_fields( $errors, $username, $email ) {
     if ( isset( $_POST['billing_first_name'] ) && empty( $_POST['billing_first_name'] ) ) {
         $errors->add( 'billing_first_name_error', ( 'Error: First name is required!', 'woocommerce' ) );
     }
     if ( isset( $_POST['billing_last_name'] ) && empty( $_POST['billing_last_name'] ) ) {
         $errors->add( 'billing_last_name_error', ( 'Error: Last name is required!.', 'woocommerce' ) );
     }
     return $errors;
 }
 ///////////////////////////////
 // 3. SAVE FIELDS
 add_action( 'woocommerce_created_customer', 'bbloomer_save_name_fields' );
 function bbloomer_save_name_fields( $customer_id ) {
     if ( isset( $_POST['billing_first_name'] ) ) {
         update_user_meta( $customer_id, 'billing_first_name', sanitize_text_field( $_POST['billing_first_name'] ) );
         update_user_meta( $customer_id, 'first_name', sanitize_text_field($_POST['billing_first_name']) );
     }
     if ( isset( $_POST['billing_last_name'] ) ) {
         update_user_meta( $customer_id, 'billing_last_name', sanitize_text_field( $_POST['billing_last_name'] ) );
         update_user_meta( $customer_id, 'last_name', sanitize_text_field($_POST['billing_last_name']) );
     }
 }

NB Also see https://github.com/woocommerce/woocommerce/issues/7667 and https://www.cloudways.com/blog/add-woocommerce-registration-form-fields/

To work with conditionals things get more complicated and of course radio buttons or select boxes are also a bit tougher still. You could use jQuery like this for example showing a field when radio button toggled:

$('.customer-type-radio input[type="radio"]').on('click', function () {
         $('.company-field').slideToggle();
 });

For code above by Business Bloomer you would need to focus on other html fields of course, but often after making choices like checkboxes or radio buttons you would like to toggle other fields.

WC Booster has some basic options including adding a user role select box https://booster.io/features/woocommerce-my-account/ . Does not seem to offer custom fields though.

There is also Yithemes Yith Woocommerce Customiz My Account Page . Does cost another €54.99. But that is really for the my-account overview for logged in users. For the WooCommerce Registration Form https://woocommerce.com/products/custom-user-registration-fields-for-woocommerce/ is recommended. It costs $49 USD.

Shop Page ACF

If you would like to add a new ACF location to do stuff on the WooCommerce Shop Page you can use

add_filter( 'acf/location/rule_values/page_type', function ( $choices ) {
  $choices['woo_shop_page'] = 'WooCommerce Shop Page';
  return $choices;
});
add_filter( 'acf/location/rule_match/page_type', function ( $match, $rule, $options ) {
  if ( $rule['value'] == 'woo_shop_page' && isset( $options['post_id'] ) ){
     if ( $rule['operator'] == '==' ){
       $match = ( $options['post_id'] == wc_get_page_id( 'shop' ) );
      }
     if ( $rule['operator'] == '!=' ){
       $match = ( $options['post_id'] != wc_get_page_id( 'shop' ) );
     }
  }
  return $match;
}, 10, 3 );

Redirect Empty Cart to Home and Checkout to Cart

So we needed an option to redirect empty cart to home. This besides the cart skip redirects we already had. I check out a few solutions with our current code base setup.

One by Etzel Storfer Web Development :

// ===========================================================================
//  Redirect Empty Checkout to Home 
// ===========================================================================

add_action('template_redirect', 'redirection_function');

function redirection_function(){
    global $woocommerce;

    if( is_checkout() && 0 == sprintf(_n('%d', '%d', $woocommerce->cart->cart_contents_count, 'woothemes'), $woocommerce->cart->cart_contents_count) && !isset($_GET['key']) ) {
        wp_redirect( home_url() ); 
        exit;
    }
}

which did not work as checkout still went to cart and cart back to checkout. Another option which is quite similar:

add_action("template_redirect", 'redirection_function');
function redirection_function(){
   global $woocommerce;
   if( is_cart() && WC()->cart->cart_contents_count == 0){
       wp_safe_redirect( get_permalink( woocommerce_get_page_id( 'shop' ) ) );
   }
}

had the same result. Redirects kept on going as it did not overcome current ones set up.

Enhancing Existing Redirect

This was because an existing redirect to checkout when cart was hit. The solutions were not bad at all. They just did not work in our setup. So I tweaked the code for to make an exception when an empty cart was there. This by adding

if( is_cart() && WC()->cart->cart_contents_count == 0) {
        wp_redirect( home_url() ); 
    } 
    
    else {

to an existing method that made it this in the end:

// Global redirect to check out when hitting cart page unless cart empty
add_action( 'template_redirect', 'redirect_to_checkout_if_cart' );
function redirect_to_checkout_if_cart() {

    if ( !is_cart() ) return;
    global $woocommerce;

    // Getting the checkout for the current language.
    // Checking whether icl_object_id() exists as a
    // function since its not a WordPress or theme function.

    if( is_cart() && WC()->cart->cart_contents_count == 0) {
        wp_redirect( home_url() ); 
    } 
    
    else {
        $current_language_checkout_url = ! function_exists( 'icl_object_id' ) ? get_permalink(
            icl_object_id(
                url_to_postid(
                    $woocommerce->cart->get_checkout_url()
                ),
                'post',
                false,
                ICL_LANGUAGE_CODE
            )
        ) : $woocommerce->cart->get_checkout_url();

        wp_redirect( $current_language_checkout_url, '301' );

        exit;
    }
}

So this means you could very well use one of the two earlier mentioned functions just fine. I just needed it integrated in an earlier set up method.

GDPR WooCommerce Checklist

To comply with the GDPR or General Data Protection Regulation which will come into force this 25th of May 2018 we have come up with a GDPR WooCommerce Checklist. One you can use to go through your website and or business setup to decide what you need to do. We will start with a general introduction and then move on to the checklist

Background

The GDPR has been in the making for a long time already. It was adapted in the EU parliament on April 2016 as a matter of fact. It was set up to protect the privacy of EU citizens and guarantee a proper way of dealing with personal data. And as stated above it will come into effect May 26th 2018. So you must have heard about it and wondered about it. And perhaps you have sorted things already. Still good to go through this article a bit too.

Parties Concerned

It applies to all business within the EU and to all businesses doing business with customers or partners within the EU that collect personal data from customers within the EU to be precise. What is personal data? Here from the horse’s mouth

Any information related to a natural person or ‘Data Subject’, that can be used to directly or indirectly identify the person. It can be anything from a name, a photo, an email address, bank details, posts on social networking websites, medical information, or a computer IP address.

So this means this new GDPR setup will be applicable to many businesses throughout the world. Many of us work with clients in the EU and collect data to identify clients. Especially if you run ecommerce like many of our customers who run WooCommerce. So therefore probably need to play ball here.

Non Compliance Fines

According to the source of the regulation, the EU, you might not want to not participate because:

Organizations can be fined up to 4% of annual global turnover for breaching GDPR or €20 Million. This is the maximum fine that can be imposed for the most serious infringements e.g.not having sufficient customer consent to process data or violating the core of Privacy by Design concepts. There is a tiered approach to fines e.g. a company can be fined 2% for not having their records in order (article 28), …….

Each member state will set up its own supervisory authority to make sure these regulations are followed and will penalize those who do not. See also CodeinWP’s article on this.

FAQ Page source

Site Checklist

Here is a short summary or checklist first based on a WooCommerce article from December last year. One with some tweaks. And then some more details bits and pieces.

  • Tell the user who you are, why you collect the data, for how long, and who receives it.
  • Get a clear consent [when required] before collecting any data.
  • No race, religion or sexual preference data can be stored
  • No checkboxes asking for personal information can be checked in advance
  • Let users access their data, and take it with them.
  • Let users delete their data.
  • Let users know if data breaches occur.

Privacy Policy and Terms & Conditions

You will need a privacy policy that clearly indicates what data you collect and how.  The WooCommerce Point of Sale Plugin has a solid one. And decent terms and conditions as well. Do go through them and adjust them properly, preferably with your lawyer before using them on your own site though. Terms and conditions are important here as you are required to have certain security measures in place and these need to be reflected in the Terms & Conditions.

Useful Plugins

Based on the WordPress Plugins repo GDPR Tag I found a couple of useful plugins:

Delete Me Plugin

We are not using this one yet as removing someone is a big step. We will make sure clients will be deleted when they want to promptly and will remove inactive customers after an x amount of time. Like with all data, it should not be stored without need and so needs to be removed when an account is inactive for a long time or is requested to be removed. However, we are following the online blogging sphere for more information on this so may update details on the right to be forgotten.

WP GDPR Compliance Plugin

WP GDPR Compliance is an amazing plugin that will either set up the needed checkboxes and or texts when possible for (order) forms like Gravity Forms and …. WooCommerce order forms or will give you tips and or warnings how to do things. It will for example tell you add a consent box on order forms using their checklist:

Make sure you add a checkbox specifically asking the user of the form if they consent to you storing and using their personal information to ship the order. This cannot be the same checkbox as the Privacy Policy checkbox you should already have in place. The checkbox must be unchecked by default. Also mention if you will send or share the data with any 3rd-parties and which.

And you can activate this for WooCommerce:

WooCommerce GDPR Checkbox

This can be done for Gravity Forms and Contact Form 7 too. It also tells you to turn off Jetpack comments if you do. This as they do not seem to have an option to opt-in and understand they share some personal info commenting – See WP Tavern article on this. Once turned off you can add a consent checkbox with the plugin:

Comment Form GDPR Compliance

Cookie Bot vs Responsive Cookie Consent vs Cookie Consent

GDPR Compliance plugin does not help with the cookie banner most of you use already. A banner to ask for consent to use cookies. For that you can try Cookiebot. You will have to sign up though . If you do not want that you can use responsive cookie consent or Cookie Consent one. I prefer the latter one. No need to sign up for something extra and it is well maintained and used a lot. It also sets up a cookie explanation page.

Responsive Cookie Consent

Google Analytics and GDPR

You need to update your site’s Privacy Policy to cover all personal information that is being collected through your site. You also need to adjust your Google Analytics settings to comply with these new rules:

If you have a business established in the territory of a member state of the European Economic Area or Switzerland or you are otherwise subject to the territorial scope of the General Data Protection Regulation (GDPR), and if you have entered into a direct customer contract with Google to use Google Analytics, then you are eligible to accept the Google Ads Data Processing Terms. Learn more

You can also setup the period of time you store the data. See some details on howto at seroundtable.com .

Google Analytics Data Retention

 

26 months is set up automatically if you agreed with the new GDPR rules:

 

Analytics Default retention

Jef makes a good point in the comments at SE Roundtable:

The point is… (Technically) With GDPR, you’re supposed to only keep user data you’re using. If you’re storing data about users, you have to have a good reason to keep it. Just having it in Google Analytics for “reporting purposes” isn’t a good enough reason either.

Obviously, they’re making these tools to hopefully take the heat off themselves, and thus reduce the risk across the board for their customers.

I think most medium sized companies can export the data they care about, anonymize it and/or aggregate it to the levels they need, and let Google handle the regular dumping of data they don’t need.

So you should keep data only if you have a good reason to use them and once that is done you should remove the data. Also you need to ask for consent so you need the cookie plugin to do this for you.

 

Article still developing ..

Custom Customer Processing Order Message

Sometimes you need to add a custom Customer Processing Order message to your email template for your clients. This so you send them additional instructions. In WooCommerce the basic email editing options are rather unlimited. Not to worry though. There are options.

Initial Trial

You can add the following action hook to the functions.php file of your theme or child theme:

add_action( 'woocommerce_email_before_order_table','add_order_email_instructions', 10, 4 );
function add_order_email_instructions( $order, $sent_to_admin, $plain_text, $email ) {
    if ( 'customer_processing_order' == $this->id && ! $sent_to_admin ) {
        echo 'Your login details are on their way. (They may take up to 10 minutes to arrive in your inbox.';
    } 
}

Snippet based on Helga the Viking’s code at SO.

Details

It (the code snippet / action hook) tells WooCommerce to add an extra Custom Customer Processing Order Message to the customer processing order email.You could just add the message to all outgoing emails by WooCommerce and drop the if statement. Or you could go for another email template where you would like to add code to. It is all possible really.

NB The actual action is added to email-order-details.php as well as some other payment gateway files.

Object Snag

With the mentioned action you will however get this error:

2018-02-19T22:31:46+00:00 CRITICAL Uncaught Error: Using $this when not in object context in /srv/www/domain.com/releases/20180218085005/web/app/themes/jupiter-child/functions.php:64
Stack trace:
#0 /srv/www/domain.com/releases/20180218085005/web/wp/wp-includes/class-wp-hook.php(286): add_order_email_instructions(Object(WC_Order), true, false, Object(WC_Email_New_Order))
#1 /srv/www/domain.com/releases/20180218085005/web/wp/wp-includes/class-wp-hook.php(310): WP_Hook->apply_filters('', Array)
#2 /srv/www/domain.com/releases/20180218085005/web/wp/wp-includes/plugin.php(453): WP_Hook->do_action(Array)
#3 /srv/www/domain.com/releases/20180218085005/web/app/plugins/woocommerce/templates/emails/email-order-details.php(24): do_action('woocommerce_ema...', Object(WC_Order), true, false, Object(WC_Email_New_Order))
#4 /srv/www/domain.com/releases/20180218085005/web/app/plugins/woocommerce/includes/wc-core-functions.php(211): include('/srv/www/publiq...')
#5 /srv/www/domain.com/releases/20180218085005/web/app/plugins/woocommerce/includes/

This is because  you can’t use $this outside of the class definition. But with the $email object added this should not occur. And that is available since WooCommerce 2.5+. You just need it as a parameter too. Well, as you will see things changed somewhat in WC 3+ and another object->property is needed.

WooCommerce 3+ Updated Trial

So with WooCommerce 3+ I then thought I needed:

add_action( 'woocommerce_email_before_order_table','add_order_email_instructions', 10, 4 );
function add_order_email_instructions( $order, $sent_to_admin, $plain_text, $email ) {
if ( 'customer_processing_order' == $order->get_id() && ! $sent_to_admin ) {
echo 'Your login details are on their way. (They may take up to 10 minutes to arrive in your inbox.';
}
}

with $order->get_id() as I otherwise still had the same error. But the if statement did not do the trick properly.

Working Solution

The final best option is with changed if statement:

add_action( 'woocommerce_email_before_order_table','add_order_email_instructions', 10, 4 );
function add_order_email_instructions( $order, $sent_to_admin, $plain_text, $email ) {
if ( $email->id == 'customer_processing_order' ) {
echo 'Your login details are on their way. (They may take up to 10 minutes to arrive in your inbox.';
}
}

So $email->id == ‘customer_processing_order works. Result:

Order being processed custom message

WooCommerce 3.0+ Single Product Image Zoom & Link Removal

Sometimes you want to do a Single Product Image Zoom & Link Removal. You just do not need the zoom effect nor do you want the image to link to the original size image. Well there are WooCommerce filters for that

Product Image Zoom Removal

To remove the product thumbnail zoom effect you can use the following filter:

function custom_single_product_image_html( $html, $post_id ) {
$post_thumbnail_id = get_post_thumbnail_id( $post_id );
return get_the_post_thumbnail( $post_thumbnail_id, apply_filters( 'single_product_large_thumbnail_size', 'shop_single' ) );
}
add_filter('woocommerce_single_product_image_thumbnail_html', 'custom_single_product_image_html', 10, 2);

It basically uses the woocommerce_single_product_image_thumbnail_html filter (see file at Github)to manipulate the product thumbnail. Instead of returning the html with zoom options it just returns:

get_the_post_thumbnail( $post_thumbnail_id, apply_filters( 'single_product_large_thumbnail_size', 'shop_single' ) );

which is just the shop single large product thumbnail.

Product Image Link Removal

The product link is removed with this same function as well as you have now completely replaced the html with just the thumbnail instead of a thumbnail that links to the original and has zoom capabilities. So we are now all done here!

Bonus: Quantity Button Removal

If you would like to remove the quantity button as you only sell your products individually and you do not allow more than one of each you can either lock this per product or use this filter:

/**
 * @desc Remove in all product type
 */
function wc_remove_all_quantity_fields( $return, $product ) {
    return true;
}
add_filter( 'woocommerce_is_sold_individually', 'wc_remove_all_quantity_fields', 10, 2 );

To do it per product

  • Edit your product.
  • Click “Inventory”.
  • Check the box that says “Sold Individually”

Disable a WooCommerce Payment Option

If you need to disable a WooCommerce Payment option because you realize you no longer needed or you need to turn of a default you mind have an initial tough time finding it. Once you know it is really easy though.

WooCommerce Checkout Payment Option

Just go to WooCommerce Settings > Checkout. This is where you will find all the possible checkout options such as PayPal, Stripe and so on.

WooCommerce Checkout Options

Just go ahead and go to the tab for the payment gateway you do not want to use anymore.

Payment Option Tab

So go to the payment option you would like to remove / disable. Then uncheck this option like we have done here below for the check payments. You can see there is no checkmark for the enable check payments. So this option is NOT in use for this WooCommerce store.

Uncheck WooCommerce Check Payments

Saving Changes

Do not forget to save your settings and or empty cache when need be. Otherwise you changes will not be stored nor displayed. To make sure all worked well simulate a purchase and check that you do not see this payment option on the checkout anymore.

Problem with your Stripe Webhook

Sometimes you will get an email from Stripe telling you there is a problem with your Stripe Webhook. For some reason it fails. Either the web hook cannot be found somehow or it cannot be loaded properly.

Webhook url

You can find the webhook to be used in your WooCommerce settings > Checkout > Stripe . It should be something like https://domain.com/?wc-api=wc_stripe . Copy it over into your Stripe Dashboard under webhooks should work just fine out of the box. This webhook can be used to:

  • Update a customer’s membership record in your database when a subscription payment succeeds
  • Email a customer when a subscription payment fails
  • Check out the Dashboard if you see that a dispute was filed
  • Make adjustments to an invoice when it’s created (but before it’s been paid)
  • Log an accounting entry when a transfer is paid

So really useful on giving feedback when payments fail. And that we had in our case. Wrote about that Stripe issue here.

Errors 404 – Webhook not found

We had the following error in our logs related to this

2018/01/24 21:56:44 [error] 16241#16241: *41904 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 213.165.184.36, server: domain, request: "POST /?wc-ajax=checkout HTTP/2.0", upstream: "fastcgi://unix:/var/run/php-fpm-wordpress.sock", host: "publiqly.com", referrer: "https://domain.com/checkout/"

and in the access log we found

54.187.205.235 - - [25/Jan/2018:06:29:14 +0000] "POST /wp/wp-admin/admin-ajax.php?action=stripe_webhook HTTP/1.1" 400 11 "-" "Stripe/1.0 (+https://stripe.com/docs/webhooks)" "-"

So somehow the webhook was not found and so no 2xx response was given. And as Stripe mentions this is one of the most common mistakes using webhooks. The wrong url is entered.

Issue & Solution

In our case we had entered the wrong webhook url  https://domain.com/wp/wp-admin/admin-ajax.php?action=stripe_webhook. It should have been https://domain.com/?wc-api=wc_stripe  as mentioned earlier. That caused it not to work and Stripe sending us an email with “Problem with your Stripe Webhook” in the title.

Stripe 402 Error – Payment Required

Setting up Stripe for a client on WooCommerce we ran into a Stripe 402 Error. So what does a 402 error mean and how can we solve this issue? Well Let’s get into the finer details shall we.

Stripe Environment

Keys used In our case this happened using a live publishable and private key. So this meant we had to use a real credit card and enter details properly. Sometimes people run test mode and forget to use a dummy credit card for testing or forget to turn on testing. So please do check what mode you are using and use the proper payment details.

Stripe 402 Code

When the payment does fail with a Stripe 402 error it means the payment went wrong.

Stripe 402 Error

I quote Stripe:

Not all errors map cleanly onto HTTP response codes, however. When a request is valid but does not complete successfully (e.g., a card is declined), we return a 402 error code. To understand why a card is declined, refer to the list of codes in the documentation.

So as there are multiple problems that can cause this error you need to find out what the error was you had. And for that you need to get the possible decline code.

Source of the Error

We can see in the page source  what the issue is once the payment is done. To view the source just after a failure use command+alt+i on Chrome on your Mac to open the inspector for this. Sometimes it is just the wrong credit card details entered. Again, check the decline code / message.

Also, when you have error message recording turned on you can check your WooCommerce system logs. These are located at /srv/www/domain.com/shared/uploads/wc-logs  for those using Trellis. For others at wp-content/uploads/wc-logs. In our case we found:

01-24-2018 @ 13:54:48 - ====Start Log====
Error: stdClass Object
(
 [error] => stdClass Object
 (
 [message] => Your card's security code is incorrect.
 [type] => card_error
 [param] => cvc
 [code] => incorrect_cvc
 [charge] => ch_111111111111111111111
 )

)

So the client did not enter the proper security code or cvc. In other cases it could be an incorrect card number or something else related tot the payment.

Solution

Well, as soon as you have the decline code or error found in the source of the page or in your WooCommerce error logs or other payment logs you will be able to add details properly. As stated we had an issue with the cvc code. Once the client entered the proper security code the payment went through.