PluginProbe
Maximum, Minimum and Multiple Quantity for WooCommerce Shops / trunk
Maximum, Minimum and Multiple Quantity for WooCommerce Shops vtrunk
3.0 1.0 1.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.3 1.4 1.4.1 1.4.2 1.4.3 1.5 1.5.1 1.5.2 1.6 All 32 releases
woocommerce-max-quantity / woocommerce-max-quantity.php

woocommerce-max-quantity.php in Maximum, Minimum and Multiple Quantity for WooCommerce Shops trunk, at woocommerce-max-quantity.php

862 lines 31.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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&section=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