Screen Shot 2014-03-20 at 8.06.42 AM

Change product category order in WooCommerce

The default setting for WooCommerce product category listing is to order them however you entered them.

I’ve never found this to be useful. Every client I’ve had wants categories to be ordered alphabetically. Today I’ll show you how.

woocommerce_product_subcategories_args filter

WooCommerce provides a nice filter to change the product category archive parameters which you can find with the rest of the documented filters.

Here’s an excerpt of the filter out of the main function in WooCommerce that displays categories. You can see the whole thing by opening wc-template-functions.php found inside the WooCommerce plugin includes folder.

        // NOTE: using child_of instead of parent - this is not ideal but due to a WP bug ( http://core.trac.wordpress.org/ticket/15626 ) pad_counts won't work
        $args = apply_filters( 'woocommerce_product_subcategories_args', array(
            'child_of'      => $parent_id,
            'menu_order'    => 'ASC',
            'hide_empty'    => 1,
            'hierarchical'  => 1,
            'taxonomy'      => 'product_cat',
            'pad_counts'    => 1
        ) );
 
        $product_categories     = get_categories( $args );

You can see here that the filter is allowing you to change the args that are sent to a standard WordPress get_categories call.

That means we can use any of the get_categories arguments here. So we’re going to change the orderby and order parameters.

/**
 * Changes the order of the product categories to be by slug ASC.
 * Essentially alphabetical.
 *
 * @since 1.0
 * @author WPTT, Curtis McHale
 */
function wptt_cat_order( $args ){
 
    $args['orderby'] = 'slug';
    $args['order'] = 'ASC';
    return $args;
 
} // wptt_cat_order
add_filter( 'woocommerce_product_subcategories_args', 'wptt_cat_order' );

Now you might be wondering why I’m using ‘slug’ instead of name. See if your category titles have fancy characters like & the alphabetical order on name gets thrown off. Slug is a normalized field, which means that it gets all the fancy text stripped out.

Setting up Stripe with WooCommerce

Today’s screencast will show you how to set up Stripe with WooCommerce since I’ve had a few questions about it.

Screen Shot 2014-02-01 at 9.19.18 PM

New Guide to deployment with Beanstalk

Quite a while ago I wrote about deployment with Beanstalk and that’s still how I do it.

Beanstalk just also released a guide to deploying WordPress for a slightly different take on the matter.

Screen Shot 2014-01-22 at 2.13.02 PM

Serializing and Saving a complete form with jQuery Form in WordPress

WordPress comes bundled with a great jQuery library called jQuery Form which makes saving forms via AJAX super easy.

Today I’m going to show you why you shouldn’t be using standard AJAX calls to save forms and how to use jQuery Form in WordPress.

You can download the plugin I use in the screencast from Github[plugin].

Screencast

Screen Shot 2014-01-22 at 1.19.02 PM

Easier Documentation Searching with Dash

Part of being a great developer is simply knowing how to search for your current issue. There is no way I could keep all the documention for WordPress, PHP, jQuery, Sass…in my head.

My typical workflow has been to simply Google for the term and then click the first link since it’s usually the WordPress Codex entry.

Wouldn’t it be nicer to be able to search across many languages right out of your editor?

How about StackExchange and Google results mixed right in?

Well you can.

It is called Dash

Dash is a Mac App that is free to trial and has a $19.99 in app purchase.

At it’s simplest level dash is just a dedicated application to search documentation than includes Google Results and StackExchange results.

Where it really gets powerful (and what convinced me to part with $19.99 in 2 minutes) is the integration with your editor.

I use Vim so for me I hop on to GitHub and download the dash Vim plugin. That gives me access to the :Dash command from within my editor.

:Dash wp_send_json will bring up all the json functions in WordPress with Google and Stackexchange right under.

:Dash esc_ wordpress will give me all the escaping functions in WordPress (since I limited it) along with the Google and StackExchange results under it. If I wanted anything with esc_ in it then I’d omit the wordpress exclusion and I’d see PHP options as well.

