How to get cart subtotal inside WC_Shipping_Method calculate_shipping() method - php

So I was trying to create a woocommerce shipping method that takes the cart subtotal and charges a user-defined percentage of the cart subtotal as a shipping fee. As a first step towards this goal, ehat I did was basically like this
class Subtotal_Percentage_Method extends WC_Shipping_Method {
// to store percentage
private $percentage_rate
// constructor that handles settings
// here is where i start calculation
public function calculate_shipping($packages = array()) {
$cost = $this->percentage_rate * 1000;
add_rate(array(
'id' => $this->id,
'label' => $this->title,
'cost' => $cost
));
}
}
This one works. However, it doesn't work when I change the calculate_shipping method to use the cart subtotal in the calculation like this
public function calculate_shipping($packages = array()) {
$subtotal = WC()->cart->subtotal;
$cost = $subtotal * $this->percentage_rate / 100;
add_rate(array(
'id' => $this->id,
'label' => $this->title,
'cost' => $cost
));
}
Can anyone show me what I'm doing wrong?

As this is related to shipping packages (as cart items can be splitted (divided) into multiple shipping packages), you need to use instead the variable $packages argument included in calculate_shipping() method.
So your code will be lightly different without using WC_Cart Object methods:
public function calculate_shipping( $packages = array() ) {
$total = $total_tax = 0; // Initializing
// Loop through shipping packages
foreach( $packages as $key => $package ){
// Loop through cart items for this package
foreach( $package['contents'] as $item ){
$total += $item['total']; // Item subtotal discounted
$total_tax += $item['total_tax']; // Item subtotal tax discounted
}
}
add_rate( array(
'id' => $this->id,
'label' => $this->title,
'cost' => $total * $this->percentage_rate / 100,
// 'calc_tax' => 'per_item'
) );
}
Code goes in functions.php file of your active child theme (active theme). Tested and works.
Note: Here the calculation is made on cart items subtotal after discount (without taxes). You can add easily add make it on cart items subtotal after discount with taxes, replacing:
'cost' => $total * $this->percentage_rate / 100,
by:
'cost' => ($total + $total_tax) * $this->percentage_rate / 100,
You can see how are made the shipping packages looking to:
WC_Cart get_shipping_packages() method source code
If you want to handle also shipping classes and more, check: WC_Shipping_Flat_Rate calculate_shipping() method source code.

Related

Disallow to remove a product with a specific ID from WooCommerce cart if another product is added

I have to implement the following logic to my Cart / Checkout pages:
If the product with ID="100" is in the Cart I need to disallow its removal from Cart if the Product with ID="101" was also added.
In simple words, the ability to remove a product is only present if a product with a specific ID was not added.
Earlier I used the following solution to disallow product removal only for specific product IDs. But this code doesn't work with variable products and I can't get how to implement my logic inside it.
add_filter('woocommerce_cart_item_remove_link', 'customized_cart_item_remove_link', 20, 2 );
function customized_cart_item_remove_link( $button_link, $cart_item_key ){
$targeted_products_ids = array( 98,99,100 );
$cart_item = WC()->cart->get_cart()[$cart_item_key];
if( in_array($cart_item['data']->get_id(), $targeted_products_ids) )
$button_link = '';
return $button_link;
}
Thank you in advance for any help.
This answer allows you if product_id_1 is in the cart it will not be removable from the cart if product_id_2 is also added.
This can be applied to multiple products via the $settings array, so a certain product cannot be removed if the corresponding product ID is in the cart.
For clarity, this works based on the product ID (simple) or the parent ID (variable) products. If you want to apply it for variations of variable products you don't have to use get_parent_id().
So you get:
function filter_woocommerce_cart_item_remove_link( $link, $cart_item_key ) {
// Settings (multiple settings arrays can be added/removed if desired)
$settings = array(
array(
'product_id_1' => 100,
'product_id_2' => 101,
),
array(
'product_id_1' => 30,
'product_id_2' => 813,
),
array(
'product_id_1' => 53,
'product_id_2' => 817,
),
);
// Get cart
$cart = WC()->cart;
// If cart
if ( $cart ) {
// Get cart item
$cart_item = $cart->get_cart()[$cart_item_key];
// Get parent/real ID
$product_id = $cart_item['data']->get_parent_id() != 0 ? $cart_item['data']->get_parent_id() : $cart_item['data']->get_id();
// Loop trough settings array
foreach ( $settings as $key => $setting ) {
// Compare, get the correct setting array
if ( $product_id == $settings[$key]['product_id_1'] ) {
// Cart id of the other product
$product_cart_id = $cart->generate_cart_id( $settings[$key]['product_id_2'] );
// Find other product in cart
$in_cart = $cart->find_product_in_cart( $product_cart_id );
// When true
if ( $in_cart ) {
// Hide remove button
$link = '';
// Break loop
break;
}
}
}
}
return $link;
}
add_filter( 'woocommerce_cart_item_remove_link', 'filter_woocommerce_cart_item_remove_link', 10, 2 );

