Better Inventory with Gravity Forms

Gravity Forms does not support the concept of inventory out of the box. This snippet provides an easy method for setting up simple, one-off inventory limits on a per field basis.
  • July 31, 2015: Updated paid status to "Paid" from "Approved" to work with GF Payment Add-on framework.

  • October 5, 2014: Added support for "remaining" action on [gravityforms] shortcode.

  • October 1, 2014: Added support for "sum" action on [gravityforms] shortcode.

  • November 11, 2012: Fixed issue where database prefix was not applied via $wpdb->prefix.

Let’s dive into the code first.

<?php
/**
* Gravity Wiz // Gravity Forms // Better Inventory with Gravity Forms
*
* Implements the concept of "inventory" with Gravity Forms by allowing the specification of a limit determined by the sum of a specific field, typically a quanity field.
*
* @version 1.0
* @author David Smith <david@gravitywiz.com>
* @license GPL-2.0+
* @link http://gravitywiz.com/2012/09/19/better-inventory-with-gravity-forms/
* @copyright 2014 Gravity Wiz
*/
class GWLimitBySum {
private $_args;
function __construct($args) {
$this->_args = wp_parse_args($args, array(
'form_id' => false,
'field_id' => false,
'limit' => 20,
'limit_message' => __('Sorry, this item is sold out.'),
'validation_message' => __('You ordered %1$s of this item. There are only %2$s of this item left.'),
'approved_payments_only' => false,
'hide_form' => false
));
$this->_args['input_id'] = $this->_args['field_id'];
extract($this->_args);
add_filter("gform_pre_render_$form_id", array(&$this, 'limit_by_field_values'));
add_filter("gform_validation_$form_id", array(&$this, 'limit_by_field_values_validation'));
// add 'sum' action for [gravityforms] shortcode
add_filter( 'gform_shortcode_sum', array( $this, 'shortcode_sum' ), 10, 2 );
add_filter( 'gform_shortcode_remaining', array( $this, 'shortcode_remaining' ), 10, 2 );
if($approved_payments_only)
add_filter('gwlimitbysum_query', array(&$this, 'limit_by_approved_only'));
}
public function limit_by_field_values($form) {
$sum = self::get_field_values_sum($form['id'], $this->_args['input_id']);
if($sum < $this->_args['limit'])
return $form;
if($this->_args['hide_form']) {
add_filter('gform_get_form_filter', create_function('', 'return "' . $this->_args['limit_message'] . '";'));
} else {
add_filter('gform_field_input', array(&$this, 'hide_field'), 10, 2);
}
return $form;
}
public function limit_by_field_values_validation($validation_result) {
extract($this->_args);
$form = $validation_result['form'];
$exceeded_limit = false;
foreach($form['fields'] as &$field) {
if($field['id'] != intval($input_id))
continue;
$requested_value = rgpost("input_" . str_replace('.', '_', $input_id));
$field_sum = self::get_field_values_sum($form['id'], $input_id);
if($field_sum + $requested_value <= $limit)
continue;
$exceeded_limit = true;
$number_left = $limit - $field_sum >= 0 ? $limit - $field_sum : 0;
$field['failed_validation'] = true;
$field['validation_message'] = sprintf($validation_message, $requested_value, $number_left);
}
$validation_result['form'] = $form;
$validation_result['is_valid'] = !$validation_result['is_valid'] ? false : !$exceeded_limit;
return $validation_result;
}
public function hide_field($field_content, $field) {
if($field['id'] == intval($this->_args['input_id']))
return "<div class=\"ginput_container\">{$this->_args['limit_message']}</div>";
return $field_content;
}
public static function get_field_values_sum($form_id, $input_id) {
global $wpdb;
$query = apply_filters( 'gwlimitbysum_query', array(
'select' => 'SELECT sum( ld.value )',
'from' => "FROM {$wpdb->prefix}rg_lead_detail ld",
'join' => '',
'where' => $wpdb->prepare( "WHERE ld.form_id = %d AND CAST( ld.field_number as unsigned ) = %d", $form_id, $input_id )
), $form_id, $input_id );
$wpdb->show_errors();
$sql = implode( ' ', $query );
$result = $wpdb->get_var( $sql );
return intval( $result );
}
public static function limit_by_approved_only( $query ) {
global $wpdb;
$query['from'] .= " INNER JOIN {$wpdb->prefix}rg_lead l ON l.id = ld.lead_id";
$query['where'] .= ' AND l.payment_status = \'Paid\'';
return $query;
}
public function shortcode_sum( $output, $atts ) {
$atts = shortcode_atts( array(
'id' => false,
'input_id' => false
), $atts );
extract( $atts ); // gives us $id, $input_id
return intval( self::get_field_values_sum( $id, $input_id ) );
}
public function shortcode_remaining( $output, $atts ) {
$atts = shortcode_atts( array(
'id' => false,
'input_id' => false,
'limit' => false
), $atts );
extract( $atts ); // gives us $id, $input_id
$remaining = $limit - intval( self::get_field_values_sum( $id, $input_id ) );
return max( 0, $remaining );
}
}
# Configuration
new GWLimitBySum( array(
'form_id' => 363,
'field_id' => 2.3,
'limit' => 2,
'limit_message' => 'Sorry, there are no more tickets!',
'validation_message' => 'You ordered %1$s tickets. There are only %2$s tickets left.',
'approved_payments_only' => false,
'hide_form' => false
) );

