PluginProbe
Tiered Pricing Table for WooCommerce / 8.1.0
Tiered Pricing Table for WooCommerce v8.1.0
8.1.0 8.0.2 7.1.7 7.1.5 7.1.4 7.1.1 6.5.0 6.4.0 6.1.0 trunk 1.0 1.1 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.2.0 2.2.1 2.2.2 2.2.3 2.3.0 2.3.1 2.3.2 All 92 releases
← All changes | src/PricingTable.php +273 -23 6.5.0 → 8.1.0 View file →
@@ -44,12 +44,8 @@
44 44 if ( ! $parentProduct ) {
45 45 return;
46 46 }
47 47
48 - if ( ! $this->productHasPricingRules( $parentProduct ) ) {
49 - return;
50 - }
51 -
52 48 $settings = wp_parse_args( $settings, $this->getDefaultSettings( $parentProductID ) );
53 49
54 50 // If the product is variable, but no specific variation is passed - check for default
55 51 if ( ! $variationID && TierPricingTablePlugin::isVariableProductSupported( $parentProduct ) ) {
@@ -84,13 +80,13 @@
84 80
85 81 ?>
86 82 <div class="clear"></div>
87 83 <div class="tpt__tiered-pricing <?php echo esc_attr( $hidden ? 'tpt__hidden' : '' ); ?>"
88 - data-settings="<?php echo esc_attr( json_encode( $settings ) ); ?>"
89 - data-display-context="<?php echo esc_attr( $settings['display_context'] ); ?>"
90 - data-display-type="<?php echo esc_attr( $settings['display_type'] ); ?>"
91 - data-product-id="<?php echo esc_attr( $parentProduct->get_id() ); ?>"
92 - data-product-type="<?php echo esc_attr( $parentProduct->get_type() ); ?>"
84 + data-settings="<?php echo esc_attr( json_encode( $settings ) ); ?>"
85 + data-display-context="<?php echo esc_attr( $settings['display_context'] ); ?>"
86 + data-display-type="<?php echo esc_attr( $settings['display_type'] ); ?>"
87 + data-product-id="<?php echo esc_attr( $parentProduct->get_id() ); ?>"
88 + data-product-type="<?php echo esc_attr( $parentProduct->get_type() ); ?>"
93 89
94 90 <?php if ( TierPricingTablePlugin::isVariableProductSupported( $parentProduct ) ) : ?>
95 91 <?php
96 92 $loadVariationByAjax = count( $parentProduct->get_children() ) > $ajaxVariationsThreshold;
@@ -106,10 +102,10 @@
106 102 <?php if ( TierPricingTablePlugin::isVariableProductSupported( $parentProduct ) && ! $loadVariationByAjax ) : ?>
107 103 <div class="tpt__tiered-pricing-preloaded-variations" style="display:none">
108 104 <?php foreach ( $parentProduct->get_available_variations( 'objects' ) as $childProduct ) : ?>
109 105 <div class="tiered-pricing-preloaded-variation"
110 - data-variation-id="<?php echo esc_attr( $childProduct->get_id() ); ?>"
111 - data-display-context="<?php echo esc_attr( $settings['display_context'] ); ?>">
106 + data-variation-id="<?php echo esc_attr( $childProduct->get_id() ); ?>"
107 + data-display-context="<?php echo esc_attr( $settings['display_context'] ); ?>">
112 108 <?php $this->renderPricingTableHTML( $parentProduct, $childProduct, $settings ); ?>
113 109 </div>
114 110 <?php endforeach; ?>
115 111 </div>
@@ -114,8 +110,16 @@
114 110 <?php endforeach; ?>
115 111 </div>
116 112 <?php endif; ?>
117 113 <?php
114 + /**
115 + * After the pricing layout and its wrapper. The savings badge uses it for its "after the table" position.
116 + *
117 + * @param WC_Product $parentProduct
118 + * @param int|null $variationID
119 + * @param array $settings
120 + */
121 + do_action( 'tiered_pricing_table/after_rendering_tiered_pricing', $parentProduct, $variationID, $settings );
118 122 }
119 123
120 124 /**
121 125 * Render pricing table without wrapper
@@ -172,9 +176,9 @@
172 176 $templateType = 'blocks';
173 177 }
174 178 }
175 179
176 - if( 'options' === $displayType ) {
180 + if ( 'options' === $displayType ) {
177 181 $style = $settings['options_style'];
178 182
179 183 if ( $style !== 'default' ) {
180 184 $templateType = 'options-' . $style;
@@ -182,8 +186,19 @@
182 186 $templateType = 'options';
183 187 }
184 188 }
185 189
190 + // layouts with a single style option: dropdown, plain text
191 + $styledLayouts = array(
192 + 'dropdown' => 'dropdown_style',
193 + 'plain-text' => 'plain_text_style',
194 + );
195 +
196 + if ( isset( $styledLayouts[ $displayType ] ) ) {
197 + $style = $settings[ $styledLayouts[ $displayType ] ] ?? 'default';
198 + $templateType = 'default' !== $style && '' !== $style ? $displayType . '-' . $style : $displayType;
199 + }
200 +
186 201 $template = "tiered-pricing-$templateType.php";
187 202
188 203 do_action( 'tiered_pricing_table/before_rendering_tiered_pricing/inner', $priceRule, $product, $settings );
189 204
@@ -200,15 +215,237 @@
200 215 'settings' => $settings,
201 216 'pricing_type' => $priceRule->getType(),
202 217 'id' => $this->getUniqueTieredPricingId(),
203 218 ) );
219 +
220 + $this->renderMoreTiersLink( $parentProduct );
221 + } else {
222 + // Provide fallback data for products without pricing rules,
223 + // which allows the frontend JS (e.g. dynamic totals, you save, etc.)
224 + // to function for all products via the 'tiered_price_update' event.
225 + $price_incl_taxes = wc_get_price_including_tax( $product, array(
226 + 'price' => $product->get_price(),
227 + ) );
228 +
229 + $price_excl_taxes = wc_get_price_excluding_tax( $product, array(
230 + 'price' => $product->get_price(),
231 + ) );
232 +
233 + $sale_price = $product->get_sale_price();
234 +
235 + if ( $sale_price ) {
236 + $sale_price = wc_get_price_to_display( $product, array(
237 + 'price' => $sale_price,
238 + ) );
239 + }
240 +
241 + $regular_price = wc_get_price_to_display( $product, array(
242 + 'price' => $product->get_regular_price(),
243 + ) );
244 +
245 + $price = wc_get_price_to_display( $product, array(
246 + 'price' => $product->get_price(),
247 + ) );
248 +
249 + ?>
250 + <div style="display:none;"
251 + class="tiered-pricing-fallback-data"
252 + data-product-id="<?php echo esc_attr( $product->get_id() ); ?>"
253 + data-price-rules="{}"
254 + data-minimum="<?php echo esc_attr( $priceRule->getMinimum( true ) ); ?>"
255 + data-product-name="<?php echo esc_attr( $product->get_name() ); ?>"
256 + data-regular-price="<?php echo esc_attr( $regular_price ); ?>"
257 + data-sale-price="<?php echo esc_attr( $sale_price ); ?>"
258 + data-price="<?php echo esc_attr( $price ); ?>"
259 + data-product-price-suffix="<?php echo esc_attr( $product->get_price_suffix() ); ?>"
260 + data-tiered-price="<?php echo esc_attr( $price ); ?>"
261 + data-tiered-price-exclude-taxes="<?php echo esc_attr( $price_excl_taxes ); ?>"
262 + data-tiered-price-include-taxes="<?php echo esc_attr( $price_incl_taxes ); ?>"
263 + ></div>
264 + <?php
204 265 }
205 266 }
206 267
268 + /**
269 + * Discount of a tier as the store shows it: a percentage, the amount saved per item, or both.
270 + *
271 + * @param float $percent Discount in percent, as the templates compute it.
272 + * @param PricingRule $pricingRule
273 + * @param WC_Product $product
274 + * @param int|null $quantity Tier quantity, or null for the base tier (its discount is the sale price).
275 + * @param array $settings Renderer settings.
276 + * @param string $style 'plain' ("10%", "$4.50", "10% ($4.50)") or 'off' ("10% off", "$4.50 off", "10% off, save $4.50").
277 + *
278 + * @return string HTML
279 + */
280 + /**
281 + * Decimals shown on a percentage discount ("10.14%" with 2, "10%" with 0).
282 + *
283 + * @param float $percent The discount percentage.
284 + * @param WC_Product|null $product
285 + * @param PricingRule|null $pricingRule
286 + *
287 + * @return int
288 + */
289 + public static function discountDecimals( float $percent, ?WC_Product $product = null, ?PricingRule $pricingRule = null ): int {
290 + /**
291 + * Filter the number of decimals of displayed percentage discounts.
292 + *
293 + * @param int $decimals Default 2. Return 0 to show whole numbers.
294 + * @param float $percent The discount percentage before rounding.
295 + * @param WC_Product|null $product
296 + * @param PricingRule|null $pricingRule
297 + */
298 + $decimals = apply_filters( 'tiered_pricing_table/discount/decimals', 2, $percent, $product, $pricingRule );
299 +
300 + return max( 0, (int) $decimals );
301 + }
302 +
303 + public static function formatDiscount(
304 + float $percent,
305 + PricingRule $pricingRule,
306 + WC_Product $product,
307 + ?int $quantity,
308 + array $settings,
309 + string $style = 'plain'
310 + ): string {
311 + $format = $settings['discount_format'] ?? 'percentage';
312 + $format = in_array( $format, array( 'percentage', 'amount', 'both' ), true ) ? $format : 'percentage';
313 +
314 + $percentLabel = round( $percent, self::discountDecimals( $percent, $product, $pricingRule ) ) . '%';
315 + $amountLabel = '';
316 +
317 + if ( 'percentage' !== $format ) {
318 + $saving = 0;
319 +
320 + if ( null === $quantity ) {
321 + if ( $product->is_on_sale() ) {
322 + $saving = (float) $product->get_regular_price() - (float) $product->get_sale_price();
323 + }
324 + } else {
325 + $base = CalculationLogic::calculateDiscountBasedOnRegularPrice() ? $product->get_regular_price() : $product->get_price();
326 + $saving = (float) $base - (float) $pricingRule->getTierPrice( $quantity, false );
327 + }
328 +
329 + $amountLabel = wc_price( wc_get_price_to_display( $product, array( 'price' => max( 0, $saving ) ) ) );
330 + }
331 +
332 + if ( 'off' === $style ) {
333 + switch ( $format ) {
334 + case 'amount':
335 + // translators: %s: amount saved per item
336 + return sprintf( __( '%s off', 'tier-pricing-table' ), $amountLabel );
337 + case 'both':
338 + // translators: 1: discount percentage, 2: amount saved per item
339 + return sprintf( __( '%1$s off, save %2$s', 'tier-pricing-table' ), $percentLabel, $amountLabel );
340 + default:
341 + // translators: %s: discount percentage
342 + return sprintf( __( '%s off', 'tier-pricing-table' ), $percentLabel );
343 + }
344 + }
345 +
346 + switch ( $format ) {
347 + case 'amount':
348 + return $amountLabel;
349 + case 'both':
350 + return $percentLabel . ' (' . $amountLabel . ')';
351 + default:
352 + return $percentLabel;
353 + }
354 + }
355 +
356 + /**
357 + * The "Spacing" and "Cell padding" options as a style attribute for a layout's root element: CSS
358 + * variables the stylesheet reads for the gap between tiers and the padding inside table cells.
359 + * Empty when neither option is set (each style keeps its own spacing and padding).
360 + */
361 + public static function layoutStyleAttribute( array $settings ): string {
362 + $variables = array();
363 +
364 + foreach ( array( 'layout_spacing' => '--tiered-pricing-gap', 'cell_padding' => '--tiered-pricing-cell-padding' ) as $option => $variable ) {
365 + $value = $settings[ $option ] ?? '';
366 +
367 + if ( '' === $value || null === $value || ! is_numeric( $value ) ) {
368 + continue;
369 + }
370 +
371 + $variables[] = $variable . ': ' . max( 0, min( 80, (int) $value ) ) . 'px';
372 + }
373 +
374 + return $variables ? ' style="' . esc_attr( implode( '; ', $variables ) ) . '"' : '';
375 + }
376 +
377 + /**
378 + * @deprecated 7.1.9 Use layoutStyleAttribute().
379 + */
380 + public static function spacingAttribute( array $settings ): string {
381 + return self::layoutStyleAttribute( $settings );
382 + }
383 +
384 + /**
385 + * Tiers the last rendered layout left out because of the "tiers per grid item" limit.
386 + */
387 + protected static $hiddenTiers = 0;
388 +
389 + /**
390 + * Tier rows in the configured order (ascending by quantity, or the biggest discount first), cut to
391 + * the "tiers per grid item" limit when one is set.
392 + *
393 + * @param string[] $rows Rendered rows, ascending.
394 + * @param array $settings Renderer settings.
395 + *
396 + * @return string HTML (the rows were escaped when rendered)
397 + */
398 + public static function orderTiers( array $rows, array $settings ): string {
399 + if ( 'desc' === ( $settings['tiers_order'] ?? 'asc' ) ) {
400 + $rows = array_reverse( $rows );
401 + }
402 +
403 + $limit = (int) ( $settings['tiers_limit'] ?? 0 );
404 + self::$hiddenTiers = 0;
405 +
406 + if ( $limit > 0 && count( $rows ) > $limit ) {
407 + self::$hiddenTiers = count( $rows ) - $limit;
408 + $rows = array_slice( $rows, 0, $limit );
409 + }
410 +
411 + return implode( '', $rows );
412 + }
413 +
414 + /**
415 + * The "+N more tiers" link to the product page, after a layout the limit shortened.
416 + */
417 + protected function renderMoreTiersLink( WC_Product $parentProduct ) {
418 + $hidden = self::$hiddenTiers;
419 + self::$hiddenTiers = 0;
420 +
421 + if ( $hidden < 1 ) {
422 + return;
423 + }
424 +
425 + /* translators: %d: number of tiers not listed */
426 + $label = sprintf( _n( '+%d more tier', '+%d more tiers', $hidden, 'tier-pricing-table' ), $hidden );
427 +
428 + echo '<a class="tiered-pricing-more-tiers" href="' . esc_url( $parentProduct->get_permalink() ) . '">' . esc_html( $label ) . '</a>';
429 + }
430 +
207 431 protected function getDefaultSettings( $productId = false ): array {
432 + $hasRules = true;
433 +
434 + if ( $productId ) {
435 + $product = wc_get_product( $productId );
436 + if ( $product ) {
437 + $hasRules = $this->productHasPricingRules( $product );
438 + }
439 + }
440 +
441 + $showTotalPrice = $hasRules ? $this->getContainer()->getSettings()->get( 'show_total_price',
442 + 'no' ) === 'yes' : $this->getContainer()->getSettings()->get( 'show_total_price_non_tiered',
443 + 'no' ) === 'yes';
444 +
208 445 $settings = array(
209 446 'display_context' => 'product-page',
210 - 'display' => $this->getContainer()->getSettings()->get( 'display', 'yes' ) === 'yes',
447 + 'display' => GeneralSection::isAutomaticDisplayEnabled(),
211 448 'display_type' => $this->getContainer()->getSettings()->get( 'display_type', 'table' ),
212 449 'title' => $this->getContainer()->getSettings()->get( 'table_title', '' ),
213 450 'table_class' => $this->getContainer()->getSettings()->get( 'table_css_class', '' ),
214 451 'quantity_column_title' => $this->getContainer()->getSettings()->get( 'head_quantity_text',
@@ -223,16 +460,25 @@
223 460 'clickable_rows' => $this->getContainer()->getSettings()->get( 'clickable_table_rows',
224 461 'yes' ) === 'yes',
225 462 'active_tier_color' => $this->getContainer()->getSettings()->get( 'selected_quantity_color',
226 463 '#3858e9' ),
464 + 'discount_format' => $this->getContainer()->getSettings()->get( 'discount_format', 'percentage' ),
465 + 'layout_spacing' => $this->getContainer()->getSettings()->get( 'layout_spacing', '' ),
466 + 'cell_padding' => $this->getContainer()->getSettings()->get( 'cell_padding', '' ),
467 + 'discount_badge_color' => $this->getContainer()->getSettings()->get( 'discount_badge_color', '' ),
468 + 'active_tier_border' => $this->getContainer()->getSettings()->get( 'table_active_border', 'yes' ),
469 + 'tiers_order' => $this->getContainer()->getSettings()->get( 'tiers_order', 'asc' ),
227 470 'tooltip_border' => $this->getContainer()->getSettings()->get( 'tooltip_border',
228 471 'yes' ) === 'yes',
229 472
230 - 'table_style' => GeneralSection::getPricingTableStyle(),
231 - 'compact_layout' => $this->getContainer()->getSettings()->get( 'compact_layout', 'no' ),
232 - 'blocks_style' => GeneralSection::getPricingBlocksStyle(),
233 - 'options_style' => GeneralSection::getPricingOptionsStyle(),
473 + 'table_style' => GeneralSection::getPricingTableStyle(),
474 + 'compact_layout' => $this->getContainer()->getSettings()->get( 'compact_layout', 'no' ),
475 + 'blocks_style' => GeneralSection::getPricingBlocksStyle(),
476 + 'options_style' => GeneralSection::getPricingOptionsStyle(),
234 477
478 + 'dropdown_style' => GeneralSection::getPricingDropdownStyle(),
479 + 'plain_text_style' => GeneralSection::getPricingPlainTextStyle(),
480 +
235 481 'options_show_total' => GeneralSection::isShowOptionTotal(),
236 482 'options_show_original_product_price' => GeneralSection::isShowOriginalProductPrice(),
237 483 'options_show_default_option' => GeneralSection::isDefaultOptionEnabled(),
238 484
@@ -246,10 +492,9 @@
246 492 'update_price_on_product_page' => $this->getContainer()->getSettings()->get( 'update_price_on_product_page',
247 493 'yes' ) === 'yes',
248 494 'show_tiered_price_as_discount' => $this->getContainer()->getSettings()->get( 'show_tiered_price_as_discount',
249 495 'yes' ) === 'yes',
250 - 'show_total_price' => $this->getContainer()->getSettings()->get( 'show_total_price',
251 - 'no' ) === 'yes',
496 + 'show_total_price' => $showTotalPrice,
252 497 );
253 498
254 499 $default_quantity_measurement = array(
255 500 'singular' => '',
@@ -375,9 +620,10 @@
375 620 ?>
376 621
377 622 <div style="font-size: .8em;">
378 623 <div class="woocommerce-error" role="alert" style="margin-bottom: 0">
379 - <?php esc_html_e( 'Pricing Conflict Detected: Your Minimum Order Quantity is equal to or greater than your first pricing tier.', 'tier-pricing-table' ); ?>
624 + <?php esc_html_e( 'Pricing Conflict Detected: Your Minimum Order Quantity is equal to or greater than your first pricing tier.',
625 + 'tier-pricing-table' ); ?>
380 626 </div>
381 627 <div style="color: #555;
382 628 padding: 10px 12px;
383 629 border: 1px solid #b32c2e;
@@ -382,15 +628,19 @@
382 628 padding: 10px 12px;
383 629 border: 1px solid #b32c2e;
384 630 background: #fff5f5;">
385 631 <p style="padding: 0; margin: 0 0 10px 0;">
386 - <?php esc_html_e( 'To fix this, please ensure your first tiered pricing rule starts at a higher quantity than your Minimum Order Quantity.', 'tier-pricing-table' ); ?>
632 + <?php esc_html_e( 'To fix this, please ensure your first tiered pricing rule starts at a higher quantity than your Minimum Order Quantity.',
633 + 'tier-pricing-table' ); ?>
387 634 </p>
388 635 <p style="padding: 0; margin: 0">
389 - <strong><?php esc_html_e( 'Why?', 'tier-pricing-table' ); ?></strong> <?php esc_html_e( 'The plugin automatically creates a base tier using your standard WooCommerce product price. This base tier covers any quantities ordered between the Minimum Order Quantity and your first discounted tier.', 'tier-pricing-table' ); ?>
636 + <strong><?php esc_html_e( 'Why?',
637 + 'tier-pricing-table' ); ?></strong> <?php esc_html_e( 'The plugin automatically creates a base tier using your standard WooCommerce product price. This base tier covers any quantities ordered between the Minimum Order Quantity and your first discounted tier.',
638 + 'tier-pricing-table' ); ?>
390 639 </p>
391 640 <br>
392 - <b><?php esc_html_e( 'This notice is only visible to store administrators.', 'tier-pricing-table' ); ?></b>
641 + <b><?php esc_html_e( 'This notice is only visible to store administrators.',
642 + 'tier-pricing-table' ); ?></b>
393 643 </div>
394 644 </div>
395 645 <?php
396 646 }