[], 'all_variable' => [], 'grouped' => [], 'all_grouped' => [], 'variable_subscription' => [], 'all_variable_subscription' => [], 'bundle' => [], 'all_bundle' => [], ); /** * Store parent type when in an inheritable sub-loop * * @var string */ protected $parent_type = ''; /** * The array of IDs of children products * * @var array */ protected $children_products = array(); /** * Elements per page (in order to obviate option default) * * @var int */ protected $per_page; /** * Array with the id's of the products in current page * * @var array */ protected $current_products = array(); /** * Used to include product variations in the Supplier filterings * * @var array */ protected $supplier_variation_products = array(); /** * Taxonomies to filter by * * @var array */ protected $taxonomies = array(); /** * Extra meta args for the list query * * @var array */ protected $extra_meta = array(); /** * The ATUM product data used in WP_Query * * @var array */ protected $atum_query_data = array(); /** * The WC product data used in WP_Query (when using the new tables) * * @var array */ protected $wc_query_data = array(); /** * IDs for views * * @var array */ protected $id_views = array( 'in_stock' => [], 'out_stock' => [], 'back_order' => [], 'low_stock' => [], 'unmanaged' => [], ); /** * Counters for views * * @var array */ protected $count_views = array( 'count_in_stock' => 0, 'count_out_stock' => 0, 'count_back_order' => 0, 'count_low_stock' => 0, 'count_unmanaged' => 0, 'count_all' => 0, ); /** * Days to re-order from settings * * @var int */ protected $days_to_reorder; /** * Time of query * * @var string */ protected $day; /** * Number of days for Sold Last Days calculations * * @var int */ protected static $sale_days; /** * Whether the currently displayed product is an expandable child product * * @var bool */ protected $is_child = FALSE; /** * Whether or not the current product should do the calculations for the columns * * @var bool */ protected $allow_calcs = TRUE; /** * Default currency symbol * * @var string */ protected static $default_currency; /** * The user meta key used for first edit popup * * @var string */ protected $first_edit_key; /** * Show the checkboxes in table rows * * @var bool */ protected $show_cb = FALSE; /** * Whether to show products controlled by ATUM or not * * @var bool */ protected $show_controlled = TRUE; /** * Columns that allow totalizers with their totals * * @var array */ protected $totalizers = array(); /** * Whether to show the totals row * * @var bool */ protected $show_totals = TRUE; /** * Whether the current list query has a filter applied * * @var bool */ protected $is_filtering = FALSE; /** * Filters being applied to the current query * * @var array */ protected $query_filters = array(); /** * Counter for the table rows * * @var int */ protected $row_count = 0; /** * Whether to show or not the unmanaged counters * * @var bool */ protected $show_unmanaged_counters; /** * The WC option where is stored whether to notify the customer when the * out of stock thresholsd is reached * * @var string */ protected $woocommerce_notify_no_stock_amount; /** * The columns that will be sticky * * @var array */ protected $sticky_columns = array(); /** * Report table flag * * @var bool */ protected static $is_report = FALSE; /** * WC's out of stock Threshold * * @var int */ protected $wc_out_stock_threshold; /** * Value for empty columns */ const EMPTY_COL = '-'; /** * AtumListTable Constructor * * The child class should call this constructor from its own constructor to override the default $args * * @since 0.0.1 * * @param array|string $args { * Array or string of arguments. * * @type array $table_columns The table columns for the list table * @type array $group_members The column grouping members * @type bool $show_cb Optional. Whether to show the row selector checkbox as first table column * @type bool $show_controlled Optional. Whether to show items controlled by ATUM or not * @type int $per_page Optional. The number of posts to show per page (-1 for no pagination) * @type array $selected Optional. The posts selected on the list table * @type array $excluded Optional. The posts excluded from the list table * } */ public function __construct( $args = array() ) { $this->is_filtering = ! empty( $_REQUEST['s'] ) || ! empty( $_REQUEST['search_column'] ) || ! empty( $_REQUEST['product_cat'] ) || ! empty( $_REQUEST['product_type'] ) || ! empty( $_REQUEST['supplier'] ); $this->query_filters = $this->get_filters_query_string(); $this->wc_out_stock_threshold = intval( get_option( 'woocommerce_notify_no_stock_amount' ) ); $this->day = Helpers::date_format( current_time( 'timestamp' ), TRUE, TRUE ); self::$sale_days = Helpers::get_sold_last_days_option(); // Filter the table data results to show specific product types only. $this->set_product_types_query_data(); $args = wp_parse_args( $args, array( 'show_cb' => FALSE, 'show_controlled' => TRUE, 'per_page' => Settings::DEFAULT_POSTS_PER_PAGE, ) ); $this->show_cb = $args['show_cb']; $this->show_controlled = $args['show_controlled']; if ( TRUE === $this->show_totals && 'no' === Helpers::get_option( 'show_totals', 'yes' ) ) { $this->show_totals = FALSE; } if ( ! empty( $args['selected'] ) ) { $this->selected = is_array( $args['selected'] ) ? $args['selected'] : explode( ',', $args['selected'] ); } if ( ! empty( $args['excluded'] ) ) { $this->excluded = is_array( $args['excluded'] ) ? $args['excluded'] : explode( ',', $args['excluded'] ); } if ( ! empty( $args['group_members'] ) ) { $this->group_members = $args['group_members']; if ( isset( $this->group_members['product-details'] ) && TRUE === $this->show_cb ) { array_unshift( $this->group_members['product-details']['members'], 'cb' ); } } // Remove _out_stock_threshold columns if not set, or add filters to get availability etc. $is_out_stock_threshold_managed = 'no' === Helpers::get_option( 'out_stock_threshold', 'no' ) ? FALSE : TRUE; if ( ! $is_out_stock_threshold_managed ) { unset( $args['table_columns'][ Globals::OUT_STOCK_THRESHOLD_KEY ] ); if ( isset( $args['group_members']['stock-counters']['members'] ) ) { $this->group_members['stock-counters']['members'] = array_diff( $this->group_members['stock-counters']['members'], array( Globals::OUT_STOCK_THRESHOLD_KEY ) ); $args['group_members']['stock-counters']['members'] = array_diff( $args['group_members']['stock-counters']['members'], array( Globals::OUT_STOCK_THRESHOLD_KEY ) ); } } // Add the checkbox column to the table if enabled. self::$table_columns = TRUE === $this->show_cb ? array_merge( [ 'cb' => 'cb' ], $args['table_columns'] ) : $args['table_columns']; $this->per_page = isset( $args['per_page'] ) ? $args['per_page'] : Helpers::get_option( 'posts_per_page', Settings::DEFAULT_POSTS_PER_PAGE ); $post_type_obj = get_post_type_object( $this->post_type ); if ( ! $post_type_obj ) { return; } // Set \WP_List_Table defaults. $args = array_merge( array( 'singular' => strtolower( $post_type_obj->labels->singular_name ), 'plural' => strtolower( $post_type_obj->labels->name ), 'ajax' => TRUE, ), $args ); parent::__construct( $args ); add_filter( 'posts_search', array( $this, 'product_search' ), 10, 2 ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); // Hook the default_hidden_columns filter used within get_hidden_columns() function. if ( ! empty( static::$default_hidden_columns ) ) { add_filter( 'default_hidden_columns', array( $this, 'hidden_columns' ), 10, 2 ); } // Allow adding searchable columns externally. if ( ! empty( $this->default_searchable_columns ) ) { $this->default_searchable_columns = (array) apply_filters( 'atum/list_table/default_serchable_columns', $this->default_searchable_columns ); } // Custom image placeholder. add_filter( 'woocommerce_placeholder_img', array( '\Atum\Inc\Helpers', 'image_placeholder' ), 10, 3 ); self::$default_currency = get_woocommerce_currency(); } /** * Extra controls to be displayed in table nav sections * * @since 1.3.0 * * @param string $which 'top' or 'bottom' table nav. */ protected function extra_tablenav( $which ) { if ( 'top' === $which ) : ?>
table_nav_filters() ?>
0, 'selected' => ! empty( $_REQUEST['product_cat'] ) ? esc_attr( $_REQUEST['product_cat'] ) : '', 'class' => 'wc-enhanced-select atum-enhanced-select dropdown_product_cat atum-tooltip', ) ); // Product type filtering. echo Helpers::product_types_dropdown( isset( $_REQUEST['product_type'] ) ? esc_attr( $_REQUEST['product_type'] ) : '', 'wc-enhanced-select atum-enhanced-select dropdown_product_type' ); // WPCS: XSS ok. // Supplier filtering. echo Helpers::suppliers_dropdown( isset( $_REQUEST['supplier'] ) ? esc_attr( $_REQUEST['supplier'] ) : '', 'yes' === Helpers::get_option( 'enhanced_suppliers_filter', 'no' ) ); // WPCS: XSS ok. } /** * Loads the current product * * @since 0.0.1 * * @param \WP_Post $item The WooCommerce product post. */ public function single_row( $item ) { $this->product = Helpers::get_atum_product( $item ); $type = $this->product->get_type(); $this->allow_calcs = TRUE; $row_classes = array( ( ++ $this->row_count % 2 ? 'even' : 'odd' ) ); // Inheritable products do not allow calcs. if ( Helpers::is_inheritable_type( $type ) ) { $this->parent_type = $type; $this->allow_calcs = FALSE; // WC product bundles compatibility. if ( class_exists( '\WC_Product_Bundle' ) && 'bundle' === $type ) { $this->allow_calcs = TRUE; } if ( 'grouped' === $type ) { $class_type = 'group'; } elseif ( 'bundle' === $type ) { $class_type = 'bundle'; } else { $class_type = 'variable'; } $row_classes[] = $class_type; if ( 'yes' === Helpers::get_option( 'expandable_rows', 'no' ) ) { $row_classes[] = 'expanded'; } } else { $this->parent_type = ''; } $row_class = ' class="main-row ' . implode( ' ', $row_classes ) . '"'; do_action( 'atum/list_table/before_single_row', $item, $this ); // Output the row. echo ''; // WPCS: XSS ok. $this->single_row_columns( $item ); echo ''; do_action( 'atum/list_table/after_single_row', $item, $this ); // If the current product has been modified within any of the columns, save it. if ( ! empty( $this->product->get_changes() ) ) { $this->product->save_atum_data(); } // Add the children products of each inheritable product type. if ( ! $this->allow_calcs || 'bundle' === $type ) { if ( 'grouped' === $type ) { $product_type = 'product'; } elseif ( 'bundle' === $type ) { $product_type = 'product_bundle'; } else { $product_type = 'product_variation'; } $child_products = $this->get_children( $type, [ $this->product->get_id() ], $product_type ); if ( ! empty( $child_products ) ) { // If the post__in filter is applied, bypass the children that are not in the query var. $post_in = get_query_var( 'post__in' ); $this->allow_calcs = TRUE; foreach ( $child_products as $child_id ) { if ( ! empty( $post_in ) && ! in_array( $child_id, $post_in ) ) { continue; } // Exclude some children if there is a "Views Filter" active. if ( ! empty( $_REQUEST['view'] ) ) { $view = esc_attr( $_REQUEST['view'] ); if ( ! in_array( $child_id, $this->id_views[ $view ] ) ) { continue; } } $this->is_child = TRUE; // Save the child product to the product prop. $this->product = Helpers::get_atum_product( $child_id ); if ( 'grouped' === $type ) { $child_type = 'grouped'; } elseif ( 'bundle' === $type ) { $child_type = 'bundle-item'; } else { $child_type = 'variation'; } $this->single_expandable_row( $this->product, $child_type ); // If the current product has been modified within any of the columns, save it. if ( ! empty( $this->product->get_changes() ) ) { $this->product->save_atum_data(); } } } } // Reset the child value. $this->is_child = FALSE; } /** * Generates content for a expandable row on the table * * @since 1.1.0 * * @param \WC_Product $item The WooCommerce product. * @param string $type The type of product. */ public function single_expandable_row( $item, $type ) { $row_style = 'yes' !== Helpers::get_option( 'expandable_rows', 'no' ) ? ' style="display: none"' : ''; do_action( 'atum/list_table/before_single_expandable_row', $item, $this ); echo ''; // WPCS: XSS ok. $this->single_row_columns( $item ); echo ''; do_action( 'atum/list_table/after_single_expandable_row', $item, $this ); } /** * The default column (when no specific column method found) * * @since 0.0.1 * * @param \WP_Post $item The WooCommerce product post. * @param string $column_name The current column name. * * @return string|bool */ protected function column_default( $item, $column_name ) { $id = $this->get_current_product_id(); $column_item = ''; // Check if it's a hidden meta key (will start with underscore). if ( '_' === substr( $column_name, 0, 1 ) ) { // If the current product has a method to get the prop, use it. if ( is_callable( array( $this->product, "get{$column_name}" ) ) ) { $column_item = call_user_func( array( $this->product, "get{$column_name}" ) ); } else { $column_item = get_post_meta( $id, $column_name, TRUE ); } } if ( '' === $column_item || FALSE === $column_item ) { $column_item = self::EMPTY_COL; } return apply_filters( "atum/list_table/column_default_$column_name", $column_item, $item, $this->product, $this, $column_name ); } /** * Generates the columns for a single row of the table * * @since 1.4.15 * * @param object $item The current item. */ public function single_row_columns( $item ) { list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); $group_members = wp_list_pluck( $this->group_members, 'members' ); foreach ( $columns as $column_name => $column_display_name ) { $classes = "$column_name column-$column_name"; if ( $primary === $column_name ) { $classes .= ' has-row-actions column-primary'; } if ( in_array( $column_name, $hidden ) ) { $classes .= ' hidden'; } // Add the group key as class. foreach ( $group_members as $group_key => $members ) { if ( in_array( $column_name, $members ) ) { $classes .= " $group_key"; break; } } // Check if it's a numeric cell. if ( ! empty( $this->default_searchable_columns['numeric'] ) && is_array( $this->default_searchable_columns['numeric'] ) && in_array( $column_name, $this->default_searchable_columns['numeric'], TRUE ) ) { $classes .= ' numeric'; } // Comments column uses HTML in the display name with screen reader text. // Instead of using esc_attr(), we strip tags to get closer to a user-friendly string. $data = 'data-colname="' . wp_strip_all_tags( $column_display_name ) . '"'; $attributes = "class='$classes' $data"; if ( 'cb' === $column_name ) { echo ''; echo $this->column_cb( $item ); // WPCS: XSS ok. echo ''; } elseif ( method_exists( apply_filters( "atum/list_table/column_source_object/_column_$column_name", $this, $item ), "_column_$column_name" ) ) { echo call_user_func( array( apply_filters( "atum/list_table/column_source_object/_column_$column_name", $this, $item ), "_column_$column_name" ), $item, $classes, $data, $primary ); // WPCS: XSS ok. } elseif ( method_exists( apply_filters( "atum/list_table/column_source_object/column_$column_name", $this, $item ), "column_$column_name" ) ) { echo ""; // WPCS: XSS ok. echo call_user_func( array( apply_filters( "atum/list_table/column_source_object/column_$column_name", $this, $item ), "column_$column_name" ), $item ); // WPCS: XSS ok. echo ''; } else { echo ""; // WPCS: XSS ok. /* @noinspection PhpParamsInspection */ echo $this->column_default( $item, $column_name ); // WPCS: XSS ok. echo ''; } } } /** * Column selector checkbox * * @since 0.0.1 * * @param object $item * * @return string */ protected function column_cb( $item ) { $product_id = $this->get_current_product_id(); return sprintf( '', checked( in_array( $product_id, $this->selected ), TRUE, FALSE ), $this->_args['singular'], $product_id ); } /** * Column for thumbnail * * @since 0.0.1 * * @param \WP_Post $item The WooCommerce product post. * * @return string */ protected function column_thumb( $item ) { $product_id = $this->get_current_product_id(); $img_src = wp_get_attachment_image_src( $this->product->get_image_id(), 'full' ); $url = $img_src ? $img_src[0] : get_edit_post_link( $product_id ); $thumb = '' . $this->product->get_image( [ 40, 40 ] ) . ''; return apply_filters( 'atum/list_table/column_thumb', $thumb, $item, $this->product, $this ); } /** * Post ID column * * @since 0.0.1 * * @param \WP_Post $item The WooCommerce product post. * * @return int */ protected function column_id( $item ) { return apply_filters( 'atum/list_table/column_id', $this->get_current_product_id(), $item, $this->product, $this ); } /** * Post title column * * @since 0.0.1 * * @param \WP_Post $item The WooCommerce product post. * * @return string */ protected function column_title( $item ) { $title = ''; $product_id = $this->get_current_product_id(); $child_arrow = $this->is_child ? '' : ''; if ( Helpers::is_child_type( $this->product->get_type() ) ) { $attributes = $this->product->get_attributes(); if ( ! empty( $attributes ) ) { $title = ucfirst( implode( ' ', $attributes ) ); } // Get the variable product ID to get the right link. $product_id = $this->product->get_parent_id(); } else { $title = $this->product->get_title(); } $title_length = absint( apply_filters( 'atum/list_table/column_title_length', 20 ) ); if ( mb_strlen( $title ) > $title_length ) { $data_tip = ! self::$is_report ? ' data-tip="' . esc_attr( $title ) . '"' : ''; $title = '' . trim( mb_substr( $title, 0, $title_length ) ) . '...' . $title . ''; } $title = '' . $child_arrow . $title . ''; return apply_filters( 'atum/list_table/column_title', $title, $item, $this->product, $this ); } /** * Product SKU column * * @since 1.1.2 * * @param \WP_Post $item The WooCommerce product post. * @param bool $editable Whether the SKU will be editable. * * @return string */ protected function column__sku( $item, $editable = TRUE ) { $sku = $this->product->get_sku(); $sku = $sku ?: self::EMPTY_COL; if ( $editable ) { $args = array( 'meta_key' => 'sku', 'value' => $sku, 'input_type' => 'text', 'tooltip' => esc_attr__( 'Click to edit the SKU', ATUM_TEXT_DOMAIN ), 'cell_name' => esc_attr__( 'SKU', ATUM_TEXT_DOMAIN ), ); $sku = self::get_editable_column( $args ); } return apply_filters( 'atum/list_table/column_sku', $sku, $item, $this->product, $this ); } /** * Supplier column * * @since 1.3.1 * * @param \WP_Post $item The WooCommerce product post. * * @return string */ protected function column__supplier( $item ) { $supplier = self::EMPTY_COL; if ( ! AtumCapabilities::current_user_can( 'read_supplier' ) ) { return $supplier; } /* @noinspection PhpUndefinedMethodInspection */ $supplier_id = $this->product->get_supplier_id(); if ( $supplier_id ) { $supplier_post = get_post( $supplier_id ); if ( $supplier_post && Suppliers::POST_TYPE === $supplier_post->post_type ) { $supplier = $supplier_post->post_title; $supplier_length = absint( apply_filters( 'atum/list_table/column_supplier_length', 20 ) ); $supplier_abb = mb_strlen( $supplier ) > $supplier_length ? trim( mb_substr( $supplier, 0, $supplier_length ) ) . '...' : $supplier; /* translators: first one is the supplier name and second is the supplier's ID */ $supplier_tooltip = sprintf( esc_attr__( '%1$s (ID: %2$d)', ATUM_TEXT_DOMAIN ), $supplier, $supplier_id ); $data_tip = ! self::$is_report ? ' data-tip="' . $supplier_tooltip . '"' : ''; $supplier = '' . $supplier_abb . '' . $supplier_tooltip . ''; } } return apply_filters( 'atum/list_table/column_supplier', $supplier, $item, $this->product, $this ); } /** * Column for supplier sku * * @since 1.2.0 * * @param \WP_Post $item The WooCommerce product post to use in calculations. * @param bool $editable Optional. Whether the current column is editable. * * @return float */ protected function column__supplier_sku( $item, $editable = TRUE ) { $supplier_sku = self::EMPTY_COL; if ( ! AtumCapabilities::current_user_can( 'read_supplier' ) ) { return $supplier_sku; } if ( $editable ) { /* @noinspection PhpUndefinedMethodInspection */ $supplier_sku = $this->product->get_supplier_sku(); if ( 0 === strlen( $supplier_sku ) ) { $supplier_sku = self::EMPTY_COL; } $args = apply_filters( 'atum/list_table/args_supplier_sku', array( 'meta_key' => 'supplier_sku', 'value' => $supplier_sku, 'input_type' => 'text', 'tooltip' => esc_attr__( 'Click to edit the supplier SKU', ATUM_TEXT_DOMAIN ), 'cell_name' => esc_attr__( 'Supplier SKU', ATUM_TEXT_DOMAIN ), ) ); $supplier_sku = self::get_editable_column( $args ); } return apply_filters( 'atum/list_table/column_supplier_sku', $supplier_sku, $item, $this->product, $this ); } /** * Column for product type * * @since 1.1.0 * * @param \WP_Post $item The WooCommerce product post. * * @return string */ protected function column_calc_type( $item ) { $type = $this->product->get_type(); $product_tip = ''; $product_types = wc_get_product_types(); if ( isset( $product_types[ $type ] ) || $this->is_child ) { if ( ! $this->is_child ) { $product_tip = $product_types[ $type ]; } switch ( $type ) { case 'simple': if ( $this->is_child ) { $type = 'grouped-item'; $product_tip = esc_attr__( 'Grouped item', ATUM_TEXT_DOMAIN ); } elseif ( $this->product->is_downloadable() ) { $type = 'downloadable'; $product_tip = esc_attr__( 'Downloadable product', ATUM_TEXT_DOMAIN ); } elseif ( $this->product->is_virtual() ) { $type = 'virtual'; $product_tip = esc_attr__( 'Virtual product', ATUM_TEXT_DOMAIN ); } break; case 'variable': case 'grouped': case 'variable-subscription': // WC Subscriptions compatibility. if ( $this->is_child ) { $type = 'grouped-item'; $product_tip = esc_attr__( 'Grouped item', ATUM_TEXT_DOMAIN ); } elseif ( $this->product->has_child() ) { $product_tip .= '
' . sprintf( /* translators: product type names */ esc_attr__( '(click to show/hide %s)', ATUM_TEXT_DOMAIN ), 'grouped' === $type ? esc_attr__( 'grouped items', ATUM_TEXT_DOMAIN ) : esc_attr__( 'variations', ATUM_TEXT_DOMAIN ) ); $type .= ' has-child'; } break; case 'variation': $product_tip = esc_attr__( 'Variation', ATUM_TEXT_DOMAIN ); break; // WC Subscriptions compatibility. case 'subscription_variation': $type = 'variation'; $product_tip = esc_attr__( 'Subscription Variation', ATUM_TEXT_DOMAIN ); break; // WC Bundle Products compatibility. case 'bundle': if ( $this->is_child ) { $type = 'bundle-item'; $product_tip = esc_attr__( 'Bundle item', ATUM_TEXT_DOMAIN ); } $children = Helpers::get_bundle_items( array( 'return' => 'id=>product_id', 'bundle_id' => $this->product->get_id(), ) ); if ( $children ) { $product_tip .= '
' . sprintf( /* translators: product type names */ esc_attr__( '(click to show/hide %s)', ATUM_TEXT_DOMAIN ), esc_attr__( 'bundle items', ATUM_TEXT_DOMAIN ) ); $type .= ' has-child'; } break; } $data_tip = ! self::$is_report ? ' data-tip="' . $product_tip . '"' : ''; return apply_filters( 'atum/list_table/column_type', '', $item, $this->product, $this ); } return ''; } /** * Column for product location * * @since 1.4.2 * * @param \WP_Post $item The WooCommerce product post. * * @return string */ protected function column_calc_location( $item ) { $has_location = $this->product->get_has_location(); if ( is_null( $has_location ) ) { $location_terms = wp_get_post_terms( $this->get_current_product_id(), Globals::PRODUCT_LOCATION_TAXONOMY ); $has_location = ! empty( $location_terms ); $this->product->set_has_location( $has_location ); } $location_terms_class = $has_location && 'no' !== $has_location ? ' not-empty' : ''; $data_tip = ! self::$is_report ? ' data-tip="' . esc_attr__( 'Show Locations', ATUM_TEXT_DOMAIN ) . '"' : ''; $locations = ''; return apply_filters( 'atum/list_table/column_locations', $locations, $item, $this->product, $this ); } /** * Column for purchase price * * @since 1.2.0 * * @param \WP_Post $item The WooCommerce product post to use in calculations. * * @return float */ protected function column__purchase_price( $item ) { $purchase_price = self::EMPTY_COL; if ( ! AtumCapabilities::current_user_can( 'view_purchase_price' ) ) { return $purchase_price; } if ( $this->allow_calcs ) { /* @noinspection PhpUndefinedMethodInspection */ $purchase_price_value = $this->product->get_purchase_price(); $purchase_price_value = is_numeric( $purchase_price_value ) ? Helpers::format_price( $purchase_price_value, [ 'trim_zeros' => TRUE, 'currency' => self::$default_currency, ] ) : $purchase_price; $args = apply_filters( 'atum/list_table/args_purchase_price', array( 'meta_key' => 'purchase_price', 'value' => $purchase_price_value, 'symbol' => get_woocommerce_currency_symbol(), 'currency' => self::$default_currency, 'tooltip' => esc_attr__( 'Click to edit the purchase price', ATUM_TEXT_DOMAIN ), 'cell_name' => esc_attr__( 'Purchase Price', ATUM_TEXT_DOMAIN ), ) ); $purchase_price = self::get_editable_column( $args ); } return apply_filters( 'atum/list_table/column_purchase_price', $purchase_price, $item, $this->product, $this ); } /** * Column out_stock_threshold column * * @since 1.4.6 * * @param \WP_Post $item The WooCommerce product post. * @param bool $editable Optional. Whether the current column is editable. * * @return double */ protected function column__out_stock_threshold( $item, $editable = TRUE ) { /* @noinspection PhpUndefinedMethodInspection */ $out_stock_threshold = $this->product->get_out_stock_threshold(); $out_stock_threshold = $out_stock_threshold ?: self::EMPTY_COL; // Check type and managed stock at product level (override $out_stock_threshold value if set and not allowed). $product_type = $this->product->get_type(); if ( ! in_array( $product_type, Globals::get_product_types_with_stock() ) ) { $editable = FALSE; $out_stock_threshold = self::EMPTY_COL; } $manage_stock = $this->product->get_manage_stock(); if ( 'no' === $manage_stock ) { $editable = FALSE; $out_stock_threshold = self::EMPTY_COL; } if ( $editable ) { $args = array( 'meta_key' => 'out_stock_threshold', 'value' => $out_stock_threshold, 'input_type' => 'number', 'tooltip' => esc_attr__( 'Click to edit the out of stock threshold', ATUM_TEXT_DOMAIN ), 'cell_name' => esc_attr__( 'Out of Stock Threshold', ATUM_TEXT_DOMAIN ), ); $out_stock_threshold = self::get_editable_column( $args ); } return apply_filters( 'atum/list_table/column_out_stock_threshold', $out_stock_threshold, $item, $this->product, $this ); } /** * Column Weight column * * @since 1.4.6 * * @param \WP_Post $item The WooCommerce product post. * @param bool $editable Optional. Whether the current column is editable. * * @return double */ protected function column__weight( $item, $editable = TRUE ) { $weight = $this->product->get_weight(); $weight = $weight ?: self::EMPTY_COL; if ( $editable ) { $args = array( 'meta_key' => 'weight', 'value' => $weight, 'input_type' => 'number', 'tooltip' => esc_attr__( 'Click to edit the weight', ATUM_TEXT_DOMAIN ), 'cell_name' => esc_attr__( 'Weight', ATUM_TEXT_DOMAIN ), ); $weight = self::get_editable_column( $args ); } return apply_filters( 'atum/list_table/column_weight', $weight, $item, $this->product, $this ); } /** * Column for stock amount * * @since 0.0.1 * * @param \WP_Post $item The WooCommerce product post to use in calculations. * @param bool $editable Whether the stock will be editable. * * @return string|int */ protected function column__stock( $item, $editable = TRUE ) { $stock = self::EMPTY_COL; $classes_title = ''; $tooltip_warning = ''; $wc_notify_no_stock_amount = wc_stock_amount( get_option( 'woocommerce_notify_no_stock_amount' ) ); $is_grouped = 'grouped' === $this->product->get_type(); $is_inheritable = Helpers::is_inheritable_type( $this->product->get_type() ); $editable = apply_filters( 'atum/list_table/editable_column_stock', $editable, $this->product ); // Do not show the stock if the product is not managed by WC. if ( ! $is_inheritable && ( ! $this->product->managing_stock() || 'parent' === $this->product->managing_stock() ) ) { return $stock; } if ( ! $is_grouped ) { $stock = wc_stock_amount( $this->product->get_stock_quantity() ); } if ( 0 < $stock ) { $this->increase_total( '_stock', $stock ); } // Setings value is enabled? $is_out_stock_threshold_managed = 'no' === Helpers::get_option( 'out_stock_threshold', 'no' ) ? FALSE : TRUE; if ( $is_out_stock_threshold_managed && ! $is_grouped ) { /* @noinspection PhpUndefinedMethodInspection */ $out_stock_threshold = $this->product->get_out_stock_threshold(); if ( strlen( $out_stock_threshold ) > 0 ) { if ( wc_stock_amount( $out_stock_threshold ) >= $stock ) { if ( ! $editable ) { $classes_title = ' class="cell-yellow" title="' . esc_attr__( 'Stock is below the Out of Stock Threshold', ATUM_TEXT_DOMAIN ) . '"'; } else { $classes_title = ' class="cell-yellow"'; $tooltip_warning = esc_attr__( "Click to edit the stock quantity (it's below the Out of Stock Threshold)", ATUM_TEXT_DOMAIN ); } } } elseif ( $wc_notify_no_stock_amount >= $stock ) { if ( ! $editable ) { $classes_title = ' class="cell-yellow" title="' . esc_attr__( 'Stock is below the Out of Stock Threshold', ATUM_TEXT_DOMAIN ) . '"'; } else { $classes_title = ' class="cell-yellow"'; $tooltip_warning = esc_attr__( "Click to edit the stock quantity (it's below the Out of Stock Threshold)", ATUM_TEXT_DOMAIN ); } } } elseif ( $wc_notify_no_stock_amount >= $stock ) { if ( ! $editable ) { $classes_title = ' class="cell-yellow" title="' . esc_attr__( 'Stock is below the Out of Stock Threshold', ATUM_TEXT_DOMAIN ) . '"'; } else { $classes_title = ' class="cell-yellow"'; $tooltip_warning = esc_attr__( "Click to edit the stock quantity (it's below the Out of Stock Threshold)", ATUM_TEXT_DOMAIN ); } } if ( $editable && ! $is_grouped ) { $args = array( 'meta_key' => 'stock', 'value' => $stock, 'tooltip' => $tooltip_warning ?: esc_attr__( 'Click to edit the stock quantity', ATUM_TEXT_DOMAIN ), 'cell_name' => esc_attr__( 'Stock Quantity', ATUM_TEXT_DOMAIN ), ); $stock = self::get_editable_column( $args ); } $stock_html = "{$stock}"; if ( $is_inheritable ) { $tooltip = esc_attr__( 'Compounded stock quantity', ATUM_TEXT_DOMAIN ); $stock_html .= " | " . self::EMPTY_COL . ''; } return apply_filters( 'atum/list_table/column_stock', $stock_html, $item, $this->product, $this ); } /** * Column for back orders amount: show amount if items pending to serve and without existences * * @since 0.0.1 * * @param \WP_Post $item The WooCommerce product post to use in calculations. * * @return int|string */ protected function column_calc_back_orders( $item ) { $back_orders = self::EMPTY_COL; if ( $this->allow_calcs ) { $back_orders = '--'; if ( $this->product->backorders_allowed() ) { // TODO: threshold recalc if needed. $stock_quantity = $this->product->get_stock_quantity(); $back_orders = 0; if ( $stock_quantity < $this->wc_out_stock_threshold ) { $back_orders = $this->wc_out_stock_threshold - $stock_quantity; } } $this->increase_total( 'calc_back_orders', $back_orders ); } return apply_filters( 'atum/list_table/column_back_orders', $back_orders, $item, $this->product, $this ); } /** * Column for inbound stock: shows sum of inbound stock within Purchase Orders * * @since 1.3.0 * * @param \WP_Post $item The WooCommerce product post to use in calculations. * * @return int */ protected function column__inbound_stock( $item ) { $inbound_stock = self::EMPTY_COL; if ( $this->allow_calcs ) { $inbound_stock = Helpers::get_product_inbound_stock( $this->product ); $this->increase_total( '_inbound_stock', $inbound_stock ); } return apply_filters( 'atum/list_table/column_inbound_stock', $inbound_stock, $item, $this->product, $this ); } /** * Column for stock indicators * * @since 0.0.1 * * @param \WP_Post $item The WooCommerce product post to use in calculations. * @param string $classes * @param string $data * @param string $primary */ protected function _column_calc_stock_indicator( $item, $classes, $data, $primary ) { $product_id = $this->get_current_product_id(); $content = self::EMPTY_COL; // Add css class to the elements depending on the quantity in stock compared to the last days sales. if ( $this->allow_calcs ) { // Stock not managed by WC. if ( ! $this->product->managing_stock() || 'parent' === $this->product->managing_stock() ) { $wc_stock_status = $this->product->get_stock_status(); switch ( $wc_stock_status ) { case 'instock': $classes .= ' cell-green'; $data_tip = ! self::$is_report ? ' data-tip="' . esc_attr__( 'In Stock (not managed by WC)', ATUM_TEXT_DOMAIN ) . '"' : ''; $content = ''; break; case 'outofstock': $classes .= ' cell-red'; $data_tip = ! self::$is_report ? ' data-tip="' . esc_attr__( 'Out of Stock (not managed by WC)', ATUM_TEXT_DOMAIN ) . '"' : ''; $content = ''; break; case 'onbackorder': $classes .= ' cell-yellow'; $data_tip = ! self::$is_report ? ' data-tip="' . esc_attr__( 'On Backorder (not managed by WC)', ATUM_TEXT_DOMAIN ) . '"' : ''; $content = ''; break; } } // Out of stock. elseif ( in_array( $product_id, $this->id_views['out_stock'] ) ) { $classes .= ' cell-red'; $data_tip = ! self::$is_report ? ' data-tip="' . esc_attr__( 'Out of Stock', ATUM_TEXT_DOMAIN ) . '"' : ''; $content = ''; } // Back Orders. elseif ( in_array( $product_id, $this->id_views['back_order'] ) ) { $classes .= ' cell-yellow'; $data_tip = ! self::$is_report ? ' data-tip="' . esc_attr__( 'Out of Stock (back orders allowed)', ATUM_TEXT_DOMAIN ) . '"' : ''; $content = ''; } // Low Stock. elseif ( in_array( $product_id, $this->id_views['low_stock'] ) ) { $classes .= ' cell-blue'; $data_tip = ! self::$is_report ? ' data-tip="' . esc_attr__( 'Low Stock', ATUM_TEXT_DOMAIN ) . '"' : ''; $content = ''; } // In Stock. elseif ( in_array( $product_id, $this->id_views['in_stock'] ) ) { $classes .= ' cell-green'; $data_tip = ! self::$is_report ? ' data-tip="' . esc_attr__( 'In Stock', ATUM_TEXT_DOMAIN ) . '"' : ''; $content = ''; } } $classes = apply_filters( 'atum/list_table/column_stock_indicator_classes', $classes, $this->product ); $classes = $classes ? ' class="' . $classes . '"' : ''; echo '' . apply_filters( 'atum/list_table/column_stock_indicator', $content, $item, $this->product, $this ) . ''; // WPCS: XSS ok. } /** * REQUIRED! This method dictates the table's columns and titles * This should return an array where the key is the column slug (and class) and the value * is the column's title text. * * @see WP_List_Table::single_row_columns() * * @since 0.0.1 * * @return array An associative array containing column information: 'slugs'=>'Visible Titles'. */ public function get_columns() { $result = array(); foreach ( self::$table_columns as $table => $slug ) { $group = $this->search_group_columns( $table ); $result[ $table ] = $group ? "$slug" : $slug; } return apply_filters( 'atum/list_table/columns', $result ); } /** * Returns primary column name * * @since 0.0.8 * * @return string Name of the default primary column. */ protected function get_default_primary_column_name() { return 'title'; } /** * Create an editable meta cell * * @since 1.2.0 * * @param array $args { * Array of arguments. * * @type int $post_id The current post ID. * @type string $meta_key The meta key name (without initial underscore) to be saved. * @type mixed $value The new value for the meta key cell. * @type string $symbol Whether to add any symbol to value. * @type string $tooltip The informational tooltip text. * @type string $input_type The input type field to use to edit the column value. * @type array $extra_meta Any extra fields will be appended to the popover (as JSON array). * @type string $tooltip_position Where to place the tooltip. * @type string $currency Product prices currency. * @type array $extra_data Any other array of data that should be added to the element. * } * * @return string */ public static function get_editable_column( $args ) { /** * Variable definitions * * @var string $meta_key * @var mixed $value * @var string $symbol * @var string $tooltip * @var string $input_type * @var array $extra_meta * @var string $tooltip_position * @var string $currency * @var string $cell_name * @var array $extra_data */ extract( wp_parse_args( $args, array( 'meta_key' => '', 'value' => '', 'symbol' => '', 'tooltip' => '', 'input_type' => 'number', 'extra_meta' => array(), 'tooltip_position' => 'top', 'currency' => self::$default_currency, 'cell_name' => '', 'extra_data' => array(), ) ) ); $extra_meta_data = ! empty( $extra_meta ) ? ' data-extra-meta="' . htmlspecialchars( wp_json_encode( $extra_meta ), ENT_QUOTES, 'UTF-8' ) . '"' : ''; $symbol_data = ! empty( $symbol ) ? ' data-symbol="' . esc_attr( $symbol ) . '"' : ''; $extra_data = ! empty( $extra_data ) ? Helpers::array_to_data( $extra_data ) : ''; $editable_col = '' . $value . ''; return apply_filters( 'atum/list_table/editable_column', $editable_col, $args ); } /** * All columns are sortable by default except cb and thumbnail * * Optional. If you want one or more columns to be sortable (ASC/DESC toggle), * you will need to register it here. This should return an array where the * key is the column that needs to be sortable, and the value is db column to * sort by. Often, the key and value will be the same, but this is not always * the case (as the value is a column name from the database, not the list table). * * This method merely defines which columns should be sortable and makes them * clickable - it does not handle the actual sorting. You still need to detect * the ORDERBY and ORDER querystring variables within prepare_items() and sort * your data accordingly (usually by modifying your query). * * @return array An associative array containing all the columns that should be sortable: 'slugs' => array('data_values', bool). */ protected function get_sortable_columns() { $not_sortable = array( 'thumb', 'cb' ); $sortable_columns = array(); foreach ( self::$table_columns as $key => $column ) { if ( ! in_array( $key, $not_sortable ) && 0 !== strpos( $key, 'calc_' ) ) { $sortable_columns[ $key ] = array( $key, FALSE ); } } return apply_filters( 'atum/list_table/sortable_columns', $sortable_columns ); } /** * Get an associative array ( id => link ) with the list of available views on this table. * * @since 1.3.0 * * @return array */ protected function get_views() { $views = array(); $view = ! empty( $_REQUEST['view'] ) ? esc_attr( $_REQUEST['view'] ) : 'all_stock'; $views_name = array( 'all_stock' => __( 'All', ATUM_TEXT_DOMAIN ), 'in_stock' => __( 'In Stock', ATUM_TEXT_DOMAIN ), 'out_stock' => __( 'Out of Stock', ATUM_TEXT_DOMAIN ), 'back_order' => __( 'on Back Order', ATUM_TEXT_DOMAIN ), 'low_stock' => __( 'Low Stock', ATUM_TEXT_DOMAIN ), 'unmanaged' => __( 'Unmanaged by WC', ATUM_TEXT_DOMAIN ), ); if ( $this->show_unmanaged_counters ) { unset( $views_name['unmanaged'] ); $views = array( 'all_stock' => array( 'all' => 'all_stock', 'managed' => 'managed', 'unmanaged' => 'unmanaged', ), 'in_stock' => array( 'all' => 'all_in_stock', 'managed' => 'in_stock', 'unmanaged' => 'unm_in_stock', ), 'out_stock' => array( 'all' => 'all_out_stock', 'managed' => 'out_stock', 'unmanaged' => 'unm_out_stock', ), 'back_order' => array( 'all' => 'all_back_order', 'managed' => 'back_order', 'unmanaged' => 'unm_back_order', ), ); } global $plugin_page; if ( ! $plugin_page && ! empty( $this->_args['screen'] ) ) { $plugin_page = str_replace( Globals::ATUM_UI_HOOK . '_page_', '', $this->_args['screen'] ); } $url = esc_url( add_query_arg( 'page', $plugin_page, admin_url() ) ); foreach ( $views_name as $key => $text ) { $class = $id = $active = $empty = ''; $classes = array(); $current_all = ! empty( $views[ $key ]['all'] ) ? $views[ $key ]['all'] : $key; if ( 'all_stock' === $current_all ) { $count = $this->count_views['count_all']; $view_url = $url; } else { if ( ! empty( $views[ $key ] ) ) { $count = $this->count_views[ 'count_' . $views[ $key ]['all'] ]; } else { $count = $this->count_views[ 'count_' . $key ]; } $view_url = esc_url( add_query_arg( array( 'view' => $current_all ), $url ) ); $id = ' id="' . $current_all . '"'; } $query_filters = $this->query_filters; if ( $current_all === $view || ( ! $view && 'all_stock' === $current_all ) ) { $classes[] = 'current'; $active = ' class="active"'; } else { $query_filters['paged'] = 1; } if ( ! $count ) { $classes[] = 'empty'; $empty = 'empty'; } if ( $classes ) { $class = ' class="' . implode( ' ', $classes ) . '"'; } $hash_params = http_build_query( array_merge( $query_filters, array( 'view' => $current_all ) ) ); if ( ! empty( $views[ $key ] ) && $this->show_controlled ) { $extra_links = ''; if ( ! empty( $views[ $key ]['managed'] ) ) { $man_class = array( 'tips' ); $man_url = esc_url( add_query_arg( array( 'view' => $views[ $key ]['managed'] ), $url ) ); $man_id = ' id="' . $views[ $key ]['managed'] . '"'; $man_count = $this->count_views[ 'count_' . $views[ $key ]['managed'] ]; $query_filters = $this->query_filters; if ( ( $views[ $key ]['managed'] === $view ) ) { $man_class[] = 'current'; $active = ' class="active"'; } else { $query_filters['paged'] = 1; } if ( ! $man_count ) { $man_class[] = 'empty'; $empty = 'empty'; } if ( $man_class ) { $man_class = ' class="' . implode( ' ', $man_class ) . '"'; } else { $man_class = ''; } $man_hash_params = http_build_query( array_merge( $query_filters, array( 'view' => $views[ $key ]['managed'] ) ) ); $data_tip = ! self::$is_report ? ' data-tip="' . esc_attr__( 'Managed by WC', ATUM_TEXT_DOMAIN ) . '"' : ''; $extra_links .= '' . $man_count . ''; } if ( ! empty( $views[ $key ]['unmanaged'] ) ) { $unm_class = array( 'tips' ); $unm_url = esc_url( add_query_arg( array( 'view' => $views[ $key ]['unmanaged'] ), $url ) ); $unm_id = ' id="' . $views[ $key ]['unmanaged'] . '"'; $unm_count = $this->count_views[ 'count_' . $views[ $key ]['unmanaged'] ]; $query_filters = $this->query_filters; if ( ( $views[ $key ]['unmanaged'] === $view ) ) { $unm_class[] = 'current'; $active = ' class="active"'; } else { $query_filters['paged'] = 1; } if ( ! $unm_count ) { $unm_class[] = 'empty'; } if ( $unm_class ) { $unm_class = ' class="' . implode( ' ', $unm_class ) . '"'; } else { $unm_class = ''; } $unm_hash_params = http_build_query( array_merge( $query_filters, array( 'view' => $views[ $key ]['unmanaged'] ) ) ); $data_tip = ! self::$is_report ? ' data-tip="' . esc_attr__( 'Unmanaged by WC', ATUM_TEXT_DOMAIN ) . '"' : ''; $extra_links .= ',' . $unm_count . ''; } $views[ $key ] = '' . $text . ' ' . $count . ' (' . $extra_links . ')'; } else { $views[ $key ] = '' . $text . ' (' . $count . ')'; } } return apply_filters( 'atum/list_table/view_filters', $views ); } /** * Display the list of views available on this table * * @since 1.4.3 */ public function views() { $views = $this->get_views(); $views = apply_filters( "views_{$this->screen->id}", $views ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound if ( empty( $views ) ) return; $this->screen->render_screen_reader_content( 'heading_views' ); ?>
    $view ) : $views[ $class ] = "\t
  • $view"; endforeach; echo implode( "
  • \n", $views ) . "\n"; // WPCS: XSS ok. ?>