How to add additional fees to different products in WooCommerce cart

I use this code to add additional fee to a specific product id's. The problem is that I can add only one fee to a different products id's.
add_action('woocommerce_cart_calculate_fees', 'add_fees_on_ids');
function add_fees_on_ids() {
if (is_admin() && !defined
('DOING_AJAX')) {return;}
foreach( WC()->cart->get_cart() as $item_keys => $item ) {
if( in_array( $item['product_id'],
fee_ids() )) {
WC()->cart->add_fee(__('ADDITIONAL FEE:'), 5);
}
}
}
function fee_ids() {
return array( 2179 );
}
I need to add different fees to different products - for example:
Product 1 with product ID 1234 will have "xx" additional fee.
Product 2 with product ID 5678 will have "xx" additional fee.
With this code I can set only one fee for a different products. How can I add different fees to different products in WooCommerce?
There are several ways. For example, you could indeed add a custom field to the admin product settings.
But it doesn't have to be complicated, installing an extra plugin for this is way too far-fetched!
Adding the same code several times is also a bad idea, because that way you will go through the cart several times. Which would not only slows down your website but will also eventually cause errors.
Adding an array where you would not only determine the product ID but also immediately determine an additional fee based on the array key seems to me to be the most simple and effective way.
So you get:
function action_woocommerce_cart_calculate_fees( $cart ) {
if ( is_admin() && ! defined( 'DOING_AJAX' ) )
return;
// Add in the following way: Additional fee => Product ID
$settings = array(
10 => 1234,
20 => 5678,
5 => 30,
2 => 815,
);
// Initialize
$additional_fee = 0;
// Loop through cart contents
foreach ( $cart->get_cart_contents() as $cart_item ) {
// Get product id
$product_id = $cart_item['product_id'];
// In array, get the key as well
if ( false !== $key = array_search( $product_id, $settings ) ) {
$additional_fee += $key;
}
}
// If greater than 0, so a matching product ID was found in the cart
if ( $additional_fee > 0 ) {
// Add additional fee (total)
$cart->add_fee( __( 'Additional fee', 'woocommerce' ), $additional_fee, false );
}
}
add_action( 'woocommerce_cart_calculate_fees', 'action_woocommerce_cart_calculate_fees', 10, 1 );
Optional:
To display the addition per fee separately, so that the customer knows which fee refers to which product, you can use a multidimensional array.
Add the necessary information to the settings array, everything else happens automatically.
So you get:
function action_woocommerce_cart_calculate_fees( $cart ) {
if ( is_admin() && ! defined( 'DOING_AJAX' ) )
return;
// Settings
$settings = array(
array(
'product_id' => 30,
'amount' => 5,
'name' => __( 'Additional service fee', 'woocommerce' ),
),
array(
'product_id' => 813,
'amount' => 10,
'name' => __( 'Packing fee', 'woocommerce' ),
),
array(
'product_id' => 815,
'amount' => 15,
'name' => __( 'Another fee', 'woocommerce' ),
),
);
// Loop through cart contents
foreach ( $cart->get_cart_contents() as $cart_item ) {
// Get product id
$product_id = $cart_item['product_id'];
// Loop trough settings array
foreach ( $settings as $setting ) {
// Search for the product ID
if ( $setting['product_id'] == $product_id ) {
// Add fee
$cart->add_fee( $setting['name'], $setting['amount'], false );
}
}
}
}
add_action( 'woocommerce_cart_calculate_fees', 'action_woocommerce_cart_calculate_fees', 10, 1 );
Related: How to sum the additional fees of added product ID's in WooCommerce cart
Hello and thank you all!
I found a solution to the problem. I just copy the code described below as many times as I have different amounts for different products and change it:
add_action('woocommerce_cart_calculate_fees', 'add_fees_on_ids');
function add_fees_on_ids() {
if (is_admin() && !defined
('DOING_AJAX')) {return;}
foreach( WC()->cart->get_cart() as $item_keys => $item ) {
if( in_array( $item['product_id'],
fee_ids() )) {
WC()->cart->add_fee(__('ADDITIONAL FEE:'), 5);
}
}
}
function fee_ids() {
return array( 2179 );
}
I change add_fees_on_ids to add_fees_on_ids_1 and fee_ids to fee_ids_1
and ADDITIONAL FEE: to ADDITIONAL FEE 1:
I know that for some it may not look professional but it is a solution to my problem.
I. You could use the WordPress plugin "Advanced Custom Fields for that:
install the plugin
set a field group with a title such as "Product with fee" and set its location rules to "Post Type" => "is equal to" => "Product"
"+ Add Field" of "Field Type" "Text" and notice its "Name" (not "Label") for later checks, for example "fee"
Set fee value at the appropriate WooCommerce product posts at that very fee meta field
Within your 'woocommerce_cart_calculate_fees' hook callback loop over cart products and check for products with set values for the meta field named "fee" and add your fee adding code within that condition:
if (get_field( "fee" )) { // get_field() returns false if empty aka not set from admin area
// fee adding code for single product in cart
};
II. Or you could achieve that by adding and displaying a WooCommerce custom product meta field as described in depth here:
https://www.cloudways.com/blog/add-custom-product-fields-woocommerce/

