| 1 |
<?php |
| 2 |
/** |
| 3 |
* Plugin Name: Maximum, Minimum and Multiple Quantity for WooCommerce Shops |
| 4 |
* Plugin URI: |
| 5 |
* Description: Set a maximum and/or minimum quantity limit, and/or force a "sell in multiples of" quantity, for the WooCommerce cart, globally or per product. |
| 6 |
* Version: 3.0 |
| 7 |
* Author: Naked Cat Plugins (by Webdados) |
| 8 |
* Author URI: https://nakedcatplugins.com |
| 9 |
* Text Domain: woocommerce-max-quantity |
| 10 |
* Requires at least: 5.9 |
| 11 |
* Tested up to: 7.0 |
| 12 |
* Requires PHP: 7.2 |
| 13 |
* WC requires at least: 7.3 |
| 14 |
* WC tested up to: 10.8 |
| 15 |
* Requires Plugins: woocommerce |
| 16 |
* License: GPLv3 |
| 17 |
*/ |
| 18 |
|
| 19 |
namespace PTWooPlugins\MaxQuantityWC; |
| 20 |
|
| 21 |
if ( ! defined( 'ABSPATH' ) ) { |
| 22 |
exit; // Exit if accessed directly |
| 23 |
} |
| 24 |
|
| 25 |
|
| 26 |
/** |
| 27 |
* Load plugin |
| 28 |
* |
| 29 |
* @since 2.0 |
| 30 |
* @return void |
| 31 |
*/ |
| 32 |
function init() { |
| 33 |
// Load textdomain |
| 34 |
load_plugin_textdomain( 'woocommerce-max-quantity' ); |
| 35 |
// Check for: WooCommerce (maybe later also check for required version) |
| 36 |
if ( class_exists( 'WooCommerce' ) ) { |
| 37 |
add_filter( 'woocommerce_inventory_settings', __NAMESPACE__ . '\wc_max_qty_options' ); |
| 38 |
add_filter( 'woocommerce_quantity_input_args', __NAMESPACE__ . '\wc_max_qty_input_args', 10, 2 ); |
| 39 |
add_filter( 'woocommerce_available_variation', __NAMESPACE__ . '\wc_max_qty_variation_input_qty_limits', 10, 3 ); |
| 40 |
add_filter( 'woocommerce_add_to_cart_validation', __NAMESPACE__ . '\wc_max_qty_add_to_cart_validation', 1, 4 ); |
| 41 |
add_filter( 'woocommerce_update_cart_validation', __NAMESPACE__ . '\wc_max_qty_update_cart_validation', 1, 4 ); |
| 42 |
add_action( 'woocommerce_product_options_inventory_product_data', __NAMESPACE__ . '\wc_max_qty_add_product_field' ); |
| 43 |
add_action( 'woocommerce_process_product_meta', __NAMESPACE__ . '\wc_max_qty_save_product_field' ); |
| 44 |
add_filter( 'woocommerce_store_api_product_quantity_limit', __NAMESPACE__ . '\blocks_cart_max_qty', 10, 2 ); |
| 45 |
add_filter( 'woocommerce_store_api_product_quantity_minimum', __NAMESPACE__ . '\blocks_cart_min_qty', 10, 2 ); |
| 46 |
add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), __NAMESPACE__ . '\wc_max_qty_action_links' ); |
| 47 |
add_filter( 'woocommerce_loop_add_to_cart_args', __NAMESPACE__ . '\wc_max_qty_loop_add_to_cart_args', 10, 2 ); |
| 48 |
add_filter( 'woocommerce_add_to_cart_quantity', __NAMESPACE__ . '\wc_max_qty_loop_add_to_cart_quantity', 10, 2 ); |
| 49 |
} |
| 50 |
} |
| 51 |
add_action( 'init', __NAMESPACE__ . '\init' ); |
| 52 |
|
| 53 |
|
| 54 |
/** |
| 55 |
* Add a "Settings" link to the plugin's entry on the Plugins list page |
| 56 |
* |
| 57 |
* @param array $links Existing plugin action links. |
| 58 |
* @return array |
| 59 |
* @since 3.0 |
| 60 |
*/ |
| 61 |
function wc_max_qty_action_links( $links ) { |
| 62 |
$settings_link = '<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=products§ion=inventory#isa_woocommerce_max_qty_settings-description' ) ) . '">' . __( 'Settings', 'woocommerce-max-quantity' ) . '</a>'; |
| 63 |
array_unshift( $links, $settings_link ); |
| 64 |
return $links; |
| 65 |
} |
| 66 |
|
| 67 |
|
| 68 |
/** |
| 69 |
* Add the option to WooCommerce products tab |
| 70 |
* |
| 71 |
* @param array $settings The current settings. |
| 72 |
*/ |
| 73 |
function wc_max_qty_options( $settings ) { |
| 74 |
$updated_settings = array(); |
| 75 |
foreach ( $settings as $section ) { |
| 76 |
$updated_settings[] = $section; |
| 77 |
// Right after the core Inventory Options section ends, add our own subsection |
| 78 |
if ( isset( $section['id'] ) && 'product_inventory_options' === $section['id'] && isset( $section['type'] ) && 'sectionend' === $section['type'] ) { |
| 79 |
$updated_settings[] = array( |
| 80 |
'title' => __( 'Maximum, Minimum and Multiple Quantity for WooCommerce Shops', 'woocommerce-max-quantity' ), |
| 81 |
'desc' => __( 'Store-wide defaults for this plugin. All settings below can be overridden for a specific product on that product\'s Inventory tab.', 'woocommerce-max-quantity' ), |
| 82 |
'id' => 'isa_woocommerce_max_qty_settings', |
| 83 |
'type' => 'title', |
| 84 |
); |
| 85 |
$updated_settings[] = array( |
| 86 |
'title' => __( 'Maximum quantity per product', 'woocommerce-max-quantity' ), |
| 87 |
'desc' => __( 'This is the default maximum quantity that can be added to the cart per product. To overide this for a specific product, set it on the "Max quantity per order" field on the product Inventory tab.', 'woocommerce-max-quantity' ), |
| 88 |
'id' => 'isa_woocommerce_max_qty_limit', |
| 89 |
'css' => 'width: 75px;', |
| 90 |
'type' => 'number', |
| 91 |
'custom_attributes' => array( |
| 92 |
'min' => 0, |
| 93 |
'step' => 1, |
| 94 |
), |
| 95 |
'default' => '', |
| 96 |
'autoload' => false, |
| 97 |
'desc_tip' => false, |
| 98 |
); |
| 99 |
$updated_settings[] = array( |
| 100 |
'title' => __( 'Minimum quantity per product', 'woocommerce-max-quantity' ), |
| 101 |
'desc' => __( 'This is the default minimum quantity that must be added to the cart per product. To overide this for a specific product, set it on the "Min quantity per order" field on the product Inventory tab.', 'woocommerce-max-quantity' ), |
| 102 |
'id' => 'isa_woocommerce_max_qty_min_limit', |
| 103 |
'css' => 'width: 75px;', |
| 104 |
'type' => 'number', |
| 105 |
'custom_attributes' => array( |
| 106 |
'min' => 0, |
| 107 |
'step' => 1, |
| 108 |
), |
| 109 |
'default' => '', |
| 110 |
'autoload' => false, |
| 111 |
'desc_tip' => false, |
| 112 |
); |
| 113 |
$updated_settings[] = array( |
| 114 |
'title' => __( 'Sell in multiples of', 'woocommerce-max-quantity' ), |
| 115 |
'desc' => __( 'This is the default multiple that quantities must be bought in (e.g. 3 allows 3, 6, 9, ...). To overide this for a specific product, set it on the "Sell in multiples of" field on the product Inventory tab. Leave empty, or use 1, for no restriction.', 'woocommerce-max-quantity' ), |
| 116 |
'id' => 'isa_woocommerce_max_qty_multiple', |
| 117 |
'css' => 'width: 75px;', |
| 118 |
'type' => 'number', |
| 119 |
'custom_attributes' => array( |
| 120 |
'min' => 1, |
| 121 |
'step' => 1, |
| 122 |
), |
| 123 |
'default' => '', |
| 124 |
'autoload' => false, |
| 125 |
'desc_tip' => false, |
| 126 |
); |
| 127 |
$updated_settings[] = array( |
| 128 |
'type' => 'sectionend', |
| 129 |
'id' => 'isa_woocommerce_max_qty_settings', |
| 130 |
); |
| 131 |
} |
| 132 |
} |
| 133 |
return $updated_settings; |
| 134 |
} |
| 135 |
|
| 136 |
|
| 137 |
/** |
| 138 |
* Display the product's "Max quantity per order" field in the Product Data metabox |
| 139 |
* |
| 140 |
* @since 1.4 |
| 141 |
*/ |
| 142 |
function wc_max_qty_add_product_field() { |
| 143 |
$default_max = (int) get_option( 'isa_woocommerce_max_qty_limit' ); |
| 144 |
$default_min = (int) get_option( 'isa_woocommerce_max_qty_min_limit' ); |
| 145 |
$default_multiple = (int) get_option( 'isa_woocommerce_max_qty_multiple' ); |
| 146 |
echo '<div class="options_group">'; |
| 147 |
woocommerce_wp_text_input( |
| 148 |
array( |
| 149 |
'id' => '_isa_wc_max_qty_product_max', |
| 150 |
'label' => __( 'Max quantity per order', 'woocommerce-max-quantity' ), |
| 151 |
'placeholder' => $default_max > 0 ? sprintf( |
| 152 |
/* translators: %d: Default maximum quantity */ |
| 153 |
esc_attr__( 'Store-wide threshold (%d)', 'woocommerce-max-quantity' ), |
| 154 |
$default_max |
| 155 |
) : '', |
| 156 |
'description' => __( 'Optional. Set a maximum quantity limit allowed per order. Enter a number, 1 or greater.', 'woocommerce-max-quantity' ), |
| 157 |
) |
| 158 |
); |
| 159 |
woocommerce_wp_text_input( |
| 160 |
array( |
| 161 |
'id' => '_isa_wc_max_qty_product_min', |
| 162 |
'label' => __( 'Min quantity per order', 'woocommerce-max-quantity' ), |
| 163 |
'placeholder' => $default_min > 0 ? sprintf( |
| 164 |
/* translators: %d: Default minimum quantity */ |
| 165 |
esc_attr__( 'Store-wide threshold (%d)', 'woocommerce-max-quantity' ), |
| 166 |
$default_min |
| 167 |
) : '', |
| 168 |
'description' => __( 'Optional. Set a minimum quantity required per order. Enter a number, 1 or greater.', 'woocommerce-max-quantity' ), |
| 169 |
) |
| 170 |
); |
| 171 |
woocommerce_wp_text_input( |
| 172 |
array( |
| 173 |
'id' => '_isa_wc_max_qty_product_multiple', |
| 174 |
'label' => __( 'Sell in multiples of', 'woocommerce-max-quantity' ), |
| 175 |
'type' => 'number', |
| 176 |
'custom_attributes' => array( |
| 177 |
'min' => 1, |
| 178 |
'step' => 1, |
| 179 |
), |
| 180 |
'placeholder' => $default_multiple > 1 ? sprintf( |
| 181 |
/* translators: %d: Default multiple */ |
| 182 |
esc_attr__( 'Store-wide threshold (%d)', 'woocommerce-max-quantity' ), |
| 183 |
$default_multiple |
| 184 |
) : '1', |
| 185 |
'description' => __( 'Optional. Only allow this product to be bought in multiples of this number (e.g. 3 allows 3, 6, 9, ...). Leave empty to use the store-wide setting, or 1 for no restriction.', 'woocommerce-max-quantity' ), |
| 186 |
) |
| 187 |
); |
| 188 |
echo '</div>'; |
| 189 |
} |
| 190 |
|
| 191 |
|
| 192 |
/** |
| 193 |
* Save product's Max Quantity field |
| 194 |
* |
| 195 |
* @param int $post_id WP post id. |
| 196 |
* @since 1.4 |
| 197 |
*/ |
| 198 |
function wc_max_qty_save_product_field( $post_id ) { |
| 199 |
$product = wc_get_product( $post_id ); |
| 200 |
if ( ! $product ) { |
| 201 |
return; |
| 202 |
} |
| 203 |
$val = $product->get_meta( '_isa_wc_max_qty_product_max' ); |
| 204 |
// phpcs:ignore WordPress.Security.NonceVerification.Missing |
| 205 |
$new = isset( $_POST['_isa_wc_max_qty_product_max'] ) ? sanitize_text_field( wp_unslash( $_POST['_isa_wc_max_qty_product_max'] ) ) : ''; // Nonce verification is already taken care by WooCommerce |
| 206 |
if ( $val !== $new ) { |
| 207 |
$product->update_meta_data( '_isa_wc_max_qty_product_max', $new ); |
| 208 |
$product->save(); |
| 209 |
} |
| 210 |
$val = $product->get_meta( '_isa_wc_max_qty_product_min' ); |
| 211 |
// phpcs:ignore WordPress.Security.NonceVerification.Missing |
| 212 |
$new = isset( $_POST['_isa_wc_max_qty_product_min'] ) ? sanitize_text_field( wp_unslash( $_POST['_isa_wc_max_qty_product_min'] ) ) : ''; // Nonce verification is already taken care by WooCommerce |
| 213 |
if ( $val !== $new ) { |
| 214 |
$product->update_meta_data( '_isa_wc_max_qty_product_min', $new ); |
| 215 |
$product->save(); |
| 216 |
} |
| 217 |
$val = $product->get_meta( '_isa_wc_max_qty_product_multiple' ); |
| 218 |
// phpcs:ignore WordPress.Security.NonceVerification.Missing |
| 219 |
$new = isset( $_POST['_isa_wc_max_qty_product_multiple'] ) ? sanitize_text_field( wp_unslash( $_POST['_isa_wc_max_qty_product_multiple'] ) ) : ''; // Nonce verification is already taken care by WooCommerce |
| 220 |
if ( $val !== $new ) { |
| 221 |
$product->update_meta_data( '_isa_wc_max_qty_product_multiple', $new ); |
| 222 |
$product->save(); |
| 223 |
} |
| 224 |
} |
| 225 |
|
| 226 |
|
| 227 |
/** |
| 228 |
* Get the individual product max limit |
| 229 |
* |
| 230 |
* @param int $product_id The product ID. |
| 231 |
* @return int|bool $limit The max limit number for this product, if set, otherwise false. |
| 232 |
* @since 1.4 |
| 233 |
*/ |
| 234 |
function wc_get_product_max_limit( $product_id ) { |
| 235 |
$product = wc_get_product( $product_id ); |
| 236 |
if ( ! $product ) { |
| 237 |
return false; |
| 238 |
} |
| 239 |
$qty = $product->get_meta( '_isa_wc_max_qty_product_max' ); |
| 240 |
if ( empty( $qty ) ) { |
| 241 |
// Honor the Sold individually setting |
| 242 |
$limit = $product->is_sold_individually() ? 1 : false; |
| 243 |
} else { |
| 244 |
$limit = (int) $qty; |
| 245 |
} |
| 246 |
return $limit; |
| 247 |
} |
| 248 |
|
| 249 |
|
| 250 |
/** |
| 251 |
* Get the individual product "Sell in multiples of" value |
| 252 |
* |
| 253 |
* @param int $product_id The product ID. |
| 254 |
* @return int|bool The multiple for this product, if set (1 or greater), otherwise false. |
| 255 |
* @since 3.0 |
| 256 |
*/ |
| 257 |
function wc_get_product_multiple_limit( $product_id ) { |
| 258 |
$product = wc_get_product( $product_id ); |
| 259 |
if ( ! $product ) { |
| 260 |
return false; |
| 261 |
} |
| 262 |
$val = (int) $product->get_meta( '_isa_wc_max_qty_product_multiple' ); |
| 263 |
return $val >= 1 ? $val : false; |
| 264 |
} |
| 265 |
|
| 266 |
|
| 267 |
/** |
| 268 |
* Get the individual product min limit |
| 269 |
* |
| 270 |
* @param int $product_id The product ID. |
| 271 |
* @return int|bool $limit The min limit number for this product, if set (1 or greater), otherwise false. |
| 272 |
* @since 3.0 |
| 273 |
*/ |
| 274 |
function wc_get_product_min_limit( $product_id ) { |
| 275 |
$product = wc_get_product( $product_id ); |
| 276 |
if ( ! $product ) { |
| 277 |
return false; |
| 278 |
} |
| 279 |
$val = (int) $product->get_meta( '_isa_wc_max_qty_product_min' ); |
| 280 |
return $val >= 1 ? $val : false; |
| 281 |
} |
| 282 |
|
| 283 |
|
| 284 |
/** |
| 285 |
* Round a quantity down to the nearest valid multiple, without going below the multiple itself. |
| 286 |
* |
| 287 |
* @param int $value The value to round down. |
| 288 |
* @param int $multiple The multiple to round to. |
| 289 |
* @return int |
| 290 |
* @since 3.0 |
| 291 |
*/ |
| 292 |
function wc_max_qty_floor_to_multiple( $value, $multiple ) { |
| 293 |
$value = (int) $value; |
| 294 |
if ( $multiple <= 1 || $value <= 0 ) { |
| 295 |
return $value; |
| 296 |
} |
| 297 |
return $value < $multiple ? $multiple : $multiple * (int) floor( $value / $multiple ); |
| 298 |
} |
| 299 |
|
| 300 |
|
| 301 |
/** |
| 302 |
* Round a quantity up to the nearest valid multiple. |
| 303 |
* |
| 304 |
* @param int $value The value to round up. |
| 305 |
* @param int $multiple The multiple to round to. |
| 306 |
* @return int |
| 307 |
* @since 3.0 |
| 308 |
*/ |
| 309 |
function wc_max_qty_ceil_to_multiple( $value, $multiple ) { |
| 310 |
$value = (int) $value; |
| 311 |
if ( $multiple <= 1 || $value <= 0 ) { |
| 312 |
return $value; |
| 313 |
} |
| 314 |
return $multiple * (int) ceil( $value / $multiple ); |
| 315 |
} |
| 316 |
|
| 317 |
|
| 318 |
/** |
| 319 |
* Set the max attribute value for the quantity input field for Add to cart forms. |
| 320 |
* This applies to Simple product Add To Cart forms, and ALL (simple and variable) products on the Cart page quantity field. |
| 321 |
* |
| 322 |
* @param array $args The current arguments. |
| 323 |
* @param object $product The product. |
| 324 |
* @return array $args |
| 325 |
* @since 1.1.6 |
| 326 |
*/ |
| 327 |
function wc_max_qty_input_args( $args, $product ) { |
| 328 |
$default_max = (int) get_option( 'isa_woocommerce_max_qty_limit' ); |
| 329 |
$default_min = (int) get_option( 'isa_woocommerce_max_qty_min_limit' ); |
| 330 |
$default_multiple = (int) get_option( 'isa_woocommerce_max_qty_multiple' ); |
| 331 |
$product_id = $product->get_parent_id() ? $product->get_parent_id() : $product->get_id(); |
| 332 |
$product_max = wc_get_product_max_limit( $product_id ); |
| 333 |
$product_min = wc_get_product_min_limit( $product_id ); |
| 334 |
$product_multiple = wc_get_product_multiple_limit( $product_id ); |
| 335 |
// Set product max or default max |
| 336 |
if ( ! empty( $product_max ) ) { |
| 337 |
$args['max_value'] = $product_max; |
| 338 |
} elseif ( ! empty( $default_max ) ) { |
| 339 |
$args['max_value'] = $default_max; |
| 340 |
} |
| 341 |
// Set product min or default min |
| 342 |
$min = ! empty( $product_min ) ? $product_min : ( $default_min > 0 ? $default_min : 0 ); |
| 343 |
// Set product multiple or default multiple |
| 344 |
$multiple = ! empty( $product_multiple ) ? $product_multiple : ( $default_multiple > 1 ? $default_multiple : 1 ); |
| 345 |
if ( $multiple > 1 && ! $product->is_sold_individually() ) { |
| 346 |
$args['step'] = $multiple; |
| 347 |
if ( ! empty( $args['max_value'] ) ) { |
| 348 |
$args['max_value'] = wc_max_qty_floor_to_multiple( $args['max_value'], $multiple ); |
| 349 |
} |
| 350 |
} |
| 351 |
// Set the min attribute from the greater of the configured min and the multiple, aligned to the multiple |
| 352 |
if ( ( $min > 0 || $multiple > 1 ) && ! $product->is_sold_individually() ) { |
| 353 |
$effective_min = max( $min, $multiple ); |
| 354 |
if ( $multiple > 1 ) { |
| 355 |
$effective_min = wc_max_qty_ceil_to_multiple( $effective_min, $multiple ); |
| 356 |
} |
| 357 |
$args['min_value'] = $effective_min; |
| 358 |
} |
| 359 |
// Limit our max by the available stock, if stock is lower |
| 360 |
if ( ! empty( $args['max_value'] ) ) { |
| 361 |
if ( $product->managing_stock() && ! $product->backorders_allowed() ) { |
| 362 |
$stock = $product->get_stock_quantity(); |
| 363 |
$args['max_value'] = min( $stock, $args['max_value'] ); |
| 364 |
} |
| 365 |
} |
| 366 |
return $args; |
| 367 |
} |
| 368 |
|
| 369 |
|
| 370 |
/** |
| 371 |
* Filter the available variation to enforce the max/min on the quantity input field |
| 372 |
* on Add to cart forms for Variable Products. |
| 373 |
* |
| 374 |
* @param array $args The current arguments. |
| 375 |
* @param object $product The product. |
| 376 |
* @param object $variation The product variation. |
| 377 |
*/ |
| 378 |
function wc_max_qty_variation_input_qty_limits( $args, $product, $variation ) { |
| 379 |
if ( is_admin() && ! is_ajax() ) { |
| 380 |
return $args; |
| 381 |
} |
| 382 |
$default_max = (int) get_option( 'isa_woocommerce_max_qty_limit' ); |
| 383 |
$default_min = (int) get_option( 'isa_woocommerce_max_qty_min_limit' ); |
| 384 |
$default_multiple = (int) get_option( 'isa_woocommerce_max_qty_multiple' ); |
| 385 |
$product_max = wc_get_product_max_limit( $variation->get_parent_id() ); |
| 386 |
$product_min = wc_get_product_min_limit( $variation->get_parent_id() ); |
| 387 |
$product_multiple = wc_get_product_multiple_limit( $variation->get_parent_id() ); |
| 388 |
// Set product max or default max |
| 389 |
if ( ! empty( $product_max ) ) { |
| 390 |
$args['max_qty'] = $product_max; |
| 391 |
} elseif ( ! empty( $default_max ) ) { |
| 392 |
$args['max_qty'] = $default_max; |
| 393 |
} |
| 394 |
// Set product min or default min |
| 395 |
$min = ! empty( $product_min ) ? $product_min : ( $default_min > 0 ? $default_min : 0 ); |
| 396 |
// Set product multiple or default multiple |
| 397 |
$multiple = ! empty( $product_multiple ) ? $product_multiple : ( $default_multiple > 1 ? $default_multiple : 1 ); |
| 398 |
if ( $multiple > 1 && ! $variation->is_sold_individually() ) { |
| 399 |
$args['step'] = $multiple; |
| 400 |
if ( ! empty( $args['max_qty'] ) ) { |
| 401 |
$args['max_qty'] = wc_max_qty_floor_to_multiple( $args['max_qty'], $multiple ); |
| 402 |
} |
| 403 |
} |
| 404 |
// Set the min attribute from the greater of the configured min and the multiple, aligned to the multiple |
| 405 |
if ( ( $min > 0 || $multiple > 1 ) && ! $variation->is_sold_individually() ) { |
| 406 |
$effective_min = max( $min, $multiple ); |
| 407 |
if ( $multiple > 1 ) { |
| 408 |
$effective_min = wc_max_qty_ceil_to_multiple( $effective_min, $multiple ); |
| 409 |
} |
| 410 |
$args['min_qty'] = $effective_min; |
| 411 |
} |
| 412 |
// Limit our max by the available stock, if stock is lower |
| 413 |
if ( ! empty( $args['max_qty'] ) ) { |
| 414 |
if ( $variation->managing_stock() && ! $variation->backorders_allowed() ) { |
| 415 |
$stock = $variation->get_stock_quantity(); |
| 416 |
$args['max_qty'] = min( $stock, $args['max_qty'] ); |
| 417 |
} |
| 418 |
} |
| 419 |
return $args; |
| 420 |
} |
| 421 |
|
| 422 |
|
| 423 |
/** |
| 424 |
* Find out how many of this product are already in the cart |
| 425 |
* |
| 426 |
* @param mixed $product_id ID of the product in question. |
| 427 |
* @param string $cart_item_key The cart key for this item in case of Updating cart. |
| 428 |
* |
| 429 |
* @return integer $running_qty The total quantity of this item, parent item in case of variations, in cart |
| 430 |
* @since 1.1.6 |
| 431 |
*/ |
| 432 |
function wc_max_qty_get_cart_qty( $product_id, $cart_item_key = '' ) { |
| 433 |
$running_qty = 0; // Keep a running total to count variations |
| 434 |
// Search the cart for the product in question |
| 435 |
foreach ( WC()->cart->get_cart() as $other_cart_item_keys => $values ) { |
| 436 |
if ( intval( $product_id ) === intval( $values['product_id'] ) ) { |
| 437 |
/* |
| 438 |
* In case of updating the cart quantity, don't count this cart item key |
| 439 |
otherwise they won't be able to REDUCE the number of items in cart because it will think it is adding the new quantity on top of the existing quantity, when in fact it is reducing the existing quantity to the new quantity. |
| 440 |
*/ |
| 441 |
if ( $cart_item_key === $other_cart_item_keys ) { |
| 442 |
continue; |
| 443 |
} |
| 444 |
// Add that quantity to our running total qty for this product |
| 445 |
$running_qty += (int) $values['quantity']; |
| 446 |
} |
| 447 |
} |
| 448 |
return $running_qty; |
| 449 |
} |
| 450 |
|
| 451 |
|
| 452 |
/** |
| 453 |
* Validate product quantity when Added to cart |
| 454 |
* |
| 455 |
* @param bool $passed If the validation passed. |
| 456 |
* @param int $product_id The product ID. |
| 457 |
* @param int $quantity The quantity added to cart. |
| 458 |
* @param int $variation_id The product variation ID. |
| 459 |
* |
| 460 |
* @since 1.1.6 |
| 461 |
*/ |
| 462 |
function wc_max_qty_add_to_cart_validation( $passed, $product_id, $quantity, $variation_id = '' ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed |
| 463 |
// If it hasn't passed, we don't need to bother validating further |
| 464 |
if ( $passed ) { |
| 465 |
$default_max = (int) get_option( 'isa_woocommerce_max_qty_limit' ); |
| 466 |
$default_min = (int) get_option( 'isa_woocommerce_max_qty_min_limit' ); |
| 467 |
$default_multiple = (int) get_option( 'isa_woocommerce_max_qty_multiple' ); |
| 468 |
$product_max = wc_get_product_max_limit( $product_id ); |
| 469 |
$product_min = wc_get_product_min_limit( $product_id ); |
| 470 |
$product_multiple = wc_get_product_multiple_limit( $product_id ); |
| 471 |
// Set product max or default max |
| 472 |
if ( ! empty( $product_max ) ) { |
| 473 |
$new_max = $product_max; |
| 474 |
} elseif ( ! empty( $default_max ) ) { |
| 475 |
$new_max = $default_max; |
| 476 |
} else { |
| 477 |
$new_max = 0; |
| 478 |
} |
| 479 |
// Set product min or default min |
| 480 |
$new_min = ! empty( $product_min ) ? $product_min : ( $default_min > 0 ? $default_min : 0 ); |
| 481 |
// Set product multiple or default multiple |
| 482 |
$multiple = ! empty( $product_multiple ) ? $product_multiple : ( $default_multiple > 1 ? $default_multiple : 1 ); |
| 483 |
if ( ! $new_max && ! $new_min && $multiple <= 1 ) { |
| 484 |
return $passed; |
| 485 |
} |
| 486 |
$product = wc_get_product( $product_id ); |
| 487 |
if ( ! $product ) { |
| 488 |
return $passed; |
| 489 |
} |
| 490 |
if ( $product->is_sold_individually() ) { |
| 491 |
return $passed; |
| 492 |
} |
| 493 |
$already_in_cart = wc_max_qty_get_cart_qty( $product_id ); |
| 494 |
$product_title = $product->get_title(); |
| 495 |
if ( $new_max ) { |
| 496 |
if ( ! empty( $already_in_cart ) ) { |
| 497 |
// There was already a quantity of this item in cart prior to this addition. |
| 498 |
// Check if the total of already_in_cart + current addition quantity is more than our max. |
| 499 |
if ( ( $already_in_cart + $quantity ) > $new_max ) { |
| 500 |
// oops. too much. |
| 501 |
$passed = false; |
| 502 |
// Add compatibility with WooCommerce Direct Checkout |
| 503 |
if ( class_exists( 'WooCommerce_Direct_Checkout' ) ) { |
| 504 |
$direct_checkout = get_option( 'direct_checkout_enabled' ); |
| 505 |
$direct_checkout_url = get_option( 'direct_checkout_cart_redirect_url' ); |
| 506 |
if ( $direct_checkout && $direct_checkout_url ) { |
| 507 |
// Redirect to submit page |
| 508 |
wp_safe_redirect( esc_url_raw( $direct_checkout_url ) ); |
| 509 |
exit; |
| 510 |
} |
| 511 |
} |
| 512 |
wc_add_notice( |
| 513 |
apply_filters( |
| 514 |
'isa_wc_max_qty_error_message_already_had', |
| 515 |
sprintf( |
| 516 |
/* translators: %1$s maximum items, %2$s product name, %3$s: cart link, %4$s number of items in cart */ |
| 517 |
__( 'You can add a maximum of %1$s %2$s’s to %3$s. You already have %4$s.', 'woocommerce-max-quantity' ), |
| 518 |
$new_max, |
| 519 |
$product_title, |
| 520 |
'<a href="' . esc_url( wc_get_cart_url() ) . '">' . __( 'your cart', 'woocommerce-max-quantity' ) . '</a>', |
| 521 |
$already_in_cart |
| 522 |
), |
| 523 |
$new_max, |
| 524 |
$already_in_cart |
| 525 |
), |
| 526 |
'error' |
| 527 |
); |
| 528 |
} |
| 529 |
} else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found |
| 530 |
// none were in cart previously |
| 531 |
// just in case they manually type in an amount greater than we allow, check the input number here too |
| 532 |
if ( $quantity > $new_max ) { |
| 533 |
// oops. too much. |
| 534 |
wc_add_notice( |
| 535 |
apply_filters( |
| 536 |
'isa_wc_max_qty_error_message', |
| 537 |
sprintf( |
| 538 |
/* translators: %1$s maximum items, %2$s product name, %3$s: cart link */ |
| 539 |
__( 'You can add a maximum of %1$s %2$s’s to %3$s.', 'woocommerce-max-quantity' ), |
| 540 |
$new_max, |
| 541 |
$product_title, |
| 542 |
'<a href="' . esc_url( wc_get_cart_url() ) . '">' . __( 'your cart', 'woocommerce-max-quantity' ) . '</a>' |
| 543 |
), |
| 544 |
$new_max |
| 545 |
), |
| 546 |
'error' |
| 547 |
); |
| 548 |
$passed = false; |
| 549 |
} |
| 550 |
} |
| 551 |
} |
| 552 |
// Just in case they manually type in a quantity that isn't a valid multiple, check it here too. |
| 553 |
if ( $multiple > 1 ) { |
| 554 |
$total_qty = $already_in_cart + $quantity; |
| 555 |
if ( $total_qty % $multiple !== 0 ) { |
| 556 |
// oops. not a valid multiple. |
| 557 |
$passed = false; |
| 558 |
wc_add_notice( |
| 559 |
apply_filters( |
| 560 |
'isa_wc_max_qty_multiple_error_message', |
| 561 |
sprintf( |
| 562 |
/* translators: %1$s: product name, %2$d: multiple */ |
| 563 |
__( '"%1$s" can only be bought in multiples of %2$d.', 'woocommerce-max-quantity' ), |
| 564 |
$product_title, |
| 565 |
$multiple |
| 566 |
), |
| 567 |
$multiple, |
| 568 |
$product_title |
| 569 |
), |
| 570 |
'error' |
| 571 |
); |
| 572 |
} |
| 573 |
} |
| 574 |
// Check if the quantity being added is below the effective minimum. |
| 575 |
if ( $new_min > 0 || $multiple > 1 ) { |
| 576 |
$effective_min = max( $new_min, $multiple ); |
| 577 |
if ( $multiple > 1 ) { |
| 578 |
$effective_min = wc_max_qty_ceil_to_multiple( $effective_min, $multiple ); |
| 579 |
} |
| 580 |
if ( $effective_min > 0 && $quantity < $effective_min ) { |
| 581 |
// oops. too little. |
| 582 |
$passed = false; |
| 583 |
wc_add_notice( |
| 584 |
apply_filters( |
| 585 |
'isa_wc_max_qty_min_error_message', |
| 586 |
sprintf( |
| 587 |
/* translators: %1$s minimum items, %2$s product name, %3$s: cart link */ |
| 588 |
__( 'You must add a minimum of %1$s %2$s’s to %3$s.', 'woocommerce-max-quantity' ), |
| 589 |
$effective_min, |
| 590 |
$product_title, |
| 591 |
'<a href="' . esc_url( wc_get_cart_url() ) . '">' . __( 'your cart', 'woocommerce-max-quantity' ) . '</a>' |
| 592 |
), |
| 593 |
$effective_min |
| 594 |
), |
| 595 |
'error' |
| 596 |
); |
| 597 |
} |
| 598 |
} |
| 599 |
} |
| 600 |
return $passed; |
| 601 |
} |
| 602 |
|
| 603 |
/** |
| 604 |
* Validate product quantity when cart is UPDATED - Not working when updating quantity inside the block-based cart |
| 605 |
* Just in case they manually type in an amount greater than we allow and the HTML5 Constraint validation doesn't work. |
| 606 |
* |
| 607 |
* @param bool $passed If the validation passed. |
| 608 |
* @param string $cart_item_key The key of the updated cart item. |
| 609 |
* @param array $values Cart item values. |
| 610 |
* @param int $quantity The quantity updated to cart. |
| 611 |
* @since 1.1.9 |
| 612 |
*/ |
| 613 |
function wc_max_qty_update_cart_validation( $passed, $cart_item_key, $values, $quantity ) { |
| 614 |
// If it hasn't passed, we don't need to bother validating further |
| 615 |
if ( $passed ) { |
| 616 |
$default_max = (int) get_option( 'isa_woocommerce_max_qty_limit' ); |
| 617 |
$default_min = (int) get_option( 'isa_woocommerce_max_qty_min_limit' ); |
| 618 |
$default_multiple = (int) get_option( 'isa_woocommerce_max_qty_multiple' ); |
| 619 |
$product_max = wc_get_product_max_limit( $values['product_id'] ); |
| 620 |
$product_min = wc_get_product_min_limit( $values['product_id'] ); |
| 621 |
$product_multiple = wc_get_product_multiple_limit( $values['product_id'] ); |
| 622 |
// Set product max or default max |
| 623 |
if ( ! empty( $product_max ) ) { |
| 624 |
$new_max = $product_max; |
| 625 |
} elseif ( ! empty( $default_max ) ) { |
| 626 |
$new_max = $default_max; |
| 627 |
} else { |
| 628 |
$new_max = 0; |
| 629 |
} |
| 630 |
// Set product min or default min |
| 631 |
$new_min = ! empty( $product_min ) ? $product_min : ( $default_min > 0 ? $default_min : 0 ); |
| 632 |
// Set product multiple or default multiple |
| 633 |
$multiple = ! empty( $product_multiple ) ? $product_multiple : ( $default_multiple > 1 ? $default_multiple : 1 ); |
| 634 |
if ( ! $new_max && ! $new_min && $multiple <= 1 ) { |
| 635 |
return $passed; |
| 636 |
} |
| 637 |
$product = wc_get_product( $values['product_id'] ); |
| 638 |
if ( ! $product ) { |
| 639 |
return $passed; |
| 640 |
} |
| 641 |
if ( $product->is_sold_individually() ) { |
| 642 |
return $passed; |
| 643 |
} |
| 644 |
$already_in_cart = wc_max_qty_get_cart_qty( $values['product_id'], $cart_item_key ); |
| 645 |
if ( $new_max && ( $already_in_cart + $quantity ) > $new_max ) { |
| 646 |
wc_add_notice( |
| 647 |
apply_filters( |
| 648 |
'isa_wc_max_qty_error_message', |
| 649 |
sprintf( |
| 650 |
/* translators: %1$s maximum items, %2$s product name, %3$s: cart link */ |
| 651 |
__( 'You can add a maximum of %1$s %2$s’s to %3$s.', 'woocommerce-max-quantity' ), |
| 652 |
$new_max, |
| 653 |
$product->get_name(), |
| 654 |
'<a href="' . esc_url( wc_get_cart_url() ) . '">' . __( 'your cart', 'woocommerce-max-quantity' ) . '</a>' |
| 655 |
), |
| 656 |
$new_max |
| 657 |
), |
| 658 |
'error' |
| 659 |
); |
| 660 |
$passed = false; |
| 661 |
} |
| 662 |
// Just in case they manually type in a quantity that isn't a valid multiple, check it here too. |
| 663 |
if ( $multiple > 1 ) { |
| 664 |
$total_qty = $already_in_cart + $quantity; |
| 665 |
if ( $total_qty % $multiple !== 0 ) { |
| 666 |
wc_add_notice( |
| 667 |
apply_filters( |
| 668 |
'isa_wc_max_qty_multiple_error_message', |
| 669 |
sprintf( |
| 670 |
/* translators: %1$s: product name, %2$d: multiple */ |
| 671 |
__( '"%1$s" can only be bought in multiples of %2$d.', 'woocommerce-max-quantity' ), |
| 672 |
$product->get_name(), |
| 673 |
$multiple |
| 674 |
), |
| 675 |
$multiple, |
| 676 |
$product->get_name() |
| 677 |
), |
| 678 |
'error' |
| 679 |
); |
| 680 |
$passed = false; |
| 681 |
} |
| 682 |
} |
| 683 |
// Check if the new quantity is below the effective minimum. |
| 684 |
if ( $new_min > 0 || $multiple > 1 ) { |
| 685 |
$effective_min = max( $new_min, $multiple ); |
| 686 |
if ( $multiple > 1 ) { |
| 687 |
$effective_min = wc_max_qty_ceil_to_multiple( $effective_min, $multiple ); |
| 688 |
} |
| 689 |
if ( $effective_min > 0 && $quantity < $effective_min ) { |
| 690 |
wc_add_notice( |
| 691 |
apply_filters( |
| 692 |
'isa_wc_max_qty_min_error_message', |
| 693 |
sprintf( |
| 694 |
/* translators: %1$s minimum items, %2$s product name, %3$s: cart link */ |
| 695 |
__( 'You must add a minimum of %1$s %2$s’s to %3$s.', 'woocommerce-max-quantity' ), |
| 696 |
$effective_min, |
| 697 |
$product->get_name(), |
| 698 |
'<a href="' . esc_url( wc_get_cart_url() ) . '">' . __( 'your cart', 'woocommerce-max-quantity' ) . '</a>' |
| 699 |
), |
| 700 |
$effective_min |
| 701 |
), |
| 702 |
'error' |
| 703 |
); |
| 704 |
$passed = false; |
| 705 |
} |
| 706 |
} |
| 707 |
} |
| 708 |
return $passed; |
| 709 |
} |
| 710 |
|
| 711 |
|
| 712 |
/** |
| 713 |
* Set maximum quantity on the block-based cart |
| 714 |
* |
| 715 |
* @since 2.0 |
| 716 |
* @param int $cart_max Current maximum quantity. |
| 717 |
* @param object $product Current product. |
| 718 |
* @return int |
| 719 |
*/ |
| 720 |
function blocks_cart_max_qty( $cart_max, $product ) { |
| 721 |
if ( ! empty( $cart_max ) ) { |
| 722 |
$default_max = (int) get_option( 'isa_woocommerce_max_qty_limit' ); |
| 723 |
$product_max = wc_get_product_max_limit( $product->get_parent_id() ? $product->get_parent_id() : $product->get_id() ); |
| 724 |
// Set product max or default max |
| 725 |
if ( ! empty( $product_max ) ) { |
| 726 |
$new_max = $product_max; |
| 727 |
} elseif ( ! empty( $default_max ) ) { |
| 728 |
$new_max = $default_max; |
| 729 |
} |
| 730 |
if ( ! empty( $new_max ) ) { |
| 731 |
$cart_max = min( $cart_max, $new_max ); |
| 732 |
} |
| 733 |
} |
| 734 |
return $cart_max; |
| 735 |
} |
| 736 |
|
| 737 |
|
| 738 |
/** |
| 739 |
* Set minimum quantity on the block-based cart |
| 740 |
* |
| 741 |
* @since 3.0 |
| 742 |
* @param int $cart_min Current minimum quantity. |
| 743 |
* @param object $product Current product. |
| 744 |
* @return int |
| 745 |
*/ |
| 746 |
function blocks_cart_min_qty( $cart_min, $product ) { |
| 747 |
if ( $product->is_sold_individually() ) { |
| 748 |
return $cart_min; |
| 749 |
} |
| 750 |
$default_min = (int) get_option( 'isa_woocommerce_max_qty_min_limit' ); |
| 751 |
$product_min = wc_get_product_min_limit( $product->get_parent_id() ? $product->get_parent_id() : $product->get_id() ); |
| 752 |
// Set product min or default min |
| 753 |
$new_min = ! empty( $product_min ) ? $product_min : ( $default_min > 0 ? $default_min : 0 ); |
| 754 |
if ( ! empty( $new_min ) ) { |
| 755 |
$cart_min = max( (int) $cart_min, $new_min ); |
| 756 |
} |
| 757 |
return $cart_min; |
| 758 |
} |
| 759 |
|
| 760 |
|
| 761 |
/** |
| 762 |
* Set the quantity used by the "Add to cart" link on product archives/lists (classic templates, |
| 763 |
* shortcodes and widgets), so it adds a valid multiple and/or the minimum instead of always |
| 764 |
* trying to add 1. |
| 765 |
* |
| 766 |
* @since 3.0 |
| 767 |
* @param array $args The current arguments. |
| 768 |
* @param WC_Product $product The product. |
| 769 |
* @return array |
| 770 |
*/ |
| 771 |
function wc_max_qty_loop_add_to_cart_args( $args, $product ) { |
| 772 |
$multiple = wc_max_qty_get_loop_multiple( $product ); |
| 773 |
$quantity = max( $multiple, wc_max_qty_get_loop_min( $product ) ); |
| 774 |
if ( $multiple > 1 ) { |
| 775 |
$quantity = wc_max_qty_ceil_to_multiple( $quantity, $multiple ); |
| 776 |
} |
| 777 |
if ( $quantity > 1 ) { |
| 778 |
$args['quantity'] = $quantity; |
| 779 |
} |
| 780 |
return $args; |
| 781 |
} |
| 782 |
|
| 783 |
|
| 784 |
/** |
| 785 |
* Set the quantity used by the block-based "Add to Cart" button on product archives/lists |
| 786 |
* (Product Collection/Products block, FSE archive templates), so it adds a valid multiple |
| 787 |
* and/or the minimum instead of always trying to add 1. |
| 788 |
* |
| 789 |
* @since 3.0 |
| 790 |
* @param int $quantity The current quantity. |
| 791 |
* @param int $product_id The product ID. |
| 792 |
* @return int |
| 793 |
*/ |
| 794 |
function wc_max_qty_loop_add_to_cart_quantity( $quantity, $product_id ) { |
| 795 |
$product = wc_get_product( $product_id ); |
| 796 |
if ( ! $product ) { |
| 797 |
return $quantity; |
| 798 |
} |
| 799 |
$multiple = wc_max_qty_get_loop_multiple( $product ); |
| 800 |
$loop_quantity = max( $multiple, wc_max_qty_get_loop_min( $product ) ); |
| 801 |
if ( $multiple > 1 ) { |
| 802 |
$loop_quantity = wc_max_qty_ceil_to_multiple( $loop_quantity, $multiple ); |
| 803 |
} |
| 804 |
return $loop_quantity > 1 ? $loop_quantity : $quantity; |
| 805 |
} |
| 806 |
|
| 807 |
|
| 808 |
/** |
| 809 |
* Resolve the effective "Sell in multiples of" value to use for a product/list "Add to cart" |
| 810 |
* button (product override, else the store-wide default, else 1 i.e. no restriction). |
| 811 |
* |
| 812 |
* @since 3.0 |
| 813 |
* @param WC_Product $product The product. |
| 814 |
* @return int |
| 815 |
*/ |
| 816 |
function wc_max_qty_get_loop_multiple( $product ) { |
| 817 |
if ( $product->is_sold_individually() ) { |
| 818 |
return 1; |
| 819 |
} |
| 820 |
$product_id = $product->get_parent_id() ? $product->get_parent_id() : $product->get_id(); |
| 821 |
$product_multiple = wc_get_product_multiple_limit( $product_id ); |
| 822 |
if ( ! empty( $product_multiple ) ) { |
| 823 |
return $product_multiple; |
| 824 |
} |
| 825 |
$default_multiple = (int) get_option( 'isa_woocommerce_max_qty_multiple' ); |
| 826 |
return $default_multiple > 1 ? $default_multiple : 1; |
| 827 |
} |
| 828 |
|
| 829 |
|
| 830 |
/** |
| 831 |
* Resolve the effective minimum quantity to use for a product/list "Add to cart" |
| 832 |
* button (product override, else the store-wide default, else 0 i.e. no restriction). |
| 833 |
* |
| 834 |
* @since 3.0 |
| 835 |
* @param WC_Product $product The product. |
| 836 |
* @return int |
| 837 |
*/ |
| 838 |
function wc_max_qty_get_loop_min( $product ) { |
| 839 |
if ( $product->is_sold_individually() ) { |
| 840 |
return 0; |
| 841 |
} |
| 842 |
$product_id = $product->get_parent_id() ? $product->get_parent_id() : $product->get_id(); |
| 843 |
$product_min = wc_get_product_min_limit( $product_id ); |
| 844 |
if ( ! empty( $product_min ) ) { |
| 845 |
return $product_min; |
| 846 |
} |
| 847 |
$default_min = (int) get_option( 'isa_woocommerce_max_qty_min_limit' ); |
| 848 |
return $default_min > 0 ? $default_min : 0; |
| 849 |
} |
| 850 |
|
| 851 |
|
| 852 |
/* Declare WooCommerce HPOS Compatibility */ |
| 853 |
add_action( |
| 854 |
'before_woocommerce_init', |
| 855 |
function () { |
| 856 |
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) { |
| 857 |
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ); |
| 858 |
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', __FILE__, true ); |
| 859 |
} |
| 860 |
} |
| 861 |
); |
| 862 |
|