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 +286 -27 6.1.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
@@ -152,8 +156,18 @@
152 156 if ( 'tooltip' === $displayType ) {
153 157 $templateType = 'table';
154 158 }
155 159
160 + if ( 'table' === $displayType ) {
161 + $style = $settings['table_style'];
162 +
163 + if ( $style !== 'default' ) {
164 + $templateType = 'table-' . $style;
165 + } else {
166 + $templateType = 'table';
167 + }
168 + }
169 +
156 170 if ( 'blocks' === $displayType ) {
157 171 $style = $settings['blocks_style'];
158 172
159 173 if ( $style !== 'default' ) {
@@ -162,9 +176,9 @@
162 176 $templateType = 'blocks';
163 177 }
164 178 }
165 179
166 - if( 'options' === $displayType ) {
180 + if ( 'options' === $displayType ) {
167 181 $style = $settings['options_style'];
168 182
169 183 if ( $style !== 'default' ) {
170 184 $templateType = 'options-' . $style;
@@ -172,8 +186,19 @@
172 186 $templateType = 'options';
173 187 }
174 188 }
175 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 +
176 201 $template = "tiered-pricing-$templateType.php";
177 202
178 203 do_action( 'tiered_pricing_table/before_rendering_tiered_pricing/inner', $priceRule, $product, $settings );
179 204
@@ -190,15 +215,237 @@
190 215 'settings' => $settings,
191 216 'pricing_type' => $priceRule->getType(),
192 217 'id' => $this->getUniqueTieredPricingId(),
193 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
194 265 }
195 266 }
196 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 +
197 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 +
198 445 $settings = array(
199 446 'display_context' => 'product-page',
200 - 'display' => $this->getContainer()->getSettings()->get( 'display', 'yes' ) === 'yes',
447 + 'display' => GeneralSection::isAutomaticDisplayEnabled(),
201 448 'display_type' => $this->getContainer()->getSettings()->get( 'display_type', 'table' ),
202 449 'title' => $this->getContainer()->getSettings()->get( 'table_title', '' ),
203 450 'table_class' => $this->getContainer()->getSettings()->get( 'table_css_class', '' ),
204 451 'quantity_column_title' => $this->getContainer()->getSettings()->get( 'head_quantity_text',
@@ -212,15 +459,26 @@
212 459 'yes' ) === 'yes',
213 460 'clickable_rows' => $this->getContainer()->getSettings()->get( 'clickable_table_rows',
214 461 'yes' ) === 'yes',
215 462 'active_tier_color' => $this->getContainer()->getSettings()->get( 'selected_quantity_color',
216 - '#96598A' ),
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' ),
217 470 'tooltip_border' => $this->getContainer()->getSettings()->get( 'tooltip_border',
218 471 'yes' ) === 'yes',
219 472
220 - 'blocks_style' => GeneralSection::getPricingBlocksStyle(),
221 - '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(),
222 477
478 + 'dropdown_style' => GeneralSection::getPricingDropdownStyle(),
479 + 'plain_text_style' => GeneralSection::getPricingPlainTextStyle(),
480 +
223 481 'options_show_total' => GeneralSection::isShowOptionTotal(),
224 482 'options_show_original_product_price' => GeneralSection::isShowOriginalProductPrice(),
225 483 'options_show_default_option' => GeneralSection::isDefaultOptionEnabled(),
226 484
@@ -234,10 +492,9 @@
234 492 'update_price_on_product_page' => $this->getContainer()->getSettings()->get( 'update_price_on_product_page',
235 493 'yes' ) === 'yes',
236 494 'show_tiered_price_as_discount' => $this->getContainer()->getSettings()->get( 'show_tiered_price_as_discount',
237 495 'yes' ) === 'yes',
238 - 'show_total_price' => $this->getContainer()->getSettings()->get( 'show_total_price',
239 - 'no' ) === 'yes',
496 + 'show_total_price' => $showTotalPrice,
240 497 );
241 498
242 499 $default_quantity_measurement = array(
243 500 'singular' => '',
@@ -363,25 +620,27 @@
363 620 ?>
364 621
365 622 <div style="font-size: .8em;">
366 623 <div class="woocommerce-error" role="alert" style="margin-bottom: 0">
367 - Duplicated quantities detected. Minimum order quantity is equal or higher than the first tiered
368 - pricing.
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' ); ?>
369 626 </div>
370 627 <div style="color: #555;
371 628 padding: 10px 12px;
372 629 border: 1px solid #b32c2e;
373 630 background: #fff5f5;">
631 + <p style="padding: 0; margin: 0 0 10px 0;">
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' ); ?>
634 + </p>
374 635 <p style="padding: 0; margin: 0">
375 - The minimum order quantity must be less than the first tiered pricing rule quantity.
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' ); ?>
376 639 </p>
377 - <p style="padding: 0; margin: 0"> The first tier always uses woocommerce price and is applied to
378 - quantities between minimum order
379 - quantity and
380 - the first tiered pricing rule quantity.
381 - </p>
382 640 <br>
383 - <b>This notice shown only for administrators.</b>
641 + <b><?php esc_html_e( 'This notice is only visible to store administrators.',
642 + 'tier-pricing-table' ); ?></b>
384 643 </div>
385 644 </div>
386 645 <?php
387 646 }