diff --git a/cost-of-goods-for-woocommerce.php b/cost-of-goods-for-woocommerce.php index 9e06fd0..b3560c9 100644 --- a/cost-of-goods-for-woocommerce.php +++ b/cost-of-goods-for-woocommerce.php @@ -3,7 +3,7 @@ Plugin Name: Cost of Goods for WooCommerce Plugin URI: https://wpfactory.com/item/cost-of-goods-for-woocommerce/ Description: Save product purchase costs (cost of goods) in WooCommerce. Beautifully. -Version: 3.2.0 +Version: 3.2.1 Author: WPFactory Author URI: https://wpfactory.com Text Domain: cost-of-goods-for-woocommerce diff --git a/includes/alg-wc-cog-functions.php b/includes/alg-wc-cog-functions.php index 9a7383d..9392dac 100644 --- a/includes/alg-wc-cog-functions.php +++ b/includes/alg-wc-cog-functions.php @@ -2,8 +2,8 @@ /** * Cost of Goods for WooCommerce - Functions. * - * @version 2.9.5 - * @since 1.4.0 + * @version 3.2.1 + * @since 3.2.1 * @author WPFactory */ @@ -351,4 +351,26 @@ function alg_wc_cog_get_cost_subtracting_tax_rate( $args = null ) { } return $cost / ( 1 + ( $tax_rate->tax_rate / 100 ) ); } +} + +if ( ! function_exists( 'alg_wc_cog_generate_wpdb_prepare_placeholders_from_array' ) ) { + /** + * alg_wc_cog_generate_wpdb_prepare_placeholders_from_array. + * + * @link https://stackoverflow.com/a/72147500/1193038 + * + * @version 3.2.1 + * @since 3.2.1 + * + * @param $array + * + * @return string + */ + function alg_wc_cog_generate_wpdb_prepare_placeholders_from_array( $array ) { + $placeholders = array_map( function ( $item ) { + return is_string( $item ) ? '%s' : ( is_float( $item ) ? '%f' : ( is_int( $item ) ? '%d' : '' ) ); + }, $array ); + + return '(' . join( ',', $placeholders ) . ')'; + } } \ No newline at end of file diff --git a/includes/analytics/build/index.asset.php b/includes/analytics/build/index.asset.php index fc8b3b7..ebbeac6 100644 --- a/includes/analytics/build/index.asset.php +++ b/includes/analytics/build/index.asset.php @@ -1 +1 @@ - array('wc-currency', 'wp-hooks', 'wp-i18n'), 'version' => 'c0714b7b3b4e9ba7bd0dc855ff0ee60a'); \ No newline at end of file + array('wc-currency', 'wp-hooks', 'wp-i18n'), 'version' => '444b8115c92b726d04d1c79a8cbfef62'); \ No newline at end of file diff --git a/includes/analytics/build/index.js b/includes/analytics/build/index.js index 7c8f762..4105864 100644 --- a/includes/analytics/build/index.js +++ b/includes/analytics/build/index.js @@ -1 +1 @@ -!function(o){var t={};function e(r){if(t[r])return t[r].exports;var c=t[r]={i:r,l:!1,exports:{}};return o[r].call(c.exports,c,c.exports,e),c.l=!0,c.exports}e.m=o,e.c=t,e.d=function(o,t,r){e.o(o,t)||Object.defineProperty(o,t,{enumerable:!0,get:r})},e.r=function(o){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(o,"__esModule",{value:!0})},e.t=function(o,t){if(1&t&&(o=e(o)),8&t)return o;if(4&t&&"object"==typeof o&&o&&o.__esModule)return o;var r=Object.create(null);if(e.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:o}),2&t&&"string"!=typeof o)for(var c in o)e.d(r,c,function(t){return o[t]}.bind(null,c));return r},e.n=function(o){var t=o&&o.__esModule?function(){return o.default}:function(){return o};return e.d(t,"a",t),t},e.o=function(o,t){return Object.prototype.hasOwnProperty.call(o,t)},e.p="",e(e.s=7)}([function(o,t){o.exports=window.wp.i18n},function(o,t,e){var r=e(4),c=e(5),a=e(6);o.exports=function(o){return r(o)||c(o)||a()}},function(o,t){o.exports=window.wp.hooks},function(o,t){o.exports=window.wc.currency},function(o,t){o.exports=function(o){if(Array.isArray(o)){for(var t=0,e=new Array(o.length);tesc_like( $prefix ) . '%'; + $query = $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $meta_key_pattern ); + $wpdb->query( $query ); + } + + /** + * get_total_cost_and_profit. + * + * @version 3.2.1 + * @since 3.2.1 + * + * @param $query_results + * + * @return mixed + */ + function get_total_cost_and_profit( $query_results ) { + if ( 'yes' !== get_option( 'alg_wc_cog_cost_and_profit_enabled_on_analytics_stock', 'no' ) ) { + return $query_results; + } + $args = array( + 'type' => isset( $_REQUEST['type'] ) ? $_REQUEST['type'] : '' + ); + $type = $args['type']; + if ( ! in_array( $type, array( 'lowstock', 'instock', 'onbackorder', 'outofstock' ) ) ) { + $type = ''; + } + + $transient_name = 'alg_wc_cog_scpt_' . md5( maybe_serialize( $args ) ); + if ( false === ( $total_cost_and_profit_info = get_transient( $transient_name ) ) ) { + $filtered_product_ids = array(); + if ( 'lowstock' === $type ) { + $filtered_product_ids = $this->get_low_stock_product_ids(); + } else { + $filtered_product_ids = $this->get_product_ids_by_stock_status( $type ); + } + $total_cost_and_profit_info = $this->get_total_cost_and_profit_from_database( $filtered_product_ids ); + set_transient( $transient_name, $total_cost_and_profit_info ); + } + + if ( ! empty( $total_cost_and_profit_info ) ) { + $query_results['cost'] = $total_cost_and_profit_info['cost']; + $query_results['cost_with_qty'] = $total_cost_and_profit_info['cost_with_qty']; + $query_results['profit'] = $total_cost_and_profit_info['profit']; + $query_results['profit_with_qty'] = $total_cost_and_profit_info['profit_with_qty']; + } + + return $query_results; + } + + /** + * get_total_cost_and_profit_from_database. + * + * @version 3.2.1 + * @since 3.2.1 + * + * @param $post_ids + * + * @return array|object|stdClass|null + */ + function get_total_cost_and_profit_from_database( $post_ids = array() ) { + global $wpdb; + + $query = " + SELECT COUNT(DISTINCT posts.ID) as total_products, SUM(alg_wc_cog_cost_pm.meta_value) AS cost, SUM(alg_wc_cog_cost_pm.meta_value * IFNULL(stock_pm.meta_value, 1)) AS cost_with_qty, SUM(alg_wc_cog_profit_pm.meta_value) AS profit, SUM(alg_wc_cog_profit_pm.meta_value * IFNULL(stock_pm.meta_value, 1)) AS profit_with_qty, SUM(IFNULL(stock_pm.meta_value, 1)) AS stock + FROM {$wpdb->posts} posts + LEFT JOIN {$wpdb->postmeta} alg_wc_cog_cost_pm ON posts.ID = alg_wc_cog_cost_pm.post_id and alg_wc_cog_cost_pm.meta_key = '_alg_wc_cog_cost' + LEFT JOIN {$wpdb->postmeta} alg_wc_cog_profit_pm ON posts.ID = alg_wc_cog_profit_pm.post_id and alg_wc_cog_profit_pm.meta_key = '_alg_wc_cog_profit' + LEFT JOIN wp_postmeta stock_pm ON posts.ID = stock_pm.post_id AND stock_pm.meta_key = '_stock' + WHERE posts.post_type IN ( 'product', 'product_variation' ) + AND alg_wc_cog_cost_pm.meta_value NOT IN ('',0) AND alg_wc_cog_profit_pm.meta_value NOT IN ('',0) + "; + + if ( ! empty( $post_ids ) ) { + $in_str = alg_wc_cog_generate_wpdb_prepare_placeholders_from_array( $post_ids ); + $query .= "AND posts . ID IN {$in_str}"; + } + + $prepare_args = $post_ids; + + return $wpdb->get_row( + $wpdb->prepare( $query, $prepare_args ), + ARRAY_A + ); + } + + /** + * Get count for the passed in stock status. + * + * @version 3.2.1 + * @since 3.2.1 + * + * @see Automattic\WooCommerce\Admin\API\Reports\Stock\Stats\DataStore::get_count() + * + * @param string $status Status slug. + */ + private function get_product_ids_by_stock_status( $status ) { + global $wpdb; + + return $wpdb->get_col( + $wpdb->prepare( + " + SELECT DISTINCT posts.ID FROM {$wpdb->posts} posts + LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON posts.ID = wc_product_meta_lookup.product_id + WHERE posts.post_type IN ( 'product', 'product_variation' ) + AND wc_product_meta_lookup.stock_status = %s + ", + $status + ) + ); + } + + /** + * Get low stock count (products with stock < low stock amount, but greater than no stock amount). + * + * @version 3.2.1 + * @since 3.2.1 + * + * @see Automattic\WooCommerce\Admin\API\Reports\Stock\Stats\DataStore::get_low_stock_count() + */ + private function get_low_stock_product_ids() { + global $wpdb; + + $no_stock_amount = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) ); + $low_stock_amount = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) ); + + return $wpdb->get_col( + $wpdb->prepare( + " + SELECT posts.ID FROM {$wpdb->posts} posts + LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON posts.ID = wc_product_meta_lookup.product_id + LEFT JOIN {$wpdb->postmeta} low_stock_amount_meta ON posts.ID = low_stock_amount_meta.post_id AND low_stock_amount_meta.meta_key = '_low_stock_amount' + WHERE posts.post_type IN ( 'product', 'product_variation' ) + AND wc_product_meta_lookup.stock_quantity IS NOT NULL + AND wc_product_meta_lookup.stock_status = 'instock' + AND ( + ( + low_stock_amount_meta.meta_value > '' + AND wc_product_meta_lookup.stock_quantity <= CAST(low_stock_amount_meta.meta_value AS SIGNED) + AND wc_product_meta_lookup.stock_quantity > %d + ) + OR ( + ( + low_stock_amount_meta.meta_value IS NULL OR low_stock_amount_meta.meta_value <= '' + ) + AND wc_product_meta_lookup.stock_quantity <= %d + AND wc_product_meta_lookup.stock_quantity > %d + ) + ) + ", + $no_stock_amount, + $low_stock_amount, + $no_stock_amount + ) + ); } /** @@ -179,7 +353,7 @@ function get_column_values( WP_REST_Response $response, $product ) { /** * add_analytics_localization_info. * - * @version 2.4.5 + * @version 3.2.1 * @since 2.4.5 * * @param $info @@ -190,6 +364,7 @@ function add_analytics_localization_info( $info ) { $info['cost_and_profit_enabled_on_stock'] = 'yes' === get_option( 'alg_wc_cog_cost_and_profit_enabled_on_analytics_stock', 'no' ); $info['category_enabled_on_stock'] = 'yes' === get_option( 'alg_wc_cog_category_enabled_on_analytics_stock', 'no' ); $info['filter_enabled_on_stock'] = 'yes' === get_option( 'alg_wc_cog_filter_enabled_on_analytics_stock', 'no' ); + $info['consider_stock_for_calculation'] = $this->consider_stock_for_calculation(); return $info; } diff --git a/includes/analytics/src/modules/stock.js b/includes/analytics/src/modules/stock.js index f537f8e..2d72f3b 100644 --- a/includes/analytics/src/modules/stock.js +++ b/includes/analytics/src/modules/stock.js @@ -15,121 +15,116 @@ const storeCurrency = CurrencyFactory(wcSettings.currency); Formatting.setStoreCurrency(storeCurrency); let stock = { - init: function () { - this.addColumns(); - this.addCOGFilter(); - }, - addCOGFilter: function () { - if (alg_wc_cog_analytics_obj.filter_enabled_on_stock) { - addFilter( - 'woocommerce_admin_stock_report_filters', - 'cost-of-goods-for-woocommerce', - (obj) => { - obj.push({ - label: __('Cost of Goods filter', 'cost-of-goods-for-woocommerce'), - staticParams: ['paged', 'per_page'], - param: 'alg_cog_stock_filter', - showFilters: () => true, - filters: [ - {label: __('Disabled', 'cost-of-goods-for-woocommerce'), value: 'all'}, - {label: __('Products with cost', 'cost-of-goods-for-woocommerce'), value: 'with_cost'} - ] - }); - return obj; - } - ); - } - }, - addColumns: function () { - // Reports table - addFilter( - 'woocommerce_admin_report_table', - 'cost-of-goods-for-woocommerce', - (reportTableData) => { - if ( - reportTableData.endpoint !== 'stock' || - !reportTableData.items || - !reportTableData.items.data || - !reportTableData.items.data.length - ) { - return reportTableData; - } - const newHeaders = [...reportTableData.headers]; - // Cost and profit - if (alg_wc_cog_analytics_obj.cost_and_profit_enabled_on_stock) { - newHeaders.push({ - label: __('Cost', 'cost-of-goods-for-woocommerce'), - key: 'product_cost', - isNumeric: true, - //isSortable: true, - }); - newHeaders.push({ - label: __('Profit', 'cost-of-goods-for-woocommerce'), - key: 'product_profit', - isNumeric: true, - //isSortable: true, - }); - } - // Category - if (alg_wc_cog_analytics_obj.category_enabled_on_stock) { - newHeaders.push({ - label: __('Category', 'cost-of-goods-for-woocommerce'), - key: 'product_cat', - //isSortable: true, - }); - } - const newRows = reportTableData.rows.map((row, index) => { - const product = reportTableData.items.data[index]; - const newRow = [...row]; - // Cost and profit - if (alg_wc_cog_analytics_obj.cost_and_profit_enabled_on_stock) { - newRow.push({ - display: storeCurrency.formatAmount(product.product_cost), - value: product.product_cost, - type: 'currency' - }); - newRow.push({ - display: storeCurrency.formatAmount(product.product_profit), - value: product.product_profit, - type: 'currency' - }); - } - // Category - if (alg_wc_cog_analytics_obj.category_enabled_on_stock) { - newRow.push({ - display: product.product_cat, - value: product.product_cat, - }); - } - return newRow; - }); - /*const costsTotal = reportTableData.items.data.reduce((sum, item) => { - return sum + item.product_cost; - }, 0); - const profitTotal = reportTableData.items.data.reduce((sum, item) => { - return sum + item.product_profit; - }, 0); - const priceTotal = reportTableData.items.data.reduce((sum, item) => { - return sum + item.product_price; - }, 0); - const newSummary = [ - ...reportTableData.summary, - { - label: 'Cost', - value: storeCurrency.formatAmount(costsTotal), - }, - { - label: 'Profit', - value: Formatting.formatProfit(alg_wc_cog_analytics_obj.profit_template,costsTotal,profitTotal,priceTotal), - }, - ]; - reportTableData.summary = newSummary;*/ - reportTableData.headers = newHeaders; - reportTableData.rows = newRows; - return reportTableData; - } - ); - } + init: function () { + this.addColumns(); + this.addCOGFilter(); + }, + addCOGFilter: function () { + if (alg_wc_cog_analytics_obj.filter_enabled_on_stock) { + addFilter( + 'woocommerce_admin_stock_report_filters', + 'cost-of-goods-for-woocommerce', + (obj) => { + obj.push({ + label: __('Cost of Goods filter', 'cost-of-goods-for-woocommerce'), + staticParams: ['paged', 'per_page'], + param: 'alg_cog_stock_filter', + showFilters: () => true, + filters: [ + {label: __('Disabled', 'cost-of-goods-for-woocommerce'), value: 'all'}, + {label: __('Products with cost', 'cost-of-goods-for-woocommerce'), value: 'with_cost'} + ] + }); + return obj; + } + ); + } + }, + addColumns: function () { + // Reports table + addFilter( + 'woocommerce_admin_report_table', + 'cost-of-goods-for-woocommerce', + (reportTableData) => { + if ( + reportTableData.endpoint !== 'stock' || + !reportTableData.items || + !reportTableData.items.data || + !reportTableData.items.data.length + ) { + return reportTableData; + } + const newHeaders = [...reportTableData.headers]; + // Cost and profit + if (alg_wc_cog_analytics_obj.cost_and_profit_enabled_on_stock) { + newHeaders.push({ + label: __('Cost', 'cost-of-goods-for-woocommerce'), + key: 'product_cost', + isNumeric: true, + //isSortable: true, + }); + newHeaders.push({ + label: __('Profit', 'cost-of-goods-for-woocommerce'), + key: 'product_profit', + isNumeric: true, + //isSortable: true, + }); + } + // Category + if (alg_wc_cog_analytics_obj.category_enabled_on_stock) { + newHeaders.push({ + label: __('Category', 'cost-of-goods-for-woocommerce'), + key: 'product_cat', + //isSortable: true, + }); + } + const newRows = reportTableData.rows.map((row, index) => { + const product = reportTableData.items.data[index]; + const newRow = [...row]; + // Cost and profit + if (alg_wc_cog_analytics_obj.cost_and_profit_enabled_on_stock) { + newRow.push({ + display: storeCurrency.formatAmount(product.product_cost), + value: product.product_cost, + type: 'currency' + }); + newRow.push({ + display: storeCurrency.formatAmount(product.product_profit), + value: product.product_profit, + type: 'currency' + }); + } + // Category + if (alg_wc_cog_analytics_obj.category_enabled_on_stock) { + newRow.push({ + display: product.product_cat, + value: product.product_cat, + }); + } + return newRow; + }); + if (alg_wc_cog_analytics_obj.cost_and_profit_enabled_on_stock) { + let costTotals = alg_wc_cog_analytics_obj.consider_stock_for_calculation ? reportTableData.totals.cost_with_qty : reportTableData.totals.cost; + let profitTotals = alg_wc_cog_analytics_obj.consider_stock_for_calculation ? reportTableData.totals.profit_with_qty : reportTableData.totals.profit; + const newSummary = [ + ...reportTableData.summary, + { + label: 'Cost', + value: storeCurrency.formatAmount(costTotals), + }, + { + label: 'Profit', + value: storeCurrency.formatAmount(profitTotals), + }, + ]; + reportTableData.summary = newSummary; + } + reportTableData.headers = newHeaders; + reportTableData.rows = newRows; + return reportTableData; + } + ); + } }; export default stock; diff --git a/includes/class-alg-wc-cog.php b/includes/class-alg-wc-cog.php index 9715726..71a4883 100644 --- a/includes/class-alg-wc-cog.php +++ b/includes/class-alg-wc-cog.php @@ -35,7 +35,7 @@ final class Alg_WC_Cost_of_Goods { * @since 1.0.0 * @var string */ - public $version = '3.2.0'; + public $version = '3.2.1'; /** * @since 1.0.0 diff --git a/langs/cost-of-goods-for-woocommerce.pot b/langs/cost-of-goods-for-woocommerce.pot index 606fa9d..5fb0df3 100644 --- a/langs/cost-of-goods-for-woocommerce.pot +++ b/langs/cost-of-goods-for-woocommerce.pot @@ -2,14 +2,14 @@ # This file is distributed under the GNU General Public License v3.0. msgid "" msgstr "" -"Project-Id-Version: cost-of-goods-for-woocommerce 3.2.0\n" +"Project-Id-Version: cost-of-goods-for-woocommerce 3.2.1\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/cost-of-goods-for-woocommerce\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2023-12-20T20:39:44+01:00\n" +"POT-Creation-Date: 2023-12-29T00:58:50+01:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.7.1\n" "X-Domain: cost-of-goods-for-woocommerce\n" @@ -73,7 +73,7 @@ msgstr "" #: includes/analytics/class-alg-wc-cog-analytics-orders.php:379 #: includes/analytics/class-alg-wc-cog-analytics-products.php:78 #: includes/analytics/class-alg-wc-cog-analytics-revenue.php:47 -#: includes/analytics/class-alg-wc-cog-analytics-stock.php:88 +#: includes/analytics/class-alg-wc-cog-analytics-stock.php:262 #: includes/class-alg-wc-cog-orders-meta-boxes.php:151 #: includes/class-alg-wc-cog-orders.php:731 #: includes/class-alg-wc-cog-orders.php:841 @@ -100,7 +100,7 @@ msgstr "" #: includes/analytics/class-alg-wc-cog-analytics-orders.php:415 #: includes/analytics/class-alg-wc-cog-analytics-products.php:114 #: includes/analytics/class-alg-wc-cog-analytics-revenue.php:48 -#: includes/analytics/class-alg-wc-cog-analytics-stock.php:89 +#: includes/analytics/class-alg-wc-cog-analytics-stock.php:263 #: includes/class-alg-wc-cog-orders-meta-boxes.php:152 #: includes/class-alg-wc-cog-orders.php:732 #: includes/class-alg-wc-cog-orders.php:851 @@ -116,7 +116,7 @@ msgstr "" msgid "Profit" msgstr "" -#: includes/analytics/class-alg-wc-cog-analytics-stock.php:93 +#: includes/analytics/class-alg-wc-cog-analytics-stock.php:267 #: includes/analytics/build/index.js:1 #: includes/analytics/src/modules/stock.js:76 msgid "Category" diff --git a/readme.txt b/readme.txt index 77e6d2b..8b0b19f 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: wpcodefactory, omardabbas, karzin, anbinder, algoritmika, kousikmu Tags: woocommerce, cost, cost of goods, profit, profit calculator Requires at least: 6.1 Tested up to: 6.4 -Stable tag: 3.2.0 +Stable tag: 3.2.1 License: GNU General Public License v3.0 License URI: http://www.gnu.org/licenses/gpl-3.0.html @@ -365,6 +365,9 @@ Once activated, access the plugin's settings by navigating to “WooCommerce > S == Changelog == += 3.2.1 - 28/12/2023 = +* Dev - Analytics - Stock - Show cost and profit totals on "Analytics > Stock". + = 3.2.0 - 20/12/2023 = * Fix - Advanced - "Avoid empty order metadata from being saved to database" option might trigger errors on checkout.