dash-documentation-results

Installing documention is a simple as clicking download from the Dash preferences.

dash-preferences

Dash is awesome, go get it.

Screencast

Screen Shot 2013-12-19 at 2.50.07 PM

Add a custom folder on Plugin activation

Recently I needed to do a custom export of WooCommerce orders so we could send them to an external service. The external service could already read orders formatted in an XML file, so all I needed to do was get the WooCommerce orders exported in the XML format and put in a directory that could be accessed by the external service.

The first part of that was creating a new place to store files in WooCommerce which meant I needed to create a new folder when my plugin was activated.

Today we’ll take a look at how to add a folder on plugin activation.

Plugin Skeleton

The first thing we need is a plugin skeleton.

<?php
/*
Plugin Name: WPTT mkdir
Plugin URI: http://wpthemetutorial.com
Description: Demo to make a directory on plugin activation
Version: 1.0
Author: WPTT, Curtis McHale
Author URI: http://wpthemetutorial.com
License: GPLv2 or later
*/
 
/*
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
 
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
 
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/
 
class WPTT_Mkdir{
 
    function __construct(){
 
        add_action( 'admin_notices', array( $this, 'check_required_plugins' ) );
 
        // Register hooks that are fired when the plugin is activated, deactivated, and uninstalled, respectively.
        register_activation_hook( __FILE__, array( $this, 'activate' ) );
        register_deactivation_hook( __FILE__, array( $this, 'deactivate' ) );
        register_uninstall_hook( __FILE__, array( __CLASS__, 'uninstall' ) );
 
    } // construct
 
    /**
     * Checks for WooCommerce and GF and kills our plugin if they aren't both active
     *
     * @uses    function_exists     Checks for the function given string
     * @uses    deactivate_plugins  Deactivates plugins given string or array of plugins
     *
     * @action  admin_notices       Provides WordPress admin notices
     *
     * @since   1.0
     * @author  SFNdesign, Curtis McHale
     */
    public function check_required_plugins(){
 
        if( ! is_plugin_active( 'woocommerce/woocommerce.php' ) ){ ?>
 
            <div id="message" class="error">
                WPTT mkdir expects WooCommerce to be active. This plugin has been deactivated.
            </div>
 
            <?php
            deactivate_plugins( '/wptt-mkdir/wptt-mkdir.php' );
        } // if
 
    } // check_required_plugins
 
    /**
     * Fired when plugin is activated
     *
     * @param   bool    $network_wide   TRUE if WPMU 'super admin' uses Network Activate option
     */
    public function activate( $network_wide ){
 
    } // activate
 
    /**
     * Fired when plugin is deactivated
     *
     * @param   bool    $network_wide   TRUE if WPMU 'super admin' uses Network Activate option
     */
    public function deactivate( $network_wide ){
 
    } // deactivate
 
    /**
     * Fired when plugin is uninstalled
     *
     * @param   bool    $network_wide   TRUE if WPMU 'super admin' uses Network Activate option
     */
    public function uninstall( $network_wide ){
 
    } // uninstall
 
} // WPTT_Mkdir
 
new WPTT_Mkdir();

The function we’re going to spend our time in today is activate. activate is called by register_activation_hook which makes any of the content of the function run on plugin activation. Sure we could also run it on some WordPress hook like init but then it would run every time that the site is visited which would be dump.

Adding to register_activation_hook

The first thing we want to add to register_activation_hook is our function that will create the directory.

    /**
     * Fired when plugin is activated
     *
     * @param   bool    $network_wide   TRUE if WPMU 'super admin' uses Network Activate option
     */
    public function activate( $network_wide ){
 
        $this->create_custom_dir();
 
    } // activate

