getDefaultSettings( $parentProductID ) );
// If the product is variable, but no specific variation is passed - check for default
if ( ! $variationID && TierPricingTablePlugin::isVariableProductSupported( $parentProduct ) ) {
$variationID = $this->getDefaultVariation( $parentProduct );
}
$ajaxVariationsThreshold = apply_filters( 'tiered_pricing_table/ajax_variations_threshold', 10 );
// Use variation if exists, otherwise the parent product is used
$productId = $variationID ? $variationID : $parentProduct->get_id();
$product = wc_get_product( $productId );
$supportedTypes = array_merge( TierPricingTablePlugin::getSupportedSimpleProductTypes(),
TierPricingTablePlugin::getSupportedVariableProductTypes() );
// Exit if product is not valid
if ( ! $product || ! in_array( $parentProduct->get_type(), $supportedTypes ) ) {
return;
}
$settings = apply_filters( 'tiered_pricing_table/display_settings', $settings, $productId );
if ( 'tooltip' === $settings['display_type'] ) {
wp_enqueue_script( 'jquery-ui-tooltip' );
}
$hidden = ( 'tooltip' === $settings['display_type'] || ! $settings['display'] );
do_action( 'tiered_pricing_table/before_rendering_tiered_pricing', $parentProduct, $variationID, $settings );
$loadVariationByAjax = true;
?>
get_children() ) > $ajaxVariationsThreshold;
$variableProductSamePrices = AdvanceOptionsForVariableProduct::isVariableProductSamePrices( $parentProduct->get_id() );
?>
data-load-variation-by-ajax=""
data-variable-product-same-prices=""
>
renderPricingTableHTML( $parentProduct, $product, $settings ); ?>
get_available_variations( 'objects' ) as $childProduct ) : ?>
renderPricingTableHTML( $parentProduct, $childProduct, $settings ); ?>
getDefaultSettings( $parentProduct->get_id() ) );
// Don't get rules from variable product when variation is empty
if ( in_array( $parentProduct->get_type(),
TierPricingTablePlugin::getSupportedVariableProductTypes() ) && ! ( $product instanceof WC_Product_Variation ) ) {
// Empty rule
$priceRule = new PricingRule( $product->get_id() );
} else {
$priceRule = PriceManager::getPricingRule( $product->get_id() );
}
// If rules are not empty
if ( ! empty( $priceRule->getRules() ) ) {
$displayType = $settings['display_type'];
$availableTemplates = TierPricingTablePlugin::getAvailablePricingLayouts();
$templateType = array_key_exists( $settings['display_type'], $availableTemplates ) ? $displayType : 'table';
if ( 'tooltip' === $displayType ) {
$templateType = 'table';
}
if ( 'table' === $displayType ) {
$style = $settings['table_style'];
if ( $style !== 'default' ) {
$templateType = 'table-' . $style;
} else {
$templateType = 'table';
}
}
if ( 'blocks' === $displayType ) {
$style = $settings['blocks_style'];
if ( $style !== 'default' ) {
$templateType = 'blocks-' . $style;
} else {
$templateType = 'blocks';
}
}
if ( 'options' === $displayType ) {
$style = $settings['options_style'];
if ( $style !== 'default' ) {
$templateType = 'options-' . $style;
} else {
$templateType = 'options';
}
}
// layouts with a single style option: dropdown, plain text
$styledLayouts = array(
'dropdown' => 'dropdown_style',
'plain-text' => 'plain_text_style',
);
if ( isset( $styledLayouts[ $displayType ] ) ) {
$style = $settings[ $styledLayouts[ $displayType ] ] ?? 'default';
$templateType = 'default' !== $style && '' !== $style ? $displayType . '-' . $style : $displayType;
}
$template = "tiered-pricing-$templateType.php";
do_action( 'tiered_pricing_table/before_rendering_tiered_pricing/inner', $priceRule, $product, $settings );
$this->checkForDuplicateQuantities( $priceRule );
$this->getContainer()->getFileManager()->includeTemplate( 'frontend/' . $template, array(
'pricing_rule' => $priceRule,
'price_rules' => $priceRule->getRules(),
'real_price' => $product->get_price(),
'product_name' => $product->get_name(),
'product_id' => $product->get_id(),
'product' => $product,
'minimum' => $priceRule->getMinimum( true ),
'settings' => $settings,
'pricing_type' => $priceRule->getType(),
'id' => $this->getUniqueTieredPricingId(),
) );
$this->renderMoreTiersLink( $parentProduct );
} else {
// Provide fallback data for products without pricing rules,
// which allows the frontend JS (e.g. dynamic totals, you save, etc.)
// to function for all products via the 'tiered_price_update' event.
$price_incl_taxes = wc_get_price_including_tax( $product, array(
'price' => $product->get_price(),
) );
$price_excl_taxes = wc_get_price_excluding_tax( $product, array(
'price' => $product->get_price(),
) );
$sale_price = $product->get_sale_price();
if ( $sale_price ) {
$sale_price = wc_get_price_to_display( $product, array(
'price' => $sale_price,
) );
}
$regular_price = wc_get_price_to_display( $product, array(
'price' => $product->get_regular_price(),
) );
$price = wc_get_price_to_display( $product, array(
'price' => $product->get_price(),
) );
?>
is_on_sale() ) {
$saving = (float) $product->get_regular_price() - (float) $product->get_sale_price();
}
} else {
$base = CalculationLogic::calculateDiscountBasedOnRegularPrice() ? $product->get_regular_price() : $product->get_price();
$saving = (float) $base - (float) $pricingRule->getTierPrice( $quantity, false );
}
$amountLabel = wc_price( wc_get_price_to_display( $product, array( 'price' => max( 0, $saving ) ) ) );
}
if ( 'off' === $style ) {
switch ( $format ) {
case 'amount':
// translators: %s: amount saved per item
return sprintf( __( '%s off', 'tier-pricing-table' ), $amountLabel );
case 'both':
// translators: 1: discount percentage, 2: amount saved per item
return sprintf( __( '%1$s off, save %2$s', 'tier-pricing-table' ), $percentLabel, $amountLabel );
default:
// translators: %s: discount percentage
return sprintf( __( '%s off', 'tier-pricing-table' ), $percentLabel );
}
}
switch ( $format ) {
case 'amount':
return $amountLabel;
case 'both':
return $percentLabel . ' (' . $amountLabel . ')';
default:
return $percentLabel;
}
}
/**
* The "Spacing" and "Cell padding" options as a style attribute for a layout's root element: CSS
* variables the stylesheet reads for the gap between tiers and the padding inside table cells.
* Empty when neither option is set (each style keeps its own spacing and padding).
*/
public static function layoutStyleAttribute( array $settings ): string {
$variables = array();
foreach ( array( 'layout_spacing' => '--tiered-pricing-gap', 'cell_padding' => '--tiered-pricing-cell-padding' ) as $option => $variable ) {
$value = $settings[ $option ] ?? '';
if ( '' === $value || null === $value || ! is_numeric( $value ) ) {
continue;
}
$variables[] = $variable . ': ' . max( 0, min( 80, (int) $value ) ) . 'px';
}
return $variables ? ' style="' . esc_attr( implode( '; ', $variables ) ) . '"' : '';
}
/**
* @deprecated 7.1.9 Use layoutStyleAttribute().
*/
public static function spacingAttribute( array $settings ): string {
return self::layoutStyleAttribute( $settings );
}
/**
* Tiers the last rendered layout left out because of the "tiers per grid item" limit.
*/
protected static $hiddenTiers = 0;
/**
* Tier rows in the configured order (ascending by quantity, or the biggest discount first), cut to
* the "tiers per grid item" limit when one is set.
*
* @param string[] $rows Rendered rows, ascending.
* @param array $settings Renderer settings.
*
* @return string HTML (the rows were escaped when rendered)
*/
public static function orderTiers( array $rows, array $settings ): string {
if ( 'desc' === ( $settings['tiers_order'] ?? 'asc' ) ) {
$rows = array_reverse( $rows );
}
$limit = (int) ( $settings['tiers_limit'] ?? 0 );
self::$hiddenTiers = 0;
if ( $limit > 0 && count( $rows ) > $limit ) {
self::$hiddenTiers = count( $rows ) - $limit;
$rows = array_slice( $rows, 0, $limit );
}
return implode( '', $rows );
}
/**
* The "+N more tiers" link to the product page, after a layout the limit shortened.
*/
protected function renderMoreTiersLink( WC_Product $parentProduct ) {
$hidden = self::$hiddenTiers;
self::$hiddenTiers = 0;
if ( $hidden < 1 ) {
return;
}
/* translators: %d: number of tiers not listed */
$label = sprintf( _n( '+%d more tier', '+%d more tiers', $hidden, 'tier-pricing-table' ), $hidden );
echo '' . esc_html( $label ) . '';
}
protected function getDefaultSettings( $productId = false ): array {
$hasRules = true;
if ( $productId ) {
$product = wc_get_product( $productId );
if ( $product ) {
$hasRules = $this->productHasPricingRules( $product );
}
}
$showTotalPrice = $hasRules ? $this->getContainer()->getSettings()->get( 'show_total_price',
'no' ) === 'yes' : $this->getContainer()->getSettings()->get( 'show_total_price_non_tiered',
'no' ) === 'yes';
$settings = array(
'display_context' => 'product-page',
'display' => GeneralSection::isAutomaticDisplayEnabled(),
'display_type' => $this->getContainer()->getSettings()->get( 'display_type', 'table' ),
'title' => $this->getContainer()->getSettings()->get( 'table_title', '' ),
'table_class' => $this->getContainer()->getSettings()->get( 'table_css_class', '' ),
'quantity_column_title' => $this->getContainer()->getSettings()->get( 'head_quantity_text',
__( 'Quantity', 'tier-pricing-table' ) ),
'price_column_title' => $this->getContainer()->getSettings()->get( 'head_price_text',
__( 'Price', 'tier-pricing-table' ) ),
'discount_column_title' => $this->getContainer()->getSettings()->get( 'head_discount_text',
__( 'Discount (%)', 'tier-pricing-table' ) ),
'quantity_type' => $this->getContainer()->getSettings()->get( 'quantity_type', 'range' ),
'show_discount_column' => $this->getContainer()->getSettings()->get( 'show_discount_column',
'yes' ) === 'yes',
'clickable_rows' => $this->getContainer()->getSettings()->get( 'clickable_table_rows',
'yes' ) === 'yes',
'active_tier_color' => $this->getContainer()->getSettings()->get( 'selected_quantity_color',
'#3858e9' ),
'discount_format' => $this->getContainer()->getSettings()->get( 'discount_format', 'percentage' ),
'layout_spacing' => $this->getContainer()->getSettings()->get( 'layout_spacing', '' ),
'cell_padding' => $this->getContainer()->getSettings()->get( 'cell_padding', '' ),
'discount_badge_color' => $this->getContainer()->getSettings()->get( 'discount_badge_color', '' ),
'active_tier_border' => $this->getContainer()->getSettings()->get( 'table_active_border', 'yes' ),
'tiers_order' => $this->getContainer()->getSettings()->get( 'tiers_order', 'asc' ),
'tooltip_border' => $this->getContainer()->getSettings()->get( 'tooltip_border',
'yes' ) === 'yes',
'table_style' => GeneralSection::getPricingTableStyle(),
'compact_layout' => $this->getContainer()->getSettings()->get( 'compact_layout', 'no' ),
'blocks_style' => GeneralSection::getPricingBlocksStyle(),
'options_style' => GeneralSection::getPricingOptionsStyle(),
'dropdown_style' => GeneralSection::getPricingDropdownStyle(),
'plain_text_style' => GeneralSection::getPricingPlainTextStyle(),
'options_show_total' => GeneralSection::isShowOptionTotal(),
'options_show_original_product_price' => GeneralSection::isShowOriginalProductPrice(),
'options_show_default_option' => GeneralSection::isDefaultOptionEnabled(),
'options_default_option_text' => GeneralSection::getDefaultOptionText(),
'options_option_text' => GeneralSection::getOptionText(),
'plain_text_show_default_option' => GeneralSection::isPlainTextFirstTierEnabled(),
'plain_text_option_text' => GeneralSection::getPlainTextTemplate(),
'plain_text_default_option_text' => GeneralSection::getPlainTextFirstTierTemplate(),
'update_price_on_product_page' => $this->getContainer()->getSettings()->get( 'update_price_on_product_page',
'yes' ) === 'yes',
'show_tiered_price_as_discount' => $this->getContainer()->getSettings()->get( 'show_tiered_price_as_discount',
'yes' ) === 'yes',
'show_total_price' => $showTotalPrice,
);
$default_quantity_measurement = array(
'singular' => '',
'plural' => '',
);
$quantity_measurement = $default_quantity_measurement;
if ( in_array( $settings['display_type'], array( 'table', 'horizontal-table', 'tooltip' ) ) ) {
$quantity_measurement = $this->getContainer()->getSettings()->get( 'table_quantity_measurement',
$default_quantity_measurement );
}
if ( 'blocks' === $settings['display_type'] ) {
$quantity_measurement = $this->getContainer()->getSettings()->get( 'blocks_quantity_measurement', array(
'singular' => _n( 'piece', 'pieces', 1, 'tier-pricing-table' ),
'plural' => _n( 'piece', 'pieces', 2, 'tier-pricing-table' ),
) );
}
$settings['quantity_measurement_singular'] = $quantity_measurement['singular'];
$settings['quantity_measurement_plural'] = $quantity_measurement['plural'];
if ( $productId ) {
$template = TieredPricingTab::getProductTemplate( $productId );
if ( 'default' !== $template ) {
$settings['display_type'] = $template;
}
$baseUnitName = TieredPricingTab::getProductBaseUnitName( $productId );
if ( $baseUnitName['singular'] && $baseUnitName['plural'] ) {
$settings['quantity_measurement_singular'] = $baseUnitName['singular'];
$settings['quantity_measurement_plural'] = $baseUnitName['plural'];
}
}
return $settings;
}
protected function getUniqueTieredPricingId() {
return preg_replace( '/[0-9]+/', '', strtolower( wp_generate_password( 20, false ) ) );
}
protected function getDefaultVariation( WC_Product_Variable $product ): ?int {
$defaultVariation = AdvanceOptionsForVariableProduct::getDefaultVariation( $product->get_id() );
if ( $defaultVariation ) {
return $defaultVariation->get_id();
}
$defaultAttributes = $product->get_default_attributes();
if ( ! empty( $defaultAttributes ) ) {
$defaultAttributesKeys = array_map( function ( $attribute ) {
return 'attribute_' . $attribute;
}, array_keys( $defaultAttributes ) );
$defaultAttributes = array_combine( $defaultAttributesKeys, $defaultAttributes );
return ( new WC_Product_Data_Store_CPT() )->find_matching_product_variation( $product, $defaultAttributes );
}
return null;
}
public function productHasPricingRules( WC_Product $product ): bool {
if ( TierPricingTablePlugin::isSimpleProductSupported( $product ) ) {
$pricingRule = PriceManager::getPricingRule( $product->get_id() );
return ! empty( $pricingRule->getRules() );
}
if ( TierPricingTablePlugin::isVariableProductSupported( $product ) && $product instanceof WC_Product_Variable ) {
// Do not check if product has tiered pricing for variable product by default.
// If a variable product has many variations, it can take many resources to check every variation for rules existing.
// The downside is that the plugin will send an AJAX request for each variant selection on the product page even if a product does not have any tiered pricing
if ( ! apply_filters( 'tiered_pricing_table/check_if_variable_product_has_rules', false, $product ) ) {
return true;
}
$hasTieredPricing = $this->getContainer()->getCache()->getProductData( $product, 'product_has_rules' );
if ( 'no' === $hasTieredPricing ) {
return false;
}
if ( 'yes' === $hasTieredPricing ) {
return true;
}
foreach ( $product->get_available_variations() as $productVariation ) {
$pricingRule = PriceManager::getPricingRule( $productVariation['variation_id'] );
if ( ! empty( $pricingRule->getRules() ) ) {
$this->getContainer()->getCache()->setProductData( $product, 'product_has_rules', 'yes' );
return true;
}
}
$this->getContainer()->getCache()->setProductData( $product, 'product_has_rules', 'no' );
}
return false;
}
public function checkForDuplicateQuantities( PricingRule $pricingRule ) {
// Show notice only for admin users
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$minimum = $pricingRule->getMinimum( true );
$pricingRules = $pricingRule->getRules();
$firstRuleQuantity = ! empty( $pricingRules ) ? array_keys( $pricingRules )[0] : false;
if ( $firstRuleQuantity && $firstRuleQuantity <= $minimum ) {
?>