Filter out unwanted order item meta data from Woocommerce email notifications

In the order email templates (for example email-order-items.php), WooCommerce uses the function wc_display_item_meta to display product details in the order table. The function code is present in the wc-template-functions.php file (line number 3011). I am copying the function code below for reference

function wc_display_item_meta( $item, $args = array() ) {
    $strings = array();
    $html    = '';
    $args    = wp_parse_args( $args, array(
        'before'    => '<ul class="wc-item-meta"><li>',
        'after'     => '</li></ul>',
        'separator' => '</li><li>',
        'echo'      => true,
        'autop'     => false,
    ) );

    foreach ( $item->get_formatted_meta_data() as $meta_id => $meta ) {
        $value     = $args['autop'] ? wp_kses_post( $meta->display_value ) : wp_kses_post( make_clickable( trim( $meta->display_value ) ) );
        $strings[] = '<strong class="wc-item-meta-label">' . wp_kses_post( $meta->display_key ) . ':</strong> ' . $value;
    }

    if ( $strings ) {
        $html = $args['before'] . implode( $args['separator'], $strings ) . $args['after'];
    }

    $html = apply_filters( 'woocommerce_display_item_meta', $html, $item, $args );

    if ( $args['echo'] ) {
        echo $html; // WPCS: XSS ok.
    } else {
        return $html;
    }
}

The problem is: it doesn’t take any arguments that can help me filter out item data that I don’t want to show in the order email. I don’t want to change this function in the wc-template-functions.php as it’s a core file. So, I want to know if there’s a piece of code that I can add to functions.php that’ll somehow modify this wc_display_item_meta function to filter out specific item meta.

Note: I know someone might suggest why not just remove that particular item data from the product details, but that data is essential to internal order processing. I just don’t want it to show to the customers.

Update #1: What meta data I don’t want to show in the order email? Below is a screenshot of an order email. I have highlighted three item data..”Qty Selector”, “Qty” and “Total”. I want all these three to not show in the order email.

enter image description here

Here is Solutions:

We have many solutions to this problem, But we recommend you to use the first solution because it is tested & true solution that will 100% work for you.

Solution 1

Try the following without any guarantee (as I don’t really have the real necessary keys):

add_filter( 'woocommerce_order_item_get_formatted_meta_data', 'unset_specific_order_item_meta_data', 10, 2);
function unset_specific_order_item_meta_data($formatted_meta, $item){
    // Only on emails notifications
    if( is_admin() || is_wc_endpoint_url() )
        return $formatted_meta;

    foreach( $formatted_meta as $key => $meta ){
        if( in_array( $meta->key, array('Qty Selector', 'Qty', 'Total') ) )
            unset($formatted_meta[$key]);
    }
    return $formatted_meta;
}

Code goes in function.php file of your active child theme (active theme). Tested with other meta data than yours and works. I hope it will work for you too.

Now, the hook used with this code is the right filter hook. It’s located in the WC_Order_Item method get_formatted_meta_data() and allows to filter the order item meta data.

Solution 2

There is a bug with the accepted answer, and all of the other snippets that I’ve found around the internet, so I’m posting my own answer here in the hopes that stores around the world don’t accidentally leak information.

The problem is that when you use the Order actions meta box to resend the email, the filter check fails because is_admin() === true.

The order actions is a meta box down the side of the Orders page:

enter image description here

So the first time, when the order is created, it filters the email like you want, but then if an admin resends the email to a customer then it will be broken and show all of the meta fields to the user in the resent email.

The code that fixes this scenario is this:

$is_resend = isset($_POST['wc_order_action']) ?  wc_clean( wp_unslash( $_POST['wc_order_action'] ) ) === 'send_order_details' : false;

if ( !$is_resend && (is_admin() || is_wc_endpoint_url() ) ) {
  return $formatted_meta;
}

So if you look at the linked snippet then you will see the meta box adds that field to the $_POST. It has to be cleaned up like that as well or it won’t match.

The full example integrated into the accepted solution’s answer is:

add_filter( 'woocommerce_order_item_get_formatted_meta_data', 'unset_specific_order_item_meta_data', 10, 2);

function unset_specific_order_item_meta_data($formatted_meta, $item){
    // Only on emails notifications
    $is_resend = isset($_POST['wc_order_action']) ?  wc_clean( wp_unslash( $_POST['wc_order_action'] ) ) === 'send_order_details' : false;

    if ( !$is_resend && (is_admin() || is_wc_endpoint_url() ) ) {
      return $formatted_meta;
    }

    foreach( $formatted_meta as $key => $meta ){
        if( in_array( $meta->key, array('Qty Selector', 'Qty', 'Total') ) )
            unset($formatted_meta[$key]);
    }
    return $formatted_meta;
}

Solution 3

I hear, that you want to show the Order Item Meta Data in the admin backend only. That’s actually a tricky one. I have played around for some hours but no solution I have found, guarentee that the Order Itema Meta Data doesn’t show up in e-mails to the customer.

The thing is that there are several ways these e-mails are fired (eg. through the resend meta box (which @rtpHarry mentions) or by changing order status either at the order overview, the single order view or an automatic/programmatically order status change). That gives many cases where it’s neccessary to unset the Order Item Meta Data – you need to find all cases except the admin backend.

Therefore, my suggestion is to first completely remove the Order Item Meta Data using the above mentioned woocommerce_order_item_get_formatted_meta_data filter and then add them again using an action like woocommerce_before_order_itemmeta which ONLY fires in the admin backend. Because the Order Item Meta Data is unset you cannot use the get_formatted_meta_data method to get the data. Instead you can use the function wc_get_order_item_meta.

Complete code (tested and works):