Now let’s take a look at our $this->create_custom_dir function.

    /**
     * Creates our custom upload directory
     *
     * @since 1.0
     * @author SFNdesign, Curtis McHale
     * @access private
     *
     * @uses $this->check_custom_dir()          Returns true if the dir already exists
     * @uses wp_mkdir_p()                       Creates dirpath recursively and sets file permissions if possible
     */
    public function create_custom_dir(){
 
        if ( $this->check_custom_dir() === true ) return;
 
        $url = WP_CONTENT_DIR . '/uploads/wpttcustom';
 
        wp_mkdir_p( $url );
 
    } // create_custom_dir

The first thing we do is check if the directory exists with another custom function (I’ll show you that in a second). If our directory already exists, because the user has already activated the plugin once, then we stop what we are doing.

If the directory hasn’t been created then we want to create it.

To do that we first use the WordPress constant WP_CONTENT_DIR which will give us the path to the wp-content folder on the server.

To that constant we append the path we want to create a directory in. For our case we use /uploads/wpttcustom.

Then we pass the path to wp_mkdir_p. wp_mkdir_p will do it’s best to create our directory recursively. So if we really wanted a path like /uploads/wptt/custom/anotherlevel it would create the whole path for us.

Checking if the directory exists

At the top of my function I had a check to see if the directory existed. Lets look at that now.

    /**
     * Returns true if the custom directory is in the uploads folder
     *
     * @since 1.0
     * @author SFNdesign, Curtis Mchale
     * @access private
     */
    private function check_custom_dir(){
 
        $url = WP_CONTENT_DIR . '/uploads/wpttcustom';
 
        if ( file_exists( $url ) ){
            return true;
        }
 
        return false;
 
    } // check_custom_dir

Here we use the same constant WP_CONTENT_DIR and define the same path and then use the PHP function file_exists to see if the directory exists.

If it does we will return true. If it doesn’t our function will send false, which would mean that we create the directory.

Now you may wonder why I broke 5 lines out in to it’s own function. A programming convention is the Single Responsibility Principle. So the only job of our create function is to create the directory. The only job of our check function is to check if the directory exists. You can read more about SOLID on Wikipedia.

Wrap up

That’s it, we have now created a custom directory on plugin activation.

Screencast

Screen Shot 2013-12-19 at 2.07.23 PM

Adding a new domain to an existing VVV (Vagrant) box

I’ve already written 2 posts on Vagrant, specifically using VVV to run your WordPress development boxes.

  1. Working with WordPress and Vagrant – Basics
  2. Vagrant and Custom Domains with WordPress

The second post did show you how you could add a custom domain to a Vagrant install, but didn’t specifically call out my day to day workflow for using Vagrant.

My VVV workflow

Most of the time I use the same VVV box for all projects. So all theme builds and plugin builds…To add a new site to an existing VVV box I use the instructions you’ll find below.

If I have to build a WordPress MU site I usually build a custom VVV box that has all the domains mapped and the dabatase setup for the user. Then the next developer just has to clone our VVV box and add the hosts file entries to get the project running.

Adding the new domain

Our first step is to open you terminal program and change directory (cd) to the VVV install you want to add a domain to. From the root type cd config/nginx/sites.

Here you should find a few files ending in the .conf extension. Copy one and rename it to screencast.wpthemetutorial.com.conf then open it in your editor of choice. You’ll need to change 2 lines.

  1. On line 28 change the server name to match the domain you want to see the site at: server_name screencast.wpthemetutorial.com;
  2. On line 31 you should see the path to the install location in your www directory: root /srv/www/wptt-screen;

Now we need to go back to the root of the Vagrant box and go in to our database directory by typing cd database.

Then open init-custom.sql so we can add our custom database commands. I copy the last entry and pasted it below. I’ll change all our values to swptt. I don’t think it’s worth making it super long/hard since this is not a live web sever and anyone that can access this has access to your machine.

CREATE DATABASE IF NOT EXISTS `swptt`;
GRANT ALL PRIVILEGES ON `swptt`.* TO 'swptt'@'localhost' IDENTIFIED BY 'swptt';

