woocommerce_wpml = $woocommerce_wpml; $this->wpdb = $wpdb; } public function add_hooks() { add_filter( 'init', [ $this, 'custom_prices_init' ] ); } public function custom_prices_init() { if ( is_admin() ) { if ( isStandAlone() ) { // In the full mode, this is done in the product sync logic. add_action( 'save_post_product', [ $this, 'save_custom_prices' ] ); add_action( 'save_post_product_variation', [ $this, 'sync_product_variations_custom_prices' ] ); add_action( 'woocommerce_ajax_save_product_variations', [ $this, 'sync_product_variations_custom_prices_on_ajax' ] ); } add_action( 'woocommerce_variation_options', [ $this, 'add_individual_variation_nonce' ], 10, 3 ); // custom prices for different currencies for products/variations [BACKEND]. add_action( 'woocommerce_product_options_pricing', [ $this, 'woocommerce_product_options_custom_pricing' ] ); add_action( 'woocommerce_product_after_variable_attributes', [ $this, 'woocommerce_product_after_variable_attributes_custom_pricing' ], 10, 3 ); } else { add_filter( 'woocommerce_product_is_on_sale', [ $this, 'filter_product_is_on_sale' ], 10, 2 ); } add_action( 'woocommerce_variation_is_visible', [ $this, 'filter_product_variations_with_custom_prices' ], 10, 2 ); add_filter( 'loop_shop_post_in', [ $this, 'filter_products_with_custom_prices' ], 100 ); add_filter( 'woocommerce_is_purchasable', [ $this, 'check_product_with_custom_prices' ], 10, 2 ); add_action( 'wc_after_products_starting_sales', [ $this, 'maybe_set_sale_prices' ] ); add_action( 'wc_after_products_ending_sales', [ $this, 'maybe_remove_sale_prices' ] ); } public function add_individual_variation_nonce( $loop, $variation_data, $variation ) { wp_nonce_field( 'wcml_save_custom_prices_variation_' . $variation->ID, '_wcml_custom_prices_variation_' . $variation->ID . '_nonce' ); } /** * @param int $product_id * @param bool $currency * * @return array|false */ public function get_product_custom_prices( $product_id, $currency = false ) { if ( empty( $currency ) ) { $currency = $this->woocommerce_wpml->multi_currency->get_client_currency(); } if ( wcml_get_woocommerce_currency_option() === $currency ) { return false; } $cache_key = $product_id . '_' . $currency; $cache_group = 'product_custom_prices'; $cache_found = false; $cache_custom_prices = wp_cache_get( $cache_key, $cache_group, false, $cache_found ); if ( $cache_found ) { return $cache_custom_prices; } $product_meta = get_post_custom( $this->woocommerce_wpml->products->get_original_product_id( $product_id ) ); $custom_prices = []; if ( ! empty( $product_meta['_wcml_custom_prices_status'][0] ) ) { foreach ( wcml_price_custom_fields( $product_id ) as $key ) { if ( isset( $product_meta[ $key . '_' . $currency ][0] ) ) { $custom_prices[ $key ] = $product_meta[ $key . '_' . $currency ][0]; } } } if ( ! isset( $custom_prices['_price'] ) ) { return false; } $current__price_value = $custom_prices['_price']; if ( $this->is_date_range_set( $product_meta, $currency ) && ! $this->is_on_sale_date_range( $product_meta, $currency ) ) { $custom_prices['_sale_price'] = ''; $custom_prices['_price'] = $custom_prices['_regular_price']; } if ( $custom_prices['_price'] !== $current__price_value ) { update_post_meta( $product_id, '_price_' . $currency, $custom_prices['_price'] ); } // detemine min/max variation prices. if ( ! empty( $product_meta['_min_variation_price'] ) ) { static $product_min_max_prices = []; if ( ! isset( $product_min_max_prices[ $product_id ] ) && 'product' === get_post_type( $product_id ) ) { $product_min_max_prices[ $product_id ] = []; // get variation ids. $variation_ids = $this->wpdb->get_col( $this->wpdb->prepare( "SELECT ID FROM {$this->wpdb->posts} WHERE post_parent = %d", $product_id ) ); // get all prices for the above variations. $rows = $this->wpdb->get_results( "SELECT post_id, meta_key, meta_value FROM {$this->wpdb->postmeta} WHERE meta_key IN ('_price', '_regular_price', '_sale_price', '_price_$currency', '_regular_price_$currency', '_sale_price_$currency') AND post_id IN (" . DB::prepareIn( $variation_ids, '%d' ) . ')' ); // $extractPricesByType :: array, string => array $extractPricesByType = function( $rows, $key ) use ( $currency, $variation_ids ) { $prices = wp_list_pluck( wp_list_filter( $rows, [ 'meta_key' => $key . '_' . $currency ] ), $key . '_' . $currency, 'post_id' ); $defaults = wp_list_pluck( wp_list_filter( $rows, [ 'meta_key' => $key ] ), $key, 'post_id' ); // calculate missing prices automatically. foreach ( $variation_ids as $id ) { if ( empty( $prices[ $id ] ) && isset( $defaults[ $id ] ) ) { $prices[ $id ] = apply_filters( 'wcml_raw_price_amount', $defaults[ $id ] ); } } return $prices; }; $regular_prices = $extractPricesByType( $rows, '_regular_price' ); $sale_prices = $extractPricesByType( $rows, '_sale_price' ); $prices = $extractPricesByType( $rows, '_price' ); if ( ! empty( $regular_prices ) ) { $product_min_max_prices[ $product_id ]['_min_variation_regular_price'] = min( $regular_prices ); $product_min_max_prices[ $product_id ]['_max_variation_regular_price'] = max( $regular_prices ); } if ( ! empty( $sale_prices ) ) { $product_min_max_prices[ $product_id ]['_min_variation_sale_price'] = min( $sale_prices ); $product_min_max_prices[ $product_id ]['_max_variation_sale_price'] = max( $sale_prices ); } if ( ! empty( $prices ) ) { $product_min_max_prices[ $product_id ]['_min_variation_price'] = min( $prices ); $product_min_max_prices[ $product_id ]['_max_variation_price'] = max( $prices ); } } if ( isset( $product_min_max_prices[ $product_id ]['_min_variation_regular_price'] ) ) { $custom_prices['_min_variation_regular_price'] = $product_min_max_prices[ $product_id ]['_min_variation_regular_price']; } if ( isset( $product_min_max_prices[ $product_id ]['_max_variation_regular_price'] ) ) { $custom_prices['_max_variation_regular_price'] = $product_min_max_prices[ $product_id ]['_max_variation_regular_price']; } if ( isset( $product_min_max_prices[ $product_id ]['_min_variation_sale_price'] ) ) { $custom_prices['_min_variation_sale_price'] = $product_min_max_prices[ $product_id ]['_min_variation_sale_price']; } if ( isset( $product_min_max_prices[ $product_id ]['_max_variation_sale_price'] ) ) { $custom_prices['_max_variation_sale_price'] = $product_min_max_prices[ $product_id ]['_max_variation_sale_price']; } if ( isset( $product_min_max_prices[ $product_id ]['_min_variation_price'] ) ) { $custom_prices['_min_variation_price'] = $product_min_max_prices[ $product_id ]['_min_variation_price']; } if ( isset( $product_min_max_prices[ $product_id ]['_max_variation_price'] ) ) { $custom_prices['_max_variation_price'] = $product_min_max_prices[ $product_id ]['_max_variation_price']; } } $custom_prices = apply_filters( 'wcml_product_custom_prices', $custom_prices, $product_id, $currency ); wp_cache_set( $cache_key, $custom_prices, $cache_group ); return $custom_prices; } private function is_date_range_set( $product_meta, $currency ) { $get_currency_schedule = function ( $suffix ) use ( $product_meta ) { return Obj::path( [ '_sale_price_dates_from' . $suffix, 0 ], $product_meta ) || Obj::path( [ '_sale_price_dates_to' . $suffix, 0 ], $product_meta ); }; return $get_currency_schedule( '' ) || $get_currency_schedule( "_$currency" ); } private function is_on_sale_date_range( $product_meta, $currency ) { if ( isset( $product_meta[ '_sale_price_dates_from_' . $currency ] ) && isset( $product_meta[ '_sale_price_dates_to_' . $currency ] ) && $product_meta[ '_sale_price_dates_from_' . $currency ][0] && $product_meta[ '_sale_price_dates_to_' . $currency ][0] ) { if ( current_time( 'timestamp' ) > $product_meta[ '_sale_price_dates_from_' . $currency ][0] && current_time( 'timestamp' ) < $product_meta[ '_sale_price_dates_to_' . $currency ][0] ) { return true; } } elseif ( isset( $product_meta['_sale_price_dates_from'] ) && isset( $product_meta['_sale_price_dates_to'] ) && current_time( 'timestamp' ) > $product_meta['_sale_price_dates_from'][0] && current_time( 'timestamp' ) < $product_meta['_sale_price_dates_to'][0] ) { return true; } return false; } public function woocommerce_product_options_custom_pricing() { global $pagenow; $this->load_custom_prices_js_css(); if ( ( isset( $_GET['post'] ) && ( get_post_type( $_GET['post'] ) !== 'product' || ! $this->woocommerce_wpml->products->is_original_product( $_GET['post'] ) ) ) || ( isset( $_GET['post_type'] ) && $_GET['post_type'] === 'product' && isset( $_GET['source_lang'] ) ) ) { return; } $product_id = 'new'; if ( $pagenow === 'post.php' && isset( $_GET['post'] ) && get_post_type( $_GET['post'] ) == 'product' ) { $product_id = $_GET['post']; } $this->custom_pricing_output( $product_id ); do_action( 'wcml_after_custom_prices_block', $product_id ); wp_nonce_field( 'wcml_save_custom_prices', '_wcml_custom_prices_nonce' ); } public function woocommerce_product_after_variable_attributes_custom_pricing( $loop, $variation_data, $variation ) { if ( $this->woocommerce_wpml->products->is_original_product( $variation->post_parent ) ) { echo ''; $this->custom_pricing_output( $variation->ID ); echo ''; } } private function load_custom_prices_js_css() { wp_register_style( 'wpml-wcml-prices', WCML_PLUGIN_URL . '/res/css/wcml-prices.css', null, WCML_VERSION ); wp_register_script( 'wcml-tm-scripts-prices', WCML_PLUGIN_URL . '/res/js/prices' . WCML_JS_MIN . '.js', [ 'jquery' ], WCML_VERSION, true ); wp_enqueue_style( 'wpml-wcml-prices' ); wp_enqueue_script( 'wcml-tm-scripts-prices' ); } private function custom_pricing_output( $post_id = false ) { $custom_prices_ui = new WCML_Custom_Prices_UI( $this->woocommerce_wpml, $post_id ); $custom_prices_ui->show(); } public function check_product_with_custom_prices( $is_purchasable, WC_Product $product ) { if ( $this->is_filtering_products_with_custom_prices_enabled() && is_product() ) { $original_id = $this->woocommerce_wpml->products->get_original_product_id( $product->get_id() ); if ( ! $this->is_custom_prices_set_for_product( $original_id ) ) { return false; } } return $is_purchasable; } public function filter_product_variations_with_custom_prices( $is_visible, $variation_id ) { if ( $this->is_filtering_products_with_custom_prices_enabled() && is_product() ) { $orig_child_id = $this->woocommerce_wpml->products->get_original_product_id( $variation_id ); if ( ! $this->is_custom_prices_set_for_product( $orig_child_id ) ) { return false; } } return $is_visible; } // display products with custom prices only if enabled "Show only products with custom prices in secondary currencies" option on settings page. public function filter_products_with_custom_prices( $filtered_posts ) { if ( $this->is_filtering_products_with_custom_prices_enabled() ) { $matched_products = []; $matched_products_query = $this->wpdb->get_results( " SELECT DISTINCT ID, post_parent, post_type FROM {$this->wpdb->posts} INNER JOIN {$this->wpdb->postmeta} ON ID = post_id WHERE post_type IN ( 'product', 'product_variation' ) AND post_status = 'publish' AND meta_key = '_wcml_custom_prices_status' AND meta_value = 1 ", OBJECT_K ); if ( $matched_products_query ) { $client_currency = $this->woocommerce_wpml->multi_currency->get_client_currency(); remove_filter( 'get_post_metadata', [ $this->woocommerce_wpml->multi_currency->prices, 'product_price_filter' ], 10 ); foreach ( $matched_products_query as $product ) { if ( ! get_post_meta( $product->ID, '_price_' . $client_currency, true ) ) { continue; } if ( $product->post_type == 'product' ) { $matched_products[] = apply_filters( 'translate_object_id', $product->ID, 'product', true ); } if ( $product->post_parent > 0 && ! in_array( $product->post_parent, $matched_products ) ) { $matched_products[] = apply_filters( 'translate_object_id', $product->post_parent, get_post_type( $product->post_parent ), true ); } } add_filter( 'get_post_metadata', [ $this->woocommerce_wpml->multi_currency->prices, 'product_price_filter' ], 10, 4 ); } // Filter the id's. if ( sizeof( $filtered_posts ) == 0 ) { $filtered_posts = $matched_products; $filtered_posts[] = 0; } else { $filtered_posts = array_intersect( $filtered_posts, $matched_products ); $filtered_posts[] = 0; } } return $filtered_posts; } /** * @return bool */ private function is_filtering_products_with_custom_prices_enabled() { return wcml_is_multi_currency_on() && isset( $this->woocommerce_wpml->settings['display_custom_prices'] ) && $this->woocommerce_wpml->settings['display_custom_prices'] && $this->woocommerce_wpml->multi_currency->get_client_currency() !== wcml_get_woocommerce_currency_option(); } public function save_custom_prices( $post_id ) { $nonce = filter_input( INPUT_POST, '_wcml_custom_prices_nonce', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); if ( isset( $_POST['_wcml_custom_prices'] ) && isset( $nonce ) && wp_verify_nonce( $nonce, 'wcml_save_custom_prices' ) && ! $this->woocommerce_wpml->products->is_variable_product( $post_id ) ) { if ( isset( $_POST['_wcml_custom_prices'][ $post_id ] ) || isset( $_POST['_wcml_custom_prices']['new'] ) ) { $wcml_custom_prices_option = isset( $_POST['_wcml_custom_prices'][ $post_id ] ) ? $_POST['_wcml_custom_prices'][ $post_id ] : $_POST['_wcml_custom_prices']['new']; } else { $current_option = get_post_meta( $post_id, '_wcml_custom_prices_status', true ); $wcml_custom_prices_option = $current_option ? $current_option : 0; } update_post_meta( $post_id, '_wcml_custom_prices_status', $wcml_custom_prices_option ); if ( $wcml_custom_prices_option == 1 ) { $currencies = $this->woocommerce_wpml->multi_currency->get_currencies(); foreach ( $currencies as $code => $currency ) { $sale_price = wc_format_decimal( $_POST['_custom_sale_price'][ $code ] ); $regular_price = wc_format_decimal( $_POST['_custom_regular_price'][ $code ] ); $schedule = $_POST['_wcml_schedule'][ $code ]; $date_from = $schedule && isset( $_POST['_custom_sale_price_dates_from'][ $code ] ) ? strtotime( $_POST['_custom_sale_price_dates_from'][ $code ] ) : ''; $date_to = $schedule && isset( $_POST['_custom_sale_price_dates_to'][ $code ] ) ? strtotime( $_POST['_custom_sale_price_dates_to'][ $code ] ) : ''; $custom_prices = apply_filters( 'wcml_update_custom_prices_values', [ '_regular_price' => $regular_price, '_sale_price' => $sale_price, '_wcml_schedule' => $schedule, '_sale_price_dates_from' => $date_from, '_sale_price_dates_to' => $date_to, ], $code, $post_id ); $product_price = $this->update_custom_prices( $post_id, $custom_prices, $code ); do_action( 'wcml_after_save_custom_prices', $post_id, $product_price, $custom_prices, $code ); } } } } public function update_custom_prices( $post_id, $custom_prices, $code ) { $price = null; $defaults = [ '_sale_price_dates_to' => '', '_sale_price_dates_from' => '', '_sale_price' => '', ]; $custom_prices = array_merge( $defaults, $custom_prices ); foreach ( $custom_prices as $custom_price_key => $custom_price_value ) { update_post_meta( $post_id, $custom_price_key . '_' . $code, $custom_price_value ); } if ( $this->is_sale_price_valid( $custom_prices ) ) { $price = stripslashes( $custom_prices['_sale_price'] ); } else { $price = stripslashes( $custom_prices['_regular_price'] ); $this->validate_and_update_sale_price_dates( $post_id, $code, $custom_prices ); } update_post_meta( $post_id, '_price_' . $code, $price ? $price : null ); return $price; } /** * @param int $post_id * @param string $code * @param array $custom_prices */ private function validate_and_update_sale_price_dates( $post_id, $code, array $custom_prices ) { $date_from = $custom_prices['_sale_price_dates_from']; $date_to = $custom_prices['_sale_price_dates_to']; if ( $date_to ) { if ( $date_to < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { update_post_meta( $post_id, '_sale_price_dates_from_' . $code, '' ); update_post_meta( $post_id, '_sale_price_dates_to_' . $code, '' ); } elseif ( ! $date_from ) { update_post_meta( $post_id, '_sale_price_dates_from_' . $code, strtotime( 'NOW', current_time( 'timestamp' ) ) ); } } } /** * @param array $custom_prices * * @return bool */ protected function is_sale_price_valid( array $custom_prices ) { $sale_price_dates_from = $custom_prices['_sale_price_dates_from']; $sale_price_dates_to = $custom_prices['_sale_price_dates_to']; $not_depend_on_date = $sale_price_dates_to === '' && $sale_price_dates_from === ''; $valid_sale_date = $sale_price_dates_from < strtotime( 'NOW', current_time( 'timestamp' ) ); return $custom_prices['_sale_price'] !== '' && ( $not_depend_on_date || $valid_sale_date ); } /** * @param int $product_id */ public function sync_product_variations_custom_prices_on_ajax( $product_id ) { Maybe::fromNullable( wc_get_product( $product_id ) ) ->map( invoke( 'get_children' ) ) ->map( Fns::map( [ $this, 'sync_product_variations_custom_prices' ] ) ); } /** * @param int $product_id */ public function sync_product_variations_custom_prices( $product_id ) { if ( isset( $_POST['_wcml_custom_prices'][ $product_id ] ) ) { // save custom prices for variation. $nonce = filter_input( INPUT_POST, '_wcml_custom_prices_variation_' . $product_id . '_nonce', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); if ( isset( $_POST['_wcml_custom_prices'][ $product_id ] ) && isset( $nonce ) && wp_verify_nonce( $nonce, 'wcml_save_custom_prices_variation_' . $product_id ) ) { update_post_meta( $product_id, '_wcml_custom_prices_status', $_POST['_wcml_custom_prices'][ $product_id ] ); $currencies = $this->woocommerce_wpml->multi_currency->get_currencies(); if ( $_POST['_wcml_custom_prices'][ $product_id ] == 1 ) { foreach ( $currencies as $code => $currency ) { $sale_price = wc_format_decimal( $_POST['_custom_variation_sale_price'][ $code ][ $product_id ] ); $regular_price = wc_format_decimal( $_POST['_custom_variation_regular_price'][ $code ][ $product_id ] ); $date_from = strtotime( $_POST['_custom_variation_sale_price_dates_from'][ $code ][ $product_id ] ); $date_to = strtotime( $_POST['_custom_variation_sale_price_dates_to'][ $code ][ $product_id ] ); $schedule = $_POST['_wcml_schedule'][ $code ][ $product_id ]; $custom_prices = apply_filters( 'wcml_update_custom_prices_values', [ '_regular_price' => $regular_price, '_sale_price' => $sale_price, '_wcml_schedule' => $schedule, '_sale_price_dates_from' => $date_from, '_sale_price_dates_to' => $date_to, ], $code, $product_id ); $price = $this->update_custom_prices( $product_id, $custom_prices, $code ); } } } } } /** * @param WC_Product $product_object * * @return bool */ private function is_on_sale( $product_object ) { $custom_prices = $this->get_product_custom_prices( $product_object->get_id() ); return $custom_prices && array_key_exists( '_sale_price', $custom_prices ) && array_key_exists( '_regular_price', $custom_prices ) && '' !== $custom_prices['_sale_price'] && $custom_prices['_sale_price'] !== $custom_prices['_regular_price']; } /** * @param bool $on_sale * @param WC_Product $product_object * * @return bool */ public function filter_product_is_on_sale( $on_sale, $product_object ) { if ( ! $on_sale && WCML_MULTI_CURRENCIES_INDEPENDENT === $this->woocommerce_wpml->settings['enable_multi_currency'] && $this->is_custom_prices_set_for_product( $product_object->get_id() ) ) { $on_sale = $this->is_on_sale( $product_object ); } return $on_sale; } /** * @param int $product_id * * @return mixed */ private function is_custom_prices_set_for_product( $product_id ) { /** * Allow to override the detection of custom prices. * * @param bool $check * @param int $productId */ return (bool) apply_filters( 'wcml_product_has_custom_prices', (bool) get_post_meta( $product_id, '_wcml_custom_prices_status', true ), $product_id ); } /** * WC when starts the sale copies price from _sale_price into _price field * we should do the same for _sale_price_{currency} and _price_{currency} * * @param array $product_ids */ public function maybe_set_sale_prices( $product_ids ) { foreach ( $product_ids as $product_id ) { if ( $this->is_custom_prices_set_for_product( $product_id ) ) { foreach ( $this->woocommerce_wpml->multi_currency->get_currencies() as $code => $currency ) { update_post_meta( $product_id, '_price_' . $code, get_post_meta( $product_id, '_sale_price_' . $code, true ) ); } } } } /** * @param array $product_ids */ public function maybe_remove_sale_prices( $product_ids ) { foreach ( $product_ids as $product_id ) { $product = wc_get_product( $product_id ); $is_product_on_sale = $product && $product->get_sale_price(); if ( $is_product_on_sale || ! $this->is_custom_prices_set_for_product( $product_id ) ) { continue; } foreach ( $this->woocommerce_wpml->multi_currency->get_currencies() as $code => $currency ) { $is_auto_schedule_set = ! get_post_meta( $product_id, '_wcml_schedule_' . $code, true ); if ( $is_auto_schedule_set ) { update_post_meta( $product_id, '_price_' . $code, get_post_meta( $product_id, '_regular_price_' . $code, true ) ); update_post_meta( $product_id, '_sale_price_' . $code, '' ); } } } } }