Prevent User Role from changing woocommerce order status - php

We would like to prevent shop manager from changing order status, we found a help here in the link below Restrict user role to change only some order statuses in Woocommerce
But the issue here that it limits certain role ( shop Manager ) to some order statuses, we need to deny the shop manager from changing the order status completely not limit it to some order statuses.
Also the snippet we mentioned remove the the order statuses from the bulk action drop down & the order details here: https://prnt.sc/mpfl3b, we need to remove the statuses too from the quick action column here https://snipboard.io/B6SYHb.jpg
Simply we try to have the shop manager to when he try to change order status from bulk, order details page, or actions column to find there is no order statuses to select to change it or disable it completely.
Best Regards

As you can see in the example code the conditions of the statuses are determined in the if statement, because you want to apply this without a limit, it's just a matter of removing that if statement and returning empty arrays
p.s; if you mark my answer as the solution, then also vote for #LoicTheAztec original answer if you have not already done this, since his code just about contained the solution.
// Admin orders list: bulk order status change dropdown
function filter_dropdown_bulk_actions_shop_order( $actions ) {
// Targeting shop_manager
if( current_user_can( 'shop_manager' ) ) {
$actions = (array) null;
}
return $actions;
}
add_filter( 'bulk_actions-edit-shop_order', 'filter_dropdown_bulk_actions_shop_order', 20, 1 );
// Admin orders list: quick action
function filter_order_actions( $actions, $order ) {
// Targeting shop_manager
if( current_user_can( 'shop_manager' ) ) {
$actions = (array) null;
}
return $actions;
}
add_filter( 'woocommerce_admin_order_actions', 'filter_order_actions', 10, 2 );
// Admin order pages: order status dropdown
function filter_order_statuses( $order_statuses ) {
global $post, $pagenow;
if( $pagenow === 'post.php' || $pagenow === 'post-new.php' ) {
// Get ID
$order_id = $post->ID;
// Get an instance of the WC_Order object
$order = wc_get_order( $order_id );
// TRUE
if ( $order ) {
// Get current order status
$order_status = 'wc-' . $order->get_status();
// New order status
$new_order_statuses = array();
foreach ($order_statuses as $key => $option ) {
// Targeting "shop_manager"
if( current_user_can('shop_manager') && $key == $order_status ) {
$new_order_statuses[$key] = $option;
}
}
if( sizeof($new_order_statuses) > 0 ) {
return $new_order_statuses;
}
}
}
return $order_statuses;
}
add_filter('wc_order_statuses', 'filter_order_statuses', 10, 1 );

//Since the suggested answer apparently causes some new issues and doesn't solve the original issue in a couple of other cases, there are options to hide elements according to user type, something like the below - which is a bit of kludge, but might serve:
First, to load an admin style sheet applying only to Shop Managers:
/**
* SHOP MANAGER STYLES
* Front (Optional) and Back End stylesheet
* Style interface for users logged in with'shop_manager' role
* Add to theme functions.php
*/
add_action('admin_enqueue_scripts', 'shop_manager_styles');
//if front end stylesheet needs to be added to cover admin bar:
//add_action('wp_enqueue_scripts', 'shop_manager_styles' ) ;
function shop_manager_styles() {
$user = wp_get_current_user() ;
//uncomment following and remove next if not confined to admin
//if ( $user && in_array( 'shop_manager', $user->roles ) ) {
if ( in_array( 'shop_manager', $user->roles ) ) {
//time() as stylesheeet version to help bust caching - may not be necessary but doesn't hurt:
wp_enqueue_style(
'shop_manager_styles', get_stylesheet_directory_uri()
. '/css/shop_manager_styles.css', array(), time()
);
}
}
...and the css to hide the order-status label and menu completely, as well as related columns in shop_order sub-pages:
/** HIDE ORDER STATUS LABEL, SELECTION MENU IN ORDER EDIT
* AND RELATED COLUMNS IN shop_order SUB-PAGE
*/
.wc-order-status,
.column-order_status,
.column-wc_actions {
display: none;
}
You'd save that in your theme css folder in a new shop_manager_styles.css.
Now, you may have some need to show the order status to shop managers without their being able to edit it. That would also be doable with CSS, if also (even more) a kludge. It may be that you have other peculiarities in your installation that will prevent the above code or a minimally customized variation of it from working, but, even if it is a little less clean than removing the option via function, this kind of thing usually will work in a pinch.
(Edited to provide option to add stylesheet on front end - in case relevant options appearing in admin bar, otherwise no need to enqueue extra non-admin script.)