Auto add or update a custom fee via admin edit orders in WooCommerce

We have a special case where we invoice our customers for payment after the order has been received instead of having them pay during checkout. Shipping charges are manually calculated and added to the order and then we add a 3% credit card fee on the grand total.
To automate this process, I created a script that calculates the 3% charge once the shipping charge has been set through the backend and adds this fee item into the order automatically. This works when we add the shipping charge and click save/recalculate the first time.
add_action( 'woocommerce_order_after_calculate_totals', "custom_order_after_calculate_totals", 10, 2);
function custom_order_after_calculate_totals($and_taxes, $order) {
if ( did_action( 'woocommerce_order_after_calculate_totals' ) >= 2 )
return;
if ( is_admin() && ! defined( 'DOING_AJAX' ) )
return;
$percentage = 0.03;
$total = $order->get_total();
$surcharge = $total * $percentage;
$feeArray = array(
'name' => '3% CC Fee',
'amount' => wc_format_decimal($surcharge),
'taxable' => false,
'tax_class' => ''
);
//Get fees
$fees = $order->get_fees();
if(empty($fees)){
//Add fee
$fee_object = (object) wp_parse_args( $feeArray );
$order->add_fee($fee_object);
} else {
//Update fee
foreach($fees as $item_id => $item_fee){
if($item_fee->get_name() == "3% CC Fee"){
$order->update_fee($item_id,$feeArray);
}
}
}
}
If we accidentally add the wrong shipping cost and try to update it, this code above does get triggered again and updates the fee however $total does not get the new order total from the updated shipping cost and so the fee does not change. Strangely enough, if I try to delete the fee item, a new fee is calculated and is added back with the correct fee amount.
Anybody know how I can solve this?
As you are using the order gran total to calculate your fee and as the hook you are using is located inside calculate_totals() method, once order get updated, you will always need to press "recalculate" button to get the correct fee total and the correct order gran total with the correct amounts.
Since WooCommerce 3 your code is outdated and a bit obsolete with some mistakes… For example add_fee() and update_fee() methods are deprecated and replaced by some other ways.
Use instead the following:
add_action( 'woocommerce_order_after_calculate_totals', "custom_order_after_calculate_totals", 10, 2 );
function custom_order_after_calculate_totals( $and_taxes, $order ) {
if ( did_action( 'woocommerce_order_after_calculate_totals' ) >= 2 )
return;
$percentage = 0.03; // Fee percentage
$fee_data = array(
'name' => __('3% CC Fee'),
'amount' => wc_format_decimal( $order->get_total() * $percentage ),
'tax_status' => 'none',
'tax_class' => ''
);
$fee_items = $order->get_fees(); // Get fees
// Add fee
if( empty($fee_items) ){
$item = new WC_Order_Item_Fee(); // Get an empty instance object
$item->set_name( $fee_data['name'] );
$item->set_amount( $fee_data['amount'] );
$item->set_tax_class($fee_data['tax_class']);
$item->set_tax_status($fee_data['tax_status']);
$item->set_total($fee_data['amount']);
$order->add_item( $item );
$item->save(); // (optional) to be sure
}
// Update fee
else {
foreach ( $fee_items as $item_id => $item ) {
if( $item->get_name() === $fee_data['name'] ) {
$item->set_amount($fee_data['amount']);
$item->set_tax_class($fee_data['tax_class']);
$item->set_tax_status($fee_data['tax_status']);
$item->set_total($fee_data['amount']);
$item->save();
}
}
}
}
Code goes in functions.php file of the active child theme (or active theme). Tested and works.
Once order get updated and after press on recalculate button (to get the correct orders totals) both auto added and updated fee will work nicely.
Related: Add a fee to an order programmatically in Woocommerce 3
Update
Now if it doesn't work for any reason, you should remove the related item to update and add a new one as follows:
add_action( 'woocommerce_order_after_calculate_totals', "custom_order_after_calculate_totals", 10, 2 );
function custom_order_after_calculate_totals( $and_taxes, $order ) {
if ( did_action( 'woocommerce_order_after_calculate_totals' ) >= 2 )
return;
$percentage = 0.03; // Fee percentage
$fee_data = array(
'name' => __('3% CC Fee'),
'amount' => wc_format_decimal( $order->get_total() * $percentage ),
'tax_status' => 'none',
'tax_class' => ''
);
$fee_items = $order->get_fees(); // Get fees
// Add fee
if( empty($fee_items) ){
$item = new WC_Order_Item_Fee(); // Get an empty instance object
$item->set_name( $fee_data['name'] );
$item->set_amount( $fee_data['amount'] );
$item->set_tax_class($fee_data['tax_class']);
$item->set_tax_status($fee_data['tax_status']);
$item->set_total($fee_data['amount']);
$order->add_item( $item );
$item->save(); // (optional) to be sure
}
// Update fee
else {
foreach ( $fee_items as $item_id => $item ) {
if( $item->get_name() === $fee_data['name'] ) {
$item->remove_item( $item_id ); // Remove the item
$item = new WC_Order_Item_Fee(); // Get an empty instance object
$item->set_name( $fee_data['name'] );
$item->set_amount( $fee_data['amount'] );
$item->set_tax_class($fee_data['tax_class']);
$item->set_tax_status($fee_data['tax_status']);
$item->set_total($fee_data['amount']);
$order->add_item( $item );
$item->save(); // (optional) to be sure
}
}
}
}
Update:
Need to use remove_item from $order object.
There is no remove_item function in WC_Order_Item_Fee Class.
After removing item from order invoke the save() function on order.
$order_fees = reset( $order->get_items('fee') );
$fee_data = array(
'name' => __( 'Delivery Fee', 'dsfw' ),
'amount' => wc_format_decimal( $day_fee + $timeslot_fee ),
);
if( !empty( $order_fees ) && $order_fees instanceof WC_Order_Item_Fee ) {
// update fee
if( $order_fees->get_name() === $fee_data['name'] ) {
$order->remove_item( (int)$order_fees->get_id() );
$order->save();
$item = new WC_Order_Item_Fee();
$item->set_name( $fee_data['name'] );
$item->set_amount( $fee_data['amount'] );
$item->set_total( $fee_data['amount'] );
$order->add_item( $item );
$item->save();
}
} else {
// add fee
$item = new WC_Order_Item_Fee();
$item->set_name( $fee_data['name'] );
$item->set_amount( $fee_data['amount'] );
$item->set_total( $fee_data['amount'] );
$order->add_item( $item );
$item->save();
}
}

