PluginProbe
Tiered Pricing Table for WooCommerce / 8.0.2
Tiered Pricing Table for WooCommerce v8.0.2
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 2.3.3 All 91 releases
← All changes | src/PricingTable.php +242 -23 6.5.0 → 8.0.2 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>
@@ -172,9 +168,9 @@
172 168 $templateType = 'blocks';
173 169 }
174 170 }
175 171
176 - if( 'options' === $displayType ) {
172 + if ( 'options' === $displayType ) {
177 173 $style = $settings['options_style'];
178 174
179 175 if ( $style !== 'default' ) {
180 176 $templateType = 'options-' . $style;
@@ -182,8 +178,19 @@
182 178 $templateType = 'options';
183 179 }
184 180 }
185 181
182 + // layouts with a single style option: dropdown, plain text
183 + $styledLayouts = array(
184 + 'dropdown' => 'dropdown_style',
185 + 'plain-text' => 'plain_text_style',
186 + );
187 +
188 + if ( isset( $styledLayouts[ $displayType ] ) ) {
189 + $style = $settings[ $styledLayouts[ $displayType ] ] ?? 'default';
190 + $templateType = 'default' !== $style && '' !== $style ? $displayType . '-' . $style : $displayType;
191 + }
192 +
186 193 $template = "tiered-pricing-$templateType.php";
187 194
188 195 do_action( 'tiered_pricing_table/before_rendering_tiered_pricing/inner', $priceRule, $product, $settings );
189 196
@@ -200,15 +207,214 @@
200 207 'settings' => $settings,
201 208 'pricing_type' => $priceRule->getType(),
202 209 'id' => $this->getUniqueTieredPricingId(),
203 210 ) );
211 +
212 + $this->renderMoreTiersLink( $parentProduct );
213 + } else {
214 + // Provide fallback data for products without pricing rules,
215 + // which allows the frontend JS (e.g. dynamic totals, you save, etc.)
216 + // to function for all products via the 'tiered_price_update' event.
217 + $price_incl_taxes = wc_get_price_including_tax( $product, array(
218 + 'price' => $product->get_price(),
219 + ) );
220 +
221 + $price_excl_taxes = wc_get_price_excluding_tax( $product, array(
222 + 'price' => $product->get_price(),
223 + ) );
224 +
225 + $sale_price = $product->get_sale_price();
226 +
227 + if ( $sale_price ) {
228 + $sale_price = wc_get_price_to_display( $product, array(
229 + 'price' => $sale_price,
230 + ) );
231 + }
232 +
233 + $regular_price = wc_get_price_to_display( $product, array(
234 + 'price' => $product->get_regular_price(),
235 + ) );
236 +
237 + $price = wc_get_price_to_display( $product, array(
238 + 'price' => $product->get_price(),
239 + ) );
240 +
241 + ?>
242 + <div style="display:none;"
243 + class="tiered-pricing-fallback-data"
244 + data-product-id="<?php echo esc_attr( $product->get_id() ); ?>"
245 + data-price-rules="{}"
246 + data-minimum="<?php echo esc_attr( $priceRule->getMinimum( true ) ); ?>"
247 + data-product-name="<?php echo esc_attr( $product->get_name() ); ?>"
248 + data-regular-price="<?php echo esc_attr( $regular_price ); ?>"
249 + data-sale-price="<?php echo esc_attr( $sale_price ); ?>"
250 + data-price="<?php echo esc_attr( $price ); ?>"
251 + data-product-price-suffix="<?php echo esc_attr( $product->get_price_suffix() ); ?>"
252 + data-tiered-price="<?php echo esc_attr( $price ); ?>"
253 + data-tiered-price-exclude-taxes="<?php echo esc_attr( $price_excl_taxes ); ?>"
254 + data-tiered-price-include-taxes="<?php echo esc_attr( $price_incl_taxes ); ?>"
255 + ></div>
256 + <?php
204 257 }
205 258 }
206 259
260 + /**
261 + * Discount of a tier as the store shows it: a percentage, the amount saved per item, or both.
262 + *
263 + * @param float $percent Discount in percent, as the templates compute it.
264 + * @param PricingRule $pricingRule
265 + * @param WC_Product $product
266 + * @param int|null $quantity Tier quantity, or null for the base tier (its discount is the sale price).
267 + * @param array $settings Renderer settings.
268 + * @param string $style 'plain' ("10%", "$4.50", "10% ($4.50)") or 'off' ("10% off", "$4.50 off", "10% off, save $4.50").
269 + *
270 + * @return string HTML
271 + */
272 + public static function formatDiscount(
273 + float $percent,
274 + PricingRule $pricingRule,
275 + WC_Product $product,
276 + ?int $quantity,
277 + array $settings,
278 + string $style = 'plain'
279 + ): string {
280 + $format = $settings['discount_format'] ?? 'percentage';
281 + $format = in_array( $format, array( 'percentage', 'amount', 'both' ), true ) ? $format : 'percentage';
282 +
283 + $percentLabel = round( $percent, 2 ) . '%';
284 + $amountLabel = '';
285 +
286 + if ( 'percentage' !== $format ) {
287 + $saving = 0;
288 +
289 + if ( null === $quantity ) {
290 + if ( $product->is_on_sale() ) {
291 + $saving = (float) $product->get_regular_price() - (float) $product->get_sale_price();
292 + }
293 + } else {
294 + $base = CalculationLogic::calculateDiscountBasedOnRegularPrice() ? $product->get_regular_price() : $product->get_price();
295 + $saving = (float) $base - (float) $pricingRule->getTierPrice( $quantity, false );
296 + }
297 +
298 + $amountLabel = wc_price( wc_get_price_to_display( $product, array( 'price' => max( 0, $saving ) ) ) );
299 + }
300 +
301 + if ( 'off' === $style ) {
302 + switch ( $format ) {
303 + case 'amount':
304 + // translators: %s: amount saved per item
305 + return sprintf( __( '%s off', 'tier-pricing-table' ), $amountLabel );
306 + case 'both':
307 + // translators: 1: discount percentage, 2: amount saved per item
308 + return sprintf( __( '%1$s off, save %2$s', 'tier-pricing-table' ), $percentLabel, $amountLabel );
309 + default:
310 + // translators: %s: discount percentage
311 + return sprintf( __( '%s off', 'tier-pricing-table' ), $percentLabel );
312 + }
313 + }
314 +
315 + switch ( $format ) {
316 + case 'amount':
317 + return $amountLabel;
318 + case 'both':
319 + return $percentLabel . ' (' . $amountLabel . ')';
320 + default:
321 + return $percentLabel;
322 + }
323 + }
324 +
325 + /**
326 + * The "Spacing" and "Cell padding" options as a style attribute for a layout's root element: CSS
327 + * variables the stylesheet reads for the gap between tiers and the padding inside table cells.
328 + * Empty when neither option is set (each style keeps its own spacing and padding).
329 + */
330 + public static function layoutStyleAttribute( array $settings ): string {
331 + $variables = array();
332 +
333 + foreach ( array( 'layout_spacing' => '--tiered-pricing-gap', 'cell_padding' => '--tiered-pricing-cell-padding' ) as $option => $variable ) {
334 + $value = $settings[ $option ] ?? '';
335 +
336 + if ( '' === $value || null === $value || ! is_numeric( $value ) ) {
337 + continue;
338 + }
339 +
340 + $variables[] = $variable . ': ' . max( 0, min( 80, (int) $value ) ) . 'px';
341 + }
342 +
343 + return $variables ? ' style="' . esc_attr( implode( '; ', $variables ) ) . '"' : '';
344 + }
345 +
346 + /**
347 + * @deprecated 7.1.9 Use layoutStyleAttribute().
348 + */
349 + public static function spacingAttribute( array $settings ): string {
350 + return self::layoutStyleAttribute( $settings );
351 + }
352 +
353 + /**
354 + * Tiers the last rendered layout left out because of the "tiers per grid item" limit.
355 + */
356 + protected static $hiddenTiers = 0;
357 +
358 + /**
359 + * Tier rows in the configured order (ascending by quantity, or the biggest discount first), cut to
360 + * the "tiers per grid item" limit when one is set.
361 + *
362 + * @param string[] $rows Rendered rows, ascending.
363 + * @param array $settings Renderer settings.
364 + *
365 + * @return string HTML (the rows were escaped when rendered)
366 + */
367 + public static function orderTiers( array $rows, array $settings ): string {
368 + if ( 'desc' === ( $settings['tiers_order'] ?? 'asc' ) ) {
369 + $rows = array_reverse( $rows );
370 + }
371 +
372 + $limit = (int) ( $settings['tiers_limit'] ?? 0 );
373 + self::$hiddenTiers = 0;
374 +
375 + if ( $limit > 0 && count( $rows ) > $limit ) {
376 + self::$hiddenTiers = count( $rows ) - $limit;
377 + $rows = array_slice( $rows, 0, $limit );
378 + }
379 +
380 + return implode( '', $rows );
381 + }
382 +
383 + /**
384 + * The "+N more tiers" link to the product page, after a layout the limit shortened.
385 + */
386 + protected function renderMoreTiersLink( WC_Product $parentProduct ) {
387 + $hidden = self::$hiddenTiers;
388 + self::$hiddenTiers = 0;
389 +
390 + if ( $hidden < 1 ) {
391 + return;
392 + }
393 +
394 + /* translators: %d: number of tiers not listed */
395 + $label = sprintf( _n( '+%d more tier', '+%d more tiers', $hidden, 'tier-pricing-table' ), $hidden );
396 +
397 + echo '<a class="tiered-pricing-more-tiers" href="' . esc_url( $parentProduct->get_permalink() ) . '">' . esc_html( $label ) . '</a>';
398 + }
399 +
207 400 protected function getDefaultSettings( $productId = false ): array {
401 + $hasRules = true;
402 +
403 + if ( $productId ) {
404 + $product = wc_get_product( $productId );
405 + if ( $product ) {
406 + $hasRules = $this->productHasPricingRules( $product );
407 + }
408 + }
409 +
410 + $showTotalPrice = $hasRules ? $this->getContainer()->getSettings()->get( 'show_total_price',
411 + 'no' ) === 'yes' : $this->getContainer()->getSettings()->get( 'show_total_price_non_tiered',
412 + 'no' ) === 'yes';
413 +
208 414 $settings = array(
209 415 'display_context' => 'product-page',
210 - 'display' => $this->getContainer()->getSettings()->get( 'display', 'yes' ) === 'yes',
416 + 'display' => GeneralSection::isAutomaticDisplayEnabled(),
211 417 'display_type' => $this->getContainer()->getSettings()->get( 'display_type', 'table' ),
212 418 'title' => $this->getContainer()->getSettings()->get( 'table_title', '' ),
213 419 'table_class' => $this->getContainer()->getSettings()->get( 'table_css_class', '' ),
214 420 'quantity_column_title' => $this->getContainer()->getSettings()->get( 'head_quantity_text',
@@ -223,16 +429,25 @@
223 429 'clickable_rows' => $this->getContainer()->getSettings()->get( 'clickable_table_rows',
224 430 'yes' ) === 'yes',
225 431 'active_tier_color' => $this->getContainer()->getSettings()->get( 'selected_quantity_color',
226 432 '#3858e9' ),
433 + 'discount_format' => $this->getContainer()->getSettings()->get( 'discount_format', 'percentage' ),
434 + 'layout_spacing' => $this->getContainer()->getSettings()->get( 'layout_spacing', '' ),
435 + 'cell_padding' => $this->getContainer()->getSettings()->get( 'cell_padding', '' ),
436 + 'discount_badge_color' => $this->getContainer()->getSettings()->get( 'discount_badge_color', '' ),
437 + 'active_tier_border' => $this->getContainer()->getSettings()->get( 'table_active_border', 'yes' ),
438 + 'tiers_order' => $this->getContainer()->getSettings()->get( 'tiers_order', 'asc' ),
227 439 'tooltip_border' => $this->getContainer()->getSettings()->get( 'tooltip_border',
228 440 'yes' ) === 'yes',
229 441
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(),
442 + 'table_style' => GeneralSection::getPricingTableStyle(),
443 + 'compact_layout' => $this->getContainer()->getSettings()->get( 'compact_layout', 'no' ),
444 + 'blocks_style' => GeneralSection::getPricingBlocksStyle(),
445 + 'options_style' => GeneralSection::getPricingOptionsStyle(),
234 446
447 + 'dropdown_style' => GeneralSection::getPricingDropdownStyle(),
448 + 'plain_text_style' => GeneralSection::getPricingPlainTextStyle(),
449 +
235 450 'options_show_total' => GeneralSection::isShowOptionTotal(),
236 451 'options_show_original_product_price' => GeneralSection::isShowOriginalProductPrice(),
237 452 'options_show_default_option' => GeneralSection::isDefaultOptionEnabled(),
238 453
@@ -246,10 +461,9 @@
246 461 'update_price_on_product_page' => $this->getContainer()->getSettings()->get( 'update_price_on_product_page',
247 462 'yes' ) === 'yes',
248 463 'show_tiered_price_as_discount' => $this->getContainer()->getSettings()->get( 'show_tiered_price_as_discount',
249 464 'yes' ) === 'yes',
250 - 'show_total_price' => $this->getContainer()->getSettings()->get( 'show_total_price',
251 - 'no' ) === 'yes',
465 + 'show_total_price' => $showTotalPrice,
252 466 );
253 467
254 468 $default_quantity_measurement = array(
255 469 'singular' => '',
@@ -375,9 +589,10 @@
375 589 ?>
376 590
377 591 <div style="font-size: .8em;">
378 592 <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' ); ?>
593 + <?php esc_html_e( 'Pricing Conflict Detected: Your Minimum Order Quantity is equal to or greater than your first pricing tier.',
594 + 'tier-pricing-table' ); ?>
380 595 </div>
381 596 <div style="color: #555;
382 597 padding: 10px 12px;
383 598 border: 1px solid #b32c2e;
@@ -382,15 +597,19 @@
382 597 padding: 10px 12px;
383 598 border: 1px solid #b32c2e;
384 599 background: #fff5f5;">
385 600 <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' ); ?>
601 + <?php esc_html_e( 'To fix this, please ensure your first tiered pricing rule starts at a higher quantity than your Minimum Order Quantity.',
602 + 'tier-pricing-table' ); ?>
387 603 </p>
388 604 <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' ); ?>
605 + <strong><?php esc_html_e( 'Why?',
606 + '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.',
607 + 'tier-pricing-table' ); ?>
390 608 </p>
391 609 <br>
392 - <b><?php esc_html_e( 'This notice is only visible to store administrators.', 'tier-pricing-table' ); ?></b>
610 + <b><?php esc_html_e( 'This notice is only visible to store administrators.',
611 + 'tier-pricing-table' ); ?></b>
393 612 </div>
394 613 </div>
395 614 <?php
396 615 }