Related

Disable AJAX to cart on Private products only Woocommerce

My initial problem is that AJAX add-to-cart button on Woocommerce doesn't seem to work on Private products (which we're only displaying to a selection of customers): the wheel appears on the add-to-cart button, the page reloads, and the product is added to cart but there's no notice that it has, so it's quite confusing.
After lots of research, all I could find is this thread https://wordpress.org/support/topic/add-to-cart-redirecting-only-on-private-products-2/ which led me to believe it's most likely a bug?
I tried to come up with a work-around: disable AJAX add-to-cart and revert to default behaviour on Private products only so we can still display some kind of notification that the product has been added to cart. I've thought of this code as a starting point:
add_action( 'wp_enqueue_scripts', 'bbloomer_disable_woocommerce_cart_fragments', 11 );
function bbloomer_disable_woocommerce_cart_fragments() {
if ( is_front_page() ) wp_dequeue_script( 'wc-cart-fragments' );
}
Would this work? And how would I modify it so the condition is "if is private product"?
You could solve the problem by thinking the other way around. Instead of showing private products only for specific users you could hide them for all other user roles (leaving the products with the "publish" status).
This way the Ajax problem does not arise.
If the user has one of the roles defined in the $user_roles array of the following function, it hides all products in the $product_ids_to_hide array:
// hide products based on user role
add_filter( 'woocommerce_product_is_visible', 'hide_products_by_user_role', 10, 2 );
function hide_products_by_user_role( $visible, $product_id ) {
// only if the user is logged in
if ( ! is_user_logged_in() ) {
return $visible;
}
// set the product ids to hide
$product_ids_to_hide = array( 12, 14 );
// sets the user roles for which products are to be hidden
$user_roles = array( 'custom_role_1', 'custom_role_2' );
$user = wp_get_current_user();
if ( array_intersect( $user_roles, $user->roles ) && in_array( $product_id, $product_ids_to_hide ) ) {
return false;
}
return $visible;
}
To reply to your comment:
If you want to allow the user to modify the product ids to be hidden based on the user role you can create a .csv file to be uploaded to FTP with the list of product ids, one per line.
If your client in the future wants to remove or add a product id, he can do so simply by overwriting the .csv file with an FTP import or directly in the file manager (if the hosting allows it).
The file I created as an example will be called product-ids-to-hide.csv and will need to be uploaded to the same directory as the functions.php file. No header is required. Here is an example:
349
235
456
745
and this will be the new updated function:
// hide products based on user role
add_filter( 'woocommerce_product_is_visible', 'hide_products_by_user_role', 10, 2 );
function hide_products_by_user_role( $visible, $product_id ) {
// only if the user is logged in
if ( ! is_user_logged_in() ) {
return $visible;
}
// sets the user roles for which products are to be hidden
$user_roles = array( 'custom_role_1', 'custom_role_2' );
$user = wp_get_current_user();
// only if the user has at least one role present in the array
if ( empty( array_intersect( $user_roles, $user->roles ) ) ) {
return $visible;
}
// get an array with product ids
$product_ids_to_hide = file( realpath( __DIR__ . '/product-ids-to-hide.csv' ), FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
if ( in_array( $product_id, $product_ids_to_hide ) ) {
return false;
}
return $visible;
}
The code has been tested and works. Add it to your active theme's functions.php.

Conditionally remove order details section in Woocommerce

I want to remove the order details table in WooCommerce in my functions.php if a if statement is true. I've searched a lot but don't know how to do this.
This is how the file is included in WooCommerce wc-template-functions.php:
if ( ! function_exists( 'woocommerce_order_details_table' ) ) {
/**
* Displays order details in a table.
*
* #param mixed $order_id Order ID.
*/
function woocommerce_order_details_table( $order_id ) {
if ( ! $order_id ) {
return;
}
wc_get_template( 'order/order-details.php', array(
'order_id' => $order_id,
) );
}
}
So I need something like this:
if ( value != true ) {
hide_order_details();
}
Updated (Optionally showing customer details)
You can simply use the following hooked function (that has the $order_id as available argument) with your condition in an if statement (where you will define $value)
The following will remove the order details table in My account > View order:
add_action( 'woocommerce_view_order', 'custom_action_view_order', 5, 1 );
function custom_action_view_order( $order_id ){
$value = false;
if( ! $value ){
remove_action( 'woocommerce_view_order', 'woocommerce_order_details_table', 10 );
## ----- Optionally show customer details (if needed) ----- ##
if ( ! $order = wc_get_order( $order_id ) ) {
return;
}
if( is_user_logged_in() ){
wc_get_template( 'order/order-details-customer.php', array( 'order' => $order ) );
}
}
}
Code goes in function.php file of your active child theme (or active theme). Tested and works.
1) With customer details:
2) Without customer details:
From what I see, there is no hook you can use in the template.
But you can easily override the order/order-details.php template in your theme to add a condition on whether to output the detail table or not.
The concerned template is in woocommerce/templates/order/order-details.php. You can copy it to your-theme/woocommerce/templates/order/order-details.php and make the required change.
This way, you don't edit original Woocommerce files and use the right way to override woocommerce outputs. Check the order-details template yourself, you'll see there is no hook here permitting to prevent outputting the table. But a simple if wrapper with your condition around the <table> code should do the trick.
Edit: it seems that the filter woocommerce_order_item_visible used in order-details-item.php template can help you to prevent displaying some lines in the order details table. But the template is called within the order detail table html, so you cannot fully remove the table using it.
Note: I'm not sure if this template part is used somewhere else. If it's the case, you should add to your display condition to check if the actual page is the one you want to apply changes on (customer dashboard order detail). If the template is used somewhere else, It'll apply your changes in every places this template is used.

Changing user role in WooCommerce on purchase

I'm working on WooCommerce with two product types. I need some of them ($products_to_check) to create an specific role account (subscriber) and the rest of them another one (customer, which is the default one for WooCommerce).
The thing is, if I have an user who's already a costumer, I don't want him to change its role to subscriber (so I check if he's logged in, which mean he's not a new costumer and have a previous defined role). On the other hand, if a subscriber purchase one of the products not included in $products_to_check, he should get a role upgrade (from subscriber to costumer).
This is my function, which is not working but I don't really know why.
add_action( 'woocommerce_order_status_completed', 'change_role_on_purchase' );
function change_role_on_purchase( $order_id ) {
$order = wc_get_order( $order_id );
$items = $order->get_items();
$products_to_check = array( ... );
$user = new WP_User( $order->user_id );
$user_meta = get_userdata( $order->user_id );
$user_roles = $user_meta->roles;
foreach ( $items as $item ) {
if ( $order->user_id > 0 && in_array( $item['product_id'], $products_to_check ) ) {
if (!is_user_logged_in()){
$user->set_role( 'subscriber' );
}
}
elseif ( $order->user_id > 0 && !(in_array( $item['product_id'], $products_to_check)) ) {
if (is_user_logged_in() && in_array('subscriber', $user_roles)){
$user->set_role( 'customer' );
}
}
}
}
I'd really appreciate any help on this issue. What am I doing wrong?
There are several things you need to take note here.
Make sure you hook is invoked after or override the WooCommerce's or any other plugins' hooks that set user roles.
Do not use your log in condition to check if users are new. user_register is the hook to use for newly registered users.
Your role logic loop here is also flawed. You are iterating through all product orders and attempts to set role on each iteration. This obviously means that each iteration can change the user role if condition satisfies and can override the previous ones. (Though it seems like you are hoping your logged in condition will do the trick, I think that's very error prone even if it could maybe work.)
The WooCommerce subscription plugin actually sets the subscriber role to users that have active subscriptions and customer role for expired subscription users. This is configureable but I think that's a more clear assignment.
So first make sure your hook triggers after or overrides the WooCommerce user role hooks. And I think you can do away completely with the new or old user logic, as the only important thing is whether they purchased a product that's going to upgrade them; if not, they should have the default role.

Showing a product to registered users that haven't purchase it yet in WooCommerce

I wanted to show a product only when the user has registered and has not bought this product (new user product).
After they buy this product once, it is no longer available to that user to purchase (because it's a 1-shot deal).
So this means after I bought it, and do things like try to navigate directly to that product's URL, or do a search for all products, etc, this item would not show up.
If I was a new user and didn't buy it, of course, it should show up everywhere.
I have a custom property (metadata) on a user that indicates whether they buy it or not. After they purchase it, I will set this user flag to true indicating it can no longer show.
I had two questions:
Is there a way to hook into the actual successful purchasing of an item, and set this user flag to be true?
How can I instruct Woo to not show this product for a certain user when their flag is true?
Thank you!
Yes this is possible trough 3 functions:
1) A conditional function that will check that customer has bought your specific product:
function has_bought_items( $user_id = 0, $product_id = 0 ) {
// The customer ID
$customer_id = $user_id == 0 || $user_id == '' ? get_current_user_id() : $user_id;
// Retrieve your customer flag '_has_bought_flag' (or replace it by your slug)
if ( get_user_meta( $customer_id, '_has_bought_flag', true ) )
return true;
else
return false;
}
Code goes in function.php file of your active child theme (or theme) or also in any plugin file.
2) A custom function hooked in pre_get_posts that will change the WP_Query on shop and archives pages, checking if user is logged in and if he has already bought this specific product:
// Changing the WP_Query loop conditionally
add_action( 'pre_get_posts', 'conditional_product_query', 10, 1 );
function conditional_product_query( $q ) {
// HERE set your product ID
$product_id = 37;
if( ! is_user_logged_in() || has_bought_items( '', $product_id ) )
$q->set( 'post__not_in', array($product_id) );
}
It will remove completely this product everywhere, when the condition match.
Code goes in function.php file of your active child theme (or theme) or also in any plugin file.
3) A custom function hooked in woocommerce_order_status_completed that will set the customer flag when order status get "completed" when the product is in that order and when the customer flag doesn't exist yet:
// When Order get the "completed" status (paid) we check and we set the user flag (if necessary)
add_action( 'woocommerce_order_status_completed', 'set_customer_specific_product_flag', 10, 2 );
function set_customer_specific_product_flag( $order_id, $order ) {
// HERE set your product ID
$product_id = 37;
// If customer has already bought the product we exit
if( has_bought_items( $order->get_user_id(), $product_id ) ) return;
// Checking order items (if it match we update user meta data
foreach( $order->get_items() as $product_item ){
if ( $product_item->get_product_id() == $product_id ){
update_user_meta( $order->get_user_id(), '_has_bought_flag', '1' );
break;
}
}
}
Code goes in function.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).
Related answer: Checking if customer has already bought something in WooCommerce

