WooCommerce auto restore stock on order cancel - php

I'm looking for a solution to automatically restore stock when order is canceled. I'm using PayU gateway on my client's site and it works by setting all orders on hold when waiting for payment and cancels them after 24 hours if there's no valid payment made. However, this means that WooCommerce built-in mechanism for freezing stock and then restocking after given time doesn't work (as the payment gateway sets the status to on hold).
There was a plugin called WooCommerce Auto Restore Stock by Gerhard Potgieter but it's an oldie and I was unable to find a similar solution in WordPress plugin repository or on Codecanyon.
Perhaps someone stumbled upon a solution to auto restore stock for cancelled orders that works with WC 3.0+?

According to woocommerce github issue here, they say that (24 hours hold stock & cancel without restock) is correct behavior. Orders can be cancelled for a variety of reasons - incorrect stock levels, faulty products, user choice etc etc therefore the re-increment of stock should be an entirely (manual) admin decision.
So, i try to override the function in my child theme`s functions.php and it works!
Here's the code :
remove_filter( 'woocommerce_cancel_unpaid_orders', 'wc_cancel_unpaid_orders' );
add_filter( 'woocommerce_cancel_unpaid_orders', 'override_cancel_unpaid_orders' );
function override_cancel_unpaid_orders() {
$held_duration = get_option( 'woocommerce_hold_stock_minutes' );
if ( $held_duration < 1 || 'yes' !== get_option( 'woocommerce_manage_stock' ) ) {
return;
}
$data_store = WC_Data_Store::load( 'order' );
$unpaid_orders = $data_store->get_unpaid_orders( strtotime( '-' . absint( $held_duration ) . ' MINUTES', current_time( 'timestamp' ) ) );
if ( $unpaid_orders ) {
foreach ( $unpaid_orders as $unpaid_order ) {
$order = wc_get_order( $unpaid_order );
if ( apply_filters( 'woocommerce_cancel_unpaid_order', 'checkout' === $order->get_created_via(), $order ) ) {
//Cancel Order
$order->update_status( 'cancelled', __( 'Unpaid order cancelled - time limit reached.', 'woocommerce' ) );
//Restock
foreach ($order->get_items() as $item_id => $item) {
// Get an instance of corresponding the WC_Product object
$product = $item->get_product();
$qty = $item->get_quantity(); // Get the item quantity
wc_update_product_stock($product, $qty, 'increase');
}
}
}
}
wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' );
wp_schedule_single_event( time() + ( absint( $held_duration ) * 60 ), 'woocommerce_cancel_unpaid_orders' );
}
Hope it helps.

Related

WooCommerce Subscriptions: Is there a hook for a manual payment retry?

I have Woocommerce orders that are pending payment. At the moment when an order hits the automatic retry payment schedule it pulls the payment information from the subscription, which works great as the customer may have corrected their wrong details by then.
But in some cases, we need to manually retry the payment. When this is done, it doesn't pull the payment information from the subscription (as the customer may have corrected their details for it to go through).
Is there a hook/action that I can use to fire the following code? woocommerce_subscriptions_before_payment_retry doesn't seem to work.
add_action('woocommerce_subscriptions_before_payment_retry', 'remove_payment_details', 10, 2 );
function remove_payment_details( $order_id ){
$order = wc_get_order( $order_id ); // Order Object
$subscriptions = wcs_get_subscriptions_for_order( $order_id, array( 'order_type' => 'any' ) ); // Array of subscriptions Objects
foreach( $subscriptions as $subscription_id => $subscription ){
$stripe_cust_id = $subscription->get_meta( '_stripe_customer_id');
$stripe_src_id = $subscription->get_meta( '_stripe_source_id' );
$order->update_meta_data( '_stripe_customer_id', $stripe_cust_id );
$order->update_meta_data( '_stripe_source_id', $stripe_src_id );
$order->save();
}
}
Please check woocommerce_order_action_wcs_retry_renewal_payment hook.
The hook is for "Retry Renewal Payment" handling, and you can use it.
add_action( 'woocommerce_order_action_wcs_retry_renewal_payment', 'custom_process_retry_renewal_payment_action_request', 20, 1 );
function custom_process_retry_renewal_payment_action_request( $order ) {
// your code is here
}

How to list all users not currently logged in and check if each user has bought a specific product in woocommerce [duplicate]

I need to check if a customer has purchased a specific product earlier in WooCommerce.
The case is this: The customer shall not be able to purchase product "c", "d", "e" unless they have purchased product "a" or "b" at an earlier time.
If the customer has purchased product "a" or "b" earlier, then the purchase button of product "c", "d" and "e" is activated and they are allowed to buy them.
If they haven´t purchased "a" or "b" earlier, they will not be allowed to purchase "c", "d", "e" and the purchase button is deactivated.
How can I achieve this?
Thanks.
Lighter and improved code version in HERE that handle multiple product IDs
Updated (compatibility for Woocommerce 3+)
Yes it's possible, writing a conditional function that returns "true" if current customer has already bought specifics defined products IDs. This code goes on function.php file of your active child theme or theme.
Here is the conditional function:
function has_bought_items() {
$bought = false;
// Set HERE ine the array your specific target product IDs
$prod_arr = array( '21', '67' );
// Get all customer orders
$customer_orders = get_posts( array(
'numberposts' => -1,
'meta_key' => '_customer_user',
'meta_value' => get_current_user_id(),
'post_type' => 'shop_order', // WC orders post type
'post_status' => 'wc-completed' // Only orders with status "completed"
) );
foreach ( $customer_orders as $customer_order ) {
// Updated compatibility with WooCommerce 3+
$order_id = method_exists( $order, 'get_id' ) ? $order->get_id() : $order->id;
$order = wc_get_order( $customer_order );
// Iterating through each current customer products bought in the order
foreach ($order->get_items() as $item) {
// WC 3+ compatibility
if ( version_compare( WC_VERSION, '3.0', '<' ) )
$product_id = $item['product_id'];
else
$product_id = $item->get_product_id();
// Your condition related to your 2 specific products Ids
if ( in_array( $product_id, $prod_arr ) )
$bought = true;
}
}
// return "true" if one the specifics products have been bought before by customer
return $bought;
}
This code is tested and works.
USAGE:
For example, you can use it in some WooCommerce templates that you will have previously copied to your active child theme or theme:
The template for Shop page concerning add-to-cart button is loop/add-to-cart.php.
The templates for Product pages concerning add-to-cart button are in single-product/add-to-cart folder depending on your product types.
Here is an example that you could use in those templates (above):
// Replace the numbers by your special restricted products IDs
$restricted_products = array( '20', '32', '75' );
// compatibility with WC +3
$product_id = method_exists( $product, 'get_id' ) ? $product->get_id() : $product->id;
// customer has NOT already bought a specific product for this restricted products
if ( !has_bought_items() && in_array( $product_id, $restricted_products ) ) {
// Displaying an INACTIVE add-to-cart button (With a custom text, style and without the link).
// (AND optionally) an explicit message for example.
// ALL OTHER PRODUCTS OR RESTRICTED PRODUCTS IF COSTUMER HAS ALREADY BOUGHT SPECIAL PRODUCTS
} else {
// place for normal Add-To-Cart button code here
}
And here the complete applied example to add-to-cart button template on Shop page:
<?php
/**
* Loop Add to Cart
*
* This template can be overridden by copying it to yourtheme/woocommerce/loop/add-to-cart.php.
*
* HOWEVER, on occasion WooCommerce will need to update template files and you
* (the theme developer) will need to copy the new files to your theme to
* maintain compatibility. We try to do this as little as possible, but it does
* happen. When this occurs the version of the template file will be bumped and
* the readme will list any important changes.
*
* #see https://docs.woocommerce.com/document/template-structure/
* #author WooThemes
* #package WooCommerce/Templates
* #version 2.5.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
global $product;
// Replace the numbers by your special restricted products IDs
$restricted_products = array( '37', '53', '70' );
// compatibility with WC +3
$product_id = method_exists( $product, 'get_id' ) ? $product->get_id() : $product->id;
if ( !has_bought_items() && in_array( $product_id, $restricted_products ) ) {
echo '<a class="button greyed_button">' . __("Disabled", "your_theme_slug") . '</a>';
echo '<br><span class="greyed_button-message">' . __("Your message goes here…", "your_theme_slug") . '</span>';
} else {
echo apply_filters( 'woocommerce_loop_add_to_cart_link',
sprintf( '<a rel="nofollow" href="%s" data-quantity="%s" data-product_id="%s" data-product_sku="%s" class="%s">%s</a>',
esc_url( $product->add_to_cart_url() ),
esc_attr( isset( $quantity ) ? $quantity : 1 ),
esc_attr( $product_id ),
esc_attr( $product->get_sku() ),
esc_attr( isset( $class ) ? $class : 'button' ),
esc_html( $product->add_to_cart_text() )
),
$product );
}
You will style the inactive button with greyed_button class in the style.css file of your active child theme or theme. Same thing for the message with greyed_button-message class.
2020 update: Now handles guest users from their billing email.
New version compact, lighter, faster and compatible with all versions of woocommerce (from version 2.4 and above)
This is a new enhanced and lighter conditional function partially based on built-in woocommerce function wc_customer_bought_product source code.
There are 2 optional arguments:
$user_var will allow you to:
specify a defined user ID (when is not used for current logged in user)
or the billing email for guests users;
$product_ids (array) will allow to specify one or multiple product Ids to check
Here is that code:
function has_bought_items( $user_var = 0, $product_ids = 0 ) {
global $wpdb;
// Based on user ID (registered users)
if ( is_numeric( $user_var) ) {
$meta_key = '_customer_user';
$meta_value = $user_var == 0 ? (int) get_current_user_id() : (int) $user_var;
}
// Based on billing email (Guest users)
else {
$meta_key = '_billing_email';
$meta_value = sanitize_email( $user_var );
}
$paid_statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() );
$product_ids = is_array( $product_ids ) ? implode(',', $product_ids) : $product_ids;
$line_meta_value = $product_ids != ( 0 || '' ) ? 'AND woim.meta_value IN ('.$product_ids.')' : 'AND woim.meta_value != 0';
// Count the number of products
$count = $wpdb->get_var( "
SELECT COUNT(p.ID) FROM {$wpdb->prefix}posts AS p
INNER JOIN {$wpdb->prefix}postmeta AS pm ON p.ID = pm.post_id
INNER JOIN {$wpdb->prefix}woocommerce_order_items AS woi ON p.ID = woi.order_id
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS woim ON woi.order_item_id = woim.order_item_id
WHERE p.post_status IN ( 'wc-" . implode( "','wc-", $paid_statuses ) . "' )
AND pm.meta_key = '$meta_key'
AND pm.meta_value = '$meta_value'
AND woim.meta_key IN ( '_product_id', '_variation_id' ) $line_meta_value
" );
// Return true if count is higher than 0 (or false)
return $count > 0 ? true : false;
}
Code goes in functions.php file of your active child theme (or theme) or also in any plugin file.
This code is tested on WooCommerce 3+ and works (It should work on previous versions too).
USAGE EXAMPLES:
Example 1 (logged in customer): Detecting if current user has bought one of the defined products (product Ids needs to be an array)
// Define the targeted Products IDs
$product_ids = array( 38, 41, 85, 95 );
if( has_bought_items( '', $product_ids ) )
echo "<p>You have already purchased one of this products</p>";
else
echo "<p>You have not yet purchased one of this products</p>";
Example 2 (for a defined user id) Detecting if the defined user has bought one of the defined products (product Ids needs to be set in an array)
// Define the user ID
$user_id = 85;
// Define the targeted Products IDs
$product_ids = array( 38, 41, 85, 95 );
if( has_bought_items( $user_id, $product_ids ) )
echo "<p>This user have already purchased one of this products</p>";
else
echo "<p>This user have not yet purchased one of this products</p>";
If the $user_id is not defined and the current user is not logged in, this function will return false.
Example 3 (for guest user) Detecting if guest user has bought one of the defined products from his billing email (product Ids needs to be set in an array)
// Define guest Billing email (string)
$user_email = 'louis.fourteen#gmail.com';
// Define the targeted Products IDs
$product_ids = array( 38, 41, 85, 95 );
if( has_bought_items( $user_email, $product_ids ) )
echo "<p>This user have already purchased one of this products</p>";
else
echo "<p>This user have not yet purchased one of this products</p>";
If the $user_id is not defined and the current user is not logged in, this function will return false.
Example 4 (logged in customer): Detecting if current user has already made a purchase
if( has_bought_items() )
echo '<p>You have already made a purchase</p>';
else
echo '<p>Welcome, for your first purchase you will get a discount of 10%</p>';
Example 5 (Defining the user id) - Detecting if the defined user has already made a purchase
// Define the user ID
$user_id = 85;
if( has_bought_items( $user_id ) )
echo '<p>customer have already made a purchase</p>';
else
echo '<p>Customer with 0 purshases</p>';
Now, if user id is equal to 0 and the current user is not logged in, this function will return false (if no billing email is defined for guest users).
The built-in woocommerce function wc_customer_bought_product can also be used in this case.
See function usage here.
I'd be doing it this way;
this won't be doing any woocommerce template modification but only use the filter woocommerce_is_purchasable
These are all the functions,
check if current product ID is already brought by customer, in version 2.6 +, woocommerce implemented the function wc_customer_bought_product to check if the customer already brought the product, I've never used it before but based on the docs you can create a function like below to check an array of product ID if one of them has been already brought;
function cmk_check_product_brought( $ids=array() ) {
if ( ! $ids ) return false;
foreach ( $ids as $product => $id ) {
if ( wc_customer_bought_product( wp_get_current_user()->user_email, get_current_user_id(), $id ) ) {
return true;
}
}
}
The previous method I do to check if specific product ID has already been brought by the customer is below, so you can use any of these function, though the one I created is not for array of ID's
function cmk_product_ordered( $id ) {
// Get All order of current user
$orders = get_posts( array(
'numberposts' => -1,
'meta_key' => '_customer_user',
'meta_value' => get_current_user_id(),
'post_type' => wc_get_order_types( 'view-orders' ),
'post_status' => array_keys( wc_get_order_statuses() )
) );
if ( !$orders ) return false; // return if no order found
$all_ordered_product = array(); // store all products ordered by ID in an array
foreach ( $orders as $order => $data ) { // Loop through each order
$order_data = new WC_Order( $data->ID ); // create new object for each order
foreach ( $order_data->get_items() as $key => $item ) { // loop through each order item
// store in array with product ID as key and order date a value
$all_ordered_product[ $item['product_id'] ] = $data->post_date;
}
}
// check if defined ID is found in array
if ( isset( $all_ordered_product[ $id ] ) ) return true;
else return false;
}
Now that we can check if the product ID is already brought or not, we can simply add a filter on woocommerce_is_purchasable to create our own condition, this function below is a simply example of what you are trying to achieve,
just changed the $required_purchased and $conditional_purchase values.
function cmk_disable_product_purchase( $purchasable, $product ) {
// array of products required to be purchase first
$required_purchased = array( 1, 2);
// array of restricted/conditional products to be purchase
$conditional_purchase = array( 3,4,5);
// Get the ID for the current product
$product_id = $product->is_type( 'variation' ) ? $product->variation_id : $product->id;
//return default $purchasable if current product ID is not in restricted array
if ( !in_array($product_id, $conditional_purchase)) return $purchasable;
/**
** Check if one required products has been purchase;
**/
// using cmk_check_product_brought() function, return false if product is not purchase
if ( ! cmk_check_product_brought( $required_purchased ) ) $purchasable = false;
// using cmk_product_ordered() function, you can use this instead
/*if ( cmk_product_ordered( 1 ) || cmk_product_ordered( 2 ) ) {
$purchasable = $purchasable; //return default if one product is purchased
} else {
$purchasable = false;
}*/
// Double-check for variations: if parent is not purchasable, then variation is not
if ( $purchasable && $product->is_type( 'variation' ) ) {
$purchasable = $product->parent->is_purchasable();
}
return $purchasable;
}
add_filter( 'woocommerce_variation_is_purchasable', 'cmk_disable_product_purchase', 10, 2 );
add_filter( 'woocommerce_is_purchasable', 'cmk_disable_product_purchase', 10, 2 );
And that should set the product as non-purchasable (add to cart button will be automatically hidden ).
Now if you want to add a message for non-purchasable product, you can simply use the same condition of cmk_disable_product_purchase, add your message and simply hook it on woocommerce_single_product_summary or anywhere you want it to display.

Store custom data using WC_Cart add_to_cart() method in Woocommerce 3

I am creating a membership site and totally created static pages for each Membership plans (have only 3 plans). However, I have added products for each plan and when I hit SELECT PLAN button I redirect to some custom form where I ask users range of info we are going to use to fulfil the plan (same as sneakertub.com).
I have written code into the PHP page which will handle SUBMIT action of the form. This PHP file, infopage.php, will process POST data I sent via POST call and stores these all data into WC session.
$customer_name = $_POST["customer_name"];
$customer_email = $_POST["customer_email"];
$customer_sex = $_POST["customer_sex"];
$customer_age = $_POST["customer_age"];
$product_id = $_POST["product_id"];
global $wp_session;
$data = array(
'customer_name' => $customer_name,
'customer_email' => $customer_email,
'customer_sex' => $customer_sex,
'customer_age' => $customer_age);
$wp_session['custom_SESSION_child']=$data;
WC()->session->set('custom_data_child', $data);
//Add product to WooCommerce cart.
WC()->cart->add_to_cart( $product_id )
However, I don't think the above code works. As I don't find values into session with any of the above technique. I have used wp_session, WC()->session and $_SESSION but no approach is working.
I am trying to access these values into functions.php this way,
add_action( 'woocommerce_before_calculate_totals', 'twf_additional_price', 1, 3 );
function twf_additional_price( $cart_object ) {
global $wpdb;
global $wp_session;
$session_data_2 = $wp_session['custom_SESSION_child'];
$session_data = WC()->session->get('custom_data_child');
var_dump($session_data);
var_dump($session_data2);
foreach ( $cart_object->cart_contents as $key => $value ) {
$extra_charge = 0;
if(isset($value['twf_user_custom_datas'])){
$extra_charge = 100;
}
$value['data']->set_price($value['data']->price + $extra_charge);
}
}
For now ignore the for loop. Main thing is
var_dump($session_data);
var_dump($session_data2);
both dumps only NULL.
My main goal is to add the all above fields into Woocommerce checkout and order pages.
Please let me know what is wrong here. I know I might be working on very bad approach but I want Plan selection to checkout process same as sneakertub.com. Please let me know if there is any tutorial on this or proper way to do this. I prefer doing this without plugins but I am ready to use plugins as well.
I appreciate your attention.
Updated - Instead of using sessions, you should use the last available argument in WC_Cart add_to_cart() method, which will allow you to add any custom cart item data.
For cart item price change based on calculations, is better to make the new price calculation before and to set it in that custom cart item data.
Try the following instead:
1) For your code in the php page:
$custom_data = array(); // Initializing
// Set the posted data as cart item custom data
if( isset($_POST['customer_name']) && ! empty($_POST['customer_name']) )
$custom_data['custom_data']['name'] = sanitize_text_field( $_POST['customer_name'] );
if( isset($_POST['customer_email']) && ! empty($_POST['customer_email']) )
$custom_data['custom_data']['email'] = sanitize_text_field( $_POST['customer_email'] );
if( isset($_POST['customer_sex']) && ! empty($_POST['customer_sex']) )
$custom_data['custom_data']['sex'] = sanitize_text_field( $_POST['customer_sex'] );
if( isset($_POST['customer_age']) && ! empty($_POST['customer_age']) )
$custom_data['custom_data']['age'] = sanitize_text_field( $_POST['customer_age'] );
// Set the calculated item price as custom cart item data
if( isset($custom_data['custom_data']) && sizeof($custom_data['custom_data']) > 0 && $product_id > 0 ) {
// Get an instance of the WC_Product object
$product = wc_get_product( $product_id );
// Save the new calculated price as custom cart item data
$custom_data['custom_data']['new_price'] = $product->get_price() + 100;
}
// Add product to cart with the custom cart item data
WC()->cart->add_to_cart( $product_id, '1', '0', array(), $custom_data );
Code goes in function.php file of your active child theme (or active theme). Tested and works.
2) Your revisited function that will change the cart item price:
add_action( 'woocommerce_before_calculate_totals', 'custom_cart_item_price', 30, 1 );
function custom_cart_item_price( $cart ) {
if ( is_admin() && ! defined( 'DOING_AJAX' ) )
return;
if ( did_action( 'woocommerce_before_calculate_totals' ) >= 2 )
return;
foreach ( $cart->get_cart() as $cart_item ) {
if( isset($cart_item['custom_data']['new_price']) )
$cart_item['data']->set_price( $cart_item['custom_data']['new_price'] );
}
}
Code goes in function.php file of your active child theme (or active theme). Tested and works.
All other custom cart item data is available under the cart item key 'custom_data' as an indexed array… So you will be able to get that data easily from the cart object, to save it in the order.

WooCommerce: autocomplete paid orders based on shipping method

I have a product that people can print directly (shipping method 1) or choose to get it via shipping service (shipping method 2). So the order should auto complete if they choose to print it directly (shipping method 2) ONLY.
Is it possible to extend that code snippet from WooCommerce?
From docs I found
this
/**
* Auto Complete all WooCommerce orders.
*/
add_action( 'woocommerce_thankyou', 'custom_woocommerce_auto_complete_order');
function custom_woocommerce_auto_complete_order( $order_id ) {
if ( ! $order_id ) {
return;
}
$order = wc_get_order( $order_id );
$order->update_status( 'completed' );
}
Here is the working solution. BIG THANKS TO LoicTheAztec:
add_action( 'woocommerce_thankyou',
'wc_auto_complete_paid_order_based_on_shipping_method', 20, 1 );
function wc_auto_complete_paid_order_based_on_shipping_method( $order_id ) {
if ( ! $order_id ) return;
// HERE define the allowed shipping methods IDs (can be names or slugs changing the code a bit)
$allowed_shipping_methods = array( '5' );
// Get an instance of the WC_Order object
$order = wc_get_order( $order_id );
// Get the shipping related data for this order:
$shipping_item = $order->get_items('shipping');
$item = reset($shipping_item);
$item_data = $item->get_data();
// Get the shipping method name, rate ID and type slug
$method_rate_id = $item_data['instance_id']; // Shipping method ID
// No updated status for orders delivered with Bank wire, Cash on
delivery and Cheque payment methods.
$avoided_statuses = array( 'bacs', 'cod', 'cheque');
if ( in_array( $order->get_payment_method(), $avoided_statuses ) ){
return;
}
// update status to "completed" for paid Orders and a defined shipping
method ID
elseif ( in_array( $method_rate_id, $allowed_shipping_methods ) ){
$order->update_status( 'completed' );
}
}
First, get_post_meta($order_id, '_payment_method', true ) or $order->get_payment_method() are completely similar and do the same thing.
The difference is that the first one use a Wordpress function to access this data from wp_postmeta table and the 2nd one is a WC_Order method that will also access the same data from wp_postmeta table…
No one is better than the other.
New enhanced revisited code (see this thread for explanations).
What is the instance ID:
For example if the Shipping method rate Id is flat_rate:14, the instance Id is 14 (unique ID)
The new version code:
add_action( 'woocommerce_payment_complete_order_status', 'auto_complete_paid_order_based_on_shipping_method', 10, 3 );
function auto_complete_paid_order_based_on_shipping_method( $status, $order_id, $order ) {
// HERE define the allowed shipping methods instance IDs
$allowed_shipping_methods_instance_ids = array( '14', '19' );
// Loop through order "shipping" items
foreach ( $order->get_shipping_methods() as $shipping_method ) {
if( in_array( $shipping_method->get_instance_id(), $allowed_shipping_methods_instance_ids ) ) {
return 'completed';
}
}
return $status;
}
Code goes in function.php file of the active child theme (or active theme). Tested and works.
Original answer:
The answer below comes from this similar answer I have made some time ago:
WooCommerce: Auto complete paid orders
add_action( 'woocommerce_thankyou', 'auto_complete_paid_order_based_on_shipping_method', 10, 1 );
function auto_complete_paid_order_based_on_shipping_method( $order_id ) {
if ( ! $order_id ) return;
// HERE define the allowed shipping methods IDs (can be names or slugs changing the code a bit)
$allowed_shipping_methods = array( 'flat_rate:14', 'flat_rate:19' );
// Get an instance of the WC_Order object
$order = wc_get_order( $order_id );
// Get the shipping related data for this order:
$shipping_item = $order->get_items('shipping');
$item = reset($shipping_item);
$item_data = $item->get_data();
// Get the shipping method name, rate ID and type slug
$shipping_name = $item_data['name']; // Shipping method name
$method_rate_id = $item_data['method_id']; // Shipping method ID
$method_arr = explode( ':', $method_rate_id );
$method_type = $method_arr[0]; // Shipping method type slug
// No updated status for orders delivered with Bank wire, Cash on delivery and Cheque payment methods.
$avoided_statuses = array( 'bacs', 'cod', 'cheque');
if ( in_array( $order->get_payment_method(), $avoided_statuses ) ){
return;
}
// update status to "completed" for paid Orders and a defined shipping method ID
elseif ( in_array( $method_rate_id, $allowed_shipping_methods ) ){
$order->update_status( 'completed' );
}
}
Code goes in function.php file of the active child theme (or active theme).
Tested and works.
I think what you're looking for is $order->has_shipping_method('name_of_method')
From: https://docs.woocommerce.com/wc-apidocs/class-WC_Abstract_Order.html#_has_shipping_method)
That is my working solution (Thanks to LoicTheAztec):
add_action( 'woocommerce_thankyou',
'wc_auto_complete_paid_order_based_on_shipping_method', 20, 1 );
function wc_auto_complete_paid_order_based_on_shipping_method( $order_id ) {
if ( ! $order_id ) return;
// HERE define the allowed shipping methods IDs (can be names or slugs changing the code a bit)
$allowed_shipping_methods = array( '5' );
// Get an instance of the WC_Order object
$order = wc_get_order( $order_id );
// Get the shipping related data for this order:
$shipping_item = $order->get_items('shipping');
$item = reset($shipping_item);
$item_data = $item->get_data();
// Get the shipping method name, rate ID and type slug
$method_rate_id = $item_data['instance_id']; // Shipping method ID
// No updated status for orders delivered with Bank wire, Cash on delivery and Cheque payment methods.
$avoided_statuses = array( 'bacs', 'cod', 'cheque');
if ( in_array( $order->get_payment_method(), $avoided_statuses ) ){
return;
}
// update status to "completed" for paid Orders and a defined shipping method ID
elseif ( in_array( $method_rate_id, $allowed_shipping_methods ) ){
$order->update_status( 'completed' );
}
}

Check if a user/guest has purchased specific products in WooCommerce

I need to check if a customer has purchased a specific product earlier in WooCommerce.
The case is this: The customer shall not be able to purchase product "c", "d", "e" unless they have purchased product "a" or "b" at an earlier time.
If the customer has purchased product "a" or "b" earlier, then the purchase button of product "c", "d" and "e" is activated and they are allowed to buy them.
If they haven´t purchased "a" or "b" earlier, they will not be allowed to purchase "c", "d", "e" and the purchase button is deactivated.
How can I achieve this?
Thanks.
Lighter and improved code version in HERE that handle multiple product IDs
Updated (compatibility for Woocommerce 3+)
Yes it's possible, writing a conditional function that returns "true" if current customer has already bought specifics defined products IDs. This code goes on function.php file of your active child theme or theme.
Here is the conditional function:
function has_bought_items() {
$bought = false;
// Set HERE ine the array your specific target product IDs
$prod_arr = array( '21', '67' );
// Get all customer orders
$customer_orders = get_posts( array(
'numberposts' => -1,
'meta_key' => '_customer_user',
'meta_value' => get_current_user_id(),
'post_type' => 'shop_order', // WC orders post type
'post_status' => 'wc-completed' // Only orders with status "completed"
) );
foreach ( $customer_orders as $customer_order ) {
// Updated compatibility with WooCommerce 3+
$order_id = method_exists( $order, 'get_id' ) ? $order->get_id() : $order->id;
$order = wc_get_order( $customer_order );
// Iterating through each current customer products bought in the order
foreach ($order->get_items() as $item) {
// WC 3+ compatibility
if ( version_compare( WC_VERSION, '3.0', '<' ) )
$product_id = $item['product_id'];
else
$product_id = $item->get_product_id();
// Your condition related to your 2 specific products Ids
if ( in_array( $product_id, $prod_arr ) )
$bought = true;
}
}
// return "true" if one the specifics products have been bought before by customer
return $bought;
}
This code is tested and works.
USAGE:
For example, you can use it in some WooCommerce templates that you will have previously copied to your active child theme or theme:
The template for Shop page concerning add-to-cart button is loop/add-to-cart.php.
The templates for Product pages concerning add-to-cart button are in single-product/add-to-cart folder depending on your product types.
Here is an example that you could use in those templates (above):
// Replace the numbers by your special restricted products IDs
$restricted_products = array( '20', '32', '75' );
// compatibility with WC +3
$product_id = method_exists( $product, 'get_id' ) ? $product->get_id() : $product->id;
// customer has NOT already bought a specific product for this restricted products
if ( !has_bought_items() && in_array( $product_id, $restricted_products ) ) {
// Displaying an INACTIVE add-to-cart button (With a custom text, style and without the link).
// (AND optionally) an explicit message for example.
// ALL OTHER PRODUCTS OR RESTRICTED PRODUCTS IF COSTUMER HAS ALREADY BOUGHT SPECIAL PRODUCTS
} else {
// place for normal Add-To-Cart button code here
}
And here the complete applied example to add-to-cart button template on Shop page:
<?php
/**
* Loop Add to Cart
*
* This template can be overridden by copying it to yourtheme/woocommerce/loop/add-to-cart.php.
*
* HOWEVER, on occasion WooCommerce will need to update template files and you
* (the theme developer) will need to copy the new files to your theme to
* maintain compatibility. We try to do this as little as possible, but it does
* happen. When this occurs the version of the template file will be bumped and
* the readme will list any important changes.
*
* #see https://docs.woocommerce.com/document/template-structure/
* #author WooThemes
* #package WooCommerce/Templates
* #version 2.5.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
global $product;
// Replace the numbers by your special restricted products IDs
$restricted_products = array( '37', '53', '70' );
// compatibility with WC +3
$product_id = method_exists( $product, 'get_id' ) ? $product->get_id() : $product->id;
if ( !has_bought_items() && in_array( $product_id, $restricted_products ) ) {
echo '<a class="button greyed_button">' . __("Disabled", "your_theme_slug") . '</a>';
echo '<br><span class="greyed_button-message">' . __("Your message goes here…", "your_theme_slug") . '</span>';
} else {
echo apply_filters( 'woocommerce_loop_add_to_cart_link',
sprintf( '<a rel="nofollow" href="%s" data-quantity="%s" data-product_id="%s" data-product_sku="%s" class="%s">%s</a>',
esc_url( $product->add_to_cart_url() ),
esc_attr( isset( $quantity ) ? $quantity : 1 ),
esc_attr( $product_id ),
esc_attr( $product->get_sku() ),
esc_attr( isset( $class ) ? $class : 'button' ),
esc_html( $product->add_to_cart_text() )
),
$product );
}
You will style the inactive button with greyed_button class in the style.css file of your active child theme or theme. Same thing for the message with greyed_button-message class.
2020 update: Now handles guest users from their billing email.
New version compact, lighter, faster and compatible with all versions of woocommerce (from version 2.4 and above)
This is a new enhanced and lighter conditional function partially based on built-in woocommerce function wc_customer_bought_product source code.
There are 2 optional arguments:
$user_var will allow you to:
specify a defined user ID (when is not used for current logged in user)
or the billing email for guests users;
$product_ids (array) will allow to specify one or multiple product Ids to check
Here is that code:
function has_bought_items( $user_var = 0, $product_ids = 0 ) {
global $wpdb;
// Based on user ID (registered users)
if ( is_numeric( $user_var) ) {
$meta_key = '_customer_user';
$meta_value = $user_var == 0 ? (int) get_current_user_id() : (int) $user_var;
}
// Based on billing email (Guest users)
else {
$meta_key = '_billing_email';
$meta_value = sanitize_email( $user_var );
}
$paid_statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() );
$product_ids = is_array( $product_ids ) ? implode(',', $product_ids) : $product_ids;
$line_meta_value = $product_ids != ( 0 || '' ) ? 'AND woim.meta_value IN ('.$product_ids.')' : 'AND woim.meta_value != 0';
// Count the number of products
$count = $wpdb->get_var( "
SELECT COUNT(p.ID) FROM {$wpdb->prefix}posts AS p
INNER JOIN {$wpdb->prefix}postmeta AS pm ON p.ID = pm.post_id
INNER JOIN {$wpdb->prefix}woocommerce_order_items AS woi ON p.ID = woi.order_id
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS woim ON woi.order_item_id = woim.order_item_id
WHERE p.post_status IN ( 'wc-" . implode( "','wc-", $paid_statuses ) . "' )
AND pm.meta_key = '$meta_key'
AND pm.meta_value = '$meta_value'
AND woim.meta_key IN ( '_product_id', '_variation_id' ) $line_meta_value
" );
// Return true if count is higher than 0 (or false)
return $count > 0 ? true : false;
}
Code goes in functions.php file of your active child theme (or theme) or also in any plugin file.
This code is tested on WooCommerce 3+ and works (It should work on previous versions too).
USAGE EXAMPLES:
Example 1 (logged in customer): Detecting if current user has bought one of the defined products (product Ids needs to be an array)
// Define the targeted Products IDs
$product_ids = array( 38, 41, 85, 95 );
if( has_bought_items( '', $product_ids ) )
echo "<p>You have already purchased one of this products</p>";
else
echo "<p>You have not yet purchased one of this products</p>";
Example 2 (for a defined user id) Detecting if the defined user has bought one of the defined products (product Ids needs to be set in an array)
// Define the user ID
$user_id = 85;
// Define the targeted Products IDs
$product_ids = array( 38, 41, 85, 95 );
if( has_bought_items( $user_id, $product_ids ) )
echo "<p>This user have already purchased one of this products</p>";
else
echo "<p>This user have not yet purchased one of this products</p>";
If the $user_id is not defined and the current user is not logged in, this function will return false.
Example 3 (for guest user) Detecting if guest user has bought one of the defined products from his billing email (product Ids needs to be set in an array)
// Define guest Billing email (string)
$user_email = 'louis.fourteen#gmail.com';
// Define the targeted Products IDs
$product_ids = array( 38, 41, 85, 95 );
if( has_bought_items( $user_email, $product_ids ) )
echo "<p>This user have already purchased one of this products</p>";
else
echo "<p>This user have not yet purchased one of this products</p>";
If the $user_id is not defined and the current user is not logged in, this function will return false.
Example 4 (logged in customer): Detecting if current user has already made a purchase
if( has_bought_items() )
echo '<p>You have already made a purchase</p>';
else
echo '<p>Welcome, for your first purchase you will get a discount of 10%</p>';
Example 5 (Defining the user id) - Detecting if the defined user has already made a purchase
// Define the user ID
$user_id = 85;
if( has_bought_items( $user_id ) )
echo '<p>customer have already made a purchase</p>';
else
echo '<p>Customer with 0 purshases</p>';
Now, if user id is equal to 0 and the current user is not logged in, this function will return false (if no billing email is defined for guest users).
The built-in woocommerce function wc_customer_bought_product can also be used in this case.
See function usage here.
I'd be doing it this way;
this won't be doing any woocommerce template modification but only use the filter woocommerce_is_purchasable
These are all the functions,
check if current product ID is already brought by customer, in version 2.6 +, woocommerce implemented the function wc_customer_bought_product to check if the customer already brought the product, I've never used it before but based on the docs you can create a function like below to check an array of product ID if one of them has been already brought;
function cmk_check_product_brought( $ids=array() ) {
if ( ! $ids ) return false;
foreach ( $ids as $product => $id ) {
if ( wc_customer_bought_product( wp_get_current_user()->user_email, get_current_user_id(), $id ) ) {
return true;
}
}
}
The previous method I do to check if specific product ID has already been brought by the customer is below, so you can use any of these function, though the one I created is not for array of ID's
function cmk_product_ordered( $id ) {
// Get All order of current user
$orders = get_posts( array(
'numberposts' => -1,
'meta_key' => '_customer_user',
'meta_value' => get_current_user_id(),
'post_type' => wc_get_order_types( 'view-orders' ),
'post_status' => array_keys( wc_get_order_statuses() )
) );
if ( !$orders ) return false; // return if no order found
$all_ordered_product = array(); // store all products ordered by ID in an array
foreach ( $orders as $order => $data ) { // Loop through each order
$order_data = new WC_Order( $data->ID ); // create new object for each order
foreach ( $order_data->get_items() as $key => $item ) { // loop through each order item
// store in array with product ID as key and order date a value
$all_ordered_product[ $item['product_id'] ] = $data->post_date;
}
}
// check if defined ID is found in array
if ( isset( $all_ordered_product[ $id ] ) ) return true;
else return false;
}
Now that we can check if the product ID is already brought or not, we can simply add a filter on woocommerce_is_purchasable to create our own condition, this function below is a simply example of what you are trying to achieve,
just changed the $required_purchased and $conditional_purchase values.
function cmk_disable_product_purchase( $purchasable, $product ) {
// array of products required to be purchase first
$required_purchased = array( 1, 2);
// array of restricted/conditional products to be purchase
$conditional_purchase = array( 3,4,5);
// Get the ID for the current product
$product_id = $product->is_type( 'variation' ) ? $product->variation_id : $product->id;
//return default $purchasable if current product ID is not in restricted array
if ( !in_array($product_id, $conditional_purchase)) return $purchasable;
/**
** Check if one required products has been purchase;
**/
// using cmk_check_product_brought() function, return false if product is not purchase
if ( ! cmk_check_product_brought( $required_purchased ) ) $purchasable = false;
// using cmk_product_ordered() function, you can use this instead
/*if ( cmk_product_ordered( 1 ) || cmk_product_ordered( 2 ) ) {
$purchasable = $purchasable; //return default if one product is purchased
} else {
$purchasable = false;
}*/
// Double-check for variations: if parent is not purchasable, then variation is not
if ( $purchasable && $product->is_type( 'variation' ) ) {
$purchasable = $product->parent->is_purchasable();
}
return $purchasable;
}
add_filter( 'woocommerce_variation_is_purchasable', 'cmk_disable_product_purchase', 10, 2 );
add_filter( 'woocommerce_is_purchasable', 'cmk_disable_product_purchase', 10, 2 );
And that should set the product as non-purchasable (add to cart button will be automatically hidden ).
Now if you want to add a message for non-purchasable product, you can simply use the same condition of cmk_disable_product_purchase, add your message and simply hook it on woocommerce_single_product_summary or anywhere you want it to display.

Categories