Remove WooCommerce Payment Gateways for defined groups of product categories

I've manged to get this code to work for removing ONE payment gateway based on one or more product categories.
What I need help with is removing multiple payment gateways as well. In other words; it should remove one or more payment gateways based on one or more product categories.
This is what I got, which might be outdated?
add_filter( 'woocommerce_available_payment_gateways', 'remove_gateway_based_on_category' );
function remove_gateway_based_on_category($available_gateways){
global $woocommerce;
if (is_admin() && !defined('DOING_AJAX')) return;
if (!(is_checkout() && !is_wc_endpoint_url())) return;
$unset = false;
$category_ids = array( '75' );
foreach ($woocommerce->cart->cart_contents as $key => $values){
$terms = get_the_terms($values['product_id'], 'product_cat');
foreach ($terms as $term){
if (in_array($term->term_id, $category_ids)){
$unset = true;
break;
}
}
}
if ($unset == true) unset($available_gateways['cod']);
return $available_gateways;
}
Yes it's possible to disable payment gateways for groups of multiple product categories.
1) In the separated function below we define our groups of product categories and payment gateways. The product categories can be either term(s) id(s), slug(s) or name(s). So in this function we define our settings, to be used:
// The settings in a function
function defined_categories_remove_payment_gateways() {
// Below, Define by groups the categories that will removed specific defined payment gateways
// The categories can be terms Ids, slugs or names
return array(
'group_1' => array(
'categories' => array( 11, 12, 16 ), // product category terms
'payment_ids' => array( 'cod' ), // <== payment(s) gateway(s) to be removed
),
'group_2' => array(
'categories' => array( 13, 17, 15 ), // product category terms
'payment_ids' => array( 'bacs', 'cheque' ), // <== payment(s) gateway(s) to be removed
),
'group_3' => array(
'categories' => array( 14, 19, 47 ), // product category terms
'payment_ids' => array( 'paypal' ), // <== payment(s) gateway(s) to be removed
),
);
}
2) Now the hooked function that will remove, in checkout page, the payment gateways based on the cart items product categories, loading our settings function:
add_filter( 'woocommerce_available_payment_gateways', 'remove_gateway_based_on_category' );
function remove_gateway_based_on_category( $available_gateways ){
// Only on checkout page
if ( is_checkout() && ! is_wc_endpoint_url() ) {
$settings_data = defined_categories_remove_payment_gateways(); // Load settings
$unset_gateways = []; // Initializing
// 1. Loop through cart items
foreach ( WC()->cart->get_cart() as $cart_item ) {
// 2. Loop through category settings
foreach ( $settings_data as $group_values ) {
// // Checking the item product category
if ( has_term( $group_values['categories'], 'product_cat', $cart_item['product_id'] ) ) {
// Add the payment gateways Ids to be removed to the array
$unset_gateways = array_merge( $unset_gateways, $group_values['payment_ids'] );
break; // Stop the loop
}
}
}
// Check that array of payment Ids is not empty
if ( count($unset_gateways) > 0 ) {
// 3. Loop through payment gateways to be removed
foreach ( array_unique($unset_gateways) as $payment_id ) {
if( isset($available_gateways[$payment_id]) ) {
// Remove the payment gateway
unset($available_gateways[$payment_id]);
}
}
}
}
return $available_gateways;
}
Code goes in functions.php file of your active child theme (or active theme). Tested and works.

