WooCommerce: how to add multiple products to cart at once? - woocommerce

I need a "get products A, B and C for $xxx" special offer, products A, B and C must be available on their own, and the bundle is a special offer accessible through a coupon code.
On a marketing page hosting outside my site, I would like a button leading to my site that carries a query string like ?add-to-cart=244,249,200 so that once on my site, all bundle products are already added to the cart (instead of adding them one by one which sounds unacceptably tedious).
If not possible, then at least I'd like a landing page on my site with a single button adding all bundle products to cart at once.
I couldn't find working solutions googling around (here's one example). Any suggestion?

After some research I found that DsgnWrks wrote a hook that does exactly this. For your convenience, and in case the blog goes offline, I bluntly copied his code to this answer:
function woocommerce_maybe_add_multiple_products_to_cart( $url = false ) {
// Make sure WC is installed, and add-to-cart qauery arg exists, and contains at least one comma.
if ( ! class_exists( 'WC_Form_Handler' ) || empty( $_REQUEST['add-to-cart'] ) || false === strpos( $_REQUEST['add-to-cart'], ',' ) ) {
return;
}
// Remove WooCommerce's hook, as it's useless (doesn't handle multiple products).
remove_action( 'wp_loaded', array( 'WC_Form_Handler', 'add_to_cart_action' ), 20 );
$product_ids = explode( ',', $_REQUEST['add-to-cart'] );
$count = count( $product_ids );
$number = 0;
foreach ( $product_ids as $id_and_quantity ) {
// Check for quantities defined in curie notation (<product_id>:<product_quantity>)
// https://dsgnwrks.pro/snippets/woocommerce-allow-adding-multiple-products-to-the-cart-via-the-add-to-cart-query-string/#comment-12236
$id_and_quantity = explode( ':', $id_and_quantity );
$product_id = $id_and_quantity[0];
$_REQUEST['quantity'] = ! empty( $id_and_quantity[1] ) ? absint( $id_and_quantity[1] ) : 1;
if ( ++$number === $count ) {
// Ok, final item, let's send it back to woocommerce's add_to_cart_action method for handling.
$_REQUEST['add-to-cart'] = $product_id;
return WC_Form_Handler::add_to_cart_action( $url );
}
$product_id = apply_filters( 'woocommerce_add_to_cart_product_id', absint( $product_id ) );
$was_added_to_cart = false;
$adding_to_cart = wc_get_product( $product_id );
if ( ! $adding_to_cart ) {
continue;
}
$add_to_cart_handler = apply_filters( 'woocommerce_add_to_cart_handler', $adding_to_cart->get_type(), $adding_to_cart );
// Variable product handling
if ( 'variable' === $add_to_cart_handler ) {
woo_hack_invoke_private_method( 'WC_Form_Handler', 'add_to_cart_handler_variable', $product_id );
// Grouped Products
} elseif ( 'grouped' === $add_to_cart_handler ) {
woo_hack_invoke_private_method( 'WC_Form_Handler', 'add_to_cart_handler_grouped', $product_id );
// Custom Handler
} elseif ( has_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler ) ){
do_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler, $url );
// Simple Products
} else {
woo_hack_invoke_private_method( 'WC_Form_Handler', 'add_to_cart_handler_simple', $product_id );
}
}
}
// Fire before the WC_Form_Handler::add_to_cart_action callback.
add_action( 'wp_loaded', 'woocommerce_maybe_add_multiple_products_to_cart', 15 );
/**
* Invoke class private method
*
* #since 0.1.0
*
* #param string $class_name
* #param string $methodName
*
* #return mixed
*/
function woo_hack_invoke_private_method( $class_name, $methodName ) {
if ( version_compare( phpversion(), '5.3', '<' ) ) {
throw new Exception( 'PHP version does not support ReflectionClass::setAccessible()', __LINE__ );
}
$args = func_get_args();
unset( $args[0], $args[1] );
$reflection = new ReflectionClass( $class_name );
$method = $reflection->getMethod( $methodName );
$method->setAccessible( true );
$args = array_merge( array( $class_name ), $args );
return call_user_func_array( array( $method, 'invoke' ), $args );
}
It works just like you'd expect, by providing a comma separated list of products. It even works with quantities using ?add-to-cart=63833:2,221916:4
I was, and am still looking for a 'pure' solution that allows to add multiple products to the cart without having to install a plugin or add custom actions. But for many, the above might be an appropriate solution

Related

Add suffix text to specific product category in Woocommerce [duplicate]

I need to add 'per metre' to the price on most of my online catalogue, I tried the code on this thread in my finctions.php but I cannot get it to omit/include particular categories- it seems to be all or nothing. What am I doing wrong?
I have edited the code as such:
/*add 'per metre' after selected items*/
add_filter( 'woocommerce_get_price_html', 'conditional_price_suffix', 20, 2 );
function conditional_price_suffix( $price, $product ) {
// HERE define your product categories (can be IDs, slugs or names)
$product_categories = array('fabric','haberdashery', 'lining',);
if( ! has_term( $product_categories, 'fasteners', 'patches', 'remnnants', $product->get_id() ) )
$price .= ' ' . __('per metre');
return $price;
}
I want 'fabrics', 'haberdashery', 'lining' to show per metre, and 'fasteners', 'patches', 'remnants' to NOT show the suffix.
I have tried variations of the code -my exclusions in the top bit and the inclusions in the second part, and with/without the "( ! has term" section, but whichever I do takes all the suffix messages away, or applies to all categories.
It would be amazing if I could get this to work as have previously been using a very bloated plug-in. I'm only basically capable in this stuff so please feel free to talk me through it as if I am an idiot.
There is a little mistake in your code in the has_term() function.
To handle parent product categories, we will use a custom conditional function instead of has_tem().
I have also added some code to handle the product variation selected price of variable products, So try this instead:
// Custom conditional function that handle parent product categories too
function has_product_categories( $categories, $product_id = 0 ) {
$parent_term_ids = $categories_ids = array(); // Initializing
$taxonomy = 'product_cat';
$product_id = $product_id == 0 ? get_the_id() : $product_id;
if( is_string( $categories ) ) {
$categories = (array) $categories; // Convert string to array
}
// Convert categories term names and slugs to categories term ids
foreach ( $categories as $category ){
$result = (array) term_exists( $category, $taxonomy );
if ( ! empty( $result ) ) {
$categories_ids[] = reset($result);
}
}
// Loop through the current product category terms to get only parent main category term
foreach( get_the_terms( $product_id, $taxonomy ) as $term ){
if( $term->parent > 0 ){
$parent_term_ids[] = $term->parent; // Set the parent product category
$parent_term_ids[] = $term->term_id; // (and the child)
} else {
$parent_term_ids[] = $term->term_id; // It is the Main category term and we set it.
}
}
return array_intersect( $categories_ids, array_unique($parent_term_ids) ) ? true : false;
}
add_filter( 'woocommerce_get_price_html', 'conditional_price_suffix', 10, 2 );
function conditional_price_suffix( $price, $product ) {
// Handling product variations
$product_id = $product->is_type('variation') ? $product->get_parent_id() : $product->get_id();
// HERE define your product categories (can be IDs, slugs or names)
$product_categories = array('fabric','haberdashery', 'lining');
if( has_product_categories( $product_categories, $product_id ) )
$price .= ' ' . __('per metre');
return $price;
}
Code goes in function.php file of your active child theme (or active theme). tested and works.

WooCommerce New order email manually trigger for some products

Please i find this code as solution for WooCommerce New order email manually trigger for some products but I don't know where exactly putting this code in functions.php or else thanks.
/**
* Modified from https://www.skyverge.com/blog/add-woocommerce-email-recipients-conditionally/
*
* Add another email recipient for admin New Order emails if a product from a specific category or with a specific tag is ordered
*
* #param string $recipient a comma-separated string of email recipients (will turn into an array after this filter!)
* #param \WC_Order $order the order object for which the email is sent
* #return string $recipient the updated list of email recipients
*/
function sv_conditional_email_recipient( $recipient, $order ) {
// Bail on WC settings pages since the order object isn't yet set yet
// Not sure why this is even a thing, but shikata ga nai
$page = $_GET['page'] = isset( $_GET['page'] ) ? $_GET['page'] : '';
if ( 'wc-settings' === $page ) {
return $recipient;
}
// just in case
if ( ! $order instanceof WC_Order ) {
return $recipient;
}
$items = $order->get_items();
// check if product from category or with tag is in order
foreach ( $items as $item ) {
$product = $order->get_product_from_item( $item );
$args = array('orderby' => 'name', 'order' => 'ASC', 'fields' => 'names');
$product_cats = wp_get_post_terms( $product->get_id, 'product_cat', $args ); // could swap product_cat for product_tag
// add our extra recipient if there's a product from the category with slug "dieta" - commas needed!
// we can bail if we've found one, no need to add the recipient more than once
if ( $product && in_array( "dieta", $product_cats ) ) {
$recipient .= ', notify#example.com';
return $recipient;
}
}
return $recipient;
}
add_filter( 'woocommerce_email_recipient_new_order', 'sv_conditional_email_recipient', 10, 2 );
I code this one and it works on my website, don't forget to replace the_name_of_the_cat by the name of the category and first_email#domain.com,second_email#domain.com by the two emails.
function change_email_recipient_depending_of_cat ( $recipient, $order ) {
global $woocommerce;
$items = $order->get_items();
foreach ( $items as $item ) {
$product_id = $item['product_id'];
if ( has_term( 'the_name_of_the_cat', 'product_cat', $product_id ) ) {
$recipient = 'first_email#domain.com,second_email#domain.com';
}
return $recipient;
}
}
add_filter( 'woocommerce_email_recipient_new_order', 'change_email_recipient_depending_of_cat', 10, 2 );

Sort taxonomy terms table in the admin by custom sort field

I am trying to sort my taxonomy terms table by a custom field in Wordpress. But I am missing something.
I have a custom taxonomy. Let’s call it education in this example.
I added a field sort-order and save a value to that field using update_term_meta().
This works.
Next I added a column to the admin table for the taxonomy:
// Add the column to the table with terms
add_filter('manage_edit-education_columns', array( $this, 'addSortOrderColumn' ));
function addSortOrderColumn( $columns ){
$columnsBefore = array_slice( $columns, 0, 2, true); // NOTE: First column is the checkbox
$columnsAfter = array_slice( $columns, 2, count( $columns ), true);
$columnsInsert = array('sort-order' => ‘Sort’ );
$columns = array_merge($columnsBefore, $columnsInsert, $columnsAfter);
return $columns;
}
// Display the contents for the column
add_filter('manage_education_custom_column',array( $this, 'addSortOrderColumnContent'), 10, 3 );
function addSortOrderColumnContent( $content, $columnName, $termId ){
if( $columnName !== 'sort-order' ){
return $content;
}
$termId = absint( $termId );
$order = get_term_meta( $termId, 'sort-order', true );
if( !empty( $order ) ){
$content .= esc_attr( $order );
}
return $content;
}
// Make the column sortable
add_filter( 'manage_edit-education_sortable_columns', array( $this, 'makeSortOrderColumnSortable' ));
function makeSortOrderColumnSortable( $sortable ){
$sortable[ 'sort-order' ] = 'sort-order';
return $sortable;
}
I see the column, I can click its header and it flips the table rows on click ( a c z e <-> e z c a ), but it does not sort it based on the contents.
What do I need to add to make the sorting aware of the column contents?
You need to tweak the underlying sql that the get_terms function runs to retrieve the taxonomy terms list. It defaults to the term name, clicking & changing the sort will change the orderby=xxx in the url, but will flow through the switch statement in the code and return to the default. The way to do it is through the terms_clauses filter:
add_filter( 'terms_clauses', 'filter_terms_clauses', 10, 3 );
/**
* Filter WP_Term_Query meta query
*
* #param object $query WP_Term_Query
* #return object
*/
function filter_terms_clauses( $pieces, $taxonomies, $args ) {
global $pagenow, $wpdb;
// Require ordering
$orderby = ( isset( $_GET['orderby'] ) ) ? trim( sanitize_text_field( $_GET['orderby'] ) ) : '';
if ( empty( $orderby ) ) { return $pieces; }
// set taxonomy
$taxonomy = $taxonomies[0];
// only if current taxonomy or edit page in admin
if ( !is_admin() || $pagenow !== 'edit-tags.php' || !in_array( $taxonomy, [ 'education' ] ) ) { return $pieces; }
// and ordering matches
if ( $orderby === 'sort-order' ) {
$pieces['join'] .= ' INNER JOIN ' . $wpdb->termmeta . ' AS tm ON t.term_id = tm.term_id ';
$pieces['where'] .= ' AND tm.meta_key = "sort-order"';
$pieces['orderby'] = ' ORDER BY tm.meta_value ';
}
return $pieces;
}
The WP_Term_Query is only from WP4.6 so this functionality is pretty fresh.