//Hide 'Qty Selector', 'Qty' and 'Total' completely
add_filter( 'woocommerce_order_item_get_formatted_meta_data', 'unset_specific_order_item_meta_data');
function unset_specific_order_item_meta_data($formatted_meta){
    foreach( $formatted_meta as $key => $meta ){
        if( in_array( $meta->key, array('Qty Selector', 'Qty', 'Total') ) )
            unset($formatted_meta[$key]);
    }
    
    return $formatted_meta;
}

//Add 'Qty Selector', 'Qty' and 'Total' in the admin backend only
add_action('woocommerce_before_order_itemmeta', 'add_specific_order_item_meta_data_in_backend', 10, 2);
function add_specific_order_item_meta_data_in_backend( $item_id, $item ) {
    //Only applies for line items
    if( $item->get_type() !== 'line_item' ) return;
    
    $qty_sel_lines = wc_get_order_item_meta($item_id, 'Qty Selector', false);
    $qty_lines = wc_get_order_item_meta($item_id, 'Qty', false);
    $total_lines = wc_get_order_item_meta($item_id, 'Total', false);
    
    foreach ($qty_sel_lines as $qty_sel_line){
        echo $qty_sel_line . '<br>';
    }
    
    foreach ($qty_lines as $qty_line){
        echo $qty_line . '<br>';
    }
    
    foreach ($total_lines as $total_line){
        echo $total_line. '<br>';
    }
}

Note:
If you need to add the Order Item Meta Data to the admin e-mails, you need to do that seperately. I have not examined the options on that.

Solution 4

I kind of agreed with @pstidsen argument. So I was thinking about how to solve this without to re-add all the metadata, since it kind of disturbed me not to handle it in the same way as it was added before. I have additional filters to add css classes and so on to the metadata. So there would’ve been a need to take care of.

So here is my approach which gives you the opportunity to use it for emails, custom emails, pdf invoices or similar scenarios. I also uses a fallback to filter for our frontend or any situation we didn’t consider.
Please keep the order of if else in mind. I checked the admin filter the last, to make sure any other filter gets fired before. The situation for an email at example is: It’s sent from the admin interface, so the admin filter is true but also is the email filter.

Functionality for a different filter for admin emails is given as well.

/**
* This function filters all unwanted item metadata, if the specific filter are hooked in
* we also use a fallback filter, if none of the hooks are fired
*
* @params array() $metadata
*
*/
add_filter( 'woocommerce_order_item_get_formatted_meta_data', 'custom_filter_item_meta_data', 50, 1);
function custom_filter_item_meta_data( $metadata ){
    if ( empty( $metadata ) ) return $metadata;
    $filter_array = array();
    if ( apply_filters( 'custom_filter_item_meta_email', false ) ){
        // email filter goes here
        $filter_array = array( 'whatever','you', 'wanna', 'filter, 'for', 'email' );
    }elseif ( apply_filters( 'custom_filter_item_meta_admin_email', false ) ){
        // admin email filter goes here
        // pass
    elseif ( apply_filters( 'custom_filter_item_meta_invoice', false ) ){
        // invoice filter goes here
        $filter_array = array( 'whatever','you', 'wanna', 'filter, 'for', 'invoices' );
    }elseif ( apply_filters( 'custom_filter_item_meta_admin', false ) ){
        // general admin filter goes here
        $filter_array = array( 'whatever','you', 'wanna', 'filter, 'for', 'admin_backend' );
    }else{
        // fallback filter
        $filter_array = array( 'whatever','you', 'wanna', 'filter, 'for', 'fallback' );
    }
    foreach ( $metadata as $key => $meta ){
        if ( in_array( $meta->key, $filter_array ) ){
            unset ( $metadata[ $key ] );
        }
    }
    return $metadata;
}

/**
* Is used to enable our item meta filter for our admin backend
* Hooked:
* @admin_init
*/
add_action( 'admin_init', 'custom_init_item_meta_filter_admin', 50, 1 );
function custom_init_item_meta_filter_admin(){
    add_filter( 'custom_filter_item_meta_admin', function(){ return true; });
}

/**
* Is used to enable our item meta filter for emails
* Hooked:
* @woocommerce_email_order_details
*/
add_action( 'woocommerce_email_order_details', 'custom_init_item_meta_filter_email' ), 10, 2);
function custom_init_item_meta_filter_email( $order, $sent_to_admin ){
    if ( $sent_to_admin ){
        add_filter('custom_filter_item_meta_admin_email', function(){ return true; } );
    }else{
        add_filter('custom_filter_item_meta_email', function(){ return true; } );
    }
}

/**
* Is used to enable our item meta filter for invoices
* Hooked:
* @wpo_wcpdf_before_order_details
*/
add_filter( 'wpo_wcpdf_before_order_details', 'custom_init_item_meta_filter_invoice', 10, 1);
function custom_init_item_meta_filter_invoice(){
    add_filter( 'custom_filter_item_meta_invoice', function(){ return true; });
}

I didn’t test it in that "flattened" format. I used it within different classes of my oop coded plugin and edited it to post it here.

Solution 5

Structure of the html code of the metadata tags

If I want to delete meta_data 2 and meta_data 3:

add_filter( 'woocommerce_display_item_meta', 'filter_woocommerce_display_item_meta', 10, 3 ); 

function filter_woocommerce_display_item_meta( $html, $item, $args ) { 
        
    $arrayPortionsTags = explode("<li", $html);
    unset($arrayPortionsTags[2],$arrayPortionsTags[3]);
    $firstLi = array( '<li' );
    $lastUl = array( '</ul>' ); 
    array_splice( $arrayPortionsTags, 1, 0, $firstLi );
    array_splice( $arrayPortionsTags, 3, 0, $lastUl );
    $html= implode('',$arrayPortionsTags);
    return $html; 
}; 

Note: Use and implement solution 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply