-
-
Save spivurno/3749223 to your computer and use it in GitHub Desktop.
<?php | |
/** | |
* WARNING! THIS SNIPPET MAY BE OUTDATED. | |
* The latest version of this snippet can be found in the Gravity Wiz Snippet Library: | |
* https://github.com/gravitywiz/snippet-library/blob/master/gravity-forms/gw-inventory.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 quantity field. | |
* | |
* @version 2.20 | |
* @author David Smith <[email protected]> | |
* @license GPL-2.0+ | |
* @link http://gravitywiz.com/better-inventory-with-gravity-forms/ | |
*/ | |
class GW_Inventory { | |
public $_args; | |
public function __construct( $args ) { | |
$this->_args = $this->parse_args( $args ); | |
add_action( 'init', array( $this, 'init' ) ); | |
} | |
public function init() { | |
// make sure we're running the required minimum version of Gravity Forms | |
if( ! property_exists( 'GFCommon', 'version' ) || ! version_compare( GFCommon::$version, '1.8', '>=' ) ) { | |
return; | |
} | |
add_filter( "gform_pre_render_{$this->_args['form_id']}", array( $this, 'limit_by_field_values' ) ); | |
add_filter( "gform_validation_{$this->_args['form_id']}", array( $this, 'limit_by_field_values_validation' ) ); | |
// check 'field_group' for date fields; if found, limit based on exhausted inventory days. | |
if( ! empty( $this->_args['field_group'] ) ) { | |
add_filter( "gpld_limit_dates_options_{$this->_args['form_id']}", array( $this, 'limit_date_fields' ), 10, 2 ); | |
} | |
// 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 ); | |
add_action( 'gwinv_before_get_sum', array( $this, 'before_get_sum' ) ); | |
add_action( 'gwinv_after_get_sum', array( $this, 'after_get_sum' ) ); | |
add_filter( 'gform_product_info', array( $this, 'handle_calculated_product_fields' ), 10, 3 ); | |
if( $this->_args['enable_notifications'] ) { | |
$this->enable_notifications(); | |
} | |
} | |
public function parse_args( $args ) { | |
$args = wp_parse_args( $args, array( | |
'form_id' => false, | |
'field_id' => false, | |
'input_id' => false, | |
'stock_qty' => false, | |
'out_of_stock_message' => __( 'Sorry, this item is out of stock.' ), | |
'not_enough_stock_message' => __( 'You ordered %1$s of this item but there are only %2$s of this item left.' ), | |
'approved_payments_only' => false, | |
'hide_form' => false, | |
'enable_notifications' => false, | |
'field_group' => array() | |
) ); | |
/** | |
* @var $stock_qty | |
* @var $field_group | |
*/ | |
extract( $args ); | |
if( ! $stock_qty && isset( $limit ) ) { | |
$args['stock_qty'] = $limit; | |
unset( $args['limit'] ); | |
} | |
if( isset( $limit_message ) ) { | |
$args['out_of_stock_message'] = $limit_message; | |
unset( $args['limit_message'] ); | |
} | |
if( isset( $validation_message ) ) { | |
$args['not_enough_stock_message'] = $validation_message; | |
unset( $args['validation_message'] ); | |
} | |
if( ! $args['input_id'] ) { | |
$args['input_id'] = $args['field_id']; | |
unset( $args['field_id'] ); | |
} | |
if( $field_group && ! is_array( $field_group ) ) { | |
$args['field_group'] = array( $field_group ); | |
} | |
return $args; | |
} | |
public function enable_notifications() { | |
if( ! class_exists( 'GW_Notification_Event' ) ) { | |
_doing_it_wrong( 'GW_Inventory::$enable_notifications', __( 'Inventory notifications require the \'GW_Notification_Event\' class.' ), '1.0' ); | |
} else { | |
$event_slug = "gwinv_out_of_stock_{$this->_args['input_id']}"; | |
$event_name = GFForms::get_page() == 'notification_edit' ? $this->get_notification_event_name() : __( 'Event name is only populated on Notification Edit view; saves a DB call to get the form on every ' ); | |
$this->_notification_event = new GW_Notification_Event( array( | |
'form_id' => $this->_args['form_id'], | |
'event_name' => $event_name, | |
'event_slug' => $event_slug, | |
'trigger' => array( $this, 'notification_event_listener' ) | |
) ); | |
} | |
} | |
public function limit_by_field_values( $form ) { | |
if( $this->is_in_stock() ) { | |
return $form; | |
} | |
if( $this->_args['hide_form'] ) { | |
add_filter( "gform_get_form_filter_{$form['id']}", array( $this, 'get_out_of_stock_message' ) ); | |
} else if( empty( $this->_args['field_group'] ) ) { | |
add_filter( 'gform_field_input', array( $this, 'hide_field' ), 10, 2 ); | |
} | |
return $form; | |
} | |
public function limit_by_field_values_validation( $validation_result ) { | |
$input_id = $this->_args['input_id']; | |
$limit = $this->get_stock_quantity(); | |
$validation_message = $this->_args['not_enough_stock_message']; | |
$form = $validation_result['form']; | |
$exceeded_limit = false; | |
foreach( $form['fields'] as &$field ) { | |
if( $field['id'] != intval( $input_id ) ) { | |
continue; | |
} | |
$requested_qty = rgpost( 'input_' . str_replace( '.', '_', $input_id ) ); | |
$field_sum = $this->get_sum(); | |
if( rgblank( $requested_qty ) || $field_sum + $requested_qty <= $limit ) { | |
continue; | |
} | |
$exceeded_limit = true; | |
$stock_left = $limit - $field_sum >= 0 ? $limit - $field_sum : 0; | |
$field['failed_validation'] = true; | |
$field['validation_message'] = sprintf( $validation_message, $requested_qty, $stock_left ); | |
} | |
if( $exceeded_limit && ! empty( $this->_args['field_group'] ) ) { | |
foreach( $form['fields'] as &$field ) { | |
if( in_array( $field->id, $this->_args['field_group'] ) ) { | |
$field['failed_validation'] = true; | |
$field['validation_message'] = sprintf( $validation_message, $requested_qty, $stock_left ); | |
} | |
} | |
} | |
$validation_result['form'] = $form; | |
$validation_result['is_valid'] = ! $validation_result['is_valid'] ? false : ! $exceeded_limit; | |
return $validation_result; | |
} | |
public function limit_by_field_group( $query, $form_id, $input_id ) { | |
global $wpdb; | |
if( $input_id != $this->_args['input_id'] ) { | |
return $query; | |
} | |
$form = GFAPI::get_form( $form_id ); | |
$join = $where = array(); | |
foreach( $this->_args['field_group'] as $index => $field_id ) { | |
$field = GFFormsModel::get_field( $form, $field_id ); | |
$alias = sprintf( 'fgld%d', $index + 1 ); | |
// Fetch entry from submission if available. Otherwise, get default/dynpop value. | |
if( rgpost( 'gform_submit' ) == $form_id ) { | |
$value = $field->get_value_save_entry( GFFormsModel::get_field_value( $field ), $form, null, null, null ); | |
} else { | |
$value = $field->get_value_default_if_empty( GFFormsModel::get_parameter_value( $field->inputName, array(), $field ) ); | |
} | |
$join[] = "\nINNER JOIN {$wpdb->prefix}gf_entry_meta {$alias} ON em.entry_id = {$alias}.entry_id"; | |
$where[] = $wpdb->prepare( "CAST( {$alias}.meta_key as unsigned ) = %d AND {$alias}.meta_value = %s ", $field_id, $value ); | |
} | |
$query['join'] .= implode( "\n", $join ); | |
$query['where'] .= sprintf( "\n AND %s", implode( "\nAND ", $where ) ); | |
return $query; | |
} | |
public function limit_date_fields( $options, $form ) { | |
global $wpdb; | |
foreach( $form['fields'] as $field ) { | |
if( ! in_array( $field->id, $this->_args['field_group'] ) || $field->get_input_type() != 'date' || $field->dateType != 'datepicker' ) { | |
continue; | |
} | |
$query = self::get_sum_query( $field->formId, $this->_args['input_id'] ); | |
if( $this->_args['approved_payments_only'] ) { | |
$query = $this->limit_by_approved_payments_only( $query ); | |
} | |
if( ! empty( $this->_args['field_group'] ) ) { | |
// add our Date field to the front of the array so we can reliably target it when replacing the queries below | |
array_unshift( $this->_args['field_group'], $field->id ); | |
$this->_args['field_group'] = array_unique( $this->_args['field_group'] ); | |
$query = $this->limit_by_field_group( $query, $field->formId, $this->_args['input_id'] ); | |
} | |
$regex = sprintf( '/(CAST\( fgld1.meta_key as unsigned \) = %d) AND fgld1.meta_value = \'(?:[\w\d]*)\'/', $field->id ); | |
preg_match( $regex , $query['where'], $match ); | |
if( ! empty( $match ) ) { | |
list( $search, $replace ) = $match; | |
$query['where'] = str_replace( $search, $replace, $query['where'] ); | |
} | |
$query['select'] = 'SELECT sum( em.meta_value ) as total, fgld1.meta_value as date'; | |
$query['group_by'] = 'GROUP BY date'; | |
$query['having'] = sprintf( 'HAVING total >= %d', $this->get_stock_quantity() ); | |
$sql = implode( "\n", $query ); | |
$results = $wpdb->get_results( $sql ); | |
foreach( $results as $result ) { | |
$options[ $field->id ]['exceptionsMode'] = 'disable'; | |
if ( ! is_array( $options[ $field->id ]['exceptions'] ) ) { | |
$options[ $field->id ]['exceptions'] = array(); | |
} | |
$options[ $field->id ]['exceptions'][] = date( 'm/d/Y', strtotime( $result->date ) ); | |
} | |
} | |
return $options; | |
} | |
public function get_stock_quantity() { | |
$stock = $this->_args['stock_qty']; | |
if( is_callable( $stock ) ) { | |
$stock = call_user_func( $stock ); | |
} | |
return $stock; | |
} | |
public function is_in_stock() { | |
$count = $this->get_sum();//self::get_field_values_sum( $this->_args['form_id'], $this->_args['input_id'] ); | |
return $count < $this->get_stock_quantity(); | |
} | |
public function hide_field( $field_content, $field ) { | |
if( $field['id'] == intval( $this->_args['input_id'] ) ) { | |
$quantity_input = ''; | |
// GF will default to a quantity of 1 if it can't find the input for a Quantity field. | |
if ( $field->type === 'quantity' ) { | |
$quantity_input = sprintf( '<input type="hidden" name="input_%d_%d" value="0" />', $field->formId, $field->id ); | |
} | |
return sprintf( '<div class="ginput_container">%s%s</div>', $this->_args['out_of_stock_message'], $quantity_input ); | |
} | |
return $field_content; | |
} | |
public function notification_event_listener() { | |
// really is no better hook to use to send custom notifications just yet | |
add_filter( "gform_confirmation_{$this->_args['form_id']}", array( $this, 'send_out_of_stock_notifications' ), 10, 3 ); | |
} | |
public function send_out_of_stock_notifications( $return, $form, $entry ) { | |
// if product is still in stock or the entry is spam, don't sent notification | |
if( $this->is_in_stock() || $entry['status'] == 'spam' ) | |
return $return; | |
// if product is out of stock and no qty of the product is in current order, assume that out of stock notifications have already been sent | |
$requested_qty = intval( rgar( $entry, (string) $this->_args['input_id'] ) ); | |
if( $requested_qty <= 0 ) | |
return $return; | |
$this->_notification_event->send_notifications( $this->_notification_event->get_event_slug(), $form, $entry ); | |
return $return; | |
} | |
public function get_notification_event_name() { | |
$form = GFAPI::get_form( $this->_args['form_id'] ); | |
$field = GFFormsModel::get_field( $form, $this->_args['input_id'] ); | |
$event_name = sprintf( __( '%s: Out of Stock' ), GFCommon::get_label( $field ) ); | |
return $event_name; | |
} | |
public function shortcode_sum( $output, $atts ) { | |
$atts = shortcode_atts( array( | |
'id' => false, | |
'input_id' => false | |
), $atts ); | |
/** | |
* @var $id | |
* @var $input_id | |
*/ | |
extract( $atts ); // gives us $id, $input_id | |
return intval( self::get_field_values_sum( $id, $input_id ) ); | |
} | |
public function shortcode_remaining( $output, $atts ) { | |
/** | |
* @var $id | |
* @var $input_id | |
* @var $limit | |
*/ | |
$atts = shortcode_atts( array( | |
'id' => false, | |
'input_id' => false, | |
'limit' => false, | |
), $atts ); | |
extract( $atts ); // gives us $id, $input_id | |
if( $input_id == $this->_args['input_id'] && $id == $this->_args['form_id'] ) { | |
$limit = $limit ? $limit : $this->get_stock_quantity(); | |
$remaining = $limit - intval( $this->get_sum() ); | |
$output = max( 0, $remaining ); | |
} | |
return $output; | |
} | |
public function get_sum() { | |
if( $this->_args['approved_payments_only'] ) { | |
add_filter( 'gwinv_query', array( $this, 'limit_by_approved_payments_only' ) ); | |
} | |
if( ! empty( $this->_args['field_group'] ) ) { | |
add_filter( 'gwinv_query', array( $this, 'limit_by_field_group' ), 10, 3 ); | |
} | |
$sum = self::get_field_values_sum( $this->_args['form_id'], $this->_args['input_id'] ); | |
remove_filter( 'gwinv_query', array( $this, 'limit_by_approved_payments_only' ) ); | |
remove_filter( 'gwinv_query', array( $this, 'limit_by_field_group' ) ); | |
return $sum; | |
} | |
public function limit_by_approved_payments_only( $query ) { | |
$valid_statuses = array( 'Approved' /* old */, 'Paid', 'Active' ); | |
$query['where'] .= sprintf( ' AND ( e.payment_status IN ( %s ) OR e.payment_status IS NULL )', self::prepare_strings_for_mysql_in_statement( $valid_statuses ) ); | |
return $query; | |
} | |
public function get_out_of_stock_message() { | |
return $this->_args['out_of_stock_message']; | |
} | |
/** | |
* Calculated Product fields limited by their default quantity should be excluded from the order summary. | |
* | |
* Since their value is calculated post submission, they function uniquely from other product fields. | |
* | |
* @param $order | |
* @param $form | |
* @param $entry | |
* | |
* @return mixed | |
*/ | |
public function handle_calculated_product_fields( $order, $form, $entry ) { | |
foreach( $order['products'] as $field_id => $product ) { | |
// Check for if target input ID is for the current field and is targeting a Product field default quantity input (i.e. 1.3). | |
if( $field_id != intval( $this->_args['input_id'] ) || rgar( explode( '.', $this->_args['input_id'] ), 1 ) != '3' ) { | |
continue; | |
} | |
$field = GFAPI::get_field( $form, $field_id ); | |
if( $field->get_input_type() !== 'calculation' ) { | |
continue; | |
} | |
if( ! $this->is_in_stock() ) { | |
unset( $order['products'][ $field_id ] ); | |
} | |
} | |
return $order; | |
} | |
public static function get_field_values_sum( $form_id, $input_id ) { | |
global $wpdb; | |
$query = self::get_sum_query( $form_id, $input_id ); | |
$sql = implode( "\n", $query ); | |
$result = $wpdb->get_var( $sql ); | |
return intval( $result ); | |
} | |
public static function get_sum_query( $form_id, $input_id, $suppress_filters = false ) { | |
global $wpdb; | |
$query = array( | |
'select' => 'SELECT sum( em.meta_value )', | |
'from' => "FROM {$wpdb->prefix}gf_entry_meta em", | |
'join' => "INNER JOIN {$wpdb->prefix}gf_entry e ON e.id = em.entry_id", | |
'where' => $wpdb->prepare( " | |
WHERE em.form_id = %d | |
AND em.meta_key = %s | |
AND e.status = 'active'\n", | |
$form_id, $input_id | |
) | |
); | |
if( class_exists( 'GF_Partial_Entries' ) ) { | |
$query['where'] .= "and em.entry_id NOT IN( SELECT entry_id FROM {$wpdb->prefix}gf_entry_meta WHERE meta_key = 'partial_entry_id' )"; | |
} | |
if( ! $suppress_filters ) { | |
$query = apply_filters( 'gwlimitbysum_query', $query, $form_id, $input_id ); | |
$query = apply_filters( 'gwinv_query', $query, $form_id, $input_id ); | |
$query = apply_filters( "gwinv_query_{$form_id}", $query, $form_id, $input_id ); | |
$query = apply_filters( "gwinv_query_{$form_id}_{$input_id}", $query, $form_id, $input_id ); | |
} | |
return $query; | |
} | |
public static function prepare_strings_for_mysql_in_statement( $strings ) { | |
$wrapped = array(); | |
foreach( $strings as $string ) { | |
$wrapped[] = sprintf( '"%s"', $string ); | |
} | |
return implode( ', ', $wrapped ); | |
} | |
} | |
class GWLimitBySum extends GW_Inventory { } |
<?php | |
new GW_Inventory( array( | |
'form_id' => 363, | |
'field_id' => 2.3, | |
'stock_qty' => 20, | |
'out_of_stock_message' => 'Sorry, there are no more tickets!', | |
'not_enough_stock_message' => 'You ordered %1$s tickets. There are only %2$s tickets left.', | |
'approved_payments_only' => false, | |
'hide_form' => false, | |
'enable_notifications' => true | |
) ); |
Hello David,
First of all, thank you so much for this snippet!!!
I wanted to bring an issue to your attention where the REMAINDER shortcode doesn't show the correct number if you use the same field_id across different forms.
I have a Form 58 on Page A and a Form 60 on Page B.
I have set a limit for the following fields for Form 58 on Page A:
95.3 & 131.3 & 101.3 & 129.3
while I have also set a limit for the following fields for Form 60 on Page B:
95.3 & 131.3
As you can tell, it happens to be the same Field ID 95.3 & 131.3 for both forms.
What I noticed is that when you use the following shortcode on Page A
[gravityforms action="remaining" id="58" input_id="95.3" limit="10" /]
[gravityforms action="remaining" id="58" input_id="131.3" limit="10" /]
[gravityforms action="remaining" id="58" input_id="101.3" limit="10" /]
[gravityforms action="remaining" id="58" input_id="129.3" limit="10" /]
and the following shortcode on Page B
[gravityforms action="remaining" id="60" input_id="95.3" limit="10" /]
[gravityforms action="remaining" id="60" input_id="131.3" limit="10" /]
[gravityforms action="remaining" id="58" input_id="95.3" limit="10" /] & [gravityforms action="remaining" id="58" input_id="131.3" limit="10" /] show the remainder for [gravityforms action="remaining" id="60" input_id="95.3" limit="10" /] & [gravityforms action="remaining" id="60" input_id="131.3" limit="10" /] instead of the actual remainder.
It makes me believe that somehow the way the snippet was coded, the shortcode treats the same input_id of different forms as one and the same field. Would you be able to look into this and possibly find a fix for it?
Here is the last part of the code:
# Configuration
new GW_Inventory( array(
'form_id' => 58,
'field_id' => 95.3,
'stock_qty' => 10,
'out_of_stock_message' => $sum_limit_message = '<div style="border: 1px solid #e6db55; background-color: #FFFFE0; padding: 10px;">Sorry, this class has SOLD OUT! Please contact us through the form below, so we can see if we can accommodate your child(ren), should we be able to open up further spots.</div>',
'not_enough_stock_message' => 'You are trying to reserve %1$s spots. However, unfortunately there are only %2$s spot(s) left.',
'approved_payments_only' => false,
'hide_form' => false,
'enable_notifications' => true
) );
new GW_Inventory( array(
'form_id' => 58,
'field_id' => 131.3,
'stock_qty' => 10,
'out_of_stock_message' => $sum_limit_message = '<div style="border: 1px solid #e6db55; background-color: #FFFFE0; padding: 10px;">Sorry, this class has SOLD OUT! Please contact us through the form below, so we can see if we can accommodate your child(ren), should we be able to open up further spots.</div>',
'not_enough_stock_message' => 'You are trying to reserve %1$s spots. However, unfortunately there are only %2$s spot(s) left.',
'approved_payments_only' => false,
'hide_form' => false,
'enable_notifications' => true
) );
new GW_Inventory( array(
'form_id' => 58,
'field_id' => 101.3,
'stock_qty' => 10,
'out_of_stock_message' => $sum_limit_message = '<div style="border: 1px solid #e6db55; background-color: #FFFFE0; padding: 10px;">Sorry, this class has SOLD OUT! Please contact us through the form below, so we can see if we can accommodate your child(ren), should we be able to open up further spots.</div>',
'not_enough_stock_message' => 'You are trying to reserve %1$s spots. However, unfortunately there are only %2$s spot(s) left.',
'approved_payments_only' => false,
'hide_form' => false,
'enable_notifications' => true
) );
new GW_Inventory( array(
'form_id' => 58,
'field_id' => 129.3,
'stock_qty' => 10,
'out_of_stock_message' => $sum_limit_message = '<div style="border: 1px solid #e6db55; background-color: #FFFFE0; padding: 10px;">Sorry, this class has SOLD OUT! Please contact us through the form below, so we can see if we can accommodate your child(ren), should we be able to open up further spots.</div>',
'not_enough_stock_message' => 'You are trying to reserve %1$s spots. However, unfortunately there are only %2$s spot(s) left.',
'approved_payments_only' => false,
'hide_form' => false,
'enable_notifications' => true
) );
new GW_Inventory( array(
'form_id' => 60,
'field_id' => 95.3,
'stock_qty' => 10,
'out_of_stock_message' => $sum_limit_message = '<div style="border: 1px solid #e6db55; background-color: #FFFFE0; padding: 10px;">Sorry, this trial class has reached its capacity! Please contact us through the form below, so we can see if we can accommodate your child(ren), should we be able to open up further spots.</div>',
'not_enough_stock_message' => 'You are trying to reserve %1$s spots. However, unfortunately there are only %2$s spot(s) left.',
'approved_payments_only' => false,
'hide_form' => false,
'enable_notifications' => true
) );
new GW_Inventory( array(
'form_id' => 60,
'field_id' => 131.3,
'stock_qty' => 10,
'out_of_stock_message' => $sum_limit_message = '<div style="border: 1px solid #e6db55; background-color: #FFFFE0; padding: 10px;">Sorry, this trial class has reached its capacity! Please contact us through the form below, so we can see if we can accommodate your child(ren), should we be able to open up further spots.</div>',
'not_enough_stock_message' => 'You are trying to reserve %1$s spots. However, unfortunately there are only %2$s spot(s) left.',
'approved_payments_only' => false,
'hide_form' => false,
'enable_notifications' => true
) );
Thank you.
Thank you for this snippet.
There is an error in the sql query when GF Partial Entries is enabled (in get_sum_query, an extra underscore causes a table not found error).
Fixed in this gist: https://gist.github.com/clickco/db27bb70013a0dfe6e5a0b3d8041c17b
👉 This Gist has been migrated to the Gravity Wiz Snippet Library:
https://github.com/gravitywiz/snippet-library/blob/master/gravity-forms/gw-inventory.php
FYI: In order to trigger notifications when using the
enable_notifications = > true
, this class must be used: https://gist.github.com/spivurno/bd9d8e118f45999fb08c