Adding secure attribute to Woocommerce cookies

I am trying to get our server PCI compliant and down to the last issue of setting the Woocommerce cookies to secure. I am running all current versions of Wordpress/Woocommerce and the server is running 100% SSL/HTTPS across the entire site.
The cookies I am trying to secure: woocommerce_recently_viewed
I have tried the following with no luck:
Added to my functions file:
add_filter( 'wc_session_use_secure_cookie', '__return_true' );
Added to index.php:
#ini_set('session.cookie_httponly', 'On');
#ini_set('session.cookie_secure', 'On');
#ini_set('session.use_only_cookies', 'On');
Added to php.ini:
session.cookie_httponly = 1
session.cookie_secure = 1
session.use_only_cookies = 1
My last resort is to adjust the server config (I'm running Nginx) BUT would rather handle this issue on the application level. Any help on this issue would be most appreciated.
First things first: woocommerce_recently_viewed isn't a "session cookie" in the PHP sense. It is a normal cookie created with the setcookie PHP function.
This means that neither ini_set nor wc_session_use_secure_cookie will change that behaviour.
I've downloaded the WooCommerce source code and found woocommerce\includes\wc-product-functions.php:
/**
* Track product views.
*/
function wc_track_product_view() {
if ( ! is_singular( 'product' ) || ! is_active_widget( false, false, 'woocommerce_recently_viewed_products', true ) ) {
return;
}
global $post;
if ( empty( $_COOKIE['woocommerce_recently_viewed'] ) )
$viewed_products = array();
else
$viewed_products = (array) explode( '|', $_COOKIE['woocommerce_recently_viewed'] );
if ( ! in_array( $post->ID, $viewed_products ) ) {
$viewed_products[] = $post->ID;
}
if ( sizeof( $viewed_products ) > 15 ) {
array_shift( $viewed_products );
}
// Store for session only
wc_setcookie( 'woocommerce_recently_viewed', implode( '|', $viewed_products ) );
}
add_action( 'template_redirect', 'wc_track_product_view', 20 );
The wc_setcookie is defined as follows (woocommerce\includes\wc-core-functions.php):
/**
* Set a cookie - wrapper for setcookie using WP constants.
*
* #param string $name Name of the cookie being set.
* #param string $value Value of the cookie.
* #param integer $expire Expiry of the cookie.
* #param string $secure Whether the cookie should be served only over https.
*/
function wc_setcookie( $name, $value, $expire = 0, $secure = false ) {
if ( ! headers_sent() ) {
setcookie( $name, $value, $expire, COOKIEPATH ? COOKIEPATH : '/', COOKIE_DOMAIN, $secure );
} elseif ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
headers_sent( $file, $line );
trigger_error( "{$name} cookie cannot be set - headers already sent by {$file} on line {$line}", E_USER_NOTICE );
}
}
As you can see, there isn't any wordpress filter to hook into (should really be a setting, ask a feature request!), so you need to add the $secure parameter to the woocommerce sources...
...but there is another way (kinda monkey-patch, but hey, at least we don't break things across updates):
function custom_wc_track_product_view() {
if ( ! is_singular( 'product' ) || ! is_active_widget( false, false, 'woocommerce_recently_viewed_products', true ) ) {
return;
}
global $post;
if ( empty( $_COOKIE['woocommerce_recently_viewed'] ) )
$viewed_products = array();
else
$viewed_products = (array) explode( '|', $_COOKIE['woocommerce_recently_viewed'] );
if ( ! in_array( $post->ID, $viewed_products ) ) {
$viewed_products[] = $post->ID;
}
if ( sizeof( $viewed_products ) > 15 ) {
array_shift( $viewed_products );
}
// Store for session only
wc_setcookie( 'woocommerce_recently_viewed', implode( '|', $viewed_products ), 0, true );
}
remove_action( 'template_redirect', 'wc_track_product_view', 20 );
add_action( 'template_redirect', 'custom_wc_track_product_view', 20 );
I've copied the function with another name, did the changes I needed, then I've substituted original hook with mine. Put this code in a new plugin or in theme functions.php.
Sadly, there isn't a better way without WooCommerce collaboration.

Woocommerce sortable columns not working

I've added a couple of custom field columns to our Woocommerce orders list in the admin of WordPress using the methods below, but the sort is not working....
add_filter( 'manage_edit-shop_order_columns', 'my_wc_columns' );
function my_wc_columns($columns){
$new_columns = (is_array($columns)) ? $columns : array();
unset( $new_columns['order_actions'] );
$new_columns['program_id'] = 'Program';
$new_columns['constituent_id'] = 'Constituent ID';
$new_columns['order_actions'] = $columns['order_actions'];
return $new_columns;
}
add_action( 'manage_shop_order_posts_custom_column', 'my_wc_column_values', 2 );
function my_wc_column_values($column){
global $post;
if ( $column == 'program_id' ) {
$program = get_post_meta( $post->ID, '_program_id', true );
$program_title = get_the_title($program);
$column_val = (isset($program) && $program>0 ? $program_title : 'All');
echo '<span>' . my_programs_get_name( $column_val ) . ' (' . $program . ')</span>';
}
if ( $column == 'constituent_id' ) {
$consid = get_post_meta( $post->ID, 'constituent_id', true );
$column_val = (isset($consid) && $consid != "") ? $consid : "";
echo '<span>' . $column_val . '</span>';
}
}
// Make column sortable
add_filter( "manage_edit-shop_order_sortable_columns", 'my_wc_column_sort' );
function my_wc_column_sort( $columns ) {
$custom = array(
'program_id' => '_program_id',
'constituent_id' => 'constituent_id',
);
return wp_parse_args( $custom, $columns );
}
I expected to have an issue perhaps with the program name, since it is an id that needs to be translated via a custom function to a name, but neither column is sorting properly. The records change order after clicking their column titles, but I cannot tell how the sort is being done. The program is not sorting on name or ID and both are seem random but consistent. Keep in mind both fields are custom fields that may or may not have a value defined. How can I make this sortable?
Here's a good tutorial on custom sortable columns. After you register the column, you need to handle the actual sorting. Sadly, that part doesn't happen automagically. Untested, but adapted from the above tutorial:
add_action( 'pre_get_posts', 'manage_wp_posts_be_qe_pre_get_posts', 1 );
function manage_wp_posts_be_qe_pre_get_posts( $query ) {
/**
* We only want our code to run in the main WP query
* AND if an orderby query variable is designated.
*/
if ( $query->is_main_query() && ( $orderby = $query->get( 'orderby' ) ) ) {
switch( $orderby ) {
// If we're ordering by 'program_id'
case 'program_id':
// set our query's meta_key, which is used for custom fields
$query->set( 'meta_key', '_program_id' );
/**
* Tell the query to order by our custom field/meta_key's
* value
*
* If your meta value are numbers, change 'meta_value'
* to 'meta_value_num'.
*/
$query->set( 'orderby', 'meta_value' );
break;
}
}
}

Resources