Now we need to go back to the root of the VVV box and type cd www to get to the web directory on our server. Here we need to move all the WordPress files in to their proper spot in the wptt-screen folder.

You can do this by typing cp -rv wordpress-default/ wptt-screen.

Finally I want to remove the wp-config.php file that was in the copied WordPress install. Type rm -rf wptt-screen/wp-config.php and the wp-config.php file will be gone.

Now you need to type vagrant up to start the VVV box.

Once Vagrant comes up all you should need to do is type screencast.wpthemetutorial.com in to your browser and then follow the WordPress install steps.

Screencast

WordPress Environment Configuration

I always work locally on client sites. It’s simply way faster to save a file and refresh the browser instead of waiting for it to upload then refreshing. You can also make mistakes locally and kill the whole site without worrying about downtime for your client.

It’s also pretty common for me to have a staging environment so clients can approve work before it launches to the live site.

These different environments do present some issues though. We don’t want users of the site to be able to access the staging site and we certainly don’t want to send any automated emails (like orders from WooCommerce) from the staging site as we test things. We do want to know if the emails got sent though and what their content was.

To catch emails I wrote a plugin that replaces the wp_mail function. There I recommended you set a constant in wp-config.php of DEVELOPMENT but there can be issues with setting those. Specifically if you’re using WPEngine and make use of their live to staging push features.

When you push live->staging on WPEngine it ends up changing wp-config.php back to it’s default values. That means your DEVELOPMENT constant is gone and you’re sending emails to clients again from the staging site. So any of the other cool options based on wp-config.php won’t work either.

To get around this quirk of how WPEngine operates I’ve released a plugin called WPTT Developer Constants. It’s meant to be run as an mu-plugin on your site. That means it will always be run and the constants will be available before your theme or plugins are loaded.

That means you could hook plugins_loaded and check to see if certain plugins are turned on based on the environment you are in. Like turn on the email logging plugin for STAGING_ENV and LOCAL_ENV and make sure it’s off for LIVE_ENV.

If you were using the older style DEVELOPMENT constant then I’ve kept that around as well. I’m not using it anymore since I found I want different environment configuration locally (like why restrict access to outsiders, it’s my computer it’s running on).

Go check the plugin out.

Screen Shot 2013-10-11 at 3.52.53 PM

Pre-filled product inquiry form with Gravity Forms and WooCommerce

Today we’re going to conitue from last weeks screencast on removing the purchase button from all products inside a category. It was great to get the button removed but makes the page almost totally useless.

Now we’re going to take a Gravity Forms form and add it to our page so that users can fill out the form and inquire about the product.

Setting up the form

To start with we need to set up our form with the fields we need. I have mine set to ask for the name, email, phone number. It also provides a textarea for the site user to ask their question.

The final field is called product name and we’re going to dynimically populate it with the name of the product that is currently on the page. Asking the customer to fill out that extra bit of information is just sucky so we’ll do it for them.

Under the field advainced tab you need to check off “Allow field to be populated dynamically” and then you need to enter a parameter. We’ll use ‘west_boat_name’.

field-dynamically-populated

The parameter is just a way for Gravity Forms to be able to reference the field for our dynamic population.

Adding the form to our product

Now that we have our form ready we want to add it to our product and here is our code.

/**
 * Our custom text
 *
 * @uses get_product()      Returns the product object for the current product in the loop
 * @uses get_the_title()    Returns the title given the post_id
 * @uses do_shortcode()     Does the shortcode content
 */
function western_boat_purchase_text(){
 
    $product = get_product();
    $product_title = get_the_title( $product->ID );
 
?>
 
    <section class="west-product-inquire">
        You can call us at 1-866-644-8111 or fill out the form below.
        <?php echo do_shortcode( '[gravityform id="4" name="Boat Inquiry Form" title="false" description="false" field_values="west_boat_name='.$product_title.'"]'); ?>
    </section><!-- /.west-product-inquire -->
<?php
} // western_boat_purchase_text

