Good morning everybody.
I need to implement a method to subtract shipping costs from the total in case the shipping destination is included in a specific array of values.
This is not a case of free shipping, because this costs will be added later for other reasons.
I cannot base my decision on the user country, for two reasons:
user can be not registered
user country and shipping country can be different.
I see that WooCommerce reload the order totals when I change billing/shipping country. I believe I need to intercept this kind of change an trigger an action to insert a new cart fee (a negative one, of course).
Well, how can I do that?
This is a part of my code
function delayShippingCosts(){
global $woocommerce;
$EUcountries = ['IT','AT','BE','BG','CY','HR','DK','EE','FI','FR','DE','GR','IE','LV','LT','LU','MT','NE','PL','PT','CZ','RO','SK','SI','ES','SE','HU'];
return in_array( $woocommerce->customer->get_country() , $EUcountries);
}
add_action( 'woocommerce_cart_calculate_fees', 'scc_detract_shipping_costs' );
function scc_detract_shipping_costs(){
global $woocommerce;
if(delayShippingCosts()){
$shippingCosts = WC()->cart->get_shipping_total() * -1;
if(current_user_can('administrator')) {
$woocommerce->cart->add_fee( 'Delayed shipping costs', $shippingCosts, true, 'standard' );
}
}
}
The problem is that now I'm looking to my customer data, and these are not dynamic (and void for unregisterd / unlogged users).
Any suggestions?
Thanks!!
EDIT
Almost OK
I managed to retrieve the shipping country from "woocommerce_checkout_update_order_review" hook, like that:
function action_woocommerce_checkout_update_order_review($posted_data) {
global $shipTo;
$data = array();
$vars = explode('&', $posted_data);
foreach ($vars as $k => $value){
$v = explode('=', urldecode($value));
$data[$v[0]] = $v[1];
}
WC()->cart->calculate_shipping();
$shipTo = $data['shipping_country'] ? $data['shipping_country'] : $data['billing_country'];
// REMOVE ALL NOTICES, IF PRESENTS...
wc_clear_notices();
}
add_action('woocommerce_checkout_update_order_review', 'action_woocommerce_checkout_update_order_review', 10, 1);
add_action( 'woocommerce_cart_calculate_fees', 'scc_detract_shipping_costs' );
function scc_detract_shipping_costs(){
global $woocommerce;
... something ...
if(condition) {
wc_add_notice("info message", "error");
}
}
My problem is that the notice is not removed when "condition" in false.
I tried to call wc_remove_notices() both in woocommerce_cart_calculate_fees and woocommerce_checkout_update_order_review. No big difference! :(
Any hint?
The delayShippingCosts() function is not needed. You can get the list of European countries via the get_european_union_countries method of the WC_Countries class (unless you want to customize the list).
Also you are getting the country with the get_country() method of the WC_Customer class.
The WC_Customer::get_country function is deprecated since version 3.0.
You can get the country like this:
WC()->customer->get_shipping_country() the country of the shipping address
WC()->customer->get_billing_country() the country of the billing address
Finally, note that to apply the standard tax class for the fee you have to set the 4th parameter as an empty string instead of 'standard'. See here for more information.
TO GET THE COUNTRY FIELD FROM A USER NOT LOGGED IN WITH THE woocommerce_cart_calculate_fees HOOK
You can send an AJAX call to send the billing and shipping country value when the respective fields change in the checkout.
To make sure that the AJAX function is executed before the woocommerce_cart_calculate_fees hook it is necessary to remove the update_totals_on_change class from the billing and shipping country fields (to avoid the AJAX call being made to update the checkout) and update the checkout only after the call AJAX has been completed.
This method may take a few extra milliseconds/second to update the checkout because you have to wait for the AJAX call to create the option to complete.
See this answer for more details on how to submit an AJAX call in Wordpress.
Add the following code inside your active theme's functions.php:
// enqueue the script for the AJAX call
add_action('wp_enqueue_scripts', 'add_js_scripts');
function add_js_scripts(){
wp_enqueue_script( 'ajax-script', get_stylesheet_directory_uri().'/js/script.js', array('jquery'), '1.0', true );
wp_localize_script( 'ajax-script', 'ajax_object', array( 'ajaxurl' => admin_url( 'admin-ajax.php' ) ) );
}
// update options with checkout country values
add_action( 'wp_ajax_nopriv_set_option_country', 'set_option_country' );
add_action( 'wp_ajax_set_option_country', 'set_option_country' );
function set_option_country() {
if ( isset( $_POST ) ) {
// get the countries valued in the checkout by the user (guest or logged in)
$countries = $_POST['countries'];
$billing_country = $countries['billing_country'];
$shipping_country = $countries['shipping_country'];
// update options
update_option( 'guest_billing_country', $billing_country );
update_option( 'guest_shipping_country', $shipping_country );
// returns the output as a response to the AJAX call
echo 'success';
}
// always die in functions echoing AJAX content
die();
}
Create a script.js file and add it inside your child theme (because I used get_stylesheet_directory_uri() instead of get_template_directory_uri()) in the directory: /child-theme/js/script.js:
jQuery(function($){
// disable AJAX update
$('#billing_country_field').removeClass('update_totals_on_change');
$('#shipping_country_field').removeClass('update_totals_on_change');
// when the country fields change
$('#billing_country, #shipping_country').change(function(){
var countries = {
'billing_country': $('#billing_country').val(),
'shipping_country': $('#shipping_country').val(),
};
$.ajax({
url: ajax_object.ajaxurl,
type : 'post',
data: {
'action': 'set_option_country',
'countries': countries
},
complete: function(){
// update checkout via AJAX
$(document.body).trigger('update_checkout');
},
success:function(data) {
console.log(data);
},
error: function(errorThrown){
console.log(errorThrown);
}
});
});
});
The code has been tested and works.
So, the correct scc_detract_shipping_costs function will be:
add_action( 'woocommerce_cart_calculate_fees', 'scc_detract_shipping_costs' );
function scc_detract_shipping_costs(){
$countries = new WC_Countries();
// get the list of countries of the european union
$eu_countries = $countries->get_european_union_countries();
// get countries from checkout
$billing_country = get_option( 'guest_billing_country' );
$shipping_country = get_option( 'guest_shipping_country' );
// if the shipping country is part of the European Union
if ( in_array( $shipping_country, $eu_countries ) ) {
$shippingCosts = WC()->cart->get_shipping_total() * -1;
if ( current_user_can('administrator') ) {
WC()->cart->add_fee( 'Delayed shipping costs', $shippingCosts, true, '' );
}
}
}
The code has been tested and works. Add it to your active theme's functions.php.
TO GET THE COUNTRY FIELD FROM A USER NOT LOGGED IN WITH THE woocommerce_calculate_totals HOOK
add_action( 'woocommerce_calculate_totals', 'get_post_checkout_data' );
function get_post_checkout_data( $cart ) {
// get post data
if ( isset( $_POST['post_data'] ) ) {
parse_str( $_POST['post_data'], $post_data );
} else {
$post_data = $_POST;
}
if ( ! empty( $post_data ) ) {
$billing_country = $post_data['billing_country'];
$shipping_country = $post_data['shipping_country'];
// ...
}
}
Add the code in your active theme's functions.php.
Related
I would like to add a function that is triggered every time that the stock quantity of a product will be changed in the admin product page, such that this function will not allow any reduce of the stock value - but only increase.
This is to prevent an admin user to reduce the stock quantity of the products.
Of course, this function should not be triggered if a product will be in an order, since then of course I would like the stock quantity to be reduced.
I tried the following function in the functions.php but unfortunately did not work.
Since I'm new to woocommerce and php, any ideas that could provide a solid solution to the problem?
// get old and new product stock quantity
function get_old_and_new_product_quantity_stock( $sql, $product_id_with_stock, $new_stock, $operation ) {
$product = wc_get_product( $product_id_with_stock );
$old_stock_quantity = $product->get_stock_quantity();
$new_stock_quantity = $new_stock;
echo $old_stock_quantity, $new_stock_quantity;
if ($new_stock_quantity < $old_stock_quantity) {
$new_stock = $old_stock_quantity;
$new_stock_quantity = $old_stock_quantity;
}
return $sql;
}
add_filter( 'woocommerce_update_product_stock_query', 'get_old_and_new_product_quantity_stock', 10, 4 );
You can use the update_post_meta action hook to check if the new value is less than the previous value and display error message.
This will work for quick edit and for product edit page. But the wp_die on product page will look bad so use the javascript to prevent submitting on product edit page (there was another question about it yesterday)
Be sure to test this snippet and create some orders that will reduce the stock automatically. I added is_admin() check but please do a good test.
add_action( 'update_post_meta', 'prevent_reducing_stock_metadata', 10, 4 );
function prevent_reducing_stock_metadata( $meta_id, $post_id, $meta_key, $meta_value ) {
// Check if the meta key is _stock and the new value is less than the previous value
if ( '_stock' == $meta_key && $meta_value < get_post_meta( $post_id, '_stock', true ) ) {
// Check if this is an update from the WordPress admin area
if ( is_admin() ) {
wp_die( __( 'Error: You cannot reduce the stock level for this product.' ), 'error' );
}
}
}
I'm looking for a way to display the last ordered product on another page.
I think it would be possible to maybe create a shortcode in the functions that takes the order details and displays them wherever I add the shortcode.
But I can't seem to figure out how to get it to work. So far I got this information to work with:
add_shortcode( 'displaylast', 'last' );
function last(){
$customer_id = get_current_user_id();
$order = wc_get_customer_last_order( $customer_id );
return $order->get_order();
}
[displaylast] is currently showing me noting. It does work when I change get_order() to get_billing_first_name().
That displays the order name. But I can't seem to get the item that was bought. Maybe there is a get_() that I'm not seeing?
You are close, however you must obtain the last product from the order object.
So you get:
function last() {
// Not available
$na = __( 'N/A', 'woocommerce' );
// For logged in users only
if ( ! is_user_logged_in() ) return $na;
// The current user ID
$user_id = get_current_user_id();
// Get the WC_Customer instance Object for the current user
$customer = new WC_Customer( $user_id );
// Get the last WC_Order Object instance from current customer
$last_order = $customer->get_last_order();
// When empty
if ( empty ( $last_order ) ) return $na;
// Get order items
$order_items = $last_order->get_items();
// Latest WC_Order_Item_Product Object instance
$last_item = end( $order_items );
// Get product ID
$product_id = $last_item->get_variation_id() > 0 ? $last_item->get_variation_id() : $last_item->get_product_id();
// Pass product ID to products shortcode
return do_shortcode("[product id='$product_id']");
}
// Register shortcode
add_shortcode( 'display_last', 'last' );
SHORTCODE USAGE
In an existing page:
[display_last]
Or in PHP:
echo do_shortcode('[display_last]');
With the below code, I can successfully add a custom order note to a subscription renewal order when the order is completed.
However, I would like to modify this to add the note to the Subscription Notes instead of the Order Notes. I think Subscription Notes use the same add_order_note function as $subscription->add_order_note instead of $order->add_order_note. But I have been unsuccessful in my attempts to get the $subscription variable to work in the below code.
// Add box contents (product excerpt) as order note for subscriptions
add_action( 'woocommerce_email_before_order_table', 'custom_action_on_completed_customer_email_notification', 10, 4 );
function custom_action_on_completed_customer_email_notification( $order, $sent_to_admin, $plain_text, $email ) {
if( 'customer_completed_renewal_order' == $email->id ){ // for processing order status customer notification…
$product_id = '';
foreach ($order->get_items() as $item_id => $item_values) {
$product_id = $item_values['product_id'];
break; // (optional) stop loop to first item
}
// The text for the note
$note = get_the_excerpt($product_id);
// Add the note
$order->add_order_note( $note, $is_customer_note = 1 );
// Save the data
$order->save();
}
}
Do you know what I need to add/change to make this add to a Subscription note instead of order note?
I redesigned my Woo checkout page to only show the billing address fields when the Stripe gateway is selected, otherwise if the cheque gateway is selected I do not want to collect their billing address. I have the template finished and the jQuery calls work like a charm. However, I'm having problems making the billing address fields optional and the shipping address fields required.
Here's what I have so far...I added the following filter to make both address fields not-required:
add_filter( 'woocommerce_default_address_fields' , 'custom_override_default_address_fields');
function custom_override_default_address_fields( $fields ) {
// add or remove billing fields you do not want
$keys = array(
'first_name',
'last_name',
'company',
'address_1',
'address_2',
'city',
'postcode',
'state',
'country'
);
foreach( $keys as $key ) {
$fields[$key]['required'] = false;
}
return $fields;}
I remove this filter when the Stripe gateway is selected using jQuery so that the billing address fields show as required (and re-apply if cheque is selected), this behavior works for the billing address fields.
I now need to make the shipping address fields to show as required, so I tried adding the following filter:
add_filter( 'woocommerce_checkout_fields', 'set_shipping_required_fields',9999 );
function set_shipping_required_fields( $fields ) {
// add or remove billing fields you do not want
$shipping_keys = array(
'first_name',
'last_name',
'address_1',
'city',
'postcode',
'state',
);
foreach( $shipping_keys as $key ) {
$fields['shipping']['shipping_'.$key]['required'] = true;
}
return $fields;
}
This successfully will display the shipping_first_name and shipping_last_name as required, however, the rest of the shipping fields initially render as required, but a split second later will revert back to being optional. Does anyone have any tips what I'm doing wrong to make the billing address fields optional and shipping address fields required? Is there a better way?
ou should need to use woocommerce_default_address_fields filter hook instead as explained here.
/ Billing and shipping addresses fields
add_filter( 'woocommerce_default_address_fields' , 'filter_default_address_fields', 20, 1 );
function filter_default_address_fields( $address_fields ) {
// Only on checkout page
if( ! is_checkout() ) return $address_fields;
// All field keys in this array
$key_fields = array('country','first_name','last_name','company','address_1','address_2','city','state','postcode');
// Loop through each address fields (billing and shipping)
foreach( $key_fields as $key_field )
$address_fields[$key_field]['required'] = false;
return $address_fields;
}
As billing email and phone are already required by default, if you want them to be not required, you should need this additional code:
// For billing email and phone - Make them not required
add_filter( 'woocommerce_billing_fields', 'filter_billing_fields', 20, 1 );
function filter_billing_fields( $billing_fields ) {
// Only on checkout page
if( ! is_checkout() ) return $billing_fields;
$billing_fields['billing_phone']['required'] = false;
$billing_fields['billing_email']['required'] = false;
return $billing_fields;
}
All code goes in function.php file of your active child theme (or active theme). Tested and works.
Hello so I'm trying to figure out how to remove some billing fields using woocommerce checkout depending on the shipping method selected. So with this code I'm trying to unset the billing address, billing city, billing state and billing postcode when the customer selects local shipping but this code isn't working. Any help would be appreciated.
add_filter('woocommerce_checkout_fields', 'xa_remove_billing_checkout_fields');
function xa_remove_billing_checkout_fields($fields) {
$shipping_method ='local_pickup:1'; // Set the desired shipping method to hide the checkout field(s).
global $woocommerce;
$chosen_methods = WC()->session->get( 'chosen_shipping_methods' );
$chosen_shipping = $chosen_methods[0];
if ($chosen_shipping == $shipping_method) {
unset($fields['billing']['billing_address_1']); // Add/change filed name to be hide
unset($fields['billing']['billing_address_2']);
unset($fields['billing']['billing_city']);
unset($fields['billing']['billing_state']);
unset($fields['billing']['billing_postcode']);
}
return $fields;
}
Here's how I would go about solving this problem.
This will involve php, css, and javascript (jQuery).
PHP
add_filter( 'woocommerce_checkout_fields', 'xa_remove_billing_checkout_fields' );
function xa_remove_billing_checkout_fields( $fields ) {
// change below for the method
$shipping_method ='local_pickup:1';
// change below for the list of fields
$hide_fields = array( 'billing_address_1', 'billing_address_2', 'billing_city', 'billing_state', 'billing_postcode' );
$chosen_methods = WC()->session->get( 'chosen_shipping_methods' );
// uncomment below line and reload checkout page to check current $chosen_methods
// print_r($chosen_methods);
$chosen_shipping = $chosen_methods[0];
foreach($hide_fields as $field ) {
if ($chosen_shipping == $shipping_method) {
$fields['billing'][$field]['required'] = false;
$fields['billing'][$field]['class'][] = 'hide';
}
$fields['billing'][$field]['class'][] = 'billing-dynamic';
}
return $fields;
}
Instead of unsetting the fields, we will just alter it's requiredness.
That means, if the chosen method is the one we want to check, we will not make it required. Then we will add a hide class. With this, we can hide these fields using css. And woocommerce will not throw an error that it is required. Using jQuery, we can show/hide these fields. So if we unset it on the first run, there's nothing to show because the fields are not there in the first place and with that the page needs to reload.
So here's the javascript and the css part.
add_action( 'wp_footer', 'cart_update_script', 999 );
function cart_update_script() {
if (is_checkout()) :
?>
<style>
.hide {display: none!important;}
</style>
<script>
jQuery( function( $ ) {
// woocommerce_params is required to continue, ensure the object exists
if ( typeof woocommerce_params === 'undefined' ) {
return false;
}
$(document).on( 'change', '#shipping_method input[type="radio"]', function() {
// change local_pickup:1 accordingly
$('.billing-dynamic').toggleClass('hide', this.value == 'local_pickup:1');
});
});
</script>
<?php
endif;
}