'Visible Title' * * @since 0.0.1 * * @return array An associative array containing all the bulk actions: 'slugs'=>'Visible Titles'. */ protected function get_bulk_actions() { $bulk_actions = array( 'manage_stock' => __( "Enable WC's Manage Stock", ATUM_TEXT_DOMAIN ), 'unmanage_stock' => __( "Disable WC's Manage Stock", ATUM_TEXT_DOMAIN ), ); if ( ( isset( $_GET['uncontrolled'] ) && 1 === absint( $_GET['uncontrolled'] ) ) || ( isset( $_REQUEST['show_controlled'] ) && 0 === absint( $_REQUEST['show_controlled'] ) ) ) { $bulk_actions['control_stock'] = __( "Enable ATUM's Stock Control", ATUM_TEXT_DOMAIN ); } else { $bulk_actions['uncontrol_stock'] = __( "Disable ATUM's Stock Control", ATUM_TEXT_DOMAIN ); } return apply_filters( 'atum/list_table/bulk_actions', $bulk_actions, $this ); } /** * Display the bulk actions dropdown * * @since 1.4.1 * * @param string $which The location of the bulk actions: 'top' or 'bottom'. * This is designated as optional for backward compatibility. */ protected function bulk_actions( $which = '' ) { if ( is_null( $this->_actions ) ) { $this->_actions = $this->get_bulk_actions(); $this->_actions = apply_filters( "atum/list_table/bulk_actions-{$this->screen->id}", $this->_actions ); $two = ''; } else { $two = '2'; } if ( empty( $this->_actions ) ) { return; } ?> add_apply_bulk_action_button(); } /** * Adds the Bulk Actions' apply button to the List Table view * * @since 1.4.1 */ public function add_apply_bulk_action_button() { ?> prepare_items_legacy(); return; } /** * Define our column headers */ $columns = $this->get_columns(); $posts = array(); $sortable = $this->get_sortable_columns(); $hidden = get_hidden_columns( $this->screen ); $this->group_columns = $this->calc_groups( $this->group_members, $hidden ); /** * REQUIRED. Build an array to be used by the class for column headers */ $this->_column_headers = array( $columns, $hidden, $sortable ); $args = array( 'post_type' => $this->post_type, 'post_status' => current_user_can( 'edit_private_products' ) ? [ 'private', 'publish' ] : [ 'publish' ], 'posts_per_page' => $this->per_page, 'paged' => $this->get_pagenum(), ); /** * Get Controlled or Uncontrolled items */ $this->set_controlled_query_data(); /** * Tax filter */ // Add product category to the tax query. if ( ! empty( $_REQUEST['product_cat'] ) ) { $this->taxonomies[] = array( 'taxonomy' => 'product_cat', 'field' => 'slug', 'terms' => esc_attr( $_REQUEST['product_cat'] ), ); } // Change the product type tax query (initialized in constructor) to the current queried type. if ( ! empty( $_REQUEST['product_type'] ) && ! empty( $this->wc_query_data['where'] ) ) { $type = esc_attr( $_REQUEST['product_type'] ); foreach ( $this->wc_query_data['where'] as $index => $query_arg ) { if ( isset( $query_arg['key'] ) && 'type' === $query_arg['key'] ) { if ( in_array( $type, [ 'downloadable', 'virtual' ] ) ) { $this->wc_query_data['where'][ $index ]['value'] = 'simple'; $this->wc_query_data['where'][] = array( 'key' => $type, 'value' => 1, 'type' => 'NUMERIC', ); } else { $this->wc_query_data['where'][ $index ]['value'] = $type; } break; } } } if ( $this->taxonomies ) { $args['tax_query'] = (array) apply_filters( 'atum/list_table/taxonomies', $this->taxonomies ); } /** * Supplier filter */ if ( ! empty( $_REQUEST['supplier'] ) && AtumCapabilities::current_user_can( 'read_supplier' ) ) { $supplier = absint( $_REQUEST['supplier'] ); if ( ! empty( $this->atum_query_data['where'] ) ) { $this->atum_query_data['where']['relation'] = 'AND'; } $this->atum_query_data['where'][] = array( 'key' => 'supplier_id', 'value' => $supplier, 'type' => 'NUMERIC', ); // This query does not get product variations and as each variation may have a distinct supplier, // we have to get them separately and to add their variables to the results. $this->supplier_variation_products = Suppliers::get_supplier_products( $supplier, [ 'product_variation' ] ); if ( ! empty( $this->supplier_variation_products ) ) { add_filter( 'atum/list_table/views_data_products', array( $this, 'add_supplier_variables_to_query' ), 10, 2 ); add_filter( 'atum/list_table/items', array( $this, 'add_supplier_variables_to_query' ), 10, 2 ); add_filter( 'atum/list_table/views_data_variations', array( $this, 'add_supplier_variations_to_query' ), 10, 2 ); } } /** * Extra meta args */ if ( ! empty( $this->extra_meta ) ) { $args['meta_query'][] = $this->extra_meta; } if ( ! empty( $_REQUEST['orderby'] ) ) { $order = ( isset( $_REQUEST['order'] ) && 'asc' === $_REQUEST['order'] ) ? 'ASC' : 'DESC'; $atum_sortable_columns = apply_filters( 'atum/list_table/atum_sortable_columns', $this->atum_sortable_columns ); // Columns starting by underscore are based in meta keys, so can be sorted. if ( '_' === substr( $_REQUEST['orderby'], 0, 1 ) ) { if ( array_key_exists( $_REQUEST['orderby'], $atum_sortable_columns ) ) { $this->atum_query_data['order'] = $atum_sortable_columns[ $_REQUEST['orderby'] ]; $this->atum_query_data['order']['order'] = $order; } // All the meta key based columns are numeric except the SKU. else { if ( '_sku' === $_REQUEST['orderby'] ) { $args['orderby'] = 'meta_value'; } else { $args['orderby'] = 'meta_value_num'; } $args['meta_key'] = $_REQUEST['orderby']; $args['order'] = $order; } } // Standard Fields. else { $args['orderby'] = $_REQUEST['orderby']; $args['order'] = $order; } } else { $args['orderby'] = 'title'; $args['order'] = 'ASC'; } /** * Searching */ if ( ! empty( $_REQUEST['search_column'] ) ) { $args['search_column'] = esc_attr( $_REQUEST['search_column'] ); } if ( ! empty( $_REQUEST['s'] ) ) { $args['s'] = sanitize_text_field( urldecode( stripslashes( $_REQUEST['s'] ) ) ); } // Let others play. $args = apply_filters( 'atum/list_table/prepare_items/args', $args ); // Build "Views Filters" and calculate totals. $this->set_views_data( $args ); $allow_query = TRUE; /** * REQUIRED. Register our pagination options & calculations */ $found_posts = isset( $this->count_views['count_all'] ) ? $this->count_views['count_all'] : 0; if ( ! empty( $_REQUEST['view'] ) ) { $view = esc_attr( $_REQUEST['view'] ); $allow_query = FALSE; foreach ( $this->id_views as $key => $post_ids ) { if ( $view === $key ) { $this->supplier_variation_products = array_intersect( $this->supplier_variation_products, $post_ids ); if ( ! empty( $post_ids ) ) { $get_parents = FALSE; foreach ( Globals::get_inheritable_product_types() as $inheritable_product_type ) { if ( ! empty( $this->container_products[ $inheritable_product_type ] ) ) { $get_parents = TRUE; break; } } // Add the parent products again to the query. $args['post__in'] = $get_parents ? array_merge( $this->get_parents( $post_ids ), $post_ids ) : $post_ids; $allow_query = TRUE; $found_posts = $this->count_views[ "count_$key" ]; } } } } if ( $allow_query ) { if ( ! empty( $this->excluded ) ) { if ( isset( $args['post__not_in'] ) ) { $args['post__not_in'] = array_merge( $args['post__not_in'], $this->excluded ); } else { $args['post__not_in'] = $this->excluded; } } // Setup the WP query. global $wp_query; // Pass through the ATUM query data and WC query data filters. add_filter( 'posts_clauses', array( $this, 'wc_product_data_query_clauses' ) ); add_filter( 'posts_clauses', array( $this, 'atum_product_data_query_clauses' ) ); $wp_query = new \WP_Query( $args ); remove_filter( 'posts_clauses', array( $this, 'wc_product_data_query_clauses' ) ); remove_filter( 'posts_clauses', array( $this, 'atum_product_data_query_clauses' ) ); $posts = $wp_query->posts; if ( $found_posts > 0 && empty( $posts ) ) { $args['paged'] = 1; $_REQUEST['paged'] = $args['paged']; // Pass through the ATUM query data filter. add_filter( 'posts_clauses', array( $this, 'wc_product_data_query_clauses' ) ); add_filter( 'posts_clauses', array( $this, 'atum_product_data_query_clauses' ) ); $wp_query = new \WP_Query( $args ); remove_filter( 'posts_clauses', array( $this, 'wc_product_data_query_clauses' ) ); remove_filter( 'posts_clauses', array( $this, 'atum_product_data_query_clauses' ) ); $posts = $wp_query->posts; } $product_ids = wp_list_pluck( $posts, 'ID' ); $this->current_products = $product_ids; // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison $total_pages = ( - 1 == $this->per_page || ! $wp_query->have_posts() ) ? 0 : ceil( $wp_query->found_posts / $this->per_page ); } else { $found_posts = $total_pages = 0; } /** * REQUIRED!!! * Save the sorted data to the items property, where can be used by the rest of the class. */ $this->items = apply_filters( 'atum/list_table/items', $posts, 'posts' ); $this->set_pagination_args( array( 'total_items' => $found_posts, 'per_page' => $this->per_page, 'total_pages' => $total_pages, 'orderby' => ! empty( $_REQUEST['orderby'] ) ? $_REQUEST['orderby'] : 'date', 'order' => ! empty( $_REQUEST['order'] ) ? $_REQUEST['order'] : 'desc', ) ); } /** * Set the query data for filtering the Controlled/Uncontrolled products. * * @since 1.5.0 */ protected function set_controlled_query_data() { if ( $this->show_controlled ) { $this->atum_query_data['where'] = array( array( 'key' => 'atum_controlled', 'value' => 1, 'type' => 'NUMERIC', ), ); } else { $this->atum_query_data['where'] = array( array( 'relation' => 'OR', array( 'key' => 'atum_controlled', 'value' => 0, 'type' => 'NUMERIC', ), array( 'key' => 'inheritable', 'value' => 1, 'type' => 'NUMERIC', ), ), ); } } /** * Filter the list table data to show compatible product types only * * @since 1.5.0 */ protected function set_product_types_query_data() { /** * If the site is not using the new tables, use the legacy way * * @since 1.5.0 * @deprecated Only for backwards compatibility and will be removed in a future version. */ if ( ! Helpers::is_using_new_wc_tables() ) { $this->taxonomies[] = array( 'taxonomy' => 'product_type', 'field' => 'slug', 'terms' => Globals::get_product_types(), ); } else { $this->wc_query_data['where'][] = array( 'key' => 'type', 'value' => Globals::get_product_types(), 'compare' => 'IN', ); } } /** * Customize the WP_Query to handle ATUM product data * * @since 1.5.0 * * @param array $pieces * * @return array */ public function atum_product_data_query_clauses( $pieces ) { return Helpers::product_data_query_clauses( $this->atum_query_data, $pieces ); } /** * Customize the WP_Query to handle WC product data from the new tables * * @since 1.5.0 * * @param array $pieces * * @return array */ public function wc_product_data_query_clauses( $pieces ) { return Helpers::product_data_query_clauses( $this->wc_query_data, $pieces, 'wc_products' ); } /** * Add the supplier's variable products to the filtered query * * @since 1.4.1.1 * * @param array $products * @param string $return_type Optional. The return type: 'ids' or 'posts'. * * @return array */ public function add_supplier_variables_to_query( $products, $return_type = 'ids' ) { foreach ( $this->supplier_variation_products as $index => $variation_id ) { $variation_product = Helpers::get_atum_product( $variation_id ); if ( ! is_a( $variation_product, '\WC_Product_Variation' ) ) { unset( $this->supplier_variation_products[ $index ] ); continue; } $is_controlled = Helpers::is_atum_controlling_stock( $variation_product ); if ( ( $this->show_controlled && ! $is_controlled ) || ( ! $this->show_controlled && $is_controlled ) ) { unset( $this->supplier_variation_products[ $index ] ); continue; } $variable_id = $variation_product->get_parent_id(); $product_ids = 'ids' === $return_type ? $products : wp_list_pluck( $products, 'ID' ); if ( ! is_array( $products ) || ! in_array( $variable_id, $product_ids ) ) { $this->container_products['all_variable'][] = $this->container_products['variable'][] = $variable_id; $products[] = 'posts' === $return_type ? get_post( $variable_id ) : $variable_id; } } return $products; } /** * Add the supplier's variation products to the filtered query * * @since 1.4.1.1 * * @param array $variations * @param array $products * * @return array */ public function add_supplier_variations_to_query( $variations, $products ) { return array_merge( $variations, $this->supplier_variation_products ); } /** * Set views for table filtering and calculate total value counters for pagination * * @since 0.0.2 * * @param array $args WP_Query arguments. */ protected function set_views_data( $args ) { /** * If the site is not using the new tables, use the legacy method * * @since 1.5.0 * @deprecated Only for backwards compatibility and will be removed in a future version. */ if ( ! Helpers::is_using_new_wc_tables() ) { $this->set_views_data_legacy( $args ); return; } global $wpdb; if ( $this->show_unmanaged_counters ) { $this->id_views = array_merge( $this->id_views, array( 'managed' => [], 'unm_in_stock' => [], 'unm_out_stock' => [], 'unm_back_order' => [], 'all_in_stock' => [], 'all_out_stock' => [], 'all_back_order' => [], ) ); $this->count_views = array_merge( $this->count_views, array( 'count_managed' => 0, 'count_unm_in_stock' => 0, 'count_unm_out_stock' => 0, 'count_unm_back_order' => 0, 'count_all_in_stock' => 0, 'count_all_out_stock' => 0, 'count_all_back_order' => 0, ) ); } // Get all the IDs in the two queries with no pagination. $args['fields'] = 'ids'; $args['posts_per_page'] = - 1; unset( $args['paged'] ); // TODO: PERHAPS THE TRANSIENT CAN BE USED MORE GENERICALLY TO AVOID REPETITIVE WORK. $all_transient = AtumCache::get_transient_key( 'list_table_all', array_merge( $args, $this->wc_query_data, $this->atum_query_data ) ); $products = AtumCache::get_transient( $all_transient ); if ( ! $products ) { global $wp_query; // Pass through the ATUM query data filter. add_filter( 'posts_clauses', array( $this, 'atum_product_data_query_clauses' ) ); $wp_query = new \WP_Query( apply_filters( 'atum/list_table/set_views_data/all_args', $args ) ); remove_filter( 'posts_clauses', array( $this, 'atum_product_data_query_clauses' ) ); $products = $wp_query->posts; // Save it as a transient to improve the performance. AtumCache::set_transient( $all_transient, $products ); } // Let others play here. $products = (array) apply_filters( 'atum/list_table/views_data_products', $products ); $this->count_views['count_all'] = count( $products ); if ( $this->is_filtering && empty( $products ) ) { return; } // If it's a search or a product filtering, include only the filtered items to search for children. $post_in = $this->is_filtering ? $products : array(); // Loop all the registered product types. if ( ! empty( $this->wc_query_data['where'] ) ) { foreach ( $this->wc_query_data['where'] as $wc_query_arg ) { if ( isset( $wc_query_arg['key'] ) && 'type' === $wc_query_arg['key'] ) { $types = (array) $wc_query_arg['value']; if ( in_array( 'variable', $types, TRUE ) ) { $variations = apply_filters( 'atum/list_table/views_data_variations', $this->get_children( 'variable', $post_in, 'product_variation' ), $post_in ); // Remove the variable containers from the array and add the variations. $products = array_unique( array_merge( array_diff( $products, $this->container_products['all_variable'] ), $variations ) ); } if ( in_array( 'grouped', $types, TRUE ) ) { $group_items = apply_filters( 'atum/list_table/views_data_grouped', $this->get_children( 'grouped', $post_in ), $post_in ); // Remove the grouped containers from the array and add the group items. $products = array_unique( array_merge( array_diff( $products, $this->container_products['all_grouped'] ), $group_items ) ); } // WC Subscriptions compatibility. if ( class_exists( '\WC_Subscriptions' ) && in_array( 'variable-subscription', $types, TRUE ) ) { $sc_variations = apply_filters( 'atum/list_table/views_data_sc_variations', $this->get_children( 'variable-subscription', $post_in, 'product_variation' ), $post_in ); // Remove the variable subscription containers from the array and add the subscription variations. $products = array_unique( array_merge( array_diff( $products, $this->container_products['all_variable_subscription'] ), $sc_variations ) ); } // Re-count the resulting products. $this->count_views['count_all'] = count( $products ); // The grouped items must count once per group they belongs to and once individually. if ( ! empty( $group_items ) ) { $this->count_views['count_all'] += count( $group_items ); } do_action( 'atum/list_table/after_children_count', $types, $this ); break; } } } // For the Uncontrolled items, we don't need to calculate stock totals. if ( ! $this->show_controlled ) { return; } if ( $products ) { $post_types = ( ! empty( $variations ) || ! empty( $sc_variations ) ) ? [ $this->post_type, 'product_variation' ] : [ $this->post_type ]; /* * Unmanaged products */ if ( $this->show_unmanaged_counters ) { $products_unmanaged = array(); $products_unmanaged_status = Helpers::get_unmanaged_products( $post_types, TRUE ); if ( ! empty( $products_unmanaged_status ) ) { // Filter the unmanaged (also removes uncontrolled). $products_unmanaged_status = array_filter( $products_unmanaged_status, function ( $row ) use ( $products ) { return in_array( $row[0], $products ); } ); $this->id_views['unm_in_stock'] = array_column( array_filter( $products_unmanaged_status, function ( $row ) { return 'instock' === $row[1]; } ), 0 ); $this->count_views['count_unm_in_stock'] = count( $this->id_views['unm_in_stock'] ); $this->id_views['unm_out_stock'] = array_column( array_filter( $products_unmanaged_status, function ( $row ) { return 'outofstock' === $row[1]; } ), 0 ); $this->count_views['count_unm_out_stock'] = count( $this->id_views['unm_out_stock'] ); $this->id_views['unm_back_order'] = array_column( array_filter( $products_unmanaged_status, function ( $row ) { return 'onbackorder' === $row[1]; } ), 0 ); $this->count_views['count_unm_back_order'] = count( $this->id_views['unm_back_order'] ); $products_unmanaged = array_column( $products_unmanaged_status, 0 ); $this->id_views['managed'] = array_diff( $products, $products_unmanaged ); $this->count_views['count_managed'] = count( $this->id_views['managed'] ); } } else { $products_unmanaged = array_column( Helpers::get_unmanaged_products( $post_types ), 0 ); } // Remove the unmanaged from the products list. if ( ! empty( $products_unmanaged ) ) { // Filter the unmanaged (also removes uncontrolled). $products_unmanaged = array_intersect( $products, $products_unmanaged ); $this->id_views['unmanaged'] = $products_unmanaged; $this->count_views['count_unmanaged'] = count( $products_unmanaged ); if ( ! empty( $products_unmanaged ) ) { $products = ! empty( $this->count_views['count_managed'] ) ? $this->id_views['managed'] : array_diff( $products, $products_unmanaged ); } } $products = (array) $products; /* * Products in stock */ $in_stock_args = array( 'post_type' => $post_types, 'posts_per_page' => - 1, 'fields' => 'ids', 'post__in' => $products, ); $temp_wc_query_data = $this->wc_query_data; $this->wc_query_data['where'][] = array( 'key' => 'stock_quantity', 'value' => 0, 'type' => 'NUMERIC', 'compare' => '>', ); $in_stock_transient = AtumCache::get_transient_key( 'list_table_in_stock', array_merge( $in_stock_args, $this->wc_query_data, $this->atum_query_data ) ); $products_in_stock = AtumCache::get_transient( $in_stock_transient ); if ( empty( $products_in_stock ) ) { // Pass through the WC query data filter (new tables). add_filter( 'posts_clauses', array( $this, 'wc_product_data_query_clauses' ) ); $products_in_stock = new \WP_Query( apply_filters( 'atum/list_table/set_views_data/in_stock_args', $in_stock_args ) ); remove_filter( 'posts_clauses', array( $this, 'wc_product_data_query_clauses' ) ); AtumCache::set_transient( $in_stock_transient, $products_in_stock ); } $products_in_stock = (array) $products_in_stock->posts; $this->wc_query_data = $temp_wc_query_data; // Restore the original value. $this->id_views['in_stock'] = $products_in_stock; $this->count_views['count_in_stock'] = count( $products_in_stock ); $products_not_stock = array_diff( $products, $products_in_stock, $products_unmanaged ); /** * Products on Back Order */ $back_order_args = array( 'post_type' => $post_types, 'posts_per_page' => - 1, 'fields' => 'ids', // The backorders prop is still being saved as meta key in the new tables. 'meta_query' => array( array( 'key' => '_backorders', 'value' => array( 'yes', 'notify' ), 'type' => 'char', 'compare' => 'IN', ), ), 'post__in' => $products_not_stock, ); $temp_wc_query_data = $this->wc_query_data; $this->wc_query_data['where'][] = array( 'key' => 'stock_quantity', 'value' => 0, 'type' => 'numeric', 'compare' => '<=', ); $back_order_transient = AtumCache::get_transient_key( 'list_table_back_order', array_merge( $back_order_args, $this->wc_query_data, $this->atum_query_data ) ); $products_back_order = AtumCache::get_transient( $back_order_transient ); if ( empty( $products_back_order ) && ! empty( $products_not_stock ) ) { // Pass through the WC query data filter (new tables). add_filter( 'posts_clauses', array( $this, 'wc_product_data_query_clauses' ) ); $products_back_order = new \WP_Query( apply_filters( 'atum/list_table/set_views_data/back_order_args', $back_order_args ) ); remove_filter( 'posts_clauses', array( $this, 'wc_product_data_query_clauses' ) ); AtumCache::set_transient( $back_order_transient, $products_back_order ); } $products_back_order = (array) $products_back_order->posts; $this->wc_query_data = $temp_wc_query_data; $this->id_views['back_order'] = $products_back_order; $this->count_views['count_back_order'] = count( $products_back_order ); // As the Group items might be displayed multiple times, we should count them multiple times too. if ( ! empty( $group_items ) && ( empty( $_REQUEST['product_type'] ) || 'grouped' !== $_REQUEST['product_type'] ) ) { $this->count_views['count_in_stock'] += count( array_intersect( $group_items, $products_in_stock ) ); $this->count_views['count_back_order'] += count( array_intersect( $group_items, $products_back_order ) ); } /** * Products with low stock */ if ( ! empty( $products_in_stock ) ) { $low_stock_transient = AtumCache::get_transient_key( 'list_table_low_stock', array_merge( $args, $this->wc_query_data, $this->atum_query_data ) ); $products_low_stock = AtumCache::get_transient( $low_stock_transient ); if ( empty( $products_low_stock ) ) { // Compare last seven days average sales per day * re-order days with current stock. $str_sales = " (SELECT ( SELECT MAX(CAST( meta_value AS SIGNED )) AS q FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key IN('_product_id', '_variation_id') AND order_item_id = itm.order_item_id ) AS IDs, CEIL(SUM(( SELECT meta_value FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_qty' AND order_item_id = itm.order_item_id ))/7*$this->days_to_reorder ) AS qty FROM $wpdb->posts AS orders INNER JOIN {$wpdb->prefix}woocommerce_order_items AS itm ON (orders.ID = itm.order_id) INNER JOIN $wpdb->postmeta AS order_meta ON (orders.ID = order_meta.post_id) WHERE orders.post_type = 'shop_order' AND orders.post_status IN ('wc-completed', 'wc-processing') AND itm.order_item_type ='line_item' AND order_meta.meta_key = '_paid_date' AND order_meta.meta_value >= '" . Helpers::date_format( '-7 days' ) . "' GROUP BY IDs) AS sales "; $str_statuses = " (SELECT p.ID, IF( CAST( IFNULL(sales.qty, 0) AS DECIMAL(10,2) ) <= CAST( IF( LENGTH(pr.stock_quantity) = 0 , 0, pr.stock_quantity) AS DECIMAL(10,2) ), TRUE, FALSE ) AS status FROM $wpdb->posts AS p LEFT JOIN {$wpdb->prefix}wc_products AS pr ON (p.ID = pr.product_id) LEFT JOIN " . $str_sales . " ON (p.ID = sales.IDs) WHERE p.post_type IN ('" . implode( "','", $post_types ) . "') AND p.ID IN (" . implode( ',', $products_in_stock ) . ') ) AS statuses '; $str_sql = apply_filters( 'atum/list_table/set_views_data/low_stock', "SELECT ID FROM $str_statuses WHERE status IS FALSE;" ); $products_low_stock = $wpdb->get_results( $str_sql ); // WPCS: unprepared SQL ok. $products_low_stock = wp_list_pluck( $products_low_stock, 'ID' ); AtumCache::set_transient( $low_stock_transient, $products_low_stock ); } $this->id_views['low_stock'] = (array) $products_low_stock; $this->count_views['count_low_stock'] = count( $products_low_stock ); } /** * Products out of stock */ $products_out_stock = array_diff( $products_not_stock, $products_back_order ); $this->id_views['out_stock'] = $products_out_stock; $this->count_views['count_out_stock'] = max( 0, $this->count_views['count_all'] - $this->count_views['count_in_stock'] - $this->count_views['count_back_order'] - $this->count_views['count_unmanaged'] ); if ( $this->show_unmanaged_counters ) { /** * Calculate totals */ $this->id_views['all_in_stock'] = array_merge( $this->id_views['in_stock'], $this->id_views['unm_in_stock'] ); $this->count_views['count_all_in_stock'] = $this->count_views['count_in_stock'] + $this->count_views['count_unm_in_stock']; $this->id_views['all_out_stock'] = array_merge( $this->id_views['out_stock'], $this->id_views['unm_out_stock'] ); $this->count_views['count_all_out_stock'] = $this->count_views['count_out_stock'] + $this->count_views['count_unm_out_stock']; $this->id_views['all_back_order'] = array_merge( $this->id_views['back_order'], $this->id_views['unm_back_order'] ); $this->count_views['count_all_back_order'] = $this->count_views['count_back_order'] + $this->count_views['count_unm_back_order']; } } } /** * Print column headers, accounting for hidden and sortable columns * * @since 1.4.5 * * @param bool $with_id Whether to set the id attribute or not. */ public function print_column_headers( $with_id = TRUE ) { list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); $group_members = wp_list_pluck( $this->group_members, 'members' ); $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); $current_url = remove_query_arg( 'paged', $current_url ); $current_orderby = isset( $_GET['orderby'] ) ? esc_attr( $_GET['orderby'] ) : ''; $current_order = ( ! isset( $_GET['order'] ) || 'desc' === $_GET['order'] ) ? 'desc' : 'asc'; if ( ! empty( $columns['cb'] ) ) { static $cb_counter = 1; $columns['cb'] = '' . ''; $cb_counter++; } foreach ( $columns as $column_key => $column_display_name ) { $class = array( 'manage-column', "column-$column_key" ); if ( in_array( $column_key, $hidden ) ) { $class[] = 'hidden'; } // Check if it's a numeric column. if ( ! empty( $this->default_searchable_columns['numeric'] ) && is_array( $this->default_searchable_columns['numeric'] ) && in_array( $column_key, $this->default_searchable_columns['numeric'], TRUE ) ) { $class[] = 'numeric'; } if ( 'cb' === $column_key ) { $class[] = 'check-column'; } elseif ( in_array( $column_key, array( 'posts', 'comments', 'links' ) ) ) { $class[] = 'num'; } if ( $column_key === $primary ) { $class[] = 'column-primary'; } // Add the group key as class. foreach ( $group_members as $group_key => $members ) { if ( in_array( $column_key, $members ) ) { $class[] = $group_key; break; } } if ( isset( $sortable[ $column_key ] ) ) { list( $orderby, $desc_first ) = $sortable[ $column_key ]; if ( $current_orderby === $orderby ) { $order = 'asc' === $current_order ? 'desc' : 'asc'; $class[] = 'sorted'; $class[] = $current_order; } else { $order = $desc_first ? 'desc' : 'asc'; $class[] = 'sortable'; $class[] = $desc_first ? 'asc' : 'desc'; } $sorting_params = compact( 'orderby', 'order' ); $sorting_url = esc_url( add_query_arg( $sorting_params, $current_url ) ); $hash_params = http_build_query( array_merge( $this->query_filters, $sorting_params ) ); $column_display_name = '' . $column_display_name . ''; } $tag = 'cb' === $column_key ? 'td' : 'th'; $scope = 'th' === $tag ? 'scope="col"' : ''; $id = $with_id ? "id='$column_key'" : ''; if ( ! empty( $class ) ) { $class = "class='" . join( ' ', $class ) . "'"; } echo "<$tag $scope $id $class>$column_display_name"; // WPCS: XSS ok. } } /** * Prints the columns that groups the distinct header columns * * @since 0.0.1 */ public function print_group_columns() { if ( ! empty( $this->group_columns ) ) { echo ''; foreach ( $this->group_columns as $group_column ) { $data = $group_column['collapsed'] ? ' data-collapsed="1"' : ''; echo '' . '' . $group_column['title'] . ''; // WPCS: XSS ok. if ( $group_column['toggler'] ) { /* translators: the column group title */ $data_tip = ! self::$is_report ? ' data-tip="' . esc_attr( sprintf( __( "Show/Hide the '%s' columns", ATUM_TEXT_DOMAIN ), $group_column['title'] ) ) . '"' : ''; echo ''; // WPCS: XSS ok. } echo ''; } echo ''; } } /** * Prints the totals columns on totals row at table footer * * @since 1.4.2 */ public function print_totals_columns() { // Does not show the totals row if there are no results. if ( empty( $this->items ) ) { return; } /* @noinspection PhpUnusedLocalVariableInspection */ list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); $group_members = wp_list_pluck( $this->group_members, 'members' ); $column_keys = array_keys( $columns ); $first_column = current( $column_keys ); $second_column = next( $column_keys ); // Let to adjust the totals externally if needed. $this->totalizers = apply_filters( 'atum/list_table/totalizers', $this->totalizers ); foreach ( $columns as $column_key => $column_display ) { $class = array( 'manage-column', "column-$column_key" ); $colspan = ''; if ( in_array( $column_key, $hidden ) ) { $class[] = 'hidden'; } if ( $first_column === $column_key ) { $class[] = 'totals-heading'; $colspan = 'colspan="2"'; $column_display = '' . __( 'Totals', ATUM_TEXT_DOMAIN ) . ''; } elseif ( $second_column === $column_key ) { continue; // Get rid of the second column as the first one will have a colspan. } elseif ( in_array( $column_key, array_keys( $this->totalizers ) ) ) { $total = $this->totalizers[ $column_key ]; $total_class = $total < 0 ? ' class="danger"' : ''; $column_display = "" . $total . ''; } else { $column_display = self::EMPTY_COL; } if ( $column_key === $primary ) { $class[] = 'column-primary'; } // Add the group key as class. foreach ( $group_members as $group_key => $members ) { if ( in_array( $column_key, $members ) ) { $class[] = $group_key; break; } } $tag = 'cb' === $column_key ? 'td' : 'th'; $scope = 'th' === $tag ? 'scope="col"' : ''; if ( ! empty( $class ) ) { $class = "class='" . join( ' ', $class ) . "'"; } echo "<$tag $scope $class $colspan>$column_display"; // WPCS: XSS ok. } } /** * Adds the data needed for ajax filtering, sorting and pagination and displays the table * * @since 0.0.1 */ public function display() { do_action( 'atum/list_table/before_display', $this ); $singular = $this->_args['singular']; $this->display_tablenav( 'top' ); $this->screen->render_screen_reader_content( 'heading_list' ); ?>
print_group_columns(); ?> print_column_headers(); ?> > display_rows_or_placeholder(); ?> show_totals ) : ?> print_totals_columns(); ?> print_column_headers( FALSE ); ?>
display_tablenav( 'bottom' ); global $plugin_page; // Prepare JS vars. $vars = array( 'listUrl' => esc_url( add_query_arg( 'page', $plugin_page, admin_url() ) ), 'perPage' => $this->per_page, 'showCb' => $this->show_cb, 'order' => isset( $this->_pagination_args['order'] ) ? $this->_pagination_args['order'] : '', 'orderby' => isset( $this->_pagination_args['orderby'] ) ? $this->_pagination_args['orderby'] : '', 'nonce' => wp_create_nonce( 'atum-list-table-nonce' ), 'stickyColumnsNonce' => wp_create_nonce( 'atum-sticky-columns-button-nonce' ), 'ajaxFilter' => Helpers::get_option( 'enable_ajax_filter', 'yes' ), 'setValue' => __( 'Set the %% value', ATUM_TEXT_DOMAIN ), 'setButton' => __( 'Set', ATUM_TEXT_DOMAIN ), 'saveButton' => __( 'Save Data', ATUM_TEXT_DOMAIN ), 'ok' => __( 'OK', ATUM_TEXT_DOMAIN ), 'noItemsSelected' => __( 'No Items Selected', ATUM_TEXT_DOMAIN ), 'selectItems' => __( 'Please, check the boxes for all the products you want to change in bulk', ATUM_TEXT_DOMAIN ), 'applyBulkAction' => __( 'Apply Bulk Action', ATUM_TEXT_DOMAIN ), 'applyAction' => __( 'Apply Action', ATUM_TEXT_DOMAIN ), 'productLocations' => __( 'Product Locations', ATUM_TEXT_DOMAIN ), 'editProductLocations' => __( 'Edit Product Locations', ATUM_TEXT_DOMAIN ), 'editLocationsInfo' => __( 'Click on the location icons you want to set for this product. Locations marked with blue icons will be set and with grey icons will be unset.', ATUM_TEXT_DOMAIN ), 'textToShow' => __( 'Text to show?', ATUM_TEXT_DOMAIN ), 'locationsSaved' => __( 'Values Saved', ATUM_TEXT_DOMAIN ), 'done' => __( 'Done!', ATUM_TEXT_DOMAIN ), 'searchableColumns' => $this->default_searchable_columns, 'stickyColumns' => $this->sticky_columns, 'dateSelectorFilters' => [ 'best_seller', 'worst_seller' ], 'setTimeWindow' => __( 'Set Time Window', ATUM_TEXT_DOMAIN ), 'selectDateRange' => __( 'Select the date range to filter the produts.', ATUM_TEXT_DOMAIN ), 'from' => __( 'From', ATUM_TEXT_DOMAIN ), 'to' => __( 'To', ATUM_TEXT_DOMAIN ), 'apply' => __( 'Apply', ATUM_TEXT_DOMAIN ), 'emptyCol' => self::EMPTY_COL, ); $vars = array_merge( $vars, Globals::get_date_time_picker_js_vars() ); if ( $this->first_edit_key ) { $vars['firstEditKey'] = $this->first_edit_key; $vars['important'] = __( 'Important!', ATUM_TEXT_DOMAIN ); $vars['preventLossNotice'] = __( "To prevent any loss of data, please, hit the blue 'Save Data' button at the top left after completing edits.", ATUM_TEXT_DOMAIN ); } $vars = apply_filters( 'atum/list_table/js_vars', $vars ); wp_localize_script( 'atum-list', 'atumListVars', $vars ); do_action( 'atum/list_table/after_display', $this ); } /** * Generate the table navigation above or below the table * Just the parent function but removing the nonce fields that are not required here * * @since 0.0.1 * * @param string $which 'top' or 'bottom' table nav. */ protected function display_tablenav( $which ) { ?>
get_bulk_actions() ) ) : ?>
bulk_actions( $which ); ?>
extra_tablenav( $which ); ?>
pagination( $which ); echo str_replace( '
post_type ); echo $post_type_obj->labels->not_found; // WPCS: XSS ok. if ( ! empty( $_REQUEST['s'] ) ) { /* translators: the search query */ printf( __( " with query '%s'", ATUM_TEXT_DOMAIN ), stripslashes( esc_attr( $_REQUEST['s'] ) ) ); // WPCS: XSS ok. } } /** * Display the pagination. * * @since 1.4.3 * * @param string $which */ protected function pagination( $which ) { if ( empty( $this->_pagination_args ) ) { $output = '' . esc_html__( '0 items', ATUM_TEXT_DOMAIN ) . ''; echo "
$output
"; // WPCS: XSS ok. return; } $total_items = $this->_pagination_args['total_items']; $total_pages = $this->_pagination_args['total_pages']; if ( 'top' === $which && $total_pages > 1 ) { $this->screen->render_screen_reader_content( 'heading_pagination' ); } /* translators: the number of items */ $output = '' . sprintf( _n( '%s item', '%s items', $total_items, ATUM_TEXT_DOMAIN ), number_format_i18n( $total_items ) ) . ''; $current = $this->get_pagenum(); $removable_query_args = wp_removable_query_args(); $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); $current_url = remove_query_arg( $removable_query_args, $current_url ); $page_links = array(); $total_pages_before = ''; $total_pages_after = ''; $disable_first = $disable_last = $disable_prev = $disable_next = FALSE; if ( 1 === $current ) { $disable_first = TRUE; $disable_prev = TRUE; } elseif ( 2 === $current ) { $disable_first = TRUE; } if ( $current === $total_pages ) { $disable_last = TRUE; $disable_next = TRUE; } elseif ( $current === $total_pages - 1 ) { $disable_last = TRUE; } if ( $disable_first ) { $page_links[] = ''; } else { $page_links[] = sprintf( "%3\$s", esc_url( remove_query_arg( 'paged', $current_url ) ), http_build_query( array_merge( $this->query_filters, [ 'paged' => 1 ] ) ), __( 'First page', ATUM_TEXT_DOMAIN ), '«' ); } if ( $disable_prev ) { $page_links[] = ''; } else { $prev_page = max( 1, $current - 1 ); $page_links[] = sprintf( "%3\$s", esc_url( add_query_arg( 'paged', $prev_page, $current_url ) ), http_build_query( array_merge( $this->query_filters, [ 'paged' => $prev_page ] ) ), __( 'Previous page', ATUM_TEXT_DOMAIN ), '‹' ); } if ( 'bottom' === $which ) { $current_page_style = 'tablenav-current-page'; $html_current_page = $current; $total_pages_before = '' . __( 'Current Page', ATUM_TEXT_DOMAIN ) . ''; } else { $current_page_style = ''; /* @noinspection PhpFormatFunctionParametersMismatchInspection */ $html_current_page = sprintf( "%1\$s", '', $current, strlen( $total_pages ) ); } $html_total_pages = sprintf( "%s", number_format_i18n( $total_pages ) ); /* translators: first one is the current page number and sesond is the total number of pages */ $page_links[] = $total_pages_before . sprintf( _x( '%1$s of %2$s', 'paging', ATUM_TEXT_DOMAIN ), '' . $html_current_page . '', $html_total_pages ) . $total_pages_after; if ( $disable_next ) { $page_links[] = ''; } else { $next_page = min( $total_pages, $current + 1 ); $page_links[] = sprintf( "%3\$s", esc_url( add_query_arg( 'paged', $next_page, $current_url ) ), http_build_query( array_merge( $this->query_filters, [ 'paged' => $next_page ] ) ), __( 'Next page', ATUM_TEXT_DOMAIN ), '›' ); } if ( $disable_last ) { $page_links[] = ''; } else { $page_links[] = sprintf( "%3\$s", esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ), http_build_query( array_merge( $this->query_filters, [ 'paged' => $total_pages ] ) ), __( 'Last page', ATUM_TEXT_DOMAIN ), '»' ); } $pagination_links_class = 'pagination-links'; $output .= "\n" . join( "\n", $page_links ) . ''; if ( $total_pages ) { $page_class = $total_pages < 2 ? ' one-page' : ''; } else { $page_class = ' no-pages'; } $this->_pagination = "
$output
"; echo $this->_pagination; // WPCS: XSS ok. } /** * Get a list of CSS classes for the WP_List_Table table tag. Deleted 'fixed' from standard function * * @since 0.0.2 * * @return array List of CSS classes for the table tag. */ protected function get_table_classes() { return array( 'widefat', $this->_args['plural'] ); } /** * A wrapper to get the right product ID (or variation ID) * * @since 1.2.1 * * @return int */ protected function get_current_product_id() { if ( 'variation' === $this->product->get_type() ) { /** * Deprecated notice * * @deprecated * The get_variation_id() method was deprecated in WC 3.0.0 * In newer versions the get_id() method always be the variation_id if it's a variation */ /* @noinspection PhpDeprecationInspection */ return version_compare( wc()->version, '3.0.0', '<' ) ? $this->product->get_variation_id() : $this->product->get_id(); } return $this->product->get_id(); } /** * Gets the array needed to print html group columns in the table * * @since 0.0.1 * * @param array $group_members Parameter from __contruct method. * @param array $hidden Hidden columns. * * @return array */ public function calc_groups( $group_members, $hidden ) { $response = array(); foreach ( $group_members as $name => $group ) { $counter = 0; foreach ( $group['members'] as $member ) { if ( ! in_array( $member, $hidden ) ) { $counter ++; } } // Add the group only if there are columns within. if ( $counter ) { $response[] = array( 'name' => $name, 'title' => $group['title'], 'colspan' => $counter, 'toggler' => ! empty( $group['toggler'] ) && $group['toggler'], 'collapsed' => ! empty( $group['collapsed'] ) && $group['collapsed'], ); } } return $response; } /** * Return the group of columns that a specific column belongs to or false * * @sinece 0.0.5 * * @param string $column The column to search to. * * @return bool|string */ public function search_group_columns( $column ) { foreach ( $this->group_members as $name => $group_member ) { if ( in_array( $column, $group_member['members'] ) ) { return $name; } } return FALSE; } /** * Search products by: A (post_title, post_excerpt, post_content ), B (posts.ID), C (posts.title), D (other meta fields wich can be numeric or not) * * @since 1.4.8 * * @param string $where * * @return string */ public function product_search( $where ) { global $pagenow, $wpdb; // Changed the WooCommerce's "product_search" filter to allow Ajax requests. /* @see \WC_Admin_Post_Types::product_search */ if ( ! is_admin() || ! in_array( $pagenow, array( 'edit.php', 'admin-ajax.php' ) ) || ! isset( $_REQUEST['s'], $_REQUEST['action'] ) || FALSE === strpos( $_REQUEST['action'], ATUM_PREFIX ) ) { return $where; } // Prevent keyUp problems (scenario: do a search with s and search_column, clean s, change search_column... and you will get nothing (s still set on url)). if ( 0 === strlen( $_REQUEST['s'] ) ) { return 'AND ( 1 = 1 )'; } // If we don't get any result looking for a field, we must force an empty result before // WP tries to query {$wpdb->posts}.ID IN ( 'empty value' ), which raises an error. $where_without_results = "AND ( {$wpdb->posts}.ID = -1 )"; $search_column = esc_attr( stripslashes( $_REQUEST['search_column'] ) ); $search_term = sanitize_text_field( urldecode( stripslashes( $_REQUEST['s'] ) ) ); $cache_key = AtumCache::get_cache_key( 'product_search', [ $search_column, $search_term ] ); $where = AtumCache::get_cache( $cache_key, ATUM_TEXT_DOMAIN, FALSE, $has_cache ); if ( $has_cache ) { return $where; } $search_terms = $this->parse_search( $search_term ); if ( empty( $search_terms ) ) { AtumCache::set_cache( $cache_key, $where_without_results ); return $where_without_results; } // // Regular search in post_title, post_excerpt and post_content (with no column selected). // --------------------------------------------------------------------------------------! if ( empty( $search_column ) ) { $search_query = $this->build_search_query( $search_terms ); $query = " SELECT ID, post_type, post_parent FROM $wpdb->posts WHERE post_type IN ('product', 'product_variation') AND $search_query "; $search_terms_ids = $wpdb->get_results( $query, ARRAY_A ); // WPCS: unprepared SQL ok. if ( empty( $search_terms_ids ) ) { AtumCache::set_cache( $cache_key, $where_without_results ); return $where_without_results; } // Remove duplicate values from a multi-dimensional array. $search_terms_ids = array_map( 'unserialize', array_unique( array_map( 'serialize', $search_terms_ids ) ) ); $search_terms_ids_arr = array(); foreach ( $search_terms_ids as $product ) { if ( 'product' === $product['post_type'] ) { array_push( $search_terms_ids_arr, $product['ID'] ); } // Add parent and current. else { array_push( $search_terms_ids_arr, $product['ID'] ); array_push( $search_terms_ids_arr, $product['post_parent'] ); } } $search_terms_ids_arr = array_unique( $search_terms_ids_arr ); $search_terms_ids_str = implode( ',', $search_terms_ids_arr ); $where = "AND ( {$wpdb->posts}.ID IN ($search_terms_ids_str) )"; } else { if ( Helpers::in_multi_array( $search_column, Globals::SEARCHABLE_COLUMNS ) ) { // // Search by ID. // -------------! if ( 'ID' === $search_column ) { $search_query = $this->build_search_query( $search_terms, $search_column, 'int' ); // Get all (parent and variations, and build where). $query = " SELECT ID, post_type, post_parent FROM $wpdb->posts WHERE $search_query AND post_type IN ('product', 'product_variation') "; $search_term_id = $wpdb->get_row( $query ); // WPCS: unprepared SQL ok. if ( empty( $search_term_id ) ) { AtumCache::set_cache( $cache_key, $where_without_results ); return $where_without_results; } $search_terms_ids_str = ''; if ( 'product' === $search_term_id->post_type ) { $search_terms_ids_str .= $search_term_id->ID . ','; // If has children, add them. $product = wc_get_product( $search_term_id->ID ); // Get an array of the children IDs (if any). $children = $product->get_children(); if ( ! empty( $children ) ) { foreach ( $children as $child ) { $search_terms_ids_str .= $child . ','; } } } // Add parent and current. else { $search_terms_ids_str .= $search_term_id->post_parent . ','; $search_terms_ids_str .= $search_term_id->ID . ','; } $search_terms_ids_str = rtrim( $search_terms_ids_str, ',' ); $where = "AND ( $wpdb->posts.ID IN ($search_terms_ids_str) )"; } // // Search by Supplier name. // ------------------------! elseif ( Suppliers::SUPPLIER_META_KEY === $search_column ) { $search_query = $this->build_search_query( $search_terms, 'post_title' ); // Get suppliers. $query = $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = %s AND $search_query", Suppliers::POST_TYPE ); // WPCS: unprepared SQL ok. $search_supplier_ids = $wpdb->get_col( $query ); // WPCS: unprepared SQL ok. if ( empty( $search_supplier_ids ) ) { AtumCache::set_cache( $cache_key, $where_without_results ); return $where_without_results; } $supplier_products = array(); // Avoid endless loops. remove_filter( 'posts_search', array( $this, 'product_search' ), 10 ); foreach ( $search_supplier_ids as $supplier_id ) { $supplier_products = array_merge( $supplier_products, Suppliers::get_supplier_products( $supplier_id ) ); } add_filter( 'posts_search', array( $this, 'product_search' ), 10, 2 ); $supplier_products = array_unique( $supplier_products ); if ( empty( $supplier_products ) ) { AtumCache::set_cache( $cache_key, $where_without_results ); return $where_without_results; } $where = "AND $wpdb->posts.ID IN (" . implode( ',', $supplier_products ) . ')'; } // // Search by title and other meta fields. // --------------------------------------! else { $atum_data_table = $wpdb->prefix . Globals::ATUM_PRODUCT_DATA_TABLE; // Title field is not in meta. if ( 'title' === $search_column ) { $search_query = $this->build_search_query( $search_terms, 'post_title' ); $query = " SELECT ID, post_type, post_parent FROM $wpdb->posts WHERE $search_query AND post_type IN ('product', 'product_variation') "; } // Numeric fields. elseif ( in_array( $search_column, Globals::SEARCHABLE_COLUMNS['numeric'] ) ) { // Search by purchase price using the new ATUM data table. if ( Globals::PURCHASE_PRICE_KEY === $search_column ) { $search_query = $this->build_search_query( $search_terms, 'purchase_price', 'float', 'apd' ); $meta_where = apply_filters( 'atum/list_table/product_search/numeric_meta_where', $search_query, $search_column, $search_terms ); $query = " SELECT DISTINCT p.ID, p.post_type, p.post_parent FROM $wpdb->posts p LEFT JOIN $atum_data_table apd ON (p.ID = apd.product_id) WHERE p.post_type IN ('product', 'product_variation') AND $meta_where "; } // Search using the new WC tables. elseif ( Helpers::is_using_new_wc_tables() ) { $search_column = ltrim( $search_column, '_' ); // The _stock meta key was renamed to stock_quantity in the new products table. if ( 'stock' === $search_column ) { $search_column = 'stock_quantity'; } $search_query = $this->build_search_query( $search_terms, $search_column, 'float', 'wcd' ); $meta_where = apply_filters( 'atum/list_table/product_search/numeric_meta_where', $search_query, $search_column, $search_terms ); $query = " SELECT DISTINCT p.ID, p.post_type, p.post_parent FROM $wpdb->posts p LEFT JOIN {$wpdb->prefix}wc_products wcd ON (p.ID = wcd.product_id) WHERE p.post_type IN ('product', 'product_variation') AND $meta_where "; } // Search using the old way (meta keys). /* @deprecated */ else { $search_query = $this->build_search_query( $search_terms, $search_column, 'string', 'pm', TRUE ); $meta_where = apply_filters( 'atum/list_table/product_search/numeric_meta_where', $search_query, $search_column, $search_terms ); $query = " SELECT DISTINCT p.ID, p.post_type, p.post_parent FROM $wpdb->posts p LEFT JOIN $wpdb->postmeta pm ON (p.ID = pm.post_id) WHERE p.post_type IN ('product', 'product_variation') AND $meta_where "; } } // String fields. else { // Search by supplier SKU. if ( Suppliers::SUPPLIER_SKU_META_KEY === $search_column ) { $search_query = $this->build_search_query( $search_terms, 'supplier_sku', 'string', 'apd' ); $meta_where = apply_filters( 'atum/list_table/product_search/string_meta_where', $search_query, $search_column, $search_terms ); $query = " SELECT DISTINCT p.ID, p.post_type, p.post_parent FROM $wpdb->posts p LEFT JOIN $atum_data_table apd ON (p.ID = apd.product_id) WHERE p.post_type IN ('product', 'product_variation') AND $meta_where "; } // Search using the new WC tables. elseif ( Helpers::is_using_new_wc_tables() ) { $search_column = ltrim( $search_column, '_' ); // The _stock meta key was renamed to stock_quantity in the new products table. if ( 'stock' === $search_column ) { $search_column = 'stock_quantity'; } $search_query = $this->build_search_query( $search_terms, $search_column, 'string', 'wcd' ); $meta_where = apply_filters( 'atum/list_table/product_search/numeric_meta_where', $search_query, $search_column, $search_terms ); $query = " SELECT DISTINCT p.ID, p.post_type, p.post_parent FROM $wpdb->posts p LEFT JOIN {$wpdb->prefix}wc_products wcd ON (p.ID = wcd.product_id) WHERE p.post_type IN ('product', 'product_variation') AND $meta_where "; } // Search using the old way. /* @deprecated */ else { $search_query = $this->build_search_query( $search_terms, $search_column, 'string', 'pm', TRUE ); $meta_where = apply_filters( 'atum/list_table/product_search/string_meta_where', $search_query, $search_column, $search_terms ); $query = "SELECT p.ID, p.post_type, p.post_parent FROM $wpdb->posts p LEFT JOIN $wpdb->postmeta pm ON (p.ID = pm.post_id) WHERE p.post_type IN ('product', 'product_variation') AND $meta_where "; } } $search_terms_ids = $wpdb->get_results( $query ); // WPCS: unprepared SQL ok. if ( empty( $search_terms_ids ) ) { AtumCache::set_cache( $cache_key, $where_without_results ); return $where_without_results; } $search_terms_ids_str = ''; foreach ( $search_terms_ids as $term_id ) { if ( 'product' === $term_id->post_type ) { $search_terms_ids_str .= "$term_id->ID,"; $product = wc_get_product( $term_id->ID ); $children = $product->get_children(); if ( ! empty( $children ) ) { foreach ( $children as $child ) { $search_terms_ids_str .= $child . ','; } } } // Add parent and current. else { $search_terms_ids_str .= "'$term_id->ID',"; $search_terms_ids_str .= "'$term_id->post_parent',"; } } // Removes last comma. $search_terms_ids_str = rtrim( $search_terms_ids_str, ',' ); $where = "AND ( $wpdb->posts.ID IN ($search_terms_ids_str) )"; } } } AtumCache::set_cache( $cache_key, $where ); return $where; } /** * Parse the search term submitted and return an array of search terms * * @since 1.5.2 * * @param string $search_term * * @return array */ protected function parse_search( $search_term ) { $search_terms = array(); // There are no line breaks in input fields. $search_term = str_replace( array( "\r", "\n" ), '', $search_term ); if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', $search_term, $matches ) ) { // // Get stopwords. // --------------! /* translators: This doesn't need to be translated in ATUM, as it's getting the translation from WordPress core. */ $words = explode( ',', _x( 'about,an,are,as,at,be,by,com,for,from,how,in,is,it,of,on,or,that,the,this,to,was,what,when,where,who,will,with,www', 'Comma-separated list of search stopwords in your language' ) ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain $stopwords = array(); foreach ( $words as $word ) { $word = trim( $word, "\r\n\t " ); if ( $word ) { $stopwords[] = $word; } } /** * Filters stopwords used when parsing search terms. * Note that it uses the same filter name as in WP_Query class for compatibility. * * @param array $stopwords Stopwords. */ $stopwords = apply_filters( 'wp_search_stopwords', $stopwords ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound // // Parse search terms. // -------------------! $strtolower = function_exists( 'mb_strtolower' ) ? 'mb_strtolower' : 'strtolower'; foreach ( $matches[0] as $term ) { // Keep before/after spaces when term is for exact match. $term = preg_match( '/^".+"$/', $term ) ? trim( $term, "\"'" ) : trim( $term, "\"' " ); // Avoid single A-Z and single dashes. if ( ! $term || ( 1 === strlen( $term ) && preg_match( '/^[a-z\-]$/i', $term ) ) ) { continue; } if ( in_array( call_user_func( $strtolower, $term ), $stopwords, TRUE ) ) { continue; } $search_terms[] = $term; } // If the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence. if ( empty( $search_terms ) || count( $search_terms ) > 9 ) { $search_terms = array( $search_term ); } } else { $search_terms = array( $search_term ); } return $search_terms; } /** * Build the search SQL query for the given search terms * * @since 1.5.2 * * @param array $search_terms An array of search terms. * @param string $column Optional. If passed will search in the specified table column. * @param string $format Optional. The format that has that column. * @param string $table_prefix Optional. If passed, this prefix will be added as table alias to the column names. * @param bool $is_meta_search Optional. Whether the search is being performed for meta keys. * * @return string */ protected function build_search_query( $search_terms, $column = '', $format = 'string', $table_prefix = '', $is_meta_search = FALSE ) { global $wpdb; $search_query = $search_and = ''; /** * Filters the prefix that indicates that a search term should be excluded from results. * Note that uses the WP_Query's filter name for compatibility. * * @param string $exclusion_prefix The prefix. Default '-'. Returning an empty value disables exclusions. */ $exclusion_prefix = apply_filters( 'wp_query_search_exclusion_prefix', '-' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound foreach ( $search_terms as $term ) { // If there is an $exclusion_prefix, terms prefixed with it should be excluded. $exclude = $exclusion_prefix && ( substr( $term, 0, 1 ) === $exclusion_prefix ); if ( $exclude ) { $operator = 'string' === $format ? 'NOT LIKE' : '!='; $andor_op = 'AND'; $term = substr( $term, 1 ); } else { $operator = 'string' === $format ? 'LIKE' : '='; $andor_op = 'OR'; } switch ( $format ) { case 'int': $term = intval( $term ); break; case 'float': $term = floatval( $term ); break; default: $term = "'%" . esc_sql( $wpdb->esc_like( $term ) ) . "%'"; break; } // Post meta search. if ( $is_meta_search ) { $meta_key_column = $table_prefix ? "$table_prefix.meta_key" : 'meta_key'; $meta_value_column = $table_prefix ? "$table_prefix.meta_value" : 'meta_value'; $search_query .= "{$search_and}(($meta_key_column = '$column' AND $meta_value_column $operator $term))"; } // Regular search. elseif ( empty( $column ) ) { $search_query .= "{$search_and}(({$wpdb->posts}.post_title $operator $term) $andor_op ({$wpdb->posts}.post_excerpt $operator $term) $andor_op ({$wpdb->posts}.post_content $operator $term))"; } // Search in column. else { $column = $table_prefix ? "$table_prefix.$column" : $column; $search_query .= "{$search_and}(($column $operator $term))"; } $search_and = ' AND '; } return $search_query; } /** * Handle an incoming ajax request * Called by the \Ajax class * * @since 0.0.1 */ public function ajax_response() { $this->prepare_items(); extract( $this->_args ); extract( $this->_pagination_args, EXTR_SKIP ); ob_start(); if ( ! empty( $_REQUEST['no_placeholder'] ) ) { $this->display_rows(); } else { $this->display_rows_or_placeholder(); } $rows = ob_get_clean(); ob_start(); $this->print_column_headers(); $headers = ob_get_clean(); ob_start(); $this->display_tablenav( 'top' ); $extra_tablenav_top = ob_get_clean(); ob_start(); $this->display_tablenav( 'bottom' ); $extra_tablenav_bottom = ob_get_clean(); ob_start(); $this->views(); $views = ob_get_clean(); $response = array( 'rows' => $rows, 'extra_t_n' => array( 'top' => $extra_tablenav_top, 'bottom' => $extra_tablenav_bottom, ), 'column_headers' => $headers, 'views' => $views, ); if ( isset( $_REQUEST['paged'] ) && ! empty( $_REQUEST['paged'] ) ) { $response['paged'] = $_REQUEST['paged']; } if ( $this->show_totals ) { ob_start(); $this->print_totals_columns(); $response['totals'] = ob_get_clean(); } if ( isset( $total_items ) ) { /* translators: the number of items */ $response['total_items_i18n'] = sprintf( _n( '%s item', '%s items', $total_items, ATUM_TEXT_DOMAIN ), number_format_i18n( $total_items ) ); } if ( isset( $total_pages ) ) { $response['total_pages'] = $total_pages; $response['total_pages_i18n'] = number_format_i18n( $total_pages ); } wp_send_json( $response ); } /** * Enqueue the required scripts * * @since 0.0.1 * * @param string $hook */ public function enqueue_scripts( $hook ) { // Sweet Alert 2. wp_register_style( 'sweetalert2', ATUM_URL . 'assets/css/vendor/sweetalert2.min.css', array(), ATUM_VERSION ); wp_register_script( 'sweetalert2', ATUM_URL . 'assets/js/vendor/sweetalert2.min.js', array(), ATUM_VERSION, TRUE ); // ATUM marketing popup. AtumMarketingPopup::maybe_enqueue_scripts(); Helpers::maybe_es6_promise(); if ( wp_script_is( 'es6-promise', 'registered' ) ) { wp_enqueue_script( 'es6-promise' ); } // List Table styles. wp_register_style( 'atum-list', ATUM_URL . 'assets/css/atum-list.css', array( 'woocommerce_admin_styles', 'sweetalert2' ), ATUM_VERSION ); wp_enqueue_style( 'atum-list' ); // If it's the first time the user edits the List Table, load the sweetalert to show the popup. // TODO: WHAT IS THIS???? $first_edit_key = ATUM_PREFIX . "first_edit_$hook"; if ( ! get_user_meta( get_current_user_id(), $first_edit_key, TRUE ) ) { $this->first_edit_key = $first_edit_key; } // List Table script. wp_register_script( 'atum-list', ATUM_URL . 'assets/js/build/atum-list-tables.js', [ 'jquery', 'jquery-blockui', 'sweetalert2', 'wc-enhanced-select' ], ATUM_VERSION, TRUE ); wp_enqueue_script( 'atum-list' ); do_action( 'atum/list_table/after_enqueue_scripts', $this ); } /** * Getter for the table_columns property * * @since 1.2.5 * * @return array */ public static function get_table_columns() { return self::$table_columns; } /** * Setter for the table_columns property * * @since 1.2.5 * * @param array $table_columns */ public static function set_table_columns( $table_columns ) { self::$table_columns = $table_columns; } /** * Getter for the group_members property * * @since 1.2.5 * * @return array */ public function get_group_members() { return $this->group_members; } /** * Setter for the group_members property * * @since 1.2.5 * * @param array $group_members */ public function set_group_members( $group_members ) { $this->group_members = $group_members; } /** * Getter for the current product prop * * @since 1.4.15 * * @return \WC_Product */ public function get_current_product() { return $this->product; } /** * Getter for the default_currency prop * * @since 1.4.16 * * @return \WC_Product */ public static function get_default_currency() { return self::$default_currency; } /** * Get all the available children products in the system * * @since 1.1.1 * * @param string $parent_type The parent product type. * @param array $post_in Optional. If is a search query, get only the children from the filtered products. * @param string $post_type Optional. The children post type. * * @return array */ protected function get_children( $parent_type, $post_in = array(), $post_type = 'product' ) { $cache_key = AtumCache::get_cache_key( 'get_children', [ $parent_type, $post_in, $post_type ] ); $children_ids = AtumCache::get_cache( $cache_key, ATUM_TEXT_DOMAIN, FALSE, $has_cache ); if ( $has_cache ) { return $children_ids; } /** * If the site is not using the new tables, use the legacy method * * @since 1.5.0 * @deprecated Only for backwards compatibility and will be removed in a future version. */ if ( ! Helpers::is_using_new_wc_tables() ) { $children_ids = $this->get_children_legacy( $parent_type, $post_in, $post_type ); AtumCache::set_cache( $cache_key, $children_ids ); return $children_ids; } global $wpdb; // Get all the published Variables first. $post_statuses = current_user_can( 'edit_private_products' ) ? [ 'private', 'publish' ] : [ 'publish' ]; $where = " p.post_type = 'product' AND p.post_status IN('" . implode( "','", $post_statuses ) . "')"; if ( ! empty( $post_in ) ) { $where .= ' AND p.ID IN (' . implode( ',', $post_in ) . ')'; } $parents = $wpdb->get_col( $wpdb->prepare( " SELECT p.ID FROM $wpdb->posts p LEFT JOIN {$wpdb->prefix}wc_products pr ON p.ID = pr.product_id WHERE $where AND pr.type = %s GROUP BY p.ID ", $parent_type ) ); // WPCS: unprepared sql ok. $parents_with_child = $grouped_products = $bundle_children = array(); if ( ! empty( $parents ) ) { switch ( $parent_type ) { case 'variable': $this->container_products['all_variable'] = array_unique( array_merge( $this->container_products['all_variable'], $parents ) ); break; case 'grouped': $this->container_products['all_grouped'] = array_unique( array_merge( $this->container_products['all_grouped'], $parents ) ); // Get all the children from their corresponding meta key. foreach ( $parents as $parent_id ) { $children = get_post_meta( $parent_id, '_children', TRUE ); if ( ! empty( $children ) && is_array( $children ) ) { $grouped_products = array_merge( $grouped_products, $children ); $parents_with_child[] = $parent_id; } } break; // WC Subscriptions compatibility. case 'variable-subscription': $this->container_products['all_variable_subscription'] = array_unique( array_merge( $this->container_products['all_variable_subscription'], $parents ) ); break; // WC Bundle Producs compatibility. case 'bundle': $this->container_products['all_bundle'] = array_unique( array_merge( $this->container_products['all_bundle'], $parents ) ); $bundle_children = Helpers::get_bundle_items( array( 'return' => 'id=>product_id', 'bundle_id' => $parents->posts, ) ); foreach ( $parents->posts as $parent_id ) { if ( ! empty( $bundle_children ) && is_array( $bundle_children ) ) { $parents_with_child[] = $parent_id; } } break; } // Store the main query data to not lose when returning back. $temp_query_data = $this->atum_query_data; $children_args = array( 'post_type' => $post_type, 'post_status' => $post_statuses, 'posts_per_page' => - 1, 'fields' => 'id=>parent', 'orderby' => 'menu_order', 'order' => 'ASC', ); if ( 'grouped' === $parent_type ) { $children_args['post__in'] = $grouped_products; } else { $children_args['post_parent__in'] = $parents; } /* * NOTE: we should apply here all the query filters related to individual child products * like the ATUM control switch or the supplier */ $this->set_controlled_query_data(); if ( ! empty( $this->supplier_variation_products ) ) { $this->atum_query_data['where'][] = array( 'key' => 'supplier_id', 'value' => absint( $_REQUEST['supplier'] ), 'type' => 'NUMERIC', ); $this->atum_query_data['where']['relation'] = 'AND'; } // Pass through the ATUM query data filter. add_filter( 'posts_clauses', array( $this, 'atum_product_data_query_clauses' ) ); $children = new \WP_Query( apply_filters( 'atum/list_table/get_children/children_args', $children_args ) ); remove_filter( 'posts_clauses', array( $this, 'atum_product_data_query_clauses' ) ); // Restore the original query_data. $this->atum_query_data = $temp_query_data; if ( $children->found_posts ) { if ( 'grouped' !== $parent_type ) { $parents_with_child = wp_list_pluck( $children->posts, 'post_parent' ); } switch ( $parent_type ) { case 'variable': $this->container_products['variable'] = array_unique( array_merge( $this->container_products['variable'], $parents_with_child ) ); // Exclude all those variations with no children from the list. $this->excluded = array_unique( array_merge( $this->excluded, array_diff( $this->container_products['all_variable'], $this->container_products['variable'] ) ) ); break; case 'grouped': $this->container_products['grouped'] = array_unique( array_merge( $this->container_products['grouped'], $parents_with_child ) ); // Exclude all those grouped with no children from the list. $this->excluded = array_unique( array_merge( $this->excluded, array_diff( $this->container_products['all_grouped'], $this->container_products['grouped'] ) ) ); break; case 'variable-subscription': $this->container_products['variable_subscription'] = array_unique( array_merge( $this->container_products['variable_subscription'], $parents_with_child ) ); // Exclude all those subscription variations with no children from the list. $this->excluded = array_unique( array_merge( $this->excluded, array_diff( $this->container_products['all_variable_subscription'], $this->container_products['variable_subscription'] ) ) ); break; } $children_ids = wp_list_pluck( $children->posts, 'ID' ); $this->children_products = array_unique( array_merge( $this->children_products, $children_ids ) ); AtumCache::set_cache( $cache_key, $children_ids ); return $children_ids; } elseif ( class_exists( '\WC_Product_Bundle' ) && 'bundle' === $parent_type ) { foreach ( $bundle_children as $key => $bundle_children ) { $product_children = Helpers::get_atum_product( $bundle_children ); if ( $product_children ) { if ( 'yes' === Helpers::get_atum_control_status( $product_children ) ) { if ( ! $this->show_controlled ) { unset( $bundle_children[ $key ] ); } } elseif ( $this->show_controlled ) { unset( $bundle_children[ $key ] ); } } } if ( empty( $bundle_children ) ) { $parents_with_child = []; } else { $bundle_parents = []; foreach ( $bundle_children as $bundle_children ) { $bundle_parents = array_merge( $bundle_parents, wc_pb_get_bundled_product_map( $bundle_children ) ); } $parents_with_child = $bundle_parents; } $this->container_products['bundle'] = array_unique( array_merge( $this->container_products['bundle'], $parents_with_child ) ); // Exclude all those subscription variations with no children from the list. $this->excluded = array_unique( array_merge( $this->excluded, array_diff( $this->container_products['all_bundle'], $this->container_products['bundle'] ) ) ); $this->children_products = array_unique( array_merge( $this->children_products, array_map( 'intval', $bundle_children ) ) ); return $bundle_children; } else { $this->excluded = array_unique( array_merge( $this->excluded, $parents ) ); } } return array(); } /** * Get the number of children for a given inheritable product * * @since 1.4.1 * * @param \WC_Product $product * @param string $product_type * * @return int */ protected function get_num_children( $product, $product_type ) { $children_count = 0; // The grouped products have the children stored within the _children meta key. if ( 'grouped' === $product_type ) { $children = $product->get_children(); foreach ( $children as $child ) { /* @noinspection PhpUndefinedMethodInspection */ $atum_control_status = $child->get_atum_controlled(); if ( ( $this->show_controlled && 'yes' === $atum_control_status ) || ( ! $this->show_controlled && 'yes' !== $atum_control_status ) ) { $children_count++; } } } // For the variable products we can query the database directly to improve the performance. else { global $wpdb; if ( $this->show_controlled ) { $join = "$wpdb->posts.ID = $wpdb->postmeta.post_id"; $where = "$wpdb->postmeta.meta_key = '" . Globals::ATUM_CONTROL_STOCK_KEY . "' AND $wpdb->postmeta.meta_value = 'yes'"; } else { $join = "($wpdb->posts.ID = $wpdb->postmeta.post_id AND $wpdb->postmeta.meta_key = '" . Globals::ATUM_CONTROL_STOCK_KEY . "')"; $where = "$wpdb->postmeta.post_id IS NULL"; } $sql = $wpdb->prepare( " SELECT COUNT(*) FROM $wpdb->posts LEFT JOIN $wpdb->postmeta ON $join WHERE $wpdb->posts.post_type = 'product_variation' AND $wpdb->posts.post_parent = %d AND $where ", $product->get_id() ); // WPCS: unprepared SQL ok. $children_count = $wpdb->get_var( $sql ); // WPCS: unprepared SQL ok. } return $children_count; } /** * Get the parent products from a list of product IDs * * @since 1.1.1 * * @param array $product_ids The array of children product IDs. * * @return array */ protected function get_parents( $product_ids ) { // TODO: WHAT IF WE HAVE TO ADD A GROUPED PRODUCT OR BUNDLE? global $wpdb; $parents = " SELECT DISTINCT post_parent FROM $wpdb->posts WHERE ID IN (" . implode( ',', $product_ids ) . ") AND post_parent > 0 AND post_type = 'product_variation' "; return $wpdb->get_col( $parents ); // WPCS: unprepared SQL ok. } /** * Increase the total of the specified column by the specified amount * * @since 1.4.2 * * @param string $column_name * @param int|float $amount */ protected function increase_total( $column_name, $amount ) { if ( $this->show_totals && isset( $this->totalizers[ $column_name ] ) && is_numeric( $amount ) && 'grouped' !== $this->parent_type && 'bundle' !== $this->parent_type ) { $this->totalizers[ $column_name ] += floatval( $amount ); } } /** * Builds a query string with the active filters * * @since 1.4.3 * * @param string $format Optional. The return format (array or string). * * @return string|array */ protected function get_filters_query_string( $format = 'array' ) { $default_filters = array( 'paged' => 1, 'order' => 'desc', 'orderby' => 'date', 'view' => 'all_stock', 'product_cat' => '', 'product_type' => '', 'supplier' => '', 'extra_filter' => '', 's' => '', 'search_column' => '', 'sold_last_days' => '', ); parse_str( $_SERVER['QUERY_STRING'], $query_string ); $params = array_filter( array_intersect_key( $query_string, $default_filters ) ); // The filters with default values should be excluded. foreach ( $params as $param => $value ) { if ( $value === $default_filters[ $param ] ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison unset( $params[ $param ] ); } } return 'string' === $format ? http_build_query( $params ) : $params; } /** * Get columns hidden by default * * @since 1.2.1 * * @return array */ public static function hidden_columns() { return apply_filters( 'atum/list_table/default_hidden_columns', static::$default_hidden_columns ); } /** * Getter fot the is_report prop * * @since 1.4.16 * * @return bool */ public static function is_report() { return self::$is_report; } }