We start by getting the full product object with get_product. Once we have that we want the name of the product and we can use get_the_title to do that by passing it the $post_id of the product.

Now we have a basic HTML wrapper for our text and code. We provide the site user with a contact phone number and then we use do_shortcode to call out the form.

You can get your form_id from the URL of the gravity form. In my case it’s form_id 4.

gf-form-id 13-10-11 3.50.18 PM

While there are a number of ways to dynamically populate a form field we’re going to pass in our custom values in the shortcode. That means we use the field_values parameter and pass in our custom value of $product_title.

That’s it, we now have a product inquiry form on our page that fills in the product title for the user.

Screencast

001-inspect-button-element

Remove the purchase button on a product in a category in WooCommerce

Recently I had a client want to remove the purchase button on their WooCommerce store for all products in a category. We also needed to remove the button for all items that were a child category of the main category. Here is a quick diagram of the product category hierarchy.

  • Boats
    • Whitewater
      • WaveSport
      • Jackson
    • Canoe
      • Clipper
    • Kayak
    • SUP

If a user lands on ‘WaveSport’ they should not see a direct purchase button for the product.

Approaching the issue

There are a few ways to approach this problem. If we only had 3 or 4 terms we could just add a bunch of conditionals for each term. If we had a match we would remove the button.

Sure that would work but I’ve got 20 categories and some of them could change each year. I don’t want to fix conditionals every year and a client is just going to think that you wrote a broken WooCommerce Theme.

Our second option is to see what WordPress has for functions to detect terms on post objects. WordPress has a function called has_term which returns true if the current post object has the term applied to it.

What it doesn’t do is return true if any of the terms applied to a post object are children of the parent term.

That leaves us with the final option that is as future proof as we can make it. Detect if the post object has_term or if it has any terms that are children of our parent term (boats in our example above).

Diving in to the code

Our first stop is to find out how WooCommerce builds our store button. Go to your single product and use ‘Inspect Element’ to view the HTML source of the button.

001-inspect-button-element

You can see that we have a class on the button that’s probably unique to the button only. This class is called single_add_to_cart_button. Now I’ll search WooCommerce for that string.

The first hits that come up are found in woocommerce/woocommerce-hooks.php and it leads us to 5 actions.

    /**
     * Product Add to cart
     *
     * @see woocommerce_template_single_add_to_cart()
     * @see woocommerce_simple_add_to_cart()
     * @see woocommerce_grouped_add_to_cart()
     * @see woocommerce_variable_add_to_cart()
     * @see woocommerce_external_add_to_cart()
     */
    add_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_add_to_cart', 30 );
    add_action( 'woocommerce_simple_add_to_cart', 'woocommerce_simple_add_to_cart', 30 );
    add_action( 'woocommerce_grouped_add_to_cart', 'woocommerce_grouped_add_to_cart', 30 );
    add_action( 'woocommerce_variable_add_to_cart', 'woocommerce_variable_add_to_cart', 30 );
    add_action( 'woocommerce_external_add_to_cart', 'woocommerce_external_add_to_cart', 30 );

These 5 actions add our ‘Add to Cart’ buttons for our various product types. That means we need to write a function which removes these action if we are inside the categories.

Now let’s take a look at our first function.

/**
 * Builds us our custom buy text if we are on any of the boat categories
 *
 * @since 1.0
 * @author SFNdesign, Curtis McHale
 *
 * @uses is_product()               Returns true if we are on a single product
 * @uses get_product()              Gets the product object
 * @uses has_term()                 Returns true if the post_object has the term
 * @uses west_is_child_of_term()    Returns true if current post_object has any terms that are a child if the specified term
 */