Woocommerce Admin Order Details - Show custom data on order details page

I'm searching and trying it for 2 days with no success, please help.
I want to filter woocommerce orders to add additional details from db to order details page based on product attribute but I can't find the right woocommerce action/filter hook for this task.
Here suppose I've variable $is_customized = false;
If $is_customized == true then I need to add custom data from database to orders detail page.
NOTE: I don't want to add additional meta box instead I want to change order detail table for:
Replacing the default Product image with the image stored in database
and,
Adding a div containing custom attributes below product name.
I've all these values in my variables but I can't figure out which action hook should I use.
I've attached an image for clarification.
Just need to know if I can change / filter these order results and how ?
I appreciate for your time and help.
Thanks
Here's a start on how to display some extra data on the woocommerce_before_order_itemmeta hook:
add_action( 'woocommerce_before_order_itemmeta', 'so_32457241_before_order_itemmeta', 10, 3 );
function so_32457241_before_order_itemmeta( $item_id, $item, $_product ){
echo '<p>bacon</p>';
}
I don't know how you are saving your data, so I can't make more a more precise suggestion. Keep in mind that immediately following that hook, anything you've saved as order item meta will automatically display.
Filtering the image is more difficult. I've found this gist as a start, but it requires some custom conditional logic as you don't want to filter the thumbnail everywhere, but only in orders.
Edit: Currently the best I can do for filtering the item thumbnails:
add_filter( 'get_post_metadata', 'so_32457241_order_thumbnail', 10, 4 );
function so_32457241_order_thumbnail( $value, $post_id, $meta_key, $single ) {
// We want to pass the actual _thumbnail_id into the filter, so requires recursion
static $is_recursing = false;
// Only filter if we're not recursing and if it is a post thumbnail ID
if ( ! $is_recursing && $meta_key === '_thumbnail_id' ) {
$is_recursing = true; // prevent this conditional when get_post_thumbnail_id() is called
$value = get_post_thumbnail_id( $post_id );
$is_recursing = false;
$value = apply_filters( 'post_thumbnail_id', $value, $post_id ); // yay!
if ( ! $single ) {
$value = array( $value );
}
}
return $value;
}
add_filter( 'post_thumbnail_id', 'so_custom_order_item_thumbnail', 10, 2 );
function so_custom_order_item_thumbnail( $id, $post_id ){
if( is_admin() ){
$screen = get_current_screen();
if( $screen->base == 'post' && $screen->post_type == 'shop_order' ){
// this gets you the shop_order $post object
global $post;
// no really *good* way to check post item, but could possibly save
// some kind of array in the order meta
$id = 68;
}
}
return $id;
}

Categories