I'm developing a WooCommerce plugin which I save extra metadata on save_post_shop_order action hook.
Now I want to add a logic which changes order status to 'pending' based on some conditions.
I figured out that no matter which status, the order status doesn't change in save_post_shop_order action hook.
function save_order_data(int $post_id)
{
$nonce_name = isset($_POST['save_invoice_nonce']) ? $_POST['save_invoice_nonce'] : '';
$nonce_action = 'save_invoice';
if (!wp_verify_nonce($nonce_name, $nonce_action)) {
return;
}
if (!current_user_can('edit_shop_orders', $post_id)) {
return;
}
if (wp_is_post_autosave($post_id)) {
return;
}
if (wp_is_post_revision($post_id)) {
return;
}
$order = wc_get_order($post_id);
$order->update_status('pending'); // This command works but it seems order status is being overwritten maybe by WooCommerce to previous status
}
add_action('save_post_shop_order', 'save_order_data', PHP_INT_MAX);
I finally had to use Transients to remember doing status change in next page load and then change order status on woocommerce_after_register_post_type hook.
Related
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 a site with Woocommerce and WPML + Multilingual Woocommerce installed. My problem is that I try to insert a product as a translation of a previously entered product without being aware of the ID of the main product. If I enter the ID as translation_of it works; both products share the same SKU and the translation has the SKU field disabled, which is how I want it to work. But I don't want to enter translation_of into the data that gets sent to Woocommerce. I want to only use the SKU and then let Wordpress first check if a product with that SKU already exists and replace sku with translation_of if it does.
This is how I went about it:
add_filter('woocommerce_api_create_product_data', '__create_product_data', -100, 2);
function __create_product_data($data, $api) {
if(isset($data['sku']) && $product_id = wc_get_product_id_by_sku($data['sku'])) {
$product_id = apply_filters('wpml_object_id', $product_id, 'product');
$data['translation_of'] = $product_id;
unset($data['sku']);
}
return $data;
}
But it seems to me that execution arrives at this point long after the SKU has been checked, because I noticed that I can return nothing and I still get product_invalid_sku error back. What would be the correct hook or does such a hook even exist?
My own solution:
add_filter('rest_pre_dispatch', '__rest_pre_dispatch', 10, 3);
function __rest_pre_dispatch($result, $server, $request) {
$sku = $request->get_param('sku');
if ($sku) {
$id = wc_get_product_id_by_sku($sku);
if ($id) {
$product_id = apply_filters('wpml_object_id', $id, 'product');
$request->set_param('translation_of', $product_id);
$request->offsetUnset('sku');
}
}
return $result;
}
I'm working on a multilingual wordpress website using the Polylang plug-in on pages and custom post types.
What I'm looking for is a way to have every post synch automatically, without user input. When creating a new post type, a translation would be automatically created and all contents copied.
So the user wouldn't see this panel at all, or at least not have the chance to edit the translation or (especially) turn the sync off. I guess this could be done by changing user roles privileges but the post would definitely have to automatically sync.
I checked this article but it didn't do anything.
Needed something similar, dug up this undocumented function:
global $polylang;
// third parameter sets synchronisation
$polylang->sync_post_model->copy_post($post_id, $lang, true);
This duplicates the content to the chosen language and enables synchronisation.
Example using 'save_post:
function auto_translate($post_id, $post, $update)
{
if (!$update) {
return;
}
// prevent recursion when publishing translations
remove_action('save_post', 'auto_translate', 999, 3);
global $polylang;
$langs = ['nb', 'se', 'dk'];
$current_translations = pll_get_post_translations($post_id);
foreach ($langs as $lang) {
if (!isset($current_translations[$lang])) {
$polylang->sync_post_model->copy_post($post_id, $lang, true);
}
}
}
// needs low priority or the synchronisation option wont be saved
add_action('save_post', 'auto_translate', 999, 3);
The solution offered by steinoy is good but generates an additional unwanted draft post. Here's an improved solution:
function auto_translate($post_id, $post, $update)
{
if (!$update) {
return;
}
// prevent creation of additional duplicate draft posts
if ( $post->post_status == 'draft' ){
return;
}
// prevent recursion when publishing translations
remove_action('save_post', 'auto_translate', 999, 3);
global $polylang;
$langs = ['nb', 'se', 'dk'];
$current_translations = pll_get_post_translations($post_id);
$post_type = get_post_type($post_id);
if ($post_type == 'post') {
foreach ($langs as $lang) {
if (!isset($current_translations[$lang])) {
$polylang->sync_post_model->copy_post($post_id, $lang, true);
}
}
}
}
// needs low priority or the synchronisation option wont be saved
add_action('save_post', 'auto_translate', 999, 3);
I have a hook to woocommerce_payment_complete, in which I send the order to the distributor. This is working fine.
Now, since I'm also selling through 3rd party marketplace, sometimes I want to create an order form the admin panel, and I expect the woocommerce_payment_complete hook to be triggered by setting the order status to 'Processing' but it's not.
Is there any way to trigger this hook by creating an order manually?
Thanks
You can use the following to set 'processing' for admin orders. action_woocommerce_process_shop_order_meta is used to detect the order update.
// define the woocommerce_admin_order_actions_end callback
function action_woocommerce_admin_order_actions_end( $order_id ) {
global $woocommerce;
if (!$order_id)
return;
$order = new WC_Order($order_id);
$order_status = $order->get_status();
if ($order_status != "failed") {
$order->update_status('processing');
}
};
// add the action
add_action( 'action_woocommerce_process_shop_order_meta', 'action_woocommerce_admin_order_actions_end', 10, 1 );