How do I install this snippet?

Easy peasy. Just copy and paste the code above into your theme's functions.php file.

Do I need to modify this snippet to work with my form?

You don’t have to modify the snippet itself. Just the parameters with which you initialize it.

new GWLimitBySum( array(
'form_id' => 363,
'field_id' => 2.3,
'limit' => 2,
'limit_message' => 'Sorry, there are no more tickets!',
'validation_message' => 'You ordered %1$s tickets. There are only %2$s tickets left.',
'approved_payments_only' => false,
'hide_form' => false
) );

Parameters

  • $form_id the ID of the form you are working with.
  • $field_id the ID of your product or quantity field. If using a Single Product field, you’ll need to specify the quantity input ID which will always be {field ID}.3 (ie if the field ID is 12, the quantity input ID will be 12.3). If using a separate Quantity field, simply specify the field ID.
  • $limit the number of this item you have available.
  • $limit_message the message which should be displayed to users when the product limit has been reached. You can get fancy with this and add HTML as well. Be sure to escape any double quotes with a backslash. Here’s a more advanced example:
    $sum_limit_message = '<div style="border: 1px solid #e6db55; background-color: #FFFFE0; padding: 10px;">Sorry, this show is sold out.</div>';
    Better Inventory: Limit Message
  • $validation_message the validation message which should be displayed on the field if the limit has not been reached, but the user’s requested quantity would exceed the product limit.
    Better Inventory: Limit Validation Message
  • $approved_payments_only true/false indicate whether all submissions for this field should be counted against the limit or only submissions with an approved payment. You’ll want to be careful here when using PayPal Standard due to the delay between the form submission and the payment being approved. It could lead to users exceeding the limit.
  • $hide_form by default this snippet will only hide the field when the limit has been reached. If you would rather hide the entire form, set this to true.

