Hello I'm trying to create a function in the mu-plugins to prevent certain users to change order status from specific order statuses to specific order statuses.
I've been looking everywhere and I have tried many different ways, but nothing seems to work.
Actually the function is running using woocommerce_order_status_changed action hook. The thing is that this hook runs after the order status has already been changed, what's causing an infinite loop.
The most useful hook I found seems to be woocommerce_before_order_object_save.
I found "Add an additional argument to prevent 'woocommerce_order_status_changed' from being called" useful related thread on WooCommerce Github.
I tried using #kloon code snippet solution:
add_filter( 'woocommerce_before_order_object_save', 'prevent_order_status_change', 10, 2 );
function prevent_order_status_change( $order, $data_store ) {
$changes = $order->get_changes();
if ( isset( $changes['status'] ) ) {
$data = $order->get_data();
$from_status = $data['status'];
$to_status = $changes['status'];
// Do your logic here and update statuses with CRUD eg $order->set_status( 'completed' );
// Be sure to return the order object
}
return $order;
}
but $changes variable is always an empty array.
I tried to use wp_insert_post_data Wordpress hook, but when I set:
$data['post_status'] = "some status";
it just prevents the whole update (the whole new data) from being saved.
This is the code I would like to run is:
function($data){
if($data['order_status'] == 'comlpeted' && $data['new_order_status'] == 'proccessing'){
// prevent the order status from being changed
$data['new_order_status'] = $data['order_status'];
}
few more if conditions...
return $data;
}
Any help or advise is appreciated.
Based on #kloon code snippet, I have been able to get the old order status and the new order status. Then I can disable any status change from a specific defined order status to a specific defined order status.
With the following code, specific defined user roles can't change order status from "processing" to "on-hold":
add_filter( 'woocommerce_before_order_object_save', 'prevent_order_status_change', 10, 2 );
function prevent_order_status_change( $order, $data_store ) {
// Below define the disallowed user roles
$disallowed_user_roles = array( 'shop_manager');
$changes = $order->get_changes();
if( ! empty($changes) && isset($changes['status']) ) {
$old_status = str_replace( 'wc-', '', get_post_status($order->get_id()) );
$new_status = $changes['status'];
$user = wp_get_current_user();
$matched_roles = array_intersect($user->roles, $disallowed_user_roles);
// Avoid status change from "processing" to "on-hold"
if ( 'processing' === $old_status && 'on-hold' === $new_status && ! empty($matched_roles) ) {
throw new Exception( sprintf( __("You are not allowed to change order from %s to %s.", "woocommerce" ), $old_status, $new_status ) );
return false;
}
}
return $order;
}
Code goes in functions.php file of the active child theme (or active theme). Tested and works.
Related
I am currently using hook wp_trash_post to trigger my custom API when a WooCommerce Product is trashed.
This is working fine for the case of single trash.
But, in case of Bulk trash this event is executing multiple times. for example if 3 products are selected to trash then this event will trigger 3 times.
I need different event for Bulk Trash which will execute one time but having all of product ids.
Hopefully it is clear to understand.
Please Help!
Your API call must be withing the foreach loop and is therefore being run foreach post ID. You can capture the IDs in a variable and execute your API call after the if and foreach statements are complete:
function wpdocs_trash_multiple_posts( $post_id = '' ) {
$trashed_ids = [];
if ( isset( $_GET['post'] ) && is_array( $_GET['post'] ) ) {
foreach ( $_GET['post'] as $post_id ) {
$trashed_ids[] = $post_id;
}
} else {
$trashed_ids[] = $post_id;
}
// Do something here, $trashed_ids array
// contains either one, or several IDs
}
add_action( 'wp_trash_post', 'wpdocs_trash_multiple_posts' );
I'm not sure which hook/action needs to setup to know when the admin is updating shipping/billing addresses once the order has been created.
So what I'm trying to achieve here is:
In WooCommerce order section when the admin updates the shipping/billing address then it triggers an action.
this action basically makes a single curl call to my custom script and lets me know that the address of the order has been changed by the admin.
I'll do some magic in my script.
I found below but I don't think its more from admin side.
// define the woocommerce_admin_order_data_after_shipping_address callback
function action_woocommerce_admin_order_data_after_shipping_address(
$delta_wccs_custom_checkout_details_pro_shipping, $int, $int ) {
// make action magic happen here...
};
// add the action
add_action( 'woocommerce_admin_order_data_after_shipping_address', 'action_woocommerce_admin_order_data_after_shipping_address', 10, 3 );
Please let me know if anyone knows the right action to trigger when order shipping/billing address change.
The woocommerce_admin_order_data_after_shipping_address hook is to display extra content on the order edit page (backend)
To trigger $order_item actions before or after saving to the DB, use:
/**
* Trigger action before saving to the DB. Allows you to adjust object props before save.
*
* #param WC_Data $this The object being saved.
* #param WC_Data_Store_WP $data_store THe data store persisting the data.
*/
function action_woocommerce_before_order_item_object_save( $order_item, $data_store ) {
// Get type
$data_type = $order_item->get_type();
// Before billing changes
if ( $data_type == 'billing' ) {
// Do..
}
// Before shipping changes
if ( $data_type == 'shipping' ) {
// Do..
}
}
add_action( 'woocommerce_before_order_item_object_save', 'action_woocommerce_before_order_item_object_save', 10, 2 );
/**
* Trigger action after saving to the DB.
*
* #param WC_Data $this The object being saved.
* #param WC_Data_Store_WP $data_store THe data store persisting the data.
*/
function action_woocommerce_after_order_item_object_save( $order_item, $data_store ) {
// Get type
$data_type = $order_item->get_type();
// After billing changes
if ( $data_type == 'billing' ) {
// Do..
}
// After shipping changes
if ( $data_type == 'shipping' ) {
// Do..
}
}
add_action( 'woocommerce_after_order_item_object_save', 'action_woocommerce_after_order_item_object_save', 10, 2 );
OR
Use the almost identical woocommerce_before_order_object_save hook that may be even more suitable, because via $order->get_changes() you can trigger/log/compare which $order data has been changed
function action_woocommerce_before_order_object_save( $order, $data_store ) {
// Get changes
$changes = $order->get_changes();
// Billing OR shipping
if ( isset( $changes['billing'] ) || isset( $changes['shipping'] ) ) {
// Do..
}
// OR even more specific (e.g.: shipping first name field was changed)
if ( isset( $changes['shipping_first_name'] ) ) {
// Do..
}
}
add_action( 'woocommerce_before_order_object_save', 'action_woocommerce_before_order_object_save', 10, 2 );
EDIT: it is a known issue that these hooks are called multiple times when they are not intended to be
See: https://github.com/woocommerce/woocommerce/issues/25771
As a workaround, add:
if ( did_action( 'replace_by_the_desired_hook_name' ) >= 2 ) return;
As first line in your callback function
// Define the woocommerce_admin_order_data_after_shipping_address callback .
function action_woocommerce_admin_order_data_after_shipping_address( $order ) {
// This hook will only fire in backend when viewing the order edit screen. Not for orders placed from checkout
};
// add the action
add_action( 'woocommerce_admin_order_data_after_shipping_address', 'action_woocommerce_admin_order_data_after_shipping_address', 10, 1 );
I would like to prevent that some categories are accidentally deleted. For this I use a meta entry for the category to be protected.
I use the following code for this:
// edit: wrong hook! ** add_action( 'delete_term_taxonomy', 'taxonomy_delete_protection', 10, 1 );
add_action( 'pre_delete_term', 'taxonomy_delete_protection', 10, 1 );
function taxonomy_delete_protection ( $term_id )
{
if (get_term_meta ($term_id, 'delete-protect', true) === true)
{
wp_die('Cannot delete this category');
}
}
Unfortunately, instead of my error message, only "Something went wrong" is displayed. Why?
Edit: The `delete_term_taxonomy` is the wrong hook for my code, because it deleted the meta before i can check the meta entry. `pre_delete_term` does fire before anything happens with the category.
The "Why" is because of the following JavaScript that ships with WordPress:
$.post(ajaxurl, data, function(r){
if ( '1' == r ) {
$('#ajax-response').empty();
tr.fadeOut('normal', function(){ tr.remove(); });
/**
* Removes the term from the parent box and the tag cloud.
*
* `data.match(/tag_ID=(\d+)/)[1]` matches the term ID from the data variable.
* This term ID is then used to select the relevant HTML elements:
* The parent box and the tag cloud.
*/
$('select#parent option[value="' + data.match(/tag_ID=(\d+)/)[1] + '"]').remove();
$('a.tag-link-' + data.match(/tag_ID=(\d+)/)[1]).remove();
} else if ( '-1' == r ) {
$('#ajax-response').empty().append('<div class="error"><p>' + wp.i18n.__( 'Sorry, you are not allowed to do that.' ) + '</p></div>');
tr.children().css('backgroundColor', '');
} else {
$('#ajax-response').empty().append('<div class="error"><p>' + wp.i18n.__( 'Something went wrong.' ) + '</p></div>');
tr.children().css('backgroundColor', '');
}
});
The expected response to this POST request is:
'1' if the term was deleted
'-1' if your user doesn't have permission to delete the term.
For all other cases, "Something went wrong" is displayed.
You are terminating the script early with wp_die, yielding an unexpected response, which comes under "other cases".
There isn't a way to provide a custom error message in the notice box here without writing some JavaScript of your own.
This is my current solution, not perfect but it works.
The "Something went wrong" message show up if you delete the taxonomy with the row action. So i unset the "delete" action so it couldn't be triggered this way.
add_filter ('category_row_actions', 'unset_taxonomy_row_actions', 10, 2);
function unset_taxonomy_row_actions ($actions, $term)
{
$delete_protected = get_term_meta ($term->term_id, 'delete-protect', true);
if ($delete_protected)
{
unset ($actions['delete']);
}
return $actions;
}
Then i hide the "Delete" Link in the taxonomy edit form with css. It's still could be triggered if you inspect the site and it's link, but there is no hook to remove this action otherwise.
add_action( 'category_edit_form', 'remove_delete_edit_term_form', 10, 2 );
function remove_delete_edit_term_form ($term, $taxonomy)
{
$delete_protected = get_term_meta ($term->term_id, 'delete-protect', true);
if ($delete_protected)
{
// insert css
echo '<style type="text/css">#delete-link {display: none !important;}</style>';
}
}
Finally the check before deleting the taxonomy. This should catch all other ways, like the bulk action "delete". I didn't found another way yet to stop the script from deleting the taxonomy.
add_action ('pre_delete_term', 'taxonomy_delete_protection', 10, 1 );
function taxonomy_delete_protection ( $term_id )
{
$delete_protected = get_term_meta ($term_id, 'delete-protect', true);
if ($delete_protected)
{
$term = get_term ($term_id);
$error = new WP_Error ();
$error->add (1, '<h2>Delete Protection Active!</h2>You cannot delete "' . $term->name . '"!');
wp_die ($error);
}
}
This solution provides a way to disable all categories from being deleted by a non Admin. This is for anyone like myself who's been searching.
function disable_delete_cat() {
global $wp_taxonomies;
if(!current_user_can('administrator')){
$wp_taxonomies[ 'category' ]->cap->delete_terms = 'do_not_allow';
}
}
add_action('init','disable_delete_cat');
The easiest solution (that will automatically take care of all different places where you can possibly delete the category/term) and in my opinion the most flexible one is using the user_has_cap hook:
function maybeDoNotAllowDeletion($allcaps, $caps, array $args, $user)
{
if ($args[0] !== 'delete_term') return $allcaps;
// you can skip protection for any user here
// let's say that for the default admin with id === 1
if ($args[1] === 1) return $allcaps;
$termId = $args[2];
$term = get_term($termId);
// you can skip protection for all taxonomies except
// some special one - let's say it is called 'sections'
if ($term->taxonomy !== 'sections') return $allcaps;
// you can protect only selected set of terms from
// the 'sections' taxonomy here
$protectedTermIds = [23, 122, 3234];
if (in_array($termId, $protectedTermIds )) {
$allcaps['delete_categories'] = false;
// if you have some custom caps set
$allcaps['delete_sections'] = false;
}
return $allcaps;
}
add_filter('user_has_cap', 'maybeDoNotAllowDeletion', 10, 4);
I have setup a fresh docker container with Wordpress 5.0.3 and the latest WC and WC Eway plugin (WooCommerce eWAY Gateway).
Created a store with some products, hooked up my Eway sandbox environment, enabled Save Cards (which would enable the token) and created an order.
After checking the post_meta in my DB for the order, I didn't see a _eway_token_customer_id field. While being logged in as a customer, I tried again and with the new order I still do not get a token.
The reason for this tests is that I got this strange behaviour in my real, new website, where the first order with a NEW customer, doesn't result in a token.
However, when I create a second order whilst being logged in, I do get a _eway_token_customer_id value within the order_meta.
It is imperative for me to get that token with the first order, because after that I will auto renew the product using the tokenp ayment option.
Debugging this issue is hell, and I find it very disconcerting that on my fresh WP installation I get no token at all.
Is there anyone that has a bright idea?
**update
After some digging around in the Eway Plugin, I found out that the first time I do an order, the function request_access_code() from the class WC_Gateway_EWAY is checking if there is a token in the database for this user.
The function body:
protected function request_access_code( $order ) {
$token_payment = $this->get_token_customer_id( $order );
if ( $token_payment && 'new' === $token_payment ) {
$result = json_decode( $this->get_api()->request_access_code( $order, 'TokenPayment', 'Recurring' ) );
} elseif ( 0 === $order->get_total() && 'shop_subscription' === ( version_compare( WC_VERSION, '3.0', '<' ) ? $order->order_type : $order->get_type() ) ) {
$result = json_decode( $this->get_api()->request_access_code( $order, 'CreateTokenCustomer', 'Recurring' ) );
} else {
$result = json_decode( $this->get_api()->request_access_code( $order ) );
}
if ( isset( $result->Errors ) && ! is_null( $result->Errors ) ) {
throw new Exception( $this->response_message_lookup( $result->Errors ) );
}
return $result;
}
The function handles three possible outcomes:
1) new customer: results in calling `$this->get_api()->request_access_code( $order, 'TokenPayment', 'Recurring' )` <-- this is the one we are after!
2) shop_subscription: calls `$this->get_api()->request_access_code( $order, 'CreateTokenCustomer', 'Recurring' )`
3) else..: calls `$this->get_api()->request_access_code( $order )`
What is happening during debugging, is that the $token_payment variable has the value of an empty string for a new customer, instead of new.
So I will attempt to fix this, either via a filter/action hook, or figure out why this is happening.
When I forced the function the always use the first if block, I got my token. :)
**Update 2:
I tested with an existing user account, created a new order.
When I look in the post_meta table:
Voila, the new value is present.
However, when I am not logged in and I create an account, the new value is not added and that is where it goes wrong.
A temp fix would be to use a hook and add the new value to the order so that when get_token_customer_id is called it retrieves a new value and not an empty string.
I think this is a bug, since this value should be added. It explains why the second transactions get the token but not the first.
If only Woocommerce Eway plugin had a git repo.... I could flag an issue or fork it.
***Solution without hack
Added this to my plugin (or functions.php if you like):
add_action( 'woocommerce_checkout_order_processed', function( $order_id, $posted_data, $order ) {
update_post_meta( $order_id, '_eway_token_customer_id', 'new' );
}, 10, 3);
This will add the new value when you checkout with a non-existent user.
The token was added nicely after adding my creditcard details.
The matter of the fact stays that the plugin still has a bug, which you can work around.
I am in the process of building a membership / subscription based site for a client of mine and we are using Woocommerce Subscriptions and Woocommerce Memberships plugin.
Now the problem is the my client is building a few promo pages which basically allows the user to purchase an upgrade. This is fine but my client only wants one unique subscription by customer (with its associated membership).
So the agreed solution is that, on a purchase of any new subscription product, all other subscriptions should be cancelled. All associated membership deleted/cancelled and only the latest subscription should remain active with its accompanying membership.
So I have tried to build this solution but it is just not working, so any advise/direction would be most welcome.
What I have tried:
function wp56908_new_order_housekeeping ($order_id)
{
$args = array(
'subscriptions_per_page' => -1,
'customer_id' => get_current_user_id(),
);
$subscriptions = wcs_get_subscriptions($args);
foreach ($subscriptions as $subscription) {
$s_order_id = method_exists( $subscription, 'get_parent_id' ) ? $subscription->get_parent_id() : $subscription->order->id;
if ($s_order_id != $order_id) {
$cancel_note = 'Customer purchased new subscription in order #' . $order_id;
$subscription->update_status( 'cancelled', $cancel_note );
}
}
}
add_action( 'woocommerce_thankyou', 'wp56908_new_order_housekeeping', 10, 1 );
This is the support email I got from WooCommerce regarding the issue with the function wcs_get_subscriptions().
Thanks for contacting support and report us this issue!
I've tried it in a local install and I confirm that this seems to be a
bug! I've already reported it to our development team and a patch
should be included in next updates. By now, if you're in a real hurry
and want to get it working, you could search for this code (in
wcs-functions.php line 483):
// We need to restrict subscriptions to those which contain a certain
product/variation if ( ( 0 != $args['product_id'] && is_numeric(
$args['product_id'] ) ) || ( 0 != $args['variation_id'] && is_numeric(
$args['variation_id'] ) ) ) { $query_args['post__in'] =
wcs_get_subscriptions_for_product( array( $args['product_id'],
$args['variation_id'] ) ); }
And replace it with something like:
// We need to restrict subscriptions to those which contain a certain
product/variation if ( ( 0 != $args['product_id'] && is_numeric(
$args['product_id'] ) ) || ( 0 != $args['variation_id'] && is_numeric(
$args['variation_id'] ) ) ) { $prod_args = array( $args['product_id'],
$args['variation_id'] ); $prod_args = array_filter($prod_args,
function($prod_args) { return ($prod_args !== 0); });
$query_args['post__in'] = wcs_get_subscriptions_for_product(
$prod_args ); }
Please take in mind that this is not the ideal solution because you'll
be modifying our plugin's code directly and these changes can be lost
after updating, but if you feel comfortable with code and want to take
the risk as a temporary workaround until we add the proper patch in
our plugin, you can do it. ;)
Let me know if you need any help with this!
Thanks, Bernat
There's a new setting in the WC Subscriptions plugin which lets you limit to one active subscription. You can find more information here: https://woocommerce.com/document/subscriptions/store-manager-guide/#:~:text=Limit%20subscriptions,the%20subscription%20from%20the%20dropdown.
The end user will get the message:
“You have a subscription to this product. Choosing a new subscription will replace your existing subscription.”