function western_custom_buy_buttons(){
 
    // die early if we aren't on a product
    if ( ! is_product() ) return;
 
    $product = get_product();
 
    if ( has_term( 'boats', 'product_cat', $product ) || west_is_child_of_term( 'boats', 'product_cat', $product ) ){
 
        // removing the purchase buttons
        remove_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_add_to_cart', 30 );
        remove_action( 'woocommerce_simple_add_to_cart', 'woocommerce_simple_add_to_cart', 30 );
        remove_action( 'woocommerce_grouped_add_to_cart', 'woocommerce_grouped_add_to_cart', 30 );
        remove_action( 'woocommerce_variable_add_to_cart', 'woocommerce_variable_add_to_cart', 30 );
        remove_action( 'woocommerce_external_add_to_cart', 'woocommerce_external_add_to_cart', 30 );
 
        // adding our own custom text
        add_action( 'woocommerce_single_product_summary', 'western_boat_purchase_text', 30 );
        add_action( 'woocommerce_simple_add_to_cart', 'western_boat_purchase_text', 30 );
        add_action( 'woocommerce_grouped_add_to_cart', 'western_boat_purchase_text', 30 );
        add_action( 'woocommerce_variable_add_to_cart', 'western_boat_purchase_text', 30 );
        add_action( 'woocommerce_external_add_to_cart', 'western_boat_purchase_text', 30 );
 
    }
 
} // western_custom_buy_buttons
add_action( 'wp', 'western_custom_buy_buttons' );

Here we start by checking if we are on a single product page with the is_product conditional in WooCommerce. If we are on a single product it will return true but if not it will return false and we’ll stop processing our function since we don’t need it.

The next thing we need is to get the full post object. WooCommerce comes with a great function called get_product which does the heavy lifting for us.

Now we get in to our conditionals. First we check if we are on the main parent category of boats using has_term then we have a custom function to check for child terms which we’ll explain in more depth in a second.

If either of these are true we then use remove_action to take away all the buttons on the products.

While this does achieve our goal it’s pretty bad user experience so let’s take a step further and add some custom actions and call a new function that gives some user feedback.

You can see after using remove_action that is exactly what we do and our new custom function is below.

/**
 * Our custom text
 */
function western_boat_purchase_text(){
    echo 'Please get in touch about boat purchases</p><br />';
} // western_boat_purchase_text

Pretty simple for the demo and is only marginally better than providing nothing. I’d suggest that you add a proper form for the user to fill out and supply the company phone number so that the user has to do as little extra work as possible.

Now on to our custom function to detect child terms.

Checking for parent term

/**
 * Should return true if any of the terms on the post object is a child if the specified term
 *
 * @since 1.0
 * @author SFNdesign, Curtis McHale
 *
 * @param string    $term           required        The term parent we are checking against
 * @param string    $taxonomy       required        The taxonomy that we are checking for
 * @param object    $post_object    required        The post object we are checking against
 *
 * @return bool     True if term is child of parent
 *
 * @uses get_term_by()          Gets term for given taxonomy by the field specified
 * @uses get_the_terms()        Returns terms for post_object
 * @uses wp_list_pluck()        Bloody magic, or pulls all the values from an array, bloody magic
 */
function west_is_child_of_term( $term, $taxonomy, $post_object ){
 
    // get id of parent term
    $term = get_term_by( 'slug', $term, $taxonomy );
 
    // get terms on post objects
    $post_terms = get_the_terms( (int) $post_object->ID, (string) $taxonomy );
 
    // build array of term parent ids
    $post_term_parent_ids = wp_list_pluck( $post_terms, 'parent' );
 
    if ( in_array( $term->term_id, $post_term_parent_ids ) ){
        return true;
    }
 
    return false;
 
} // west_is_child_of_term

Here we start by getting the term object for our parent term. Ultimately we’ll want the term id to do a comparison with any child terms.

Next we get all of the terms on the product by using get_the_terms. get_the_terms takes 2 parameters.

  1. $post_id – the ID of the post object
  2. $taxonomy – the taxonomy we want to look up the terms for

Now we need a list of all the parent values for the terms on the post. parent will be the term_id of the parent term. So our ‘WaveSport’ category would have the parent value of our ‘boats’ category.

