I have a function that runs on the front-end of my WooCommerce Wordpress site that generates ticket numbers for people who answer a simple question correctly and no ticket order for those that do not answer correctly. It saves the users answer in the order item meta data and I have it so that the answer is displayed in the backend in an editable WooCommerce Text input field for each item within the order as each item would have a different answer.
add_action( 'woocommerce_admin_order_item_values', 'pd_admin_order_item_values', 10, 3);
function pd_admin_order_item_values( $product, $item, $item_id ) {
$user_answer = $item->get_meta( "User Answer" );
?>
<div class="edit_custom_field"> <!-- use same css class in h4 tag -->
<?php
woocommerce_wp_text_input( array(
'id' => 'custom_field_name',
'label' => 'Update User Answer:',
'value' => $user_answer,
'wrapper_class' => 'form-field-wide'
) );
?>
</div>
<?php
}
I need to make it so that if an admin changes it to the correct field and updates the post it will save the new data and run the function to assign a ticket number. I am using the correct hook I believe as it crashes when I hit update on the order but the logs are saying that it has too few arguments to run. I have seen some people have used $post_id and $post instead of the order variants but changing it to call these variables hasn't worked either.
add_action( 'woocommerce_process_shop_order_meta', 'custom_woocommerce_process_shop_order_meta', 10, 4 );
function custom_woocommerce_process_shop_order_meta( $item, $order_id, $order ){
// Loop through order items
foreach ($item->get_items() as $item_id => $item ) {
$admin_question = wc_sanitize_textarea( $_POST[ 'custom_field_name' ] );
$user_answer = $item->get_meta( $admin_question );
$admin_answer = $item->get_meta( "Answer" );
if( $admin_answer == $user_answer ){
// Check that tickets numbers haven't been generated yet for this item
if( $item->get_meta( "_tickets_number") )
continue;
$product = $item->get_product(); // Get the WC_Produt Object
$quantity = (int) $item->get_quantity(); // Get item quantity
// Get last ticket sold index from product meta data
$now_index = (int) $product->get_meta('_tickets_sold');
$tickets_ids = array(); // Initializing
// Generate an array of the customer tickets Ids from the product registered index (last ticket ID)
for ($i = 1; $i <= $quantity; $i++) {
$tickets_ids[] = $now_index + $i;
};
// Save the tickets numbers as order item custom meta data
$item->update_meta_data( "Tickets numbers", implode(' ', $tickets_ids) ); // Displayed string of tickets numbers on customer orders and emails
$item->update_meta_data( "User Answer", $user_answer ); // Displayed string of tickets numbers on customer orders and emails
$item->update_meta_data( "_tickets_number", $tickets_ids ); // (Optional) Array of the ticket numbers (Not displayed to the customer)
$item->save(); // Save item meta data
// Update the Ticket index for the product (custom meta data)
$product->update_meta_data('_tickets_sold', $now_index + $quantity );
$product->save(); // Save product data
} else {
$item->update_meta_data( "User Answer", $user_answer ); // Displayed string of tickets numbers on customer orders and emails
$item->save(); // Save item meta data
}
}
$order->save(); // Save all order data
}
My issue I believe lies in it not being able to call the order and order id variable when it runs the function but I am unsure.
Turns out that if I moved the order back to on hold, edited the meta data there and then moved it forward to processing again it runs the original function and assigned a number.
Related
I have only one product ('testare-psihologica'). When the buyer buys it, he must associate this product with additional information (specific 'test' title) that will influence the price. I do not want to use variable products because those 'test' titles will be a lot, so variable products are not very convenient for me. In this situation, I chose to add this product multiple times as separate items but with different 'test' titles.
add_filter( 'woocommerce_add_cart_item_data', 'add_cart_simple_product_custom_price', 20, 2 );
function add_cart_simple_product_custom_price( $cart_item_data, $product_id ) {
//add selected test ID to 'testare-psihologica' cart item
if( isset( $_GET['test'] ) ) {
$test_id = get_page_by_path( $_GET['test'], OBJECT, 'tests' )->ID;
$test_title = get_post_field( 'post_title', $test_id );
$cart_item_data['test_title'] = $test_title;
}
// add product to cart multiple times, but as different items
$unique_cart_item_key = md5( microtime() . rand() );
$cart_item_data['unique_key'] = $unique_cart_item_key;
return $cart_item_data;
}
After this, the 'test' titles will be displayed in the cart and checkout pages under the product name, and then they will be added to the order as metadata (with other code).
The only disadvantage of this approach is that when the product is added to the cart several times but with the same (identical) 'test' title, it also appears as separate items, but I would like these items to appear grouped as one and only increase their quantity. So, instead of this:
I want this:
And I want to do this programmatically. How can this be achieved?
It seems that the following lines of code are redundant:
$unique_cart_item_key = md5( microtime() . rand() );
$cart_item_data['unique_key'] = $unique_cart_item_key;
So to answer your question you can just delete those lines
Because I don't have the same get data that you use in your answer, I made a proof of concept, divided into 3 steps:
1. Add input field before the add to cart button on the single product page
function action_woocommerce_before_add_to_cart_button() {
// Add a new input field, allowing the customer to set "test"
echo '<div class="my-div"><input type="text" id="test" name="test"></div>';
}
add_action( 'woocommerce_before_add_to_cart_button', 'action_woocommerce_before_add_to_cart_button' );
2. Add 'test' value to cart item
function filter_add_cart_item_data( $cart_item_data, $product_id, $variation_id ) {
// Add test to cart item
if ( ! empty ( $_POST[ 'test' ] ) ) {
$cart_item_data['test_title'] = sanitize_text_field( $_POST['test'] );
}
return $cart_item_data;
}
add_filter( 'woocommerce_add_cart_item_data', 'filter_add_cart_item_data', 10, 3 );
3. Optionally display custom data in cart and checkout pages
// Optionally display custom data in cart and checkout pages
function filter_woocommerce_get_item_data( $cart_data, $cart_item = null ) {
if ( isset( $cart_item['test_title'] ) ) {
$cart_data[] = array(
'name' => 'Test title',
'value' => $cart_item['test_title']
);
}
return $cart_data;
}
add_filter( 'woocommerce_get_item_data', 'filter_woocommerce_get_item_data', 99, 2 );
Then apply the following steps on the first step:
Insert group1 as value and press the add to cart button
Insert group2 as value and press the add to cart button
Insert group1 as value and press the add to cart button
The product with value group1 will automatically be grouped based on the corresponding value:
There is an issue when including "stock status" of the purchased items on the client email.
If the client orders the last unit of an item, even though when he ordered it was clearly instock (available for immediate shipping), then the stock status turns to outofstock/onbackorder and the email that is sent to the client (which I assume is sent/generated a few seconds after the stock value is updated from the clients own order) shows this status updated to outofstock/onbackorder, so after completing the purchase now the client thinks the product is out of stock when in fact it was not.
I'm using this code hooked onto my emails:
$product = $item->get_product();
$stockstatus = get_post_meta( $product->get_id(), '_stock_status', true );
if ($stockstatus == 'instock') { 'Available for immediate shipping'; }
elseif ($stockstatus == 'onbackorder') { 'On Preorder - slow shipping';}
We use Stock status to define shipping time of our products, it's simple, if it's in stock = immediate shipping, if it's outofstock/onbackorder = Preorder ( slow shipping )
Clients can view this information individually for each product on the product page and on the cart, however to make it as clear as possible and also keep a record of the Stock Status of when the order was made (so we know and can show to a client if a product was or not in stock when ordered) we also send this information to the client email. The issue being it displays the stock status at the time the email is sent and not at the time of the order (So if it was the last unit and the product turns to out of stock, then clients get the wrong message)
What would be the correct way to go about this situation and instead of displaying the current stock status, displaying the correct stock status at the time that the order was placed, so that the customers get the correct information?
Thank you in advance for the attention and advice
Edit:
Here you can find the full code on how I apply this upper snippet to my emails,
I apologize I did not share all the code above (the part for editing custom woocommerce email), my intention was to simplify as I felt it would be unrelated/filler/distracting from the main point as there are a few posts already covering how to customize woocommerce emails.
These are I believe the main posts that cover this:
Credits #Loictheaztec & #7uc1f3r
Customize order item meta only for WooCommerce admin email notifications
Display product ACF fields on a specific WooCommerce email
Here is the full code I am using :
// Setting the "sent_to_admin" as a global variable
function email_order_id_as_a_global($order, $sent_to_admin, $plain_text, $email) {
$GLOBALS['email_data'] = array(
'sent_to_admin' => $sent_to_admin, // <== HERE we set "$sent_to_admin" value
'email_id' => $email->id, // The email ID (to target specific email notification)
);
}
add_action('woocommerce_email_before_order_table', 'email_order_id_as_a_global', 1, 4);
function custom_order_item_name( $item_name, $item ) {
if ( ! is_wc_endpoint_url() && $item->is_type('line_item') ) {
// Getting the custom 'email_data' global variable
$refNameGlobalsVar = $GLOBALS;
$email_data = $refNameGlobalsVar['email_data'];
// Only for new order
if( is_array( $email_data ) && $email_data['email_id'] == 'new_order' ) {
// Get the WC_Product object (from order item)
$product = $item->get_product();
$product = $item->get_product();
$stockstatus = get_post_meta( $product->get_id(), '_stock_status', true );
if ($stockstatus == 'instock') { 'Available for immediate shipping'; }
elseif ($stockstatus == 'onbackorder') { 'On Preorder - slow shipping';}
}
}
}
return $item_name;
}
add_filter( 'woocommerce_order_item_name', 'custom_order_item_name', 10, 2 ); ```
You can use the following that will save the product stock status as custom order item meta data when customer place an order (so you will always get the stock status when the order was placed):
add_action('woocommerce_checkout_create_order_line_item', 'save_stock_status_order_item_meta', 10, 4 );
function save_stock_status_order_item_meta( $item, $cart_item_key, $values, $order ) {
$item->update_meta_data( '_stock_status', $values['data']->get_stock_status() );
}
Code goes in functions.php file of the active child theme (or active theme).
Then you will replace your "code hooked onto your emails" by this one:
$stock_status = $item->get_meta('_stock_status');
if ( 'instock' === $stock_status ) {
echo __('Available for immediate shipping');
} elseif ( 'onbackorder' === $stock_status ) {
echo __('On Preorder - slow shipping');
}
It should work.
I am creating a 'Build Your Own' page. After selecting 4 products from the options, 1 main product gets added to the basket with 4 of the selected products being added as meta_data under that main product.
You can see below the main product with 4 selected products (IDs).
After paying for this item, I need to add each selected product to the order, so that they are within the order on the backend. I'm having to do it like this, because I need the stock of the selected product to go down, eventually pulling into a stock management system we use (veeqo)
Any help is appreciated. The code below allows me get the some meta_data for woocommerce_thankyou but I am not sure if it will work then...
add_action('woocommerce_thankyou', 'BuildYourOwn', 10, 1);
function BuildYourOwn( $order_id ) {
if ( !$order_id ){
return;
}
$firstTime = get_post_meta( $order_id, '_thankyou_action_done', true );
// Allow code execution only once
if( !$firstTime ) {
// Get an instance of the WC_Order object
$order = wc_get_order( $order_id );
$exItems = '';
// Loop through order items
foreach ( $order->get_items() as $item_id => $item ) {
//print_r($item);
// Get the product object
$product = $item->get_product();
// Get the product sku
$product_sku = $product->get_sku();
// Get the product name
$product_id = $product->get_name();
$extras = $item->get_formatted_meta_data('_', true);
$exItems.=$product_sku;
if(!empty($extras)){
$exItems.=$product_sku.' -';
foreach($extras as $extra){
$exItems.= ' ['.$extra->key.' : '. preg_replace("/[^A-Za-z0-9?#,.&%!\s]/","",$extra->value).'] ';
}
}
$exItems.="\n";
}
var_dump($exItems);
Maybe I worded the question wrong - But I figured it out:
I used the code below to get the meta_data which I then looped through and got individual item id and added it to the basket this way.
// Get the product meta data
$extras = $item->get_formatted_meta_data('_', true);
if ($product_id == 60023){
if(!empty($extras)){
$args = array(
'subtotal' => 0,
'total' => 0,
);
foreach($extras as $extra){
$mini_name = $extra->value;
$mini = get_page_by_title( $mini_name, OBJECT, 'product' );
$mini_id = $mini->ID;
$order->add_product( wc_get_product($mini_id), 1, $args); // Add Minis
}
}
}
The only slight issue is woocommerce_thankyou hook not firing if paypal users don't come back to the site after paying.
I've a single piece of custom metadata to a WooCommerce order and now I want to display this on the thank you page after checkout, however, the data isn't available. The data is saved and available in the admin, I just can't seem to access it.
function custom_order_item_meta( $item_id, $values ) {
if ( ! empty( $values['custom_option'] ) ) {
woocommerce_add_order_item_meta( $item_id, 'custom_option', $values['custom_option'] );
}
}
add_action( 'woocommerce_add_order_item_meta', 'custom_order_item_meta', 10, 2 );
But when I dump out the wc_get_order my meta data isn't there.
I'm using;
woocommerce_add_order_item_meta()
to save the data but dumping out var_dump(wc_get_order( $order->id )); also doesn't show my custom meta field
is there another hook I should be using to access this data?
The data that you are looking for is not order meta data, but order item meta data and is located in wp_woocommerce_order_itemmeta database table (see below how to access this data).
Since woocommerce 3, a much better hook replace old woocommerce_add_order_item_meta hook.
Displayed and readable order item meta data:
To make custom order item meta data displayed everywhere, the meta key should be a readable label name and without starting by an underscore, as this data will be displayed under each order item.
The code:
add_action( 'woocommerce_checkout_create_order_line_item', 'custom_order_item_meta', 20, 4 );
function custom_order_item_meta( $item, $cart_item_key, $values, $order ) {
if ( isset( $values['custom_option'] ) ) {
$item->update_meta_data( __('Custom option', 'woocommerce'), $values['custom_option'] );
}
}
In "Order received" (thankyou) page, you will get something like:
This will be displayed too in backend and email notifications.
To access this order item data you need to get items from the order object in a foreach loop:
foreach( $order->get_items() as $item_id => $item ){
$custom_data = $item->get_meta( 'Custom option' );
}
To Get the first order item (avoiding a foreach loop), you will use:
$items = $order->get_items(); // Order items
$item = reset($items); // The first Order item
$custom_data = $item->get_meta( 'Custom option' ); // Your custom meta data
Related: Replace woocommerce_add_order_item_meta hook in Woocommerce 3.4
I'm creating a Plugin in WooCommerce and have a small issue with adding custom discounts to the CART / CHECKOUT page.
How can I apply custom discount to the cart without creating coupons?
Say I want to give some discount of 5 dollars on the cart page. How can I do that?
Below is my code from the plugin file where I have used a coupon to apply discount, but I want to add another custom discount without the use of coupon.
Action Hook in the plugin file :
add_action('woocommerce_calculate_totals',array(&$this,'cart_order_total_action'));
and its function in the plugin file is :
public function cart_order_total_action(){
if ( is_user_logged_in() ){
global $woocommerce;
global $current_user;
global $wpdb;
$u_id = $current_user->ID;
$table_name = $wpdb->prefix."woocommerce_customer_reward_ms";
$thetable2 = $wpdb->prefix . "woocommerce_customer_reward_cart_ms";
$table_name3 = $wpdb->prefix."woocommerce_customer_reward_points_log_ms";
$data = $wpdb->get_row("SELECT * from $table_name where id=$u_id");
$data2 = $wpdb->get_row("SELECT * from $thetable2");
/* Order Id goes here */
$orders=array();//order ids
$args = array(
'numberposts' => -1,
'meta_key' => '_customer_user',
'meta_value' => $current_user->ID,
'post_type' => 'shop_order',
'post_status' => 'publish',
'tax_query'=>array(
array(
'taxonomy' =>'shop_order_status',
'field' => 'slug',
'terms' =>'on-hold'
)
)
);
$posts=get_posts($args);
$orders=wp_list_pluck( $posts, 'ID' );
$order = $orders[0];
/* Order Id ends here */
if($data){
$user_points = $data->points;
$points_set = $data2->woo_pts_set;
$coupon_code = 'wooreward_discount';
if($user_points>=$points_set){
// this following Code is optional and can be removed......as there is no need of if statement here
if ( $woocommerce->cart->has_discount( $coupon_code ) ) {
/*$woocommerce->add_error( __('Coupon Code Already Applied.!!','woocommerce'));*/
return false;
}else{
$woocommerce->cart->add_discount(sanitize_text_field($coupon_code));
$woocommerce->add_message( __('Taxco925 Reward Discount Applied.!!','woocommerce'));
}
}else{
$woocommerce->add_error( __('Not Enough Taxco925 Points.!!','woocommerce'));
}
}else{
$woocommerce->add_error( __('You have have not earned any Taxco925 Points yet.!!','woocommerce'));
}
}
}
As you can see this line $woocommerce->cart->add_discount(sanitize_text_field($coupon_code));
adds my discount to the cart. But it uses coupon in the background to do so . Is there any way I can add a custom discount without the use of coupon.
add_action('woocommerce_checkout_order_processed','custom_disount',10,1);
function custom_disount($order_id){
$order = wc_get_order($order_id);
$order_items = $order->get_items();
foreach ($order_items as $order_item_key => $order_item) {
$product = new WC_Product((int) $order_item['product_id']);
$quantity = (int) $order_item['qty'];
$discount=($product->regular_price*30)/100; //30% disount.
wc_update_order_item_meta($order_item_key,'_line_total',($product->regular_price*$quantity)-($discount*$quantity));
}
}
You can add discount to each and every product in the cart using "woocommerce_get_discounted_price" hook.
For Eg.:
function filter_woocommerce_get_discounted_price( $price, $values, $instance ) {
//$price represents the current product price without discount
//$values represents the product object
//$instance represent the cart object
$discount = 300; // add custom discount rule , This is just an example
return ($price - $discount);
};
add_filter('woocommerce_get_discounted_price','filter_woocommerce_get_discounted_price', 10, 3 );
Maybe too late, but If someone have another solution tell me.
I use something like:
$discount = floatval(10);
if(!empty($discount) || $discount != 0){
$discount *= -1; // convert positive to negative fees
$woocommerce->cart->add_fee('discount', $discount, true, '' ); // add negative fees
}
If you use paypal standard payment, you got an error because you can't submit a product with negative pricing.
You just need to edit the paypal woocommerce plugin to pass this value.
But other Payment method is ok!
Best Regards,
Add fee with negative value will not produce the right total fee.
Tax is added on the fee amount resulting in higher total fee than expected.
You need to create a "coupon" and apply it to the cart before you create the order from the cart (it will not calculate right if you apply it on $order directly). Then recalculate the cart->total and finally create an order from the cart, after you have saved the order you can remove the "dynamic" created "coupon" if you want. You can create dynamic coupons with any dynamic $value and of any type (fixed, percent etc etc).
This is the only way to add discounts in woo3+.
Fee is doing it wrong when it comes to discounts. Also woo say about fee "Not use negative values here!".
I guessed you wanted some example?
here....
<?php
// this code inside wordpress and with woo3+ of course......
// you have to figure out the rest yourself, how to implement it. but here it is...
$order_data = array (
'status' => 'on-hold' // or whatever order staus
// can have more data if need here...
);
// below creates a coupon with discount_type = fixed_cart, default.
$coupon = array (
'post_title' => 'coupon_discount',
'post_status' => 'publish',
'post_type' => 'shop_coupon'
);
// can be modified with update_post_meta discount_type = percent and so on....
$dynamic_discount = 20; // yes, just a number can be from another dynamic input source....
$new_coupon_id = wp_insert_post( $coupon ); // add the coupon to the cart
add_post_meta( $new_coupon_id , 'coupon_amount' , $dynamic_discount , true ); // add the "discount" value ($dynamic_discount)..... depends on discount_type... in this case fixed_cart
WC()->cart->add_to_cart( 2122 , 2 ); // add products, product_id , quantity ..... can be in a loop.
WC()->cart->add_discount( 'coupon_discount' ); // APPLY THE COUPON WITH DISCOUNT -> This is the trick....
WC()->cart->calculate_totals(); // do some math on the "cart"
WC()->checkout(); // yes, checkout the "cart", now with the discount....
$order_id = WC()->checkout()->create_order( $order_data ); // basic order data, see the top in this script.. get new created order_id.
$order = wc_get_order( $order_id ); // get the order...
// can do more with $order here if want, but NOT any coupons... it just not work in $order as is...
$order->calculate_totals(); // math
WC()->cart->empty_cart(); // empty cart....
$order->save(); // save the order...
wp_delete_post( $new_coupon_id , true ); // IF you want to delete the "dynamic" coupon created above... up 2 u, if not you will end up with a lot of coupons
// sorry, a bad example, uggly code, but at least it work.... :)
// btw, i like Pattaya, send bitcoins :)
// Again, sorry for uggly code...
?>