Display the discounted subtotal after discount on pdf invoice in Woocommerce

I want to display the cost of the product after a discount coupon on the invoice.
I have the following code, but it displays only base price, not the cost after coupon code:
add_filter( 'wpo_wcpdf_woocommerce_totals', 'wpo_wcpdf_woocommerce_totals_custom', 10, 2 );
function wpo_wcpdf_woocommerce_totals_custom( $totals, $order ) {
$totals = array(
'subtotal' => array(
'label' => __('Subtotal', 'wpo_wcpdf'),
'value' => $order->get_subtotal_to_display(),
),
);
return $totals;
}
I tred to change $totals to $discount, but it didn't work.
Your actual code is just removing all totals, displaying the subtotal only. Please try the following hooked function, that will display the discounted subtotal after the discount amount:
add_filter( 'wpo_wcpdf_woocommerce_totals', 'add_discounted_subtotal_to_pdf_invoices', 10, 2 );
function add_discounted_subtotal_to_pdf_invoices( $totals, $order ) {
// Get 'subtotal' raw amount value
$subtotal = strip_tags($totals['cart_subtotal']['value']);
$subtotal = (float) preg_replace('/[^0-9.]+/', '', $subtotal);
// Get 'discount' raw amount value
$discount = strip_tags($totals['discount']['value']);
$discount = (float) preg_replace('/[^0-9.]+/', '', $discount);
$new_totals = array();
// Loop through totals lines
foreach( $totals as $key => $values ){
$new_totals[$key] = $totals[$key];
// Inset new calculated 'Subtotal discounted' after total discount
if( $key == 'discount' && $discount != 0 && !empty($discount) ){
$new_totals['subtotal_discounted'] = array(
'label' => __('Subtotal discounted', 'wpo_wcpdf'),
'value' => wc_price($subtotal - $discount)
);
}
}
return $new_totals;
}
Code goes in function.php file of your active child theme (or theme).
Tested and works. It should work for you too.

Categories