To build our list of term_ids we use an awesome function called wp_list_pluck. wp_list_pluck will search through our whole array and just pull out the values of the field we pass in.

Finally once we have the array of parent term ids we use in_array to check if our $term->term_id (needle) is in $post_parent_term_ids (haystack).

If we get a match then we return true. If there are no matches (meaning it’s not a term that is a child of our special term) then our function ends by returning false.

Now we have a set of functions that can remove the product purchase button from a WooCommerce store based on a special category we provide.

Here is all the code in one shot.

<?php
/**
 * Builds us our custom buy text if we are on any of the boat categories
 *
 * @since 1.0
 * @author SFNdesign, Curtis McHale
 *
 * @uses is_product()               Returns true if we are on a single product
 * @uses get_product()              Gets the product object
 * @uses has_term()                 Returns true if the post_object has the term
 * @uses west_is_child_of_term()    Returns true if current post_object has any terms that are a child if the specified term
 */
function western_custom_buy_buttons(){
 
    // die early if we aren't on a product
    if ( ! is_product() ) return;
 
    $product = get_product();
 
    if ( has_term( 'boats', 'product_cat', $product ) || west_is_child_of_term( 'boats', 'product_cat', $product ) ){
 
        // removing the purchase buttons
        remove_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_add_to_cart', 30 );
        remove_action( 'woocommerce_simple_add_to_cart', 'woocommerce_simple_add_to_cart', 30 );
        remove_action( 'woocommerce_grouped_add_to_cart', 'woocommerce_grouped_add_to_cart', 30 );
        remove_action( 'woocommerce_variable_add_to_cart', 'woocommerce_variable_add_to_cart', 30 );
        remove_action( 'woocommerce_external_add_to_cart', 'woocommerce_external_add_to_cart', 30 );
 
        // adding our own custom text
        add_action( 'woocommerce_single_product_summary', 'western_boat_purchase_text', 30 );
        add_action( 'woocommerce_simple_add_to_cart', 'western_boat_purchase_text', 30 );
        add_action( 'woocommerce_grouped_add_to_cart', 'western_boat_purchase_text', 30 );
        add_action( 'woocommerce_variable_add_to_cart', 'western_boat_purchase_text', 30 );
        add_action( 'woocommerce_external_add_to_cart', 'western_boat_purchase_text', 30 );
 
    }
 
} // western_custom_buy_buttons
add_action( 'wp', 'western_custom_buy_buttons' );
 
/**
 * Our custom text
 */
function western_boat_purchase_text(){
    echo 'Please get in touch about boat purchases</p><br />';
} // western_boat_purchase_text
 
/**
 * Should return true if any of the terms on the post object is a child if the specified term
 *
 * @since 1.0
 * @author SFNdesign, Curtis McHale
 *
 * @param string    $term           required        The term parent we are checking against
 * @param string    $taxonomy       required        The taxonomy that we are checking for
 * @param object    $post_object    required        The post object we are checking against
 *
 * @return bool     True if term is child of parent
 *
 * @uses get_term_by()          Gets term for given taxonomy by the field specified
 * @uses get_the_terms()        Returns terms for post_object
 * @uses wp_list_pluck()        Bloody magic, or pulls all the values from an array, bloody magic
 */
function west_is_child_of_term( $term, $taxonomy, $post_object ){
 
    // get id of parent term
    $term = get_term_by( 'slug', $term, $taxonomy );
 
    // get terms on post objects
    $post_terms = get_the_terms( (int) $post_object->ID, (string) $taxonomy );
 
    // build array of term parent ids
    $post_term_parent_ids = wp_list_pluck( $post_terms, 'parent' );
 
    if ( in_array( $term->term_id, $post_term_parent_ids ) ){
        return true;
    }
 
    return false;
 
} // west_is_child_of_term

Screencast

https://www.youtube.com/watch?v=Hd6e2SceKl8