| 1 |
<?php |
| 2 |
/** |
| 3 |
* Plugin Name: Maximum Quantity for WooCommerce Shops |
| 4 |
* Plugin URI: |
| 5 |
* Description: Set a limit for the maximum quantity that can be added to the cart, globally or per product. |
| 6 |
* Version: 2.1 |
| 7 |
* Author: PT Woo Plugins (by Webdados) |
| 8 |
* Author URI: https://ptwooplugins.com |
| 9 |
* Text Domain: woocommerce-max-quantity |
| 10 |
* Requires at least: 5.6 |
| 11 |
* Tested up to: 6.6 |
| 12 |
* Requires PHP: 7.0 |
| 13 |
* WC requires at least: 5.0 |
| 14 |
* WC tested up to: 9.1 |
| 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_max', 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 |
} |
| 46 |
} |
| 47 |
add_action( 'plugins_loaded', __NAMESPACE__ . '\init' ); |
| 48 |
|
| 49 |
|
| 50 |
/** |
| 51 |
* Add the option to WooCommerce products tab |
| 52 |
* |
| 53 |
* @param array $settings The current settings. |
| 54 |
*/ |
| 55 |
function wc_max_qty_options( $settings ) { |
| 56 |
$updated_settings = array(); |
| 57 |
foreach ( $settings as $section ) { |
| 58 |
// At the bottom of the Inventory Options section |
| 59 |
if ( isset( $section['id'] ) && 'product_inventory_options' === $section['id'] && isset( $section['type'] ) && 'sectionend' === $section['type'] ) { |
| 60 |
$updated_settings[] = array( |
| 61 |
'title' => __( 'Maximum quantity per product', 'woocommerce-max-quantity' ), |
| 62 |
'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' ), |
| 63 |
'id' => 'isa_woocommerce_max_qty_limit', |
| 64 |
'css' => 'width: 75px;', |
| 65 |
'type' => 'number', |
| 66 |
'custom_attributes' => array( |
| 67 |
'min' => 0, |
| 68 |
'step' => 1, |
| 69 |
), |
| 70 |
'default' => '', |
| 71 |
'autoload' => false, |
| 72 |
'desc_tip' => false, |
| 73 |
); |
| 74 |
} |
| 75 |
$updated_settings[] = $section; |
| 76 |
} |
| 77 |
return $updated_settings; |
| 78 |
} |
| 79 |
|
| 80 |
|
| 81 |
/** |
| 82 |
* Display the product's "Max quantity per order" field in the Product Data metabox |
| 83 |
* |
| 84 |
* @since 1.4 |
| 85 |
*/ |
| 86 |
function wc_max_qty_add_product_field() { |
| 87 |
$default_max = (int) get_option( 'isa_woocommerce_max_qty_limit' ); |
| 88 |
echo '<div class="options_group">'; |
| 89 |
woocommerce_wp_text_input( |
| 90 |
array( |
| 91 |
'id' => '_isa_wc_max_qty_product_max', |
| 92 |
'label' => __( 'Max quantity per order', 'woocommerce-max-quantity' ), |
| 93 |
'placeholder' => $default_max > 0 ? sprintf( |
| 94 |
/* translators: %d: Default maximum quantity */ |
| 95 |
esc_attr__( 'Store-wide threshold (%d)', 'woocommerce-max-quantity' ), |
| 96 |
$default_max |
| 97 |
) : '', |
| 98 |
'description' => __( 'Optional. Set a maximum quantity limit allowed per order. Enter a number, 1 or greater.', 'woocommerce-max-quantity' ), |
| 99 |
) |
| 100 |
); |
| 101 |
echo '</div>'; |
| 102 |
} |
| 103 |
|
| 104 |
|
| 105 |
/** |
| 106 |
* Save product's Max Quantity field |
| 107 |
* |
| 108 |
* @param int $post_id WP post id. |
| 109 |
* @since 1.4 |
| 110 |
*/ |
| 111 |
function wc_max_qty_save_product_field( $post_id ) { |
| 112 |
$product = wc_get_product( $post_id ); |
| 113 |
$val = $product->get_meta( '_isa_wc_max_qty_product_max' ); |
| 114 |
// phpcs:ignore WordPress.Security.NonceVerification.Missing |
| 115 |
$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 |
| 116 |
if ( $val !== $new ) { |
| 117 |
$product->update_meta_data( '_isa_wc_max_qty_product_max', $new ); |
| 118 |
$product->save(); |
| 119 |
} |
| 120 |
} |
| 121 |
|
| 122 |
|
| 123 |
/** |
| 124 |
* Get the individual product max limit |
| 125 |
* |
| 126 |
* @param int $product_id The product ID. |
| 127 |
* @return int|bool $limit The max limit number for this product, if set, otherwise false. |
| 128 |
* @since 1.4 |
| 129 |
*/ |
| 130 |
function wc_get_product_max_limit( $product_id ) { |
| 131 |
$product = wc_get_product( $product_id ); |
| 132 |
$qty = $product->get_meta( '_isa_wc_max_qty_product_max' ); |
| 133 |
if ( empty( $qty ) ) { |
| 134 |
// Honor the Sold individually setting |
| 135 |
$limit = $product->is_sold_individually() ? 1 : false; |
| 136 |
} else { |
| 137 |
$limit = (int) $qty; |
| 138 |
} |
| 139 |
return $limit; |
| 140 |
} |
| 141 |
|
| 142 |
|
| 143 |
/** |
| 144 |
* Set the max attribute value for the quantity input field for Add to cart forms. |
| 145 |
* This applies to Simple product Add To Cart forms, and ALL (simple and variable) products on the Cart page quantity field. |
| 146 |
* |
| 147 |
* @param array $args The current arguments. |
| 148 |
* @param object $product The product. |
| 149 |
* @return array $args |
| 150 |
* @since 1.1.6 |
| 151 |
*/ |
| 152 |
function wc_max_qty_input_args( $args, $product ) { |
| 153 |
$default_max = (int) get_option( 'isa_woocommerce_max_qty_limit' ); |
| 154 |
$product_id = $product->get_parent_id() ? $product->get_parent_id() : $product->get_id(); |
| 155 |
$product_max = wc_get_product_max_limit( $product_id ); |
| 156 |
// Set product max or default max |
| 157 |
if ( ! empty( $product_max ) ) { |
| 158 |
$args['max_value'] = $product_max; |
| 159 |
} elseif ( ! empty( $default_max ) ) { |
| 160 |
$args['max_value'] = $default_max; |
| 161 |
} |
| 162 |
// Limit our max by the available stock, if stock is lower |
| 163 |
if ( ! empty( $args['max_value'] ) ) { |
| 164 |
if ( $product->managing_stock() && ! $product->backorders_allowed() ) { |
| 165 |
$stock = $product->get_stock_quantity(); |
| 166 |
$args['max_value'] = min( $stock, $args['max_value'] ); |
| 167 |
} |
| 168 |
} |
| 169 |
return $args; |
| 170 |
} |
| 171 |
|
| 172 |
|
| 173 |
/** |
| 174 |
* Filter the available variation to enforce the max on the quantity input field |
| 175 |
* on Add to cart forms for Variable Products. |
| 176 |
* |
| 177 |
* @param array $args The current arguments. |
| 178 |
* @param object $product The product. |
| 179 |
* @param object $variation The product variation. |
| 180 |
*/ |
| 181 |
function wc_max_qty_variation_input_qty_max( $args, $product, $variation ) { |
| 182 |
if ( is_admin() && ! is_ajax() ) { |
| 183 |
return $args; |
| 184 |
} |
| 185 |
$default_max = (int) get_option( 'isa_woocommerce_max_qty_limit' ); |
| 186 |
$product_max = wc_get_product_max_limit( $variation->get_parent_id() ); |
| 187 |
// Set product max or default max |
| 188 |
if ( ! empty( $product_max ) ) { |
| 189 |
$args['max_qty'] = $product_max; |
| 190 |
} elseif ( ! empty( $default_max ) ) { |
| 191 |
$args['max_qty'] = $default_max; |
| 192 |
} |
| 193 |
// Limit our max by the available stock, if stock is lower |
| 194 |
if ( ! empty( $args['max_qty'] ) ) { |
| 195 |
if ( $variation->managing_stock() && ! $variation->backorders_allowed() ) { |
| 196 |
$stock = $variation->get_stock_quantity(); |
| 197 |
$args['max_qty'] = min( $stock, $args['max_qty'] ); |
| 198 |
} |
| 199 |
} |
| 200 |
return $args; |
| 201 |
} |
| 202 |
|
| 203 |
|
| 204 |
/** |
| 205 |
* Find out how many of this product are already in the cart |
| 206 |
* |
| 207 |
* @param mixed $product_id ID of the product in question. |
| 208 |
* @param string $cart_item_key The cart key for this item in case of Updating cart. |
| 209 |
* |
| 210 |
* @return integer $running_qty The total quantity of this item, parent item in case of variations, in cart |
| 211 |
* @since 1.1.6 |
| 212 |
*/ |
| 213 |
function wc_max_qty_get_cart_qty( $product_id, $cart_item_key = '' ) { |
| 214 |
$running_qty = 0; // Keep a running total to count variations |
| 215 |
// Search the cart for the product in question |
| 216 |
foreach ( WC()->cart->get_cart() as $other_cart_item_keys => $values ) { |
| 217 |
if ( intval( $product_id ) === intval( $values['product_id'] ) ) { |
| 218 |
/* |
| 219 |
* In case of updating the cart quantity, don't count this cart item key |
| 220 |
otherwise they won't be able to REDUCE the number of items in cart becuase 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. |
| 221 |
*/ |
| 222 |
if ( $cart_item_key === $other_cart_item_keys ) { |
| 223 |
continue; |
| 224 |
} |
| 225 |
// Add that quantity to our running total qty for this product |
| 226 |
$running_qty += (int) $values['quantity']; |
| 227 |
} |
| 228 |
} |
| 229 |
return $running_qty; |
| 230 |
} |
| 231 |
|
| 232 |
|
| 233 |
/** |
| 234 |
* Validate product quantity when Added to cart |
| 235 |
* |
| 236 |
* @param bool $passed If the validation passed. |
| 237 |
* @param int $product_id The product ID. |
| 238 |
* @param int $quantity The quantity added to cart. |
| 239 |
* @param int $variation_id The product variation ID. |
| 240 |
* |
| 241 |
* @since 1.1.6 |
| 242 |
*/ |
| 243 |
function wc_max_qty_add_to_cart_validation( $passed, $product_id, $quantity, $variation_id = '' ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed |
| 244 |
// If it hasn't passed, we don't need to bother validating further |
| 245 |
if ( $passed ) { |
| 246 |
$default_max = (int) get_option( 'isa_woocommerce_max_qty_limit' ); |
| 247 |
$product_max = wc_get_product_max_limit( $product_id ); |
| 248 |
// Set product max or default max |
| 249 |
if ( ! empty( $product_max ) ) { |
| 250 |
$new_max = $product_max; |
| 251 |
} elseif ( ! empty( $default_max ) ) { |
| 252 |
$new_max = $default_max; |
| 253 |
} else { |
| 254 |
return $passed; |
| 255 |
} |
| 256 |
$already_in_cart = wc_max_qty_get_cart_qty( $product_id ); |
| 257 |
$product = wc_get_product( $product_id ); |
| 258 |
$product_title = $product->get_title(); |
| 259 |
if ( ! empty( $already_in_cart ) ) { |
| 260 |
// There was already a quantity of this item in cart prior to this addition. |
| 261 |
// Check if the total of already_in_cart + current addition quantity is more than our max. |
| 262 |
if ( ( $already_in_cart + $quantity ) > $new_max ) { |
| 263 |
// oops. too much. |
| 264 |
$passed = false; |
| 265 |
// Add compatibility with WooCommerce Direct Checkout |
| 266 |
if ( class_exists( 'WooCommerce_Direct_Checkout' ) ) { |
| 267 |
$direct_checkout = get_option( 'direct_checkout_enabled' ); |
| 268 |
$direct_checkout_url = get_option( 'direct_checkout_cart_redirect_url' ); |
| 269 |
if ( $direct_checkout && $direct_checkout_url ) { |
| 270 |
// Redirect to submit page |
| 271 |
wp_safe_redirect( esc_url_raw( $direct_checkout_url ) ); |
| 272 |
exit; |
| 273 |
} |
| 274 |
} |
| 275 |
wc_add_notice( |
| 276 |
apply_filters( |
| 277 |
'isa_wc_max_qty_error_message_already_had', |
| 278 |
sprintf( |
| 279 |
/* translators: %1$s maximum items, %2$s product name, %3$s: cart link, %4$s number of items in cart */ |
| 280 |
__( 'You can add a maximum of %1$s %2$s’s to %3$s. You already have %4$s.', 'woocommerce-max-quantity' ), |
| 281 |
$new_max, |
| 282 |
$product_title, |
| 283 |
'<a href="' . esc_url( wc_get_cart_url() ) . '">' . __( 'your cart', 'woocommerce-max-quantity' ) . '</a>', |
| 284 |
$already_in_cart |
| 285 |
), |
| 286 |
$new_max, |
| 287 |
$already_in_cart |
| 288 |
), |
| 289 |
'error' |
| 290 |
); |
| 291 |
} |
| 292 |
} else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found |
| 293 |
// none were in cart previously |
| 294 |
// just in case they manually type in an amount greater than we allow, check the input number here too |
| 295 |
if ( $quantity > $new_max ) { |
| 296 |
// oops. too much. |
| 297 |
wc_add_notice( |
| 298 |
apply_filters( |
| 299 |
'isa_wc_max_qty_error_message', |
| 300 |
sprintf( |
| 301 |
/* translators: %1$s maximum items, %2$s product name, %3$s: cart link */ |
| 302 |
__( 'You can add a maximum of %1$s %2$s’s to %3$s.', 'woocommerce-max-quantity' ), |
| 303 |
$new_max, |
| 304 |
$product_title, |
| 305 |
'<a href="' . esc_url( wc_get_cart_url() ) . '">' . __( 'your cart', 'woocommerce-max-quantity' ) . '</a>' |
| 306 |
), |
| 307 |
$new_max |
| 308 |
), |
| 309 |
'error' |
| 310 |
); |
| 311 |
$passed = false; |
| 312 |
} |
| 313 |
} |
| 314 |
} |
| 315 |
return $passed; |
| 316 |
} |
| 317 |
|
| 318 |
/** |
| 319 |
* Validate product quantity when cart is UPDATED - Not working when updating quantity inside the block-based cart |
| 320 |
* Just in case they manually type in an amount greater than we allow and the HTML5 Constraint validation doesn't work. |
| 321 |
* |
| 322 |
* @param bool $passed If the validation passed. |
| 323 |
* @param string $cart_item_key The key of the updated cart item. |
| 324 |
* @param array $values Cart item values. |
| 325 |
* @param int $quantity The quantity updated to cart. |
| 326 |
* @since 1.1.9 |
| 327 |
*/ |
| 328 |
function wc_max_qty_update_cart_validation( $passed, $cart_item_key, $values, $quantity ) { |
| 329 |
// If it hasn't passed, we don't need to bother validating further |
| 330 |
if ( $passed ) { |
| 331 |
$default_max = (int) get_option( 'isa_woocommerce_max_qty_limit' ); |
| 332 |
$product_max = wc_get_product_max_limit( $values['product_id'] ); |
| 333 |
// Set product max or default max |
| 334 |
if ( ! empty( $product_max ) ) { |
| 335 |
$new_max = $product_max; |
| 336 |
} elseif ( ! empty( $default_max ) ) { |
| 337 |
$new_max = $default_max; |
| 338 |
} else { |
| 339 |
return $passed; |
| 340 |
} |
| 341 |
$already_in_cart = wc_max_qty_get_cart_qty( $values['product_id'], $cart_item_key ); |
| 342 |
$product = wc_get_product( $values['product_id'] ); |
| 343 |
if ( ( $already_in_cart + $quantity ) > $new_max ) { |
| 344 |
wc_add_notice( |
| 345 |
apply_filters( |
| 346 |
'isa_wc_max_qty_error_message', |
| 347 |
sprintf( |
| 348 |
/* translators: %1$s maximum items, %2$s product name, %3$s: cart link */ |
| 349 |
__( 'You can add a maximum of %1$s %2$s’s to %3$s.', 'woocommerce-max-quantity' ), |
| 350 |
$new_max, |
| 351 |
$product->get_name(), |
| 352 |
'<a href="' . esc_url( wc_get_cart_url() ) . '">' . __( 'your cart', 'woocommerce-max-quantity' ) . '</a>' |
| 353 |
), |
| 354 |
$new_max |
| 355 |
), |
| 356 |
'error' |
| 357 |
); |
| 358 |
$passed = false; |
| 359 |
} |
| 360 |
} |
| 361 |
return $passed; |
| 362 |
} |
| 363 |
|
| 364 |
|
| 365 |
/** |
| 366 |
* Set maximum quantity on the block-based cart |
| 367 |
* |
| 368 |
* @since 2.0 |
| 369 |
* @param int $cart_max Current maximum quantity. |
| 370 |
* @param object $product Current product. |
| 371 |
* @return int |
| 372 |
*/ |
| 373 |
function blocks_cart_max_qty( $cart_max, $product ) { |
| 374 |
if ( ! empty( $cart_max ) ) { |
| 375 |
$default_max = (int) get_option( 'isa_woocommerce_max_qty_limit' ); |
| 376 |
$product_max = wc_get_product_max_limit( $product->get_parent_id() ? $product->get_parent_id() : $product->get_id() ); |
| 377 |
// Set product max or default max |
| 378 |
if ( ! empty( $product_max ) ) { |
| 379 |
$new_max = $product_max; |
| 380 |
} elseif ( ! empty( $default_max ) ) { |
| 381 |
$new_max = $default_max; |
| 382 |
} |
| 383 |
if ( ! empty( $new_max ) ) { |
| 384 |
$cart_max = min( $cart_max, $new_max ); |
| 385 |
} |
| 386 |
} |
| 387 |
return $cart_max; |
| 388 |
} |
| 389 |
|
| 390 |
|
| 391 |
/* Declare WooCommerce HPOS Compatibility */ |
| 392 |
add_action( |
| 393 |
'before_woocommerce_init', |
| 394 |
function () { |
| 395 |
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) { |
| 396 |
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ); |
| 397 |
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', __FILE__, true ); |
| 398 |
} |
| 399 |
} |
| 400 |
); |
| 401 |
|