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 +255 -27 6.1.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>
@@ -152,8 +148,18 @@
152 148 if ( 'tooltip' === $displayType ) {
153 149 $templateType = 'table';
154 150 }
155 151
152 + if ( 'table' === $displayType ) {
153 + $style = $settings['table_style'];
154 +
155 + if ( $style !== 'default' ) {
156 + $templateType = 'table-' . $style;
157 + } else {
158 + $templateType = 'table';
159 + }
160 + }
161 +
156 162 if ( 'blocks' === $displayType ) {
157 163 $style = $settings['blocks_style'];
158 164
159 165 if ( $style !== 'default' ) {
@@ -162,9 +168,9 @@
162 168 $templateType = 'blocks';
163 169 }
164 170 }
165 171
166 - if( 'options' === $displayType ) {
172 + if ( 'options' === $displayType ) {
167 173 $style = $settings['options_style'];
168 174
169 175 if ( $style !== 'default' ) {
170 176 $templateType = 'options-' . $style;
@@ -172,8 +178,19 @@
172 178 $templateType = 'options';
173 179 }
174 180 }
175 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 +
176 193 $template = "tiered-pricing-$templateType.php";
177 194
178 195 do_action( 'tiered_pricing_table/before_rendering_tiered_pricing/inner', $priceRule, $product, $settings );
179 196
@@ -190,15 +207,214 @@
190 207 'settings' => $settings,
191 208 'pricing_type' => $priceRule->getType(),
192 209 'id' => $this->getUniqueTieredPricingId(),
193 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
194 257 }
195 258 }
196 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 +
197 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 +
198 414 $settings = array(
199 415 'display_context' => 'product-page',
200 - 'display' => $this->getContainer()->getSettings()->get( 'display', 'yes' ) === 'yes',
416 + 'display' => GeneralSection::isAutomaticDisplayEnabled(),
201 417 'display_type' => $this->getContainer()->getSettings()->get( 'display_type', 'table' ),
202 418 'title' => $this->getContainer()->getSettings()->get( 'table_title', '' ),
203 419 'table_class' => $this->getContainer()->getSettings()->get( 'table_css_class', '' ),
204 420 'quantity_column_title' => $this->getContainer()->getSettings()->get( 'head_quantity_text',
@@ -212,15 +428,26 @@
212 428 'yes' ) === 'yes',
213 429 'clickable_rows' => $this->getContainer()->getSettings()->get( 'clickable_table_rows',
214 430 'yes' ) === 'yes',
215 431 'active_tier_color' => $this->getContainer()->getSettings()->get( 'selected_quantity_color',
216 - '#96598A' ),
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' ),
217 439 'tooltip_border' => $this->getContainer()->getSettings()->get( 'tooltip_border',
218 440 'yes' ) === 'yes',
219 441
220 - 'blocks_style' => GeneralSection::getPricingBlocksStyle(),
221 - '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(),
222 446
447 + 'dropdown_style' => GeneralSection::getPricingDropdownStyle(),
448 + 'plain_text_style' => GeneralSection::getPricingPlainTextStyle(),
449 +
223 450 'options_show_total' => GeneralSection::isShowOptionTotal(),
224 451 'options_show_original_product_price' => GeneralSection::isShowOriginalProductPrice(),
225 452 'options_show_default_option' => GeneralSection::isDefaultOptionEnabled(),
226 453
@@ -234,10 +461,9 @@
234 461 'update_price_on_product_page' => $this->getContainer()->getSettings()->get( 'update_price_on_product_page',
235 462 'yes' ) === 'yes',
236 463 'show_tiered_price_as_discount' => $this->getContainer()->getSettings()->get( 'show_tiered_price_as_discount',
237 464 'yes' ) === 'yes',
238 - 'show_total_price' => $this->getContainer()->getSettings()->get( 'show_total_price',
239 - 'no' ) === 'yes',
465 + 'show_total_price' => $showTotalPrice,
240 466 );
241 467
242 468 $default_quantity_measurement = array(
243 469 'singular' => '',
@@ -363,25 +589,27 @@
363 589 ?>
364 590
365 591 <div style="font-size: .8em;">
366 592 <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.
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' ); ?>
369 595 </div>
370 596 <div style="color: #555;
371 597 padding: 10px 12px;
372 598 border: 1px solid #b32c2e;
373 599 background: #fff5f5;">
600 + <p style="padding: 0; margin: 0 0 10px 0;">
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' ); ?>
603 + </p>
374 604 <p style="padding: 0; margin: 0">
375 - The minimum order quantity must be less than the first tiered pricing rule quantity.
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' ); ?>
376 608 </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 609 <br>
383 - <b>This notice shown only for administrators.</b>
610 + <b><?php esc_html_e( 'This notice is only visible to store administrators.',
611 + 'tier-pricing-table' ); ?></b>
384 612 </div>
385 613 </div>
386 614 <?php
387 615 }