Comments

  1. Steve says

    Another great snippet, thanks!

    The function get_field_values_sum casts the field_number and input_id as unsigned, so it may not work for subfields with ID format ‘x.y’.

    ‘where’ => $wpdb->prepare( “WHERE ld.form_id = %d AND CAST( ld.field_number as unsigned ) = %d”, $form_id, $input_id )

    This works for me.

    ‘where’ => $wpdb->prepare( “WHERE ld.form_id = %d AND CAST( ld.field_number as char ) = %s”, $form_id, $input_id )

    • says

      Thanks for the input, Steve. Just tested to confirm. Casting as unsigned works for both the integer and float field numbers (“5″, “5.3”).

  2. says

    For an event scenario, how difficult would it be to have two ticket types: an individual ticket and a “table” or group ticket where each one counts against a total capacity, or inventory limit? Someone could choose 1 ticket, 4 tickets, up to 9 tickets. Or, a second ticket type would be “Table” which includes 10 individual tickets, and they could choose up to 5 tables, which would be 50 tickets. But regardless of the quantity of individual or table tickets they purchase, the total “seats” are being counted. Does that make sense? A little more code and a little bit of conditional logic and some hidden fields?

    • says

      Hi Kevin, the hook is available in the code that would make such a modification possible but it would required additional code to accomplish. The hook is “gwlimitbysum_query”. It lets you modify the query that is made to retrieve the current quantity of the specified product that has been ordered. You could update the query to include the quantity of a second (or more) product field as well.

      If you’re interested in commissioning me to write such an enhancement, get in touch.

    • says

      Thanks David. Just sent you a message. In the existing version of code, does the sum change if entries are deleted? For example, I run some test purchases, but then delete those entries. The function is such that it’s counting quantity of real, existing entries, so once an entry is deleted, even in the case of a refund and we manually deleted the entry, would that adjust the count accurately?

    • says

      Does using Stripe rather than PayPal have any impact on functionality of the code snippet? If not, would using either PayPal or Stripe in “Test Mode” have any impact on functionality? I’m in test mode using Stripe with the test credit card numbers. The form/purchase is submitted/approved, and when viewing the form entries I see the Payment Details Status as “Paid”, but the limit reached message is not displaying unless I set $approved_payments_only to false. I also haven’t embedded the form into a page yet, I’m just using Gravity’s Preview Form feature to test. Thanks so much for this great piece of code and all your help.

    • says

      Hi Kevin, I’ve just updated the snippet to use the new and correct status of “Paid”. Prior to the new GF Payment Add-on framework, the value was “Approved”.

  3. says

    Hi David,

    I want to be able to use an inventory system limiting the quantity of tickets for different options. For example, have 100 tickets available for EACH date. 60 of which are ‘reserved ticketing’ and 40 for ‘general admission’.

    Any ideas on how I would best achieve this?

    Thanks.

    • says

      I can’t think of any simple way to achieve this without additional custom code. You could add two product fields, one for reserved and one for general admission. Configure the snippet separately for both fields. Then you’d need to write some custom code to modify the query based on the selected date.

      If this is something you’d be interested in commissioning me to write, let’s talk about it.

  4. says

    I am using this code to limit number of tickets purchased for an event:

    new GWLimitBySum( array( ‘form_id’ => 35, ‘field_id’ => 8, ‘limit’ => 20, ‘limit_message’ => ‘Sorry, there are no more tickets!’, ‘validation_message’ => ‘You ordered %1$s tickets. There are only %2$s tickets left.’, ‘approved_payments_only’ => false, ‘hide_form’ => true ) );

    Let’s say that there are 15 tickets available and 11 have already been purchased. Then someone tries to order 5 more tickets. I am expecting the validation message to stop them from purchasing the tickets by telling them that they ordered 5 tickets, but there are one 4 tickets remaining, but it doesn’t, it accepts the order. Then on the next attempt to purchase tickets, the form disappears and says “sorry, there are no more tickets.” What am I doing wrong? How can I fix it?

    • says

      Hi Michele, the expected behavior is correct. I tried to recreate your issue locally but was unable to. Can you provide me an export of your form and the configuration code you’ve used to instantiate this snippet?

  5. says

    Hiya David,

    Thanks for the code! I was trying to use this code for multiple forms on the same site at once. So basically I have a form that takes enrollments for a class, the class meets over multiple sessions and so every instance of the class has it’s own form (6 total forms). Is there a way to use this code for multiple forms? If I call new GWLimitBySum() more than once, I get a WP database error and adding additional form_ids on the same call doesn’t work for more than the first form it seems. Any insight you could give would be appreciated. Thanks!

    • says

      Thx for the help David but still no dice. (#):(

      I’m using the exact code below:

      new GWLimitBySum( array( 'form_id' => 7, 'field_id' => 11.3, 'limit' => 16, 'limit_message' => 'Sorry, there is no more space in this class.!', 'validation_message' => '', 'approved_payments_only' => true, 'hide_form' => false ) );

      new GWLimitBySum( array( 'form_id' => 8, 'field_id' => 11.3, 'limit' => 16, 'limit_message' => 'Sorry, there is no more space in this class.!', 'validation_message' => '', 'approved_payments_only' => true, 'hide_form' => false ) );

      And then I get the following screen : screen shot

      Thoughts?

    • says

      Hey David,

      Thanks for the code! It DID take away the error message but unfortunately it didn’t work for multiple forms and for one form it wouldn’t pop the “limit_message” and instead would just Zero the form total… I had to change “hide_form” to “true” in order to get it where the user wouldn’t be confused as to what was going on. But I think it’s probably very close!

  6. Jack says

    Hi David,

    This is great however, any idea if it would work for the following:

    I need to have my users select 1 of 10 categories in a radio button list and, to better the user experience, I’d like to show the remaining quantity for each category at the end of each radio button description. Then, when the user selects the radio button, a payment gateway will appear.

    Thanks!

  7. Jenn says

    Hey David!

    I’m using gravity forms with Gravity Forms Product Add-Ons for my ecommerce site. Im looking at getting something like GP Limit Choices to help with inventory management on some radio button options. What happens with a choice is no longer available? Does it become grayed out, alternate displayed or just not displayed? Thanks so much!

    • Jenn says

      Ah sorry just read the description. So it says ” If a choice reaches its specified limit it will no longer appear in the field on the front-end.” Is there a way to gray/disable it instead? Or display an alternate option text like “Sold Out”?

      Crossing my fingers this would be perfect if so!

    • says

      Yes, this is supported. :)

      There is a filter you can add to not remove the choices which will disable them instead:

      add_filter( ‘gplc_remove_choices’, ‘__return_false’ );

  8. says

    This snippet is awesome. Thanks for putting it together.

    My question is whether or not there is a snippet that allows someone to enter the constructor arguments directly in the form editor in Gravity Forms?

    If not, how complicated do you think it would be to add this functionality? That way it could be used easily within the WordPress admin without having to edit any PHP files.

    Thanks again.

  9. Jenn says

    Hey David! I have a site with products that have variations (radio button options). I want to be able to set inventory limits or disable certain variations/options once they become unavailable. Is this something your snippet would be able to help me with? :)

  10. says

    We have a website where we sell trainings. Our form is set up as ‘Select a Training’ (product field). We have a ‘Select a Training Date’ (Drop Down field) which we delete past dates and add new ones as time goes on. We have a user input ‘Number of participants’ (quantity field). We want to be able to limit the number of “tickets” we sell for each date and also show how many tickets are available for each date. Pricing is based on ‘Select a Training’ and not ‘Select a Training Date’

    Code: http://pastie.org/10042267 Site: https://nationalmedicalacademy.com/enrollment/

    I am not to familiaer with codeing but I got this far lol.

  11. says

    Hi David,

    Is there a way to call your snippet from a post where you pass the limit parameter from a custom field specified within that post? (And maybe even the form-id parameter) That way it would be possible to use it to registere for different events with a different amount of available seats.

    Thanks, Carl

    • David Smith says

      The issue is you would also need to limit by the post ID as well so that when using the same form, only entries specific to the current post are applied to the limit. I’m still waiting to hear back from another user on their interest in commissioning this extra functionality. If you’d like to commission it, please feel free to get in touch.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>