diff --git a/Gemfile b/Gemfile index 32d3ecd..922dd87 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'spree', github: 'spree/spree', branch: '3-0-stable' -gem 'spree_events_tracker', git: "https://github.com/vinsol/spree_events_tracker.git", branch: 'add-tracker' +gem 'spree', github: 'spree/spree', branch: 'master' +gem 'spree_events_tracker', git: "https://github.com/vinsol/spree_events_tracker.git", branch: 'master' gemspec diff --git a/README.md b/README.md index 7eec37a..12e4adf 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,53 @@ -SpreeReportify +[SpreeAdminInsights](http://vinsol.com/spreecommerce-admin-insights) ============== -Introduction goes here. +When it comes to driving an Ecommerce business, knowing the right metrics and access to relevant data is half the battle won! This allows you to take immediate and more importantly, the right action. + +This extension provides extensive and targeted reports for the Admin. Which products were viewed the most yesterday, which brand is most popular in a particular geography, which user is a consistent buyer and much more, all the reports a website owner could probably need are a click away! + +Dependency +--------- +you need to install [spree_events_tracker](https://github.com/vinsol-spree-contrib/spree_events_tracker) gem. + +Features +-------- +Elaborate reporting from the following categories are available: +* Financial Analysis - Involves reports around sales, payment methods and shipping etc +* Product Analysis - Insights of product purchase, abandoned cart etc +* Promotional analysis - Reports of promotional costs etc are available. +* Search Analysis - Search details reports. +* User Analysis - Includes elaborate user analysis. + +**Other features include :** +* Search and Filter +* Save reports in various formats. +* Refresh reports +* Reset report +* Remove pagination or change pagination count. Installation ------------ -Add spree_reportify to your Gemfile: +1. Add spree_admin_insights to your Gemfile: -```ruby -gem 'spree_reportify' -``` + ```ruby + gem 'spree_admin_insights', git: 'https://github.com/vinsol-spree-contrib/spree-admin-insights' + ``` -Bundle your dependencies and run the installation generator: +2. Bundle your dependencies and run the installation generator: -```shell -bundle -bundle exec rails g spree_reportify:install -``` + ```shell + bundle + bundle exec rails g spree_admin_insights:install + ``` + +3. Restart your server + +Usage +------- +Once installed it will automatically starts all data loging and statistical analysis and provides you a user friendly graphical representation of reports. This extension also allows you to download the reports in multiple formats. For more detailed usage please see [this](http://vinsol.com/spreecommerce-admin-insights) blog. + +To access these reports goto admin section and click on 'Insights' section in the vertical menu bar. Testing ------- @@ -26,14 +56,12 @@ First bundle your dependencies, then run `rake`. `rake` will default to building ```shell bundle -bundle exec rake +bundle exec rspec spec ``` -When testing your applications integration with this extension you may use it's factories. -Simply add this require statement to your spec_helper: +Credits +------- -```ruby -require 'spree_reportify/factories' -``` +[![vinsol.com: Ruby on Rails, iOS and Android developers](http://vinsol.com/vin_logo.png "Ruby on Rails, iOS and Android developers")](http://vinsol.com) -Copyright (c) 2016 [name of extension creator], released under the New BSD License +Copyright (c) 2016 [vinsol.com](http://vinsol.com "Ruby on Rails, iOS and Android developers"), released under the New MIT License diff --git a/Rakefile b/Rakefile index b6da392..4a5707e 100644 --- a/Rakefile +++ b/Rakefile @@ -16,6 +16,6 @@ end desc 'Generates a dummy app for testing' task :test_app do - ENV['LIB_NAME'] = 'spree_reportify' + ENV['LIB_NAME'] = 'spree_admin_insights' Rake::Task['extension:test_app'].invoke end diff --git a/app/assets/javascripts/spree/backend/spree_admin_insights.js b/app/assets/javascripts/spree/backend/spree_admin_insights.js new file mode 100644 index 0000000..4bca314 --- /dev/null +++ b/app/assets/javascripts/spree/backend/spree_admin_insights.js @@ -0,0 +1,2 @@ +//= require spree/backend/spree_admin_insights/report_loader +//= require spree/backend/tmpl diff --git a/app/assets/javascripts/spree/backend/spree_reportify/paginator.js b/app/assets/javascripts/spree/backend/spree_admin_insights/paginator.js similarity index 98% rename from app/assets/javascripts/spree/backend/spree_reportify/paginator.js rename to app/assets/javascripts/spree/backend/spree_admin_insights/paginator.js index 6ad79ae..e3bb41b 100644 --- a/app/assets/javascripts/spree/backend/spree_reportify/paginator.js +++ b/app/assets/javascripts/spree/backend/spree_admin_insights/paginator.js @@ -91,7 +91,7 @@ Paginator.prototype.removePagination = function(currentElement) { sorted_attributes = this.tableSorter.fetchSortedAttribute(), attribute = sorted_attributes[0], sortOrder = sorted_attributes[1], - requestUrl = $element.data('url') + '&sort%5Battribute%5D=' + attribute + '&sort%5Btype%5D=' + sortOrder + '&' + $('#filter-search').serialize() + '&no_pagination=true'; + requestUrl = $element.data('url') + '&sort%5Battribute%5D=' + attribute + '&sort%5Btype%5D=' + sortOrder + '&' + $('#filter-search').serialize() + '&paginate=false'; $(currentElement).attr('href', requestUrl); _this.reportLoader.requestUrl = requestUrl; $element.val(''); diff --git a/app/assets/javascripts/spree/backend/spree_reportify/report_loader.js b/app/assets/javascripts/spree/backend/spree_admin_insights/report_loader.js similarity index 93% rename from app/assets/javascripts/spree/backend/spree_reportify/report_loader.js rename to app/assets/javascripts/spree/backend/spree_admin_insights/report_loader.js index d8ebb11..5684a9a 100644 --- a/app/assets/javascripts/spree/backend/spree_reportify/report_loader.js +++ b/app/assets/javascripts/spree/backend/spree_admin_insights/report_loader.js @@ -1,6 +1,6 @@ -//= require spree/backend/spree_reportify/paginator -//= require spree/backend/spree_reportify/searcher -//= require spree/backend/spree_reportify/table_sorter +//= require spree/backend/spree_admin_insights/paginator +//= require spree/backend/spree_admin_insights/searcher +//= require spree/backend/spree_admin_insights/table_sorter function ReportLoader(inputs) { this.$selectList = inputs.reportsSelectBox; @@ -83,9 +83,9 @@ ReportLoader.prototype.bindEvents = function() { ReportLoader.prototype.resetFilters = function(event) { event.preventDefault(); var $element = $(event.target), - noPagination = this.removePaginationButton.closest('span').hasClass('hide'); - $element.attr('href', this.perPageSelector.data('url') + '&no_pagination=' + noPagination); - $element.data('url', this.perPageSelector.data('url') + '&no_pagination=' + noPagination); + paginated = !this.removePaginationButton.closest('span').hasClass('hide'); + $element.attr('href', this.perPageSelector.data('url') + '&paginate=' + paginated); + $element.data('url', this.perPageSelector.data('url') + '&paginate=' + paginated); this.loadChart($element); this.searcherObject.clearSearchFields(); }; @@ -136,7 +136,7 @@ ReportLoader.prototype.fetchChartData = function(url, $selectedOption) { $(object).removeClass('col-md-3').addClass('col-md-2'); }); } - _this.perPageSelector.data('url', data['request_path'] + '?type=' + data['report_type']); + _this.perPageSelector.data('url', data['request_path'] + '?report_category=' + data['report_category']); _this.setDownloadLinksPath(); _this.searcherObject.refreshSearcher($selectedOption, data); _this.paginatorObject.refreshPaginator(data); @@ -187,7 +187,7 @@ ReportLoader.prototype.populateInsightsData = function(data) { ReportLoader.prototype.setDownloadLinksPath = function($selectedOption) { var _this = this; $.each(this.downloadLinks, function() { - $(this).attr('href', $(this).data('url') + '?id=' + _this.$selectList.val() + '&no_pagination=true'); + $(this).attr('href', $(this).data('url') + '?id=' + _this.$selectList.val() + '&paginate=false'); }); }; diff --git a/app/assets/javascripts/spree/backend/spree_reportify/searcher.js b/app/assets/javascripts/spree/backend/spree_admin_insights/searcher.js similarity index 92% rename from app/assets/javascripts/spree/backend/spree_reportify/searcher.js rename to app/assets/javascripts/spree/backend/spree_admin_insights/searcher.js index 08554f3..f4c5ae8 100644 --- a/app/assets/javascripts/spree/backend/spree_reportify/searcher.js +++ b/app/assets/javascripts/spree/backend/spree_admin_insights/searcher.js @@ -1,4 +1,4 @@ -//= require spree/backend/spree_reportify/paginator +//= require spree/backend/spree_admin_insights/paginator function Searcher(inputs, reportLoader) { this.$insightsTableList = inputs.insightsDiv; @@ -28,12 +28,12 @@ Searcher.prototype.refreshSearcher = function($selectedInsight, data) { _this.setFormActions(_this.$filterForm, requestPath); _this.$filterForm.on('submit', function() { - var noPagination = _this.reportLoader.removePaginationButton.closest('span').hasClass('hide'); + var paginated = !_this.reportLoader.removePaginationButton.closest('span').hasClass('hide'); _this.addSearchStatus(); $.ajax({ type: "GET", url: _this.$filterForm.attr('action'), - data: _this.$filterForm.serialize() + "&per_page=" + _this.reportLoader.pageSelector.find(':selected').attr('value') + '&no_pagination=' + noPagination, + data: _this.$filterForm.serialize() + "&per_page=" + _this.reportLoader.pageSelector.find(':selected').attr('value') + '&paginate=' + paginated, dataType: 'json', success: function(data) { _this.clearFormFields(); diff --git a/app/assets/javascripts/spree/backend/spree_reportify/table_sorter.js b/app/assets/javascripts/spree/backend/spree_admin_insights/table_sorter.js similarity index 91% rename from app/assets/javascripts/spree/backend/spree_reportify/table_sorter.js rename to app/assets/javascripts/spree/backend/spree_admin_insights/table_sorter.js index bd4d64f..503ab01 100644 --- a/app/assets/javascripts/spree/backend/spree_reportify/table_sorter.js +++ b/app/assets/javascripts/spree/backend/spree_admin_insights/table_sorter.js @@ -11,8 +11,8 @@ TableSorter.prototype.bindEvents = function() { this.$insightsTableList.on('click', '#admin-insight .sortable-link', function() { event.preventDefault(); var currentPage = _this.paginatorDiv.find('li.active a').html() - 1, - noPagination = _this.reportLoader.removePaginationButton.closest('span').hasClass('hide'), - requestPath = $(event.target).attr('href') + '&' + $('#filter-search').serialize() + '&page=' + currentPage + "&per_page=" + _this.reportLoader.pageSelector.find(':selected').attr('value') + '&no_pagination=' + noPagination; + paginated = !_this.reportLoader.removePaginationButton.closest('span').hasClass('hide'), + requestPath = $(event.target).attr('href') + '&' + $('#filter-search').serialize() + '&page=' + currentPage + "&per_page=" + _this.reportLoader.pageSelector.find(':selected').attr('value') + '&paginate=' + paginated; _this.reportLoader.requestUrl = requestPath; $.ajax({ diff --git a/app/assets/javascripts/spree/backend/spree_reportify.js b/app/assets/javascripts/spree/backend/spree_reportify.js deleted file mode 100644 index 916b945..0000000 --- a/app/assets/javascripts/spree/backend/spree_reportify.js +++ /dev/null @@ -1,2 +0,0 @@ -//= require spree/backend/spree_reportify/report_loader -//= require spree/backend/tmpl diff --git a/app/assets/javascripts/spree/frontend/spree_reportify.js b/app/assets/javascripts/spree/frontend/spree_admin_insights.js similarity index 100% rename from app/assets/javascripts/spree/frontend/spree_reportify.js rename to app/assets/javascripts/spree/frontend/spree_admin_insights.js diff --git a/app/assets/stylesheets/spree/backend/spree_reportify.css b/app/assets/stylesheets/spree/backend/spree_admin_insights.css similarity index 100% rename from app/assets/stylesheets/spree/backend/spree_reportify.css rename to app/assets/stylesheets/spree/backend/spree_admin_insights.css diff --git a/app/assets/stylesheets/spree/frontend/spree_reportify.css b/app/assets/stylesheets/spree/frontend/spree_admin_insights.css similarity index 100% rename from app/assets/stylesheets/spree/frontend/spree_reportify.css rename to app/assets/stylesheets/spree/frontend/spree_admin_insights.css diff --git a/app/controllers/spree/admin/insights_controller.rb b/app/controllers/spree/admin/insights_controller.rb index e54c73d..c3648d3 100644 --- a/app/controllers/spree/admin/insights_controller.rb +++ b/app/controllers/spree/admin/insights_controller.rb @@ -2,6 +2,7 @@ module Spree module Admin class InsightsController < Spree::Admin::BaseController before_action :ensure_report_exists, :set_default_pagination, only: [:show, :download] + before_action :set_reporting_period, only: [:index, :show, :download] before_action :load_reports, only: [:index, :show] def index @@ -12,46 +13,29 @@ def index end def show - @headers, @stats, @total_pages, @search_attributes, @chart_json, @resource = ReportGenerationService.generate_report( - @report_name, - params.merge(@pagination_hash) - ) - - @report_data_json = { - current_page: params[:page] || 0, - headers: @headers, - report_type: params[:type], - request_path: request.path, - search_attributes: @search_attributes, - stats: @stats, - total_pages: @total_pages, - url: request.url, - searched_fields: params[:search], - per_page: @pagination_hash[:records_per_page], - chart_json: @chart_json, - pagination_required: !@resource.no_pagination? - } + report = ReportGenerationService.generate_report(@report_name, params.merge(@pagination_hash)) + @report_data = shared_data.merge(report.to_h) respond_to do |format| format.html { render :index } - format.json { render json: @report_data_json } + format.json { render json: @report_data } end end def download - @headers, @stats = ReportGenerationService.generate_report(@report_name, params.merge(@pagination_hash)) + @report = ReportGenerationService.generate_report(@report_name, params.merge(@pagination_hash)) respond_to do |format| format.csv do - send_data ReportGenerationService.download(@headers, @stats), + send_data ReportGenerationService.download(@report), filename: "#{ @report_name.to_s }.csv" end format.xls do - send_data ReportGenerationService.download({ col_sep: "\t" }, @headers, @stats), + send_data ReportGenerationService.download(@report, { col_sep: "\t" }), filename: "#{ @report_name.to_s }.xls" end format.text do - send_data ReportGenerationService.download(@headers, @stats), + send_data ReportGenerationService.download(@report), filename: "#{ @report_name.to_s }.txt" end format.pdf do @@ -65,27 +49,60 @@ def download private def ensure_report_exists @report_name = params[:id].to_sym - unless ReportGenerationService::REPORTS[get_reports_type].include? @report_name + unless ReportGenerationService.report_exists?(get_report_category, @report_name) redirect_to admin_insights_path, alert: Spree.t(:not_found, scope: [:reports]) end end def load_reports - @reports = ReportGenerationService::REPORTS[get_reports_type] + @reports = ReportGenerationService.reports_for_category(get_report_category) + end + + def shared_data + { + current_page: params[:page] || 0, + report_category: params[:report_category], + request_path: request.path, + url: request.url, + searched_fields: params[:search], + } + end + + def get_report_category + params[:report_category] = if params[:report_category] + params[:report_category].to_sym + else + session[:report_category].try(:to_sym) || ReportGenerationService.default_report_category + end + session[:report_category] = params[:report_category] end - def get_reports_type - params[:type] = if params[:type] - params[:type].to_sym + def set_reporting_period + if params[:search].present? + if params[:search][:start_date] == "" + # When clicking on 'x' to remove the filter + params[:search][:start_date] = nil + else + params[:search][:start_date] = params[:search][:start_date] || session[:search_start_date] + end + if params[:search][:end_date] == "" + params[:search][:end_date] = nil + else + params[:search][:end_date] = params[:search][:end_date].presence || session[:search_end_date] + end else - session[:report_category].try(:to_sym) || ReportGenerationService::REPORTS.keys.first + params[:search] = {} + params[:search][:start_date] = session[:search_start_date] + params[:search][:end_date] = session[:search_end_date] end - session[:report_category] = params[:type] + session[:search_start_date] = params[:search][:start_date] + session[:search_end_date] = params[:search][:end_date] end def set_default_pagination - @pagination_hash = {} - if params[:no_pagination] != 'true' + @pagination_hash = { paginate: false } + unless params[:paginate] == 'false' + @pagination_hash[:paginate] = true @pagination_hash[:records_per_page] = params[:per_page].try(:to_i) || Spree::Config[:records_per_page] @pagination_hash[:offset] = params[:page].to_i * @pagination_hash[:records_per_page] end diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb new file mode 100644 index 0000000..47afb2c --- /dev/null +++ b/app/models/spree/product_decorator.rb @@ -0,0 +1,3 @@ +Spree::Product.class_eval do + has_many :page_view_events, -> { viewed.product }, class_name: 'Spree::PageEvent', foreign_key: :target_id +end diff --git a/app/models/spree/promotion_action_decorator.rb b/app/models/spree/promotion_action_decorator.rb new file mode 100644 index 0000000..e957bc9 --- /dev/null +++ b/app/models/spree/promotion_action_decorator.rb @@ -0,0 +1,3 @@ +Spree::PromotionAction.class_eval do + has_one :adjustment, -> { promotion }, class_name: 'Spree::Adjustment', foreign_key: :source_id +end diff --git a/app/models/spree/return_authorization_decorator.rb b/app/models/spree/return_authorization_decorator.rb new file mode 100644 index 0000000..651305b --- /dev/null +++ b/app/models/spree/return_authorization_decorator.rb @@ -0,0 +1,4 @@ +Spree::ReturnAuthorization.class_eval do + has_many :variants, through: :inventory_units + has_many :products, through: :variants +end diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb new file mode 100644 index 0000000..8654a38 --- /dev/null +++ b/app/models/spree/user_decorator.rb @@ -0,0 +1,3 @@ +Spree::User.class_eval do + has_many :spree_orders, class_name: 'Spree::Order' +end diff --git a/app/reports/spree/annual_promotional_cost_report.rb b/app/reports/spree/annual_promotional_cost_report.rb deleted file mode 100644 index f200e63..0000000 --- a/app/reports/spree/annual_promotional_cost_report.rb +++ /dev/null @@ -1,58 +0,0 @@ -module Spree - class AnnualPromotionalCostReport < Spree::PromotionalCostReport - DEFAULT_SORTABLE_ATTRIBUTE = :promotion_name - HEADERS = { promotion_name: :string, usage_count: :integer, promotion_discount: :integer } - SEARCH_ATTRIBUTES = {} - SORTABLE_ATTRIBUTES = [] - - def generate - super - data = [] - group_by_promotion_name.each_pair do |promotion_name, collection| - data << { promotion_name: promotion_name, promotion_discount: collection.sum { |r| r[:promotion_discount] }, usage_count: collection.sum { |r| r[:usage_count] } } - end - data - end - - def chart_data - data = generate - total_discount = (data.sum { |r| r[:promotion_discount] } / 100) - data.map { |r| { name: r[:promotion_name], y: (r[:promotion_discount] / total_discount).to_f } } - end - - def chart_json - { - chart: true, - charts: [ - { - name: 'annual-promotional-cost', - json: { - chart: { type: 'pie' }, - title: { - useHTML: true, - text: 'Annual Promotional Cost' - }, - tooltip: { - pointFormat: 'Cost %: {point.percentage:.1f}%' - }, - plotOptions: { - pie: { - allowPointSelect: true, - cursor: 'pointer', - dataLabels: { - enabled: false - }, - showInLegend: true - } - }, - series: [{ - name: 'Annual Promotion', - data: chart_data - }] - } - } - ] - } - end - end -end diff --git a/app/reports/spree/best_selling_products_report.rb b/app/reports/spree/best_selling_products_report.rb index 2e3ecd7..05b6e98 100644 --- a/app/reports/spree/best_selling_products_report.rb +++ b/app/reports/spree/best_selling_products_report.rb @@ -1,34 +1,42 @@ module Spree class BestSellingProductsReport < Spree::Report DEFAULT_SORTABLE_ATTRIBUTE = :sold_count - HEADERS = { sku: :string, product_name: :string, sold_count: :integer } - SEARCH_ATTRIBUTES = { start_date: :orders_completed_from, end_date: :orders_completed_to } - SORTABLE_ATTRIBUTES = [:product_name, :sku, :sold_count] + HEADERS = { sku: :string, product_name: :string, sold_count: :integer } + SEARCH_ATTRIBUTES = { start_date: :orders_completed_from, end_date: :orders_completed_to } + SORTABLE_ATTRIBUTES = [:product_name, :sku, :sold_count] - def initialize(options) - super - @name = @search[:name].present? ? "%#{ @search[:name] }%" : '%' - @sortable_type = :desc if options[:sort].blank? - set_sortable_attributes(options, DEFAULT_SORTABLE_ATTRIBUTE) + deeplink product_name: { template: %Q{{%# o.product_name %}} } + + class Result < Spree::Report::Result + class Observation < Spree::Report::Observation + observation_fields [:product_name, :product_slug, :sku, :sold_count] + + def sku + @sku.presence || @product_name + end + end end - def generate - ::SpreeReportify::ReportDb[:spree_line_items___line_items]. - join(:spree_orders___orders, id: :order_id). - join(:spree_variants___variants, variants__id: :line_items__variant_id). - join(:spree_products___products, products__id: :variants__product_id). - where(orders__state: 'complete'). - where(orders__completed_at: @start_date..@end_date). #filter by params - group(:variant_id). - order(sortable_sequel_expression) + def report_query + Spree::LineItem + .joins(:order) + .joins(:variant) + .joins(:product) + .where(Spree::Product.arel_table[:name].matches(search_name)) + .where(spree_orders: { state: 'complete' }) + .where(spree_orders: { completed_at: reporting_period }) + .group(:variant_id, :product_name, :product_slug, 'spree_variants.sku') + .select( + 'spree_products.name as product_name', + 'spree_products.slug as product_slug', + 'spree_variants.sku as sku', + 'sum(quantity) as sold_count' + ) end - def select_columns(dataset) - dataset.select{[ - products__name.as(product_name), - Sequel.as(IF(STRCMP(variants__sku, ''), variants__sku, products__name), :sku), - sum(quantity).as(sold_count) - ]} + private def search_name + search[:name].present? ? "%#{ search[:name] }%" : '%' end + end end diff --git a/app/reports/spree/cart_additions_report.rb b/app/reports/spree/cart_additions_report.rb index f92e99c..9fa059c 100644 --- a/app/reports/spree/cart_additions_report.rb +++ b/app/reports/spree/cart_additions_report.rb @@ -1,32 +1,36 @@ module Spree class CartAdditionsReport < Spree::Report DEFAULT_SORTABLE_ATTRIBUTE = :product_name - HEADERS = { sku: :string, product_name: :string, additions: :integer, quantity_change: :integer } - SEARCH_ATTRIBUTES = { start_date: :product_added_from, end_date: :product_added_to } - SORTABLE_ATTRIBUTES = [:product_name, :sku, :additions, :quantity_change] + HEADERS = { sku: :string, product_name: :string, additions: :integer, quantity_change: :integer } + SEARCH_ATTRIBUTES = { start_date: :product_added_from, end_date: :product_added_to } + SORTABLE_ATTRIBUTES = [:product_name, :sku, :additions, :quantity_change] - def initialize(options) - super - set_sortable_attributes(options, DEFAULT_SORTABLE_ATTRIBUTE) - end + deeplink product_name: { template: %Q{{%# o.product_name %}} } + + class Result < Spree::Report::Result + class Observation < Spree::Report::Observation + observation_fields [:product_name, :product_slug, :additions, :quantity_change, :sku] - def generate - SpreeReportify::ReportDb[:spree_cart_events___cart_events]. - join(:spree_variants___variants, id: :variant_id). - join(:spree_products___products, id: :product_id). - where(cart_events__activity: 'add'). - where(cart_events__created_at: @start_date..@end_date). - group(:variant_id). - order(sortable_sequel_expression) + def sku + @sku.presence || @product_name + end + end end - def select_columns(dataset) - dataset.select{[ - products__name.as(product_name), - Sequel.as(IF(STRCMP(variants__sku, ''), variants__sku, products__name), :sku), - Sequel.as(count(:products__name), :additions), - Sequel.as(sum(cart_events__quantity), :quantity_change) - ]} + def report_query + Spree::CartEvent + .added + .joins(:variant) + .joins(:product) + .where(created_at: reporting_period) + .group('product_name', 'product_slug', 'spree_variants.sku') + .select( + 'spree_products.name as product_name', + 'spree_products.slug as product_slug', + 'spree_variants.sku as sku', + 'count(spree_products.name) as additions', + 'sum(spree_cart_events.quantity) as quantity_change' + ) end end end diff --git a/app/reports/spree/cart_removals_report.rb b/app/reports/spree/cart_removals_report.rb index 1b86638..eeece76 100644 --- a/app/reports/spree/cart_removals_report.rb +++ b/app/reports/spree/cart_removals_report.rb @@ -1,32 +1,36 @@ module Spree class CartRemovalsReport < Spree::Report DEFAULT_SORTABLE_ATTRIBUTE = :product_name - HEADERS = { sku: :string, product_name: :string, removals: :integer, quantity_change: :integer } - SEARCH_ATTRIBUTES = { start_date: :product_removed_from, end_date: :product_removed_to } - SORTABLE_ATTRIBUTES = [:product_name, :sku, :removals, :quantity_change] + HEADERS = { sku: :string, product_name: :string, removals: :integer, quantity_change: :integer } + SEARCH_ATTRIBUTES = { start_date: :product_removed_from, end_date: :product_removed_to } + SORTABLE_ATTRIBUTES = [:product_name, :sku, :removals, :quantity_change] - def initialize(options) - super - set_sortable_attributes(options, DEFAULT_SORTABLE_ATTRIBUTE) - end + deeplink product_name: { template: %Q{{%# o.product_name %}} } + + class Result < Spree::Report::Result + class Observation < Spree::Report::Observation + observation_fields [:product_name, :product_slug, :removals, :quantity_change, :sku] - def generate - SpreeReportify::ReportDb[:spree_cart_events___cart_events]. - join(:spree_variants___variants, id: :variant_id). - join(:spree_products___products, id: :product_id). - where(cart_events__activity: 'remove'). - where(cart_events__created_at: @start_date..@end_date). #filter by params - group(:variant_id). - order(sortable_sequel_expression) + def sku + @sku.presence || @product_name + end + end end - def select_columns(dataset) - dataset.select{[ - products__name.as(product_name), - Sequel.as(IF(STRCMP(variants__sku, ''), variants__sku, products__name), :sku), - Sequel.as(count(:products__name), :removals), - Sequel.as(sum(cart_events__quantity), :quantity_change) - ]} + def report_query + Spree::CartEvent + .removed + .joins(:variant) + .joins(:product) + .where(created_at: reporting_period) + .group('product_name', 'product_slug', 'spree_variants.sku') + .select( + 'spree_products.name as product_name', + 'spree_products.slug as product_slug', + 'spree_variants.sku as sku', + 'count(spree_products.name) as removals', + 'sum(spree_cart_events.quantity) as quantity_change' + ) end end end diff --git a/app/reports/spree/cart_updations_report.rb b/app/reports/spree/cart_updations_report.rb index a14af9a..b377cca 100644 --- a/app/reports/spree/cart_updations_report.rb +++ b/app/reports/spree/cart_updations_report.rb @@ -1,33 +1,40 @@ module Spree class CartUpdationsReport < Spree::Report DEFAULT_SORTABLE_ATTRIBUTE = :product_name - HEADERS = { sku: :string, product_name: :string, updations: :integer, quantity_increase: :integer, quantity_decrease: :integer } - SEARCH_ATTRIBUTES = { start_date: :product_updated_from, end_date: :product_updated_to } - SORTABLE_ATTRIBUTES = [:product_name, :sku, :updations, :quantity_increase, :quantity_decrease] + HEADERS = { sku: :string, product_name: :string, updations: :integer, quantity_increase: :integer, quantity_decrease: :integer } + SEARCH_ATTRIBUTES = { start_date: :product_updated_from, end_date: :product_updated_to } + SORTABLE_ATTRIBUTES = [:product_name, :sku, :updations, :quantity_increase, :quantity_decrease] - def initialize(options) - super - set_sortable_attributes(options, DEFAULT_SORTABLE_ATTRIBUTE) - end + deeplink product_name: { template: %Q{{%# o.product_name %}} } + + class Result < Spree::Report::Result + class Observation < Spree::Report::Observation + observation_fields [:product_name, :product_slug, :updations, :quantity_increase, :sku, :quantity_decrease] - def generate - SpreeReportify::ReportDb[:spree_cart_events___cart_events]. - join(:spree_variants___variants, id: :variant_id). - join(:spree_products___products, id: :product_id). - where(activity: 'update'). - where(cart_events__created_at: @start_date..@end_date). #filter by params - group(:variant_id). - order(sortable_sequel_expression) + def sku + @sku.presence || @product_name + end + end end - def select_columns(dataset) - dataset.select{[ - products__name.as(product_name), - Sequel.as(IF(STRCMP(variants__sku, ''), variants__sku, products__name), :sku), - Sequel.as(count(:products__name), :updations), - Sequel.as(sum(IF(cart_events__quantity >= 0, cart_events__quantity, 0)), :quantity_increase), - Sequel.as(sum(IF(cart_events__quantity <= 0, cart_events__quantity, 0)), :quantity_decrease) - ]} + def report_query + quantity_increase_sql = "CASE WHEN quantity > 0 then spree_cart_events.quantity ELSE 0 END" + quantity_decrease_sql = "CASE WHEN quantity < 0 then spree_cart_events.quantity ELSE 0 END" + + Spree::CartEvent + .updated + .joins(:variant) + .joins(:product) + .where(created_at: reporting_period) + .group('product_name', 'product_slug', 'spree_variants.sku') + .select( + 'spree_products.name as product_name', + 'spree_products.slug as product_slug', + 'spree_variants.sku as sku', + 'count(spree_products.name) as updations', + "SUM(#{ quantity_increase_sql }) as quantity_increase", + "SUM(#{ quantity_decrease_sql }) as quantity_decrease" + ) end end end diff --git a/app/reports/spree/payment_method_transactions_conversion_rate_report.rb b/app/reports/spree/payment_method_transactions_conversion_rate_report.rb index 96f116c..cc9bfda 100644 --- a/app/reports/spree/payment_method_transactions_conversion_rate_report.rb +++ b/app/reports/spree/payment_method_transactions_conversion_rate_report.rb @@ -1,93 +1,74 @@ module Spree class PaymentMethodTransactionsConversionRateReport < Spree::Report DEFAULT_SORTABLE_ATTRIBUTE = :payment_method_name - HEADERS = { payment_method_name: :string, payment_state: :string, months_name: :string, count: :integer } - SEARCH_ATTRIBUTES = { start_date: :payments_created_from, end_date: :payments_created_to } - SORTABLE_ATTRIBUTES = [:payment_method_name, :successful_payments_count, :failed_payments_count, :pending_payments_count, :invalid_payments_count] + HEADERS = { payment_method_name: :string, payment_state: :string, months_name: :string, count: :integer } + SEARCH_ATTRIBUTES = { start_date: :payments_created_from, end_date: :payments_created_to } + SORTABLE_ATTRIBUTES = [:payment_method_name, :successful_payments_count, :failed_payments_count, :pending_payments_count, :invalid_payments_count] - def no_pagination? - true - end - - def generate - payment_methods = SpreeReportify::ReportDb[:spree_payment_methods___payment_methods]. - join(:spree_payments___payments, payment_method_id: :id). - where(payments__created_at: @start_date..@end_date). #filter by params - select{[ - payment_method_id, - Sequel.as(name, :payment_method_name), - Sequel.as(IF(STRCMP(state, 'pending'), state, concat('capturing ', state)), :payment_state), - Sequel.as(MONTHNAME(:payments__created_at), :month_name), - Sequel.as(MONTH(:payments__created_at), :number), - Sequel.as(YEAR(:payments__created_at), :year) - ]} + class Result < Spree::Report::TimedResult + charts PaymentMethodStateDistributionChart - group_by_months = SpreeReportify::ReportDb[payment_methods]. - group(:months_name, :payment_method_name, :payment_state). - order(:year, :number). - select{[ - payment_method_name, - number, - payment_state, - year, - Sequel.as(concat(month_name, ' ', year), :months_name), - Sequel.as(COUNT(payment_method_id), :count), - ]} + def build_empty_observations + super + @_payment_methods = @results.collect { |result| result['payment_method_name'] }.uniq + @observations = @_payment_methods.collect do |payment_method_name| + payment_states = @results + .select { |result| result['payment_method_name'] == payment_method_name } + .collect { |result| result['payment_state'] } + .uniq - grouped_by_payment_method_name = group_by_months.all.group_by { |record| record[:payment_method_name] } - data = [] - grouped_by_payment_method_name.each_pair do |name, collection| - collection.group_by { |r| r[:payment_state] }.each_pair do |state, collection| - data << fill_missing_values({ payment_method_name: name, payment_state: state, count: 0 }, collection) - end + payment_states.collect do |state| + @observations.collect do |observation| + _d_observation = observation.dup + _d_observation.payment_method_name = payment_method_name + _d_observation.payment_state = state + _d_observation.count = 0 + _d_observation + end + end + end.flatten end - @data = data.flatten - end - def group_by_payment_method_name - @grouped_by_payment_method_name ||= @data.group_by { |record| record[:payment_method_name] } - end + class Observation < Spree::Report::TimedObservation + observation_fields [:payment_method_name, :payment_state, :count] - def chart_data - { - months_name: group_by_payment_method_name.first.try(:second).try(:map) { |record| record[:months_name] }, - collection: group_by_payment_method_name - } - end - - def chart_json - { - chart: true, - charts: chart_data[:collection].map do |method_name, collection| - { - id: 'payment-state-' + method_name, - json: { - chart: { type: 'column' }, - title: { - useHTML: true, - text: "#{ method_name } Conversion Status" - }, + def payment_state + if @payment_state == 'pending' + @payment_state + else + "capturing #{ @payment_state }" + end + end - xAxis: { categories: chart_data[:months_name] }, - yAxis: { - title: { text: 'Count' } - }, - tooltip: { valuePrefix: '#' }, - legend: { - layout: 'vertical', - align: 'right', - verticalAlign: 'middle', - borderWidth: 0 - }, - series: collection.group_by { |r| r[:payment_state] }.map { |key, value| { name: key, data: value.map { |r| r[:count].to_i } } } - } - } + def describes?(result, time_scale) + (result['payment_method_name'] == payment_method_name && result['payment_state'] == @payment_state) && super end - } + end + end + + def report_query + Spree::Report::QueryFragments + .from_subquery(payment_methods) + .group(*time_scale_columns_to_s, 'payment_method_name', 'payment_state') + .order(*time_scale_columns) + .project( + *time_scale_columns, + 'payment_method_name', + 'payment_state', + 'COUNT(payment_method_id) as count' + ) end - def select_columns(dataset) - dataset + private def payment_methods + Spree::PaymentMethod + .joins(:payments) + .where(spree_payments: { created_at: reporting_period }) + .select( + 'spree_payment_methods.id as payment_method_id', + 'name as payment_method_name', + 'state as payment_state', + *time_scale_selects('spree_payments') + ) end end end diff --git a/app/reports/spree/payment_method_transactions_conversion_rate_report/payment_method_state_distribution_chart.rb b/app/reports/spree/payment_method_transactions_conversion_rate_report/payment_method_state_distribution_chart.rb new file mode 100644 index 0000000..67da3fd --- /dev/null +++ b/app/reports/spree/payment_method_transactions_conversion_rate_report/payment_method_state_distribution_chart.rb @@ -0,0 +1,39 @@ +class Spree::PaymentMethodTransactionsConversionRateReport::PaymentMethodStateDistributionChart + attr_accessor :chart_data + + def initialize(result) + @time_dimension = result.time_dimension + @grouped_by_payment_method = result.observations.group_by(&:payment_method_name) + @time_series = [] + @time_series = @grouped_by_payment_method.values.first.collect { |observation| observation.send(@time_dimension) } if @grouped_by_payment_method.first.present? + end + + def to_h + @grouped_by_payment_method.collect do |method_name, observations| + { + id: 'payment-state-' + method_name, + json: { + chart: { type: 'column' }, + title: { + useHTML: true, + text: %Q(#{ method_name } Conversion Status + ) + }, + + xAxis: { categories: @time_series }, + yAxis: { + title: { text: 'Count' } + }, + tooltip: { valuePrefix: '#' }, + legend: { + layout: 'vertical', + align: 'right', + verticalAlign: 'middle', + borderWidth: 0 + }, + series: observations.group_by(&:payment_state).map { |key, value| { name: key, data: value.map(&:count) } } + } + } + end + end +end diff --git a/app/reports/spree/payment_method_transactions_report.rb b/app/reports/spree/payment_method_transactions_report.rb index c02498a..0c6e079 100644 --- a/app/reports/spree/payment_method_transactions_report.rb +++ b/app/reports/spree/payment_method_transactions_report.rb @@ -1,88 +1,60 @@ module Spree class PaymentMethodTransactionsReport < Spree::Report DEFAULT_SORTABLE_ATTRIBUTE = :payment_method_name - HEADERS = { payment_method_name: :string, months_name: :string, payment_amount: :integer } - SEARCH_ATTRIBUTES = { start_date: :payments_created_from, end_date: :payments_created_till } - SORTABLE_ATTRIBUTES = [] - - def no_pagination? - true - end + HEADERS = { payment_method_name: :string, payment_amount: :integer } + SEARCH_ATTRIBUTES = { start_date: :payments_created_from, end_date: :payments_created_till } + SORTABLE_ATTRIBUTES = [] + + class Result < Spree::Report::TimedResult + charts PaymentMethodRevenueDistributionChart + + def build_empty_observations + super + @_payment_methods = @results.collect { |result| result['payment_method_name'] }.uniq + @observations = @_payment_methods.collect do |payment_method_name| + @observations.collect do |observation| + _d_observation = observation.dup + _d_observation.payment_amount = 0 + _d_observation.payment_method_name = payment_method_name + _d_observation + end + end.flatten + end - def generate - payments = SpreeReportify::ReportDb[:spree_payment_methods___payment_methods]. - join(:spree_payments___payments, payment_method_id: :id). - where(payments__created_at: @start_date..@end_date). #filter by params - select{[ - Sequel.as(payment_methods__name, :payment_method_name), - Sequel.as(payments__amount, :payment_amount), - Sequel.as(MONTHNAME(:payments__created_at), :month_name), - Sequel.as(MONTH(:payments__created_at), :number), - Sequel.as(YEAR(:payments__created_at), :year) - ]} + class Observation < Spree::Report::TimedObservation + observation_fields [:payment_method_name, :payment_amount] - group_by_months = SpreeReportify::ReportDb[payments]. - group(:months_name, :payment_method_name). - order(:year, :number). - select{[ - number, - payment_method_name, - year, - Sequel.as(concat(month_name, ' ', year), :months_name), - Sequel.as(SUM(payment_amount), :payment_amount) - ]} + def describes?(result, time_scale) + (result['payment_method_name'] == payment_method_name) && super + end - grouped_by_payment_method_name = group_by_months.all.group_by { |record| record[:payment_method_name] } - data = [] - grouped_by_payment_method_name.each_pair do |name, collection| - data << fill_missing_values({ payment_method_name: name, payment_amount: 0 }, collection) + def payment_amount + @payment_amount.to_f + end end - @data = data.flatten - end - - def group_by_payment_method_name - @grouped_by_payment_method_name ||= @data.group_by { |record| record[:payment_method_name] } - end - - def chart_data - { - months_name: group_by_payment_method_name.first.try(:second).try(:map) { |record| record[:months_name] }, - collection: group_by_payment_method_name - } end - def chart_json - { - chart: true, - charts: [ - { - id: 'payment-methods', - json: { - chart: { type: 'column' }, - title: { - useHTML: true, - text: "Payment Methods" - }, - xAxis: { categories: chart_data[:months_name] }, - yAxis: { - title: { text: 'value($)' } - }, - tooltip: { valuePrefix: '$' }, - legend: { - layout: 'vertical', - align: 'right', - verticalAlign: 'middle', - borderWidth: 0 - }, - series: chart_data[:collection].map { |key, value| { name: key, data: value.map { |r| r[:payment_amount].to_f } } } - } - } - ] - } + def report_query + Spree::Report::QueryFragments + .from_subquery(payments) + .group(*time_scale_columns_to_s, 'payment_method_name') + .order(*time_scale_columns) + .project( + *time_scale_columns, + 'payment_method_name', + 'SUM(payment_amount) as payment_amount' + ) end - def select_columns(dataset) - dataset + private def payments + Spree::PaymentMethod + .joins(:payments) + .where(spree_payments: { created_at: reporting_period }) + .select( + *time_scale_selects('spree_payments'), + 'spree_payment_methods.name as payment_method_name', + 'spree_payments.amount as payment_amount', + ) end end end diff --git a/app/reports/spree/payment_method_transactions_report/payment_method_revenue_distribution_chart.rb b/app/reports/spree/payment_method_transactions_report/payment_method_revenue_distribution_chart.rb new file mode 100644 index 0000000..c7310b7 --- /dev/null +++ b/app/reports/spree/payment_method_transactions_report/payment_method_revenue_distribution_chart.rb @@ -0,0 +1,36 @@ +class Spree::PaymentMethodTransactionsReport::PaymentMethodRevenueDistributionChart + def initialize(result) + @time_dimension = result.time_dimension + @grouped_by_payment_method = result.observations.group_by(&:payment_method_name) + @time_series = [] + if @grouped_by_payment_method.values.first.present? + @time_series = @grouped_by_payment_method.values.first.collect { |observation| observation.send(@time_dimension) } + end + end + + def to_h + { + id: 'payment-methods', + json: { + chart: { type: 'column' }, + title: { + useHTML: true, + text: "Payment Methods" + }, + xAxis: { categories: @time_series }, + yAxis: { + title: { text: 'value($)' } + }, + tooltip: { valuePrefix: '$' }, + legend: { + layout: 'vertical', + align: 'right', + verticalAlign: 'middle', + borderWidth: 0 + }, + series: @grouped_by_payment_method.collect { |key, value| { name: key, data: value.map(&:payment_amount) } + } + } + } + end +end diff --git a/app/reports/spree/product_views_report.rb b/app/reports/spree/product_views_report.rb index e86f572..dee1348 100644 --- a/app/reports/spree/product_views_report.rb +++ b/app/reports/spree/product_views_report.rb @@ -5,37 +5,42 @@ class ProductViewsReport < Spree::Report SEARCH_ATTRIBUTES = { start_date: :product_view_from, end_date: :product_view_till, name: :name} SORTABLE_ATTRIBUTES = [:product_name, :views, :users, :guest_sessions] - def initialize(options) - super - @name = @search[:name].present? ? "%#{ @search[:name] }%" : '%' - set_sortable_attributes(options, DEFAULT_SORTABLE_ATTRIBUTE) - end + deeplink product_name: { template: %Q{{%# o.product_name %}} } - def generate - unique_session_results = ::SpreeReportify::ReportDb[:spree_products___products]. - join(:spree_page_events___page_events, target_id: :id). - where(page_events__target_type: 'Spree::Product', page_events__activity: 'view'). - where(page_events__created_at: @start_date..@end_date).where(Sequel.ilike(:products__name, @name)). - group(:product_name, :page_events__actor_id, :page_events__session_id). - select{[ - products__name.as(product_name), - count('*').as(total_views_per_session), - page_events__session_id.as(session_id), - page_events__actor_id.as(actor_id) - ]}.as(:unique_session_results) + class Result < Spree::Report::Result + class Observation < Spree::Report::Observation + observation_fields [:product_name, :product_slug, :views, :users, :guest_sessions] + end + end - ::SpreeReportify::ReportDb[unique_session_results]. - group(:product_name). - order(sortable_sequel_expression) + def report_query + viewed_events = + Spree::Product + .where(Spree::Product.arel_table[:name].matches(search_name)) + .joins(:page_view_events) + .where(spree_page_events: { created_at: reporting_period }) + .group('product_name', 'product_slug', 'spree_page_events.actor_id', 'spree_page_events.session_id') + .select( + 'spree_products.name as product_name', + 'spree_products.slug as product_slug', + 'COUNT(*) as total_views_per_session', + 'spree_page_events.session_id as session_id', + 'spree_page_events.actor_id as actor_id' + ) + Spree::Report::QueryFragments + .from_subquery(viewed_events) + .group('product_name', 'product_slug') + .project( + 'product_name', + 'product_slug', + 'SUM(total_views_per_session) as views', + 'COUNT(DISTINCT actor_id) as users', + '(COUNT(DISTINCT session_id) - COUNT(actor_id)) as guest_sessions' + ) end - def select_columns(dataset) - dataset.select{[ - product_name, - sum(total_views_per_session).as(views), - count(DISTINCT actor_id).as(users), - (COUNT(DISTINCT session_id) - COUNT(actor_id)).as(guest_sessions) - ]} + private def search_name + search[:name].present? ? "%#{ search[:name] }%" : '%' end end end diff --git a/app/reports/spree/product_views_to_cart_additions_report.rb b/app/reports/spree/product_views_to_cart_additions_report.rb index fef4192..6640920 100644 --- a/app/reports/spree/product_views_to_cart_additions_report.rb +++ b/app/reports/spree/product_views_to_cart_additions_report.rb @@ -1,49 +1,53 @@ module Spree class ProductViewsToCartAdditionsReport < Spree::Report DEFAULT_SORTABLE_ATTRIBUTE = :product_name - HEADERS = { product_name: :string, views: :integer, cart_additions: :integer, cart_to_view_ratio: :string } - SEARCH_ATTRIBUTES = { start_date: :product_view_from, end_date: :product_view_till } - SORTABLE_ATTRIBUTES = [:product_name, :views, :cart_additions] + HEADERS = { product_name: :string, views: :integer, cart_additions: :integer, cart_to_view_ratio: :string } + SEARCH_ATTRIBUTES = { start_date: :product_view_from, end_date: :product_view_till } + SORTABLE_ATTRIBUTES = [:product_name, :views, :cart_additions] - def initialize(options) - super - set_sortable_attributes(options, DEFAULT_SORTABLE_ATTRIBUTE) - end + deeplink product_name: { template: %Q{{%# o.product_name %}} } - def generate(options = {}) - cart_additions = SpreeReportify::ReportDb[:spree_cart_events___cart_events]. - join(:spree_variants___variants, id: :variant_id). - join(:spree_products___products, id: :product_id). - where(cart_events__activity: 'add'). - where(cart_events__created_at: @start_date..@end_date). #filter by params - group(:product_name). - select{[( - :products__name___product_name), - Sequel.as(sum(cart_events__quantity), :cart_additions) - ]}.as(:cart_additions) + class Result < Spree::Report::Result + class Observation < Spree::Report::Observation + observation_fields [:product_name, :product_slug, :views, :cart_additions, :cart_to_view_ratio] + def cart_to_view_ratio + (cart_additions.to_f / views.to_f).round(2) + end + end + end - total_views_results = ::SpreeReportify::ReportDb[:spree_products___products]. - join(:spree_page_events___page_events, target_id: :id). - where(page_events__target_type: 'Spree::Product', page_events__activity: 'view'). - group(:product_name). - select{[ - products__name.as(product_name), - count('*').as(views) - ]} + def report_query + cart_additions = + Spree::CartEvent + .added + .joins(:variant) + .joins(:product) + .where(created_at: reporting_period) + .group('spree_products.name', 'spree_products.slug') + .select( + 'spree_products.name as product_name', + 'spree_products.slug as product_slug', + 'SUM(spree_cart_events.quantity) as cart_additions' + ) + total_views = + Spree::Product + .joins(:page_view_events) + .group(:name) + .select( + 'spree_products.name as product_name', + 'COUNT(*) as views' + ) - ::SpreeReportify::ReportDb[total_views_results]. - join(cart_additions, product_name: :product_name). - order(sortable_sequel_expression) + Spree::Report::QueryFragments + .from_join(cart_additions, total_views, "q1.product_name = q2.product_name") + .project( + 'q1.product_name', + 'q1.product_slug', + 'q2.views', + 'q1.cart_additions' + ) end - def select_columns(dataset) - dataset.select{[ - cart_additions__product_name, - views, - cart_additions__cart_additions, - Sequel.as(ROUND(cart_additions__cart_additions/ views, 2), :cart_to_view_ratio) - ]} - end end end diff --git a/app/reports/spree/product_views_to_purchases_report.rb b/app/reports/spree/product_views_to_purchases_report.rb index f20cfa7..c3a65f9 100644 --- a/app/reports/spree/product_views_to_purchases_report.rb +++ b/app/reports/spree/product_views_to_purchases_report.rb @@ -1,43 +1,54 @@ module Spree class ProductViewsToPurchasesReport < Spree::Report DEFAULT_SORTABLE_ATTRIBUTE = :product_name - HEADERS = { product_name: :string, views: :integer, purchases: :integer, purchase_to_view_ratio: :integer } - SEARCH_ATTRIBUTES = { start_date: :product_view_from, end_date: :product_view_till } - SORTABLE_ATTRIBUTES = [:product_name, :views, :purchases] + HEADERS = { product_name: :string, views: :integer, purchases: :integer, purchase_to_view_ratio: :integer } + SEARCH_ATTRIBUTES = { start_date: :product_view_from, end_date: :product_view_till } + SORTABLE_ATTRIBUTES = [:product_name, :views, :purchases] - def initialize(options) - super - set_sortable_attributes(options, DEFAULT_SORTABLE_ATTRIBUTE) + class Result < Spree::Report::Result + class Observation < Spree::Report::Observation + observation_fields [:product_name, :product_slug, :views, :purchases, :purchase_to_view_ratio] + + def purchase_to_view_ratio # This is inconsistent across postgres and mysql + (purchases.to_f / views.to_f).round(2) + end + end end - def generate(options = {}) - line_items = ::SpreeReportify::ReportDb[:spree_line_items___line_items]. - join(:spree_orders___orders, id: :order_id). - join(:spree_variants___variants, variants__id: :line_items__variant_id). - join(:spree_products___products, products__id: :variants__product_id). - where(orders__state: 'complete'). - where(orders__created_at: @start_date..@end_date). #filter by params - select{[line_items__quantity, - line_items__id, - line_items__variant_id, - sum(quantity).as(purchases), - products__name.as(product_name), - products__id.as(product_id)]}. - group(:products__name).as(:line_items) + deeplink product_name: { template: %Q{{%# o.product_name %}} } + + def report_query + page_events_ar = Arel::Table.new(:spree_page_events) + purchase_line_items_ar = Arel::Table.new(:purchase_line_items) - ::SpreeReportify::ReportDb[line_items].join(:spree_page_events___page_events, page_events__target_id: :product_id). - where(page_events__target_type: 'Spree::Product', page_events__activity: 'view'). - group(:product_name). - order(sortable_sequel_expression) + Spree::Report::QueryFragments.from_subquery(purchase_line_items, as: :purchase_line_items) + .join(page_events_ar) + .on(page_events_ar[:target_id].eq(purchase_line_items_ar[:product_id])) + .where(page_events_ar[:target_type].eq(Arel::Nodes::Quoted.new('Spree::Product'))) + .where(page_events_ar[:activity].eq(Arel::Nodes::Quoted.new('view'))) + .group(purchase_line_items_ar[:product_id], purchase_line_items_ar[:product_name], + purchase_line_items_ar[:product_slug], purchase_line_items_ar[:purchases]) + .project( + 'product_name', + 'product_slug', + 'COUNT(*) as views', + 'purchases' + ) end - def select_columns(dataset) - dataset.select{[ - product_name, - count('*').as(views), - purchases, - Sequel.as(ROUND(purchases / count('*'), 2), :purchase_to_view_ratio) - ]} + private def purchase_line_items + Spree::LineItem + .joins(:order) + .joins(:variant) + .joins(:product) + .where(spree_orders: { state: 'complete', created_at: reporting_period }) + .group('spree_products.id', 'spree_products.name') + .select( + 'SUM(quantity) as purchases', + 'spree_products.name as product_name', + 'spree_products.slug as product_slug', + 'spree_products.id as product_id' + ) end end end diff --git a/app/reports/spree/promotional_cost_report.rb b/app/reports/spree/promotional_cost_report.rb index 39ea325..caa22da 100644 --- a/app/reports/spree/promotional_cost_report.rb +++ b/app/reports/spree/promotional_cost_report.rb @@ -1,133 +1,84 @@ module Spree class PromotionalCostReport < Spree::Report DEFAULT_SORTABLE_ATTRIBUTE = :promotion_name - HEADERS = { promotion_name: :string, usage_count: :integer, promotion_discount: :integer, promotion_code: :string, promotion_start_date: :date, promotion_end_date: :date } - SEARCH_ATTRIBUTES = { start_date: :promotion_created_from, end_date: :promotion_created_till } - SORTABLE_ATTRIBUTES = [:promotion_name, :usage_count, :promotion_discount, :promotion_code, :promotion_start_date, :promotion_end_date] + HEADERS = { promotion_name: :string, usage_count: :integer, promotion_discount: :integer, promotion_code: :string, promotion_start_date: :date, promotion_end_date: :date } + SEARCH_ATTRIBUTES = { start_date: :promotion_applied_from, end_date: :promotion_applied_till } + SORTABLE_ATTRIBUTES = [:promotion_name, :usage_count, :promotion_discount, :promotion_code, :promotion_start_date, :promotion_end_date] - def no_pagination? - true - end + class Result < Spree::Report::TimedResult + charts PromotionalCostChart, UsageCountChart - def initialize(options) - super - set_sortable_attributes(options, DEFAULT_SORTABLE_ATTRIBUTE) - end + def build_empty_observations + super + @_promotions = @results.collect { |result| result['promotion_name'] }.uniq + @observations = @_promotions.collect do |promotion_name| + @observations.collect do |observation| + _d_observation = observation.dup + _d_observation.promotion_name = promotion_name + _d_observation.usage_count = 0 + _d_observation + end + end.flatten + end - def generate(options = {}) - adjustments_with_month_name = SpreeReportify::ReportDb[:spree_adjustments___adjustments]. - join(:spree_promotion_actions___promotion_actions, id: :source_id). - join(:spree_promotions___promotions, id: :promotion_id). - where(adjustments__source_type: "Spree::PromotionAction"). - where(adjustments__created_at: @start_date..@end_date). #filter by params - select{[ - Sequel.as(abs(:amount), :promotion_discount), - Sequel.as(:promotions__id, :promotions_id), - :promotions__name___promotion_name, - :promotions__code___promotion_code, - Sequel.as(DATE_FORMAT(promotions__starts_at,'%d %b %y'), :promotion_start_date), - Sequel.as(DATE_FORMAT(promotions__expires_at,'%d %b %y'), :promotion_end_date), - Sequel.as(MONTHNAME(:adjustments__created_at), :month_name), - Sequel.as(YEAR(:adjustments__created_at), :year), - Sequel.as(MONTH(:adjustments__created_at), :number) - ]} + class Observation < Spree::Report::TimedObservation + observation_fields [ + :promotion_name, :usage_count, + :promotion_discount, :promotion_code, + :promotion_start_date, :promotion_end_date + ] - group_by_months = SpreeReportify::ReportDb[adjustments_with_month_name]. - group(:months_name, :promotions_id). - order(:year, :number). - select{[ - number, - promotion_name, - year, - promotion_code, - promotion_start_date, - promotion_end_date, - Sequel.as(concat(month_name, ' ', year), :months_name), - Sequel.as(SUM(promotion_discount), :promotion_discount), - Sequel.as(count(:promotions_id), :usage_count), - promotions_id - ]} - grouped_by_promotion = group_by_months.all.group_by { |record| record[:promotion_name] } - data = [] - grouped_by_promotion.each_pair do |promotion_name, collection| - data << fill_missing_values({ promotion_discount: 0, usage_count: 0, promotion_name: promotion_name }, collection) - end - @data = data.flatten - end + def promotion_start_date + @promotion_start_date.present? ? @promotion_start_date.to_date.strftime("%B %d %Y") : "-" + end - def group_by_promotion_name - @grouped_by_promotion_name ||= @data.group_by { |record| record[:promotion_name] } - end + def promotion_end_date + @promotion_end_date.present? ? @promotion_end_date.to_date.strftime("%B %d %Y") : "-" + end - def chart_data - { - months_name: group_by_promotion_name.first.try(:second).try(:map) { |record| record[:months_name] }, - collection: group_by_promotion_name - } - end + def promotion_discount + @promotion_discount.to_f.abs + end - def chart_json - { - chart: true, - charts: [ - promotional_cost_chart_json, - usage_count_chart_json - ] - } + def describes?(result, time_scale) + result['promotion_name'] == promotion_name && super + end + end end - def promotional_cost_chart_json - { - id: 'promotional-cost', - json: { - chart: { type: 'column' }, - title: { - useHTML: true, - text: "Promotional Cost" - }, - xAxis: { categories: chart_data[:months_name] }, - yAxis: { - title: { text: 'Value($)' } - }, - tooltip: { valuePrefix: '$' }, - legend: { - layout: 'vertical', - align: 'right', - verticalAlign: 'middle', - borderWidth: 0 - }, - series: chart_data[:collection].map { |key, value| { type: 'column', name: key, data: value.map { |r| r[:promotion_discount].to_f } } } - } - } + def report_query + Spree::Report::QueryFragments + .from_subquery(eligible_promotions) + .group(*time_scale_columns, :promotion_id, :promotion_name, + :promotion_code, :promotion_start_date, :promotion_end_date) + .order(*time_scale_columns_to_s) + .project( + *time_scale_columns, + 'promotion_name', + 'promotion_code', + 'promotion_start_date', + 'promotion_end_date', + 'SUM(promotion_discount) as promotion_discount', + 'COUNT(promotion_id) as usage_count', + 'promotion_id' + ) end - def usage_count_chart_json - { - id: 'promotion-usage-count', - json: { - chart: { type: 'spline' }, - title: { - useHTML: true, - text: "Promotion Usage Count" - }, - xAxis: { categories: chart_data[:months_name] }, - yAxis: { - title: { text: 'Count' } - }, - tooltip: { valuePrefix: '#' }, - legend: { - layout: 'vertical', - align: 'right', - verticalAlign: 'middle', - borderWidth: 0 - }, - series: chart_data[:collection].map { |key, value| { name: key, data: value.map { |r| r[:usage_count].to_i } } } - } - } + private def eligible_promotions + Spree::PromotionAction + .joins(:promotion) + .joins(:adjustment) + .where(spree_adjustments: { created_at: reporting_period }) + .select( + 'spree_promotions.starts_at as promotion_start_date', + 'spree_promotions.expires_at as promotion_end_date', + 'spree_adjustments.amount as promotion_discount', + 'spree_promotions.id as promotion_id', + 'spree_promotions.name as promotion_name', + 'spree_promotions.code as promotion_code', + *time_scale_selects('spree_adjustments') + ) end - def select_columns(dataset) - dataset - end end end diff --git a/app/reports/spree/promotional_cost_report/promotional_cost_chart.rb b/app/reports/spree/promotional_cost_report/promotional_cost_chart.rb new file mode 100644 index 0000000..c67cb23 --- /dev/null +++ b/app/reports/spree/promotional_cost_report/promotional_cost_chart.rb @@ -0,0 +1,37 @@ +class Spree::PromotionalCostReport::PromotionalCostChart + attr_accessor :time, :series + + def initialize(result) + @grouped_by_promotion = result.observations.group_by(&:promotion_name) + @time_dimension = result.time_dimension + self.time = [] + self.time = @grouped_by_promotion.values.first.collect { |observation_value| observation_value.send(@time_dimension) } if @grouped_by_promotion.first.present? + self.series = @grouped_by_promotion.collect { |promotion, values| { type: 'column', name: promotion, data: values.collect(&:promotion_discount) } } + end + + def to_h + { + id: 'promotional-cost', + json: { + chart: { type: 'column' }, + title: { + useHTML: true, + text: "Promotional Cost" + }, + xAxis: { categories: time }, + yAxis: { + title: { text: 'Value($)' } + }, + tooltip: { valuePrefix: '$' }, + legend: { + layout: 'vertical', + align: 'right', + verticalAlign: 'middle', + borderWidth: 0 + }, + series: series + } + } + end + +end diff --git a/app/reports/spree/promotional_cost_report/usage_count_chart.rb b/app/reports/spree/promotional_cost_report/usage_count_chart.rb new file mode 100644 index 0000000..f775cc6 --- /dev/null +++ b/app/reports/spree/promotional_cost_report/usage_count_chart.rb @@ -0,0 +1,41 @@ +class Spree::PromotionalCostReport::UsageCountChart + + attr_accessor :time, :series + + def initialize(result) + @grouped_by_promotion = result.observations.group_by(&:promotion_name) + @time_dimension = result.time_dimension + self.time = [] + if @grouped_by_promotion.values.first.present? + self.time = @grouped_by_promotion.values.first.collect { |observation_value| observation_value.send(@time_dimension) } + end + self.series = @grouped_by_promotion.collect { |promotion, values| { type: 'column', name: promotion, data: values.collect(&:usage_count) } } + end + + + def to_h + { + id: 'promotion-usage-count', + json: { + chart: { type: 'spline' }, + title: { + useHTML: true, + text: "Promotion Usage Count" + }, + xAxis: { categories: time }, + yAxis: { + title: { text: 'Count' } + }, + tooltip: { valuePrefix: '#' }, + legend: { + layout: 'vertical', + align: 'right', + verticalAlign: 'middle', + borderWidth: 0 + }, + series: series + } + } + end + +end diff --git a/app/reports/spree/report.rb b/app/reports/spree/report.rb index eefc2aa..bcc4259 100644 --- a/app/reports/spree/report.rb +++ b/app/reports/spree/report.rb @@ -1,29 +1,71 @@ module Spree class Report - attr_accessor :sortable_attribute, :sortable_type + attr_accessor :sortable_attribute, :sortable_type, :total_records, + :records_per_page, :current_page, :paginate, :search, :reporting_period + alias_method :sort_direction, :sortable_type + alias_method :paginate?, :paginate - def no_pagination? + + TIME_SCALES = [:hourly, :daily, :monthly, :yearly] + + def paginated? false end + def pagination_required? + paginated? && paginate? + end + + def deeplink_properties + { + deeplinked: false + } + end + + def self.deeplink(template_for_headers = {}) + define_method :deeplink_properties do + { deeplinked: true }.merge(template_for_headers) + end + end + def generate(options = {}) - raise 'Please define this method in inherited class' + self.class::Result.new do |report| + report.start_date = @start_date + report.end_date = @end_date + report.time_scale = @time_scale + report.report = self + end end - def initialize(options) - @search = options.fetch(:search, {}) - start_date = @search[:start_date] - @start_date = start_date.present? ? Date.parse(start_date) : Date.new(Date.current.year) - end_date = @search[:end_date] - # 1.day is added to date so that we can get current date records - # since date consider time at midnight - @end_date = (end_date.present? ? Date.parse(end_date) : Date.new(Date.current.year, 12, 30)) + 1.day + def initialize(options) + self.search = options.fetch(:search, {}) + self.records_per_page = options[:records_per_page] + self.current_page = options[:offset] + self.paginate = options[:paginate] + extract_reporting_period + determine_report_time_scale + if self.class::SORTABLE_ATTRIBUTES.present? + set_sortable_attributes(options, self.class::DEFAULT_SORTABLE_ATTRIBUTE) + end end def header_sorted?(header) - sortable_attribute.eql?(header) + sortable_attribute.present? && sortable_attribute.eql?(header) + end + + def get_results + query = + if pagination_required? + paginated_report_query + else + report_query + end + + query = query.order(active_record_sort) if sortable_attribute.present? + query_sql = query.to_sql + r = ActiveRecord::Base.connection.exec_query(query_sql) end def set_sortable_attributes(options, default_sortable_attribute) @@ -31,32 +73,59 @@ def set_sortable_attributes(options, default_sortable_attribute) self.sortable_attribute = options[:sort] ? options[:sort][:attribute].to_sym : default_sortable_attribute end - def sortable_sequel_expression - sortable_type.eql?(:desc) ? Sequel.desc(sortable_attribute) : Sequel.asc(sortable_attribute) + def active_record_sort + "#{ sortable_attribute } #{ sortable_type }" end - def fill_missing_values(default_object, incomplete_result_set) - complete_result_set = [] - year_month_list = (@start_date..@end_date).map{ |date| [date.year, date.month] }.uniq - year_month_list.each do |year_month| - index = incomplete_result_set.index { |obj| obj[:year] == year_month.first && obj[:number] == year_month.second } - if index - complete_result_set.push(incomplete_result_set[index]) - else - filling_object = default_object.merge({ year: year_month.first, number: year_month.second, months_name: [Date::MONTHNAMES[year_month.second], year_month.first].join(' ') }) - complete_result_set.push(filling_object) - end + def total_records + ActiveRecord::Base.connection.select_value(record_count_query.to_sql) + end + + def total_pages + if pagination_required? + total_pages = total_records / records_per_page + total_pages -= 1 if total_records % records_per_page == 0 + total_pages end - complete_result_set end - def chart_json - { - chart: false, - charts: [] - } + def time_scale_selects(time_scale_on = nil) + QueryTimeScale.select(@time_scale, time_scale_on) + end + + def time_scale_columns + @_time_scale_columns ||= QueryTimeScale.time_scale_columns(@time_scale) + end + + def time_scale_columns_to_s + @_time_scale_columns_to_s ||= time_scale_columns.collect(&:to_s) + end + + def name + @_report_name ||= self.class.to_s.demodulize.underscore.gsub("_report", "") + end + + private def extract_reporting_period + start_date = @search[:start_date] + @start_date = start_date.present? ? Date.parse(start_date) : Date.current.beginning_of_year + end_date = @search[:end_date] + @end_date = (end_date.present? ? Date.parse(end_date).next_day : Date.current.end_of_year) + self.reporting_period = (@start_date.beginning_of_day)..(@end_date.end_of_day) end + private def determine_report_time_scale + @time_scale = + case (@end_date - @start_date).to_i + when 0..1 + :hourly + when 1..60 + :daily + when 61..600 + :monthly + else + :yearly + end + end end end diff --git a/app/reports/spree/report/chart.rb b/app/reports/spree/report/chart.rb new file mode 100644 index 0000000..57d3ce8 --- /dev/null +++ b/app/reports/spree/report/chart.rb @@ -0,0 +1,11 @@ +module Spree + class Report + module Chart + extend ActiveSupport::Concern + + def chart_json + self.class::Chart.new(self, time_dimension).to_h + end + end + end +end diff --git a/app/reports/spree/report/configuration.rb b/app/reports/spree/report/configuration.rb new file mode 100644 index 0000000..30dc757 --- /dev/null +++ b/app/reports/spree/report/configuration.rb @@ -0,0 +1,40 @@ +class Spree::Report::Configuration + attr_accessor :default_report_category, :default_report + attr_reader :reports + + def initialize + @reports = {} + end + + def register_report_category(category) + @reports[category] = [] + end + + def register_report(category, report_name) + @reports[category] << report_name + end + + def report_exists?(category, name) + @reports.key?(category) && @reports[category].include?(name) + end + + def reports_for_category(category) + if category_exists? category + @reports[category] + else + [] + end + end + + def default_report_category + @default_report_category || @reports.keys.first + end + + def default_report + @default_report || @reports[default_report_category].first + end + + def category_exists?(category) + @reports.key? category + end +end diff --git a/app/reports/spree/report/date_slicer.rb b/app/reports/spree/report/date_slicer.rb new file mode 100644 index 0000000..d472b66 --- /dev/null +++ b/app/reports/spree/report/date_slicer.rb @@ -0,0 +1,61 @@ +module Spree::Report::DateSlicer + def self.slice_into(start_date, end_date, time_scale, klass) + case time_scale + when :hourly + slice_hours_into(start_date, end_date, klass) + when :daily + slice_days_into(start_date, end_date, klass) + when :monthly + slice_months_into(start_date, end_date, klass) + when :yearly + slice_years_into(start_date, end_date, klass) + end + end + + def self.slice_hours_into(start_date, end_date, klass) + current_date = start_date + slices = [] + while current_date < end_date + slices << (0..23).collect do |hour| + obj = klass.new + obj.date = current_date + obj.hour = hour + obj + end + current_date = current_date.next_day + end + slices.flatten + end + + def self.slice_days_into(start_date, end_date, klass) + current_date = start_date + slices = [] + while current_date < end_date + obj = klass.new + obj.date = current_date + slices << obj + current_date = current_date.next_day + end + slices + end + + def self.slice_months_into(start_date, end_date, klass) + current_date = start_date + slices = [] + while current_date < end_date + obj = klass.new + obj.date = current_date + slices << obj + current_date = current_date.end_of_month.next_day + end + slices + end + + def self.slice_years_into(start_date, end_date, klass) + (start_date.year..end_date.year).collect do |year| + obj = klass.new + obj.date = Date.new(year).end_of_year + obj + end + end +end diff --git a/app/reports/spree/report/observation.rb b/app/reports/spree/report/observation.rb new file mode 100644 index 0000000..f4432e4 --- /dev/null +++ b/app/reports/spree/report/observation.rb @@ -0,0 +1,49 @@ +class Spree::Report::Observation + + def initialize + set_defaults + end + + class << self + def observation_fields(records) + case records + when Hash + build_from_hash(records) + else + build_from_list(records) + end + end + + def build_from_hash(records) + build_from_list(records.keys) + + define_method :set_defaults do + records.keys.each do |key| + self.send("#{ key }=", records[key]) + end + end + end + + def build_from_list(records) + attr_accessor *records + + define_method :populate do |result| + records.each do |record| + record_name = record.to_s + self.send("#{ record }=", result[record_name]) if result[record_name] + end + end + + define_method :set_defaults do + end + + define_method :observations_to_h do + records.inject({}) { |acc, record| acc[record] = self.send(record); acc } + end + end + end + + def to_h + observations_to_h + end +end diff --git a/app/reports/spree/report/query_fragments.rb b/app/reports/spree/report/query_fragments.rb new file mode 100644 index 0000000..bd21c26 --- /dev/null +++ b/app/reports/spree/report/query_fragments.rb @@ -0,0 +1,45 @@ +module Spree::Report::QueryFragments + def self.from_subquery(subquery, as: 'results') + Arel::SelectManager.new(Arel::Table.engine, Arel.sql("(#{subquery.to_sql}) as #{ as }")) + end + + def self.from_join(subquery1, subquery2, join_expr) + Arel::SelectManager.new(Arel::Table.engine, Arel.sql("((#{ subquery1.to_sql }) as q1 JOIN (#{ subquery2.to_sql }) as q2 ON #{ join_expr })")) + end + + def self.from_union(subquery1, subquery2, as: 'results') + Arel::SelectManager.new(Arel::Table.engine, Arel.sql("((#{ subquery1.to_sql }) UNION (#{ subquery2.to_sql })) as #{ as }")) + end + + def self.year(column, as='year') + extract_from_date(:year, column, as) + end + + def self.month(column, as='month') + extract_from_date(:month, column, as) + end + + def self.week(column, as='week') + extract_from_date(:week, column, as) + end + + def self.day(column, as='day') + extract_from_date(:day, column, as) + end + + def self.hour(column, as='hour') + extract_from_date(:hour, column, as) + end + + def self.extract_from_date(part, column, as) + "EXTRACT(#{ part } from #{ column }) AS #{ as }" + end + + def self.if_null(val, default_val) + Arel::Nodes::NamedFunction.new('COALESCE', [val, default_val]) + end + + def self.sum(node) + Arel::Nodes::NamedFunction.new('SUM', [node]) + end +end diff --git a/app/reports/spree/report/query_time_scale.rb b/app/reports/spree/report/query_time_scale.rb new file mode 100644 index 0000000..1b139a0 --- /dev/null +++ b/app/reports/spree/report/query_time_scale.rb @@ -0,0 +1,19 @@ +module Spree::Report::QueryTimeScale + def self.select(time_scale, time_scale_on) + db_col_name = time_scale_on.present? ? "#{ time_scale_on }.created_at" : "created_at" + time_scale_columns(time_scale).collect { |time_scale_column| ::Spree::Report::QueryFragments.public_send(time_scale_column, db_col_name) } + end + + def self.time_scale_columns(time_scale) + case time_scale + when :hourly + [:day, :hour] + when :daily + [:month, :day] + when :monthly + [:year, :month] + when :yearly + [:year] + end + end +end diff --git a/app/reports/spree/report/result.rb b/app/reports/spree/report/result.rb new file mode 100644 index 0000000..d3e7de7 --- /dev/null +++ b/app/reports/spree/report/result.rb @@ -0,0 +1,100 @@ +module Spree + class Report + class Result + attr_accessor :start_date, :end_date, :time_scale, :report + attr_reader :observations + + def initialize + yield self + build_report_observations + end + + def build_report_observations + query_results + populate_observations + end + + def query_results + @results = report.get_results + end + + def populate_observations + @observations = @results.collect do |result| + _observation = self.class::Observation.new + _observation.populate(result) + _observation + end + end + + + def to_h + { + deeplink: report.deeplink_properties, + total_pages: report.total_pages, + per_page: report.records_per_page, + pagination_required: report.pagination_required?, + headers: headers, + search_attributes: search_attributes, + stats: observations.collect(&:to_h), + chart_json: chart_json + } + end + + def chart_json + { + chart: false, + charts: [] + } + end + + def self.charts(*report_charts) + define_method :chart_json do + { + chart: true, + charts: report_charts.collect { |report_chart| report_chart.new(self).to_h }.flatten + } + end + end + + def search_attributes + report.class::SEARCH_ATTRIBUTES.transform_values { |value| value.to_s.humanize } + end + + def total_pages # O indexed + if report.pagination_required? + total_pages = report.total_records / report.records_per_page + if report.total_records % report.records_per_page == 0 + total_pages -= 1 + end + total_pages + end + end + + def headers + report.class::HEADERS.keys.collect do |header| + header_description = { + name: Spree.t(header.to_sym, scope: [:insight, report.name]), + value: header, + type: report.class::HEADERS[header], + sortable: header.in?(report.class::SORTABLE_ATTRIBUTES) + } + header_description[:sorted] = report.sort_direction if report.header_sorted?(header) + header_description + end + end + + def time_dimension + case time_scale + when :hourly + :hour_name + when :daily + :day_name + when :monthly + :month_name + when :yearly + :year + end + end + end + end +end diff --git a/app/reports/spree/report/timed_observation.rb b/app/reports/spree/report/timed_observation.rb new file mode 100644 index 0000000..81b889c --- /dev/null +++ b/app/reports/spree/report/timed_observation.rb @@ -0,0 +1,47 @@ +class Spree::Report::TimedObservation < Spree::Report::Observation + + extend Forwardable + + attr_accessor :date, :hour, :reportable_keys + + def_delegators :date, :day, :month, :year + + def initialize + super + self.hour = 0 + end + + def describes?(result, time_scale) + case time_scale + when :hourly + result['hour'] == hour && result['day'] == day + when :daily + result['day'] == day && result['month'] == month + when :monthly + result['month'] == month && result['year'] == year + when :yearly + result['year'] == year + end + end + + def month_name + Date::MONTHNAMES[month] + end + + def hour_name + if hour == 23 + return "23:00 - 00:00" + else + return "#{ hour }:00 - #{ hour + 1 }:00" + end + end + + def day_name + "#{ day } #{ month_name }" + end + + def to_h + super.merge({day_name: day_name, month_name: month_name, year: year, hour_name: hour_name}) + end + +end diff --git a/app/reports/spree/report/timed_result.rb b/app/reports/spree/report/timed_result.rb new file mode 100644 index 0000000..a2fac3b --- /dev/null +++ b/app/reports/spree/report/timed_result.rb @@ -0,0 +1,48 @@ +module Spree + class Report + class TimedResult < Result + + def build_report_observations + query_results + build_empty_observations + populate_observations + end + + def build_empty_observations + @observations = Spree::Report::DateSlicer.slice_into(start_date, end_date, time_scale, self.class::Observation) + end + + def populate_observations + observation_iter = @observations.each + current_observation = @observations.present? ? observation_iter.next : nil + @results.each do |result| + if current_observation.present? + begin + until current_observation.describes? result, time_scale + current_observation = observation_iter.next + end + + current_observation.populate(result) + current_observation = observation_iter.next + rescue StopIteration + break + end + end + end + end + + def headers + [time_headers] + super + end + + private def time_headers + { + name: Spree.t(time_dimension, scope: [:admin]), + value: time_dimension, + type: :string, + sortable: false + } + end + end + end +end diff --git a/app/reports/spree/returned_products_report.rb b/app/reports/spree/returned_products_report.rb index 71787ca..7f41997 100644 --- a/app/reports/spree/returned_products_report.rb +++ b/app/reports/spree/returned_products_report.rb @@ -1,32 +1,37 @@ module Spree class ReturnedProductsReport < Spree::Report DEFAULT_SORTABLE_ATTRIBUTE = :product_name - HEADERS = { sku: :string, product_name: :string, return_count: :integer } - SEARCH_ATTRIBUTES = { start_date: :product_returned_from, end_date: :product_returned_till } - SORTABLE_ATTRIBUTES = [:product_name, :sku, :return_count] + HEADERS = { sku: :string, product_name: :string, return_count: :integer } + SEARCH_ATTRIBUTES = { start_date: :product_returned_from, end_date: :product_returned_till } + SORTABLE_ATTRIBUTES = [:product_name, :sku, :return_count] - def initialize(options) - super - set_sortable_attributes(options, DEFAULT_SORTABLE_ATTRIBUTE) - end + deeplink product_name: { template: %Q{{%# o.product_name %}} } + + class Result < Spree::Report::Result + class Observation < Spree::Report::Observation + observation_fields [:sku, :product_name, :return_count, :product_slug] - def generate - SpreeReportify::ReportDb[:spree_return_authorizations]. - join(:spree_return_items, return_authorization_id: :spree_return_authorizations__id). - join(:spree_inventory_units, spree_inventory_units__id: :inventory_unit_id). - join(:spree_variants, spree_variants__id: :variant_id). - join(:spree_products, id: :product_id). - where(spree_return_items__created_at: @start_date..@end_date). - group(:variant_id). - order(sortable_sequel_expression) + def sku + @sku.presence || @product_name + end + end end - def select_columns(dataset) - dataset.select{[ - spree_products__name.as(product_name), - Sequel.as(IF(STRCMP(spree_variants__sku, ''), spree_variants__sku, spree_products__name), :sku), - Sequel.as(count(:variant_id), :return_count) - ]} + def report_query + Spree::ReturnAuthorization + .joins(:return_items) + .joins(:inventory_units) + .joins(:variants) + .joins(:products) + .where(spree_return_items: { created_at: reporting_period }) + .group('spree_variants.id', 'spree_products.name', 'spree_products.slug', 'spree_variants.sku') + .select( + 'spree_products.name as product_name', + 'spree_products.slug as product_slug', + 'spree_variants.sku as sku', + 'COUNT(spree_variants.id) as return_count' + ) end + end end diff --git a/app/reports/spree/sales_performance_report.rb b/app/reports/spree/sales_performance_report.rb index f80bfdd..c6fc4a6 100644 --- a/app/reports/spree/sales_performance_report.rb +++ b/app/reports/spree/sales_performance_report.rb @@ -1,206 +1,107 @@ module Spree class SalesPerformanceReport < Spree::Report - HEADERS = { months_name: :string, sale_price: :integer, cost_price: :integer, promotion_discount: :integer, profit_loss: :integer, profit_loss_percent: :integer } - SEARCH_ATTRIBUTES = { start_date: :orders_created_from, end_date: :orders_created_till } + HEADERS = { sale_price: :integer, cost_price: :integer, promotion_discount: :integer, profit_loss: :integer, profit_loss_percent: :integer } + SEARCH_ATTRIBUTES = { start_date: :orders_created_from, end_date: :orders_created_till } SORTABLE_ATTRIBUTES = [] - def no_pagination? - true - end + class Result < Spree::Report::TimedResult + charts ProfitLossChart, ProfitLossPercentChart, SaleCostPriceChart - def generate(options = {}) - order_join_line_item = SpreeReportify::ReportDb[:spree_orders___orders]. - exclude(completed_at: nil). - where(orders__created_at: @start_date..@end_date). #filter by params - join(:spree_line_items___line_items, order_id: :id). - group(:line_items__order_id). - select{[ - Sequel.as(SUM(IFNULL(line_items__cost_price, line_items__price) * line_items__quantity), :cost_price), - Sequel.as(orders__item_total, :sale_price), - Sequel.as(orders__item_total - SUM(IFNULL(line_items__cost_price, line_items__price) * line_items__quantity), :profit_loss), - Sequel.as(MONTHNAME(:orders__created_at), :month_name), - Sequel.as(MONTH(:orders__created_at), :number), - Sequel.as(YEAR(:orders__created_at), :year) - ]} + class Observation < Spree::Report::TimedObservation + observation_fields cost_price: 0, sale_price: 0, profit_loss: 0, profit_loss_percent: 0, promotion_discount: 0 - group_by_months = SpreeReportify::ReportDb[order_join_line_item]. - group(:months_name). - order(:year, :number). - select{[ - number, - Sequel.as(IFNULL(year, 2016), :year), - Sequel.as(concat(month_name, ' ', IFNULL(year, 2016)), :months_name), - Sequel.as(IFNULL(SUM(sale_price), 0), :sale_price), - Sequel.as(IFNULL(SUM(cost_price), 0), :cost_price), - Sequel.as(IFNULL(SUM(profit_loss), 0), :profit_loss), - Sequel.as((IFNULL(SUM(profit_loss), 0) / SUM(cost_price)) * 100, :profit_loss_percent), - Sequel.as(0, :promotion_discount) - ]} + def cost_price + @cost_price.to_f + end - adjustments_with_month_name = SpreeReportify::ReportDb[:spree_adjustments___adjustments]. - where(adjustments__source_type: "Spree::PromotionAction"). - where(adjustments__created_at: @start_date..@end_date). #filter by params - select{[ - Sequel.as(abs(:amount), :promotion_discount), - Sequel.as(MONTHNAME(:adjustments__created_at), :month_name), - Sequel.as(YEAR(:adjustments__created_at), :year), - Sequel.as(MONTH(:adjustments__created_at), :number) - ]} + def sale_price + @sale_price.to_f + end - promotions_group_by_months = SpreeReportify::ReportDb[adjustments_with_month_name]. - group(:months_name). - order(:year, :number). - select{[ - number, - year, - Sequel.as(concat(month_name, ' ', year), :months_name), - Sequel.as(0, :sale_price), - Sequel.as(0, :cost_price), - Sequel.as(SUM(promotion_discount) * (-1), :profit_loss), - Sequel.as(0, :profit_loss_percent), - Sequel.as(SUM(promotion_discount), :promotion_discount) - ]} + def profit_loss + @profit_loss.to_f + end - union_stats = SpreeReportify::ReportDb[group_by_months.union(promotions_group_by_months)]. - group(:months_name). - order(:year, :number). - select{[ - number, - year, - months_name, - Sequel.as(SUM(sale_price), :sale_price), - Sequel.as(SUM(cost_price), :cost_price), - Sequel.as(SUM(profit_loss), :profit_loss), - Sequel.as(ROUND(ABS((SUM(profit_loss) / SUM(cost_price))) * 100, 2), :profit_loss_percent), - Sequel.as(SUM(promotion_discount), :promotion_discount) - ]} - fill_missing_values({ cost_price: 0, sale_price: 0, profit_loss: 0, profit_loss_percent: 0, promotion_discount: 0 }, union_stats.all) - end + def profit_loss_percent + return (profit_loss * 100 / cost_price).round(2) unless cost_price.zero? + 0.0 + end - def select_columns(dataset) - dataset - end + def promotion_discount + @promotion_discount.to_f + end + end - def chart_json - { - chart: true, - charts: [ - profit_loss_chart_json, - profit_loss_percent_chart_json, - sale_cost_price_chart_json - ] - } end - # extract it in report.rb - def chart_data - unless @data - @data = Hash.new {|h, k| h[k] = [] } - generate.each do |object| - object.each_pair do |key, value| - @data[key].push(value) - end - end - end - @data + private def report_query + Spree::Report::QueryFragments + .from_union(order_with_line_items_grouped_by_time, promotions_grouped_by_time) + .group(*time_scale_columns_to_s) + .order(*time_scale_columns) + .project( + *time_scale_columns, + 'SUM(sale_price) as sale_price', + 'SUM(cost_price) as cost_price', + 'SUM(profit_loss) as profit_loss', + 'SUM(promotion_discount) as promotion_discount' + ) end - # ---------------------------------------------------- Graph Jsons -------------------------------------------------- + private def promotions_grouped_by_time + Spree::Report::QueryFragments + .from_subquery(promotion_adjustments_with_time) + .group(*time_scale_columns_to_s, 'sale_price', 'cost_price') + .order(*time_scale_columns) + .project( + *time_scale_columns, + '0 as sale_price', + '0 as cost_price', + 'SUM(promotion_discount) * -1 as profit_loss', + 'SUM(promotion_discount) as promotion_discount' + ) + end - def profit_loss_chart_json - { - id: 'profit-loss', - json: { - title: { - useHTML: true, - text: "Profit/Loss" - }, - xAxis: { categories: chart_data[:months_name] }, - yAxis: { - title: { text: 'Value($)' } - }, - legend: { - layout: 'vertical', - align: 'right', - verticalAlign: 'middle', - borderWidth: 0 - }, - series: [ - { - name: 'Profit Loss', - tooltip: { valuePrefix: '$' }, - data: chart_data[:profit_loss].map(&:to_f) - } - ] - } - } + private def promotion_adjustments_with_time + Spree::Adjustment + .promotion + .where(created_at: reporting_period) + .select( + 'abs(amount) as promotion_discount', + *time_scale_selects('spree_adjustments') + ) end - def profit_loss_percent_chart_json - { - id: 'profit-loss', - json: { - title: { - useHTML: true, - text: "Profit/Loss %" - }, - xAxis: { categories: chart_data[:months_name] }, - yAxis: { - title: { text: 'Percentage(%)' } - }, - legend: { - layout: 'vertical', - align: 'right', - verticalAlign: 'middle', - borderWidth: 0 - }, - series: [ - { - name: 'Profit Loss Percent(%)', - tooltip: { valueSuffix: '%' }, - data: chart_data[:profit_loss_percent].map(&:to_f) - } - ] - } - } + private def order_with_line_items_grouped_by_time + order_with_line_items_ar = Arel::Table.new(:order_with_line_items) + zero = Arel::Nodes.build_quoted(0.0) + Spree::Report::QueryFragments + .from_subquery(order_with_line_items, as: :order_with_line_items) + .group(*time_scale_columns_to_s) + .order(*time_scale_columns) + .project( + *time_scale_columns, + Spree::Report::QueryFragments.if_null(Spree::Report::QueryFragments.sum(order_with_line_items_ar[:sale_price]), zero).as('sale_price'), + Spree::Report::QueryFragments.if_null(Spree::Report::QueryFragments.sum(order_with_line_items_ar[:cost_price]), zero).as('cost_price'), + Spree::Report::QueryFragments.if_null(Spree::Report::QueryFragments.sum(order_with_line_items_ar[:profit_loss]), zero).as('profit_loss'), + '0 as promotion_discount' + ) end - def sale_cost_price_chart_json - { - id: 'sale-price', - json: { - chart: { type: 'column' }, - title: { - useHTML: true, - text: "Sales Performance %" - }, - xAxis: { categories: chart_data[:months_name] }, - yAxis: { - title: { text: 'Value($)' } - }, - tooltip: { valuePrefix: '$' }, - legend: { - layout: 'vertical', - align: 'right', - verticalAlign: 'middle', - borderWidth: 0 - }, - series: [ - { - name: 'Sale Price', - data: chart_data[:sale_price].map(&:to_f) - }, - { - name: 'Cost Price', - data: chart_data[:cost_price].map(&:to_f) - }, - { - name: 'Promotional Cost', - data: chart_data[:promotion_discount].map(&:to_f) - } - ] - } - } + private def order_with_line_items + line_item_ar = Spree::LineItem.arel_table + Spree::Order + .where.not(completed_at: nil) + .where(created_at: reporting_period) + .joins(:line_items) + .group('spree_orders.id', *time_scale_columns_to_s) + .select( + *time_scale_selects('spree_orders'), + "spree_orders.item_total as sale_price", + "SUM(#{ Spree::Report::QueryFragments.if_null(line_item_ar[:cost_price], line_item_ar[:price]).to_sql } * spree_line_items.quantity) as cost_price", + "(spree_orders.item_total - SUM(#{ Spree::Report::QueryFragments.if_null(line_item_ar[:cost_price], line_item_ar[:price]).to_sql } * spree_line_items.quantity)) as profit_loss" + ) end + end end diff --git a/app/reports/spree/sales_performance_report/profit_loss_chart.rb b/app/reports/spree/sales_performance_report/profit_loss_chart.rb new file mode 100644 index 0000000..c366f56 --- /dev/null +++ b/app/reports/spree/sales_performance_report/profit_loss_chart.rb @@ -0,0 +1,37 @@ +class Spree::SalesPerformanceReport::ProfitLossChart + def initialize(result) + time_dim = result.time_dimension + @time_series = result.observations.collect(&time_dim) + @data = result.observations.collect(&:profit_loss) + end + + def to_h + { + id: 'profit-loss', + json: { + title: { + useHTML: true, + text: "Profit/Loss" + }, + xAxis: { categories: @time_series }, + yAxis: { + title: { text: 'Value($)' } + }, + legend: { + layout: 'vertical', + align: 'right', + verticalAlign: 'middle', + borderWidth: 0 + }, + series: [ + { + name: 'Profit Loss', + tooltip: { valuePrefix: '$' }, + data: @data + } + ] + } + } + + end +end diff --git a/app/reports/spree/sales_performance_report/profit_loss_percent_chart.rb b/app/reports/spree/sales_performance_report/profit_loss_percent_chart.rb new file mode 100644 index 0000000..bef653d --- /dev/null +++ b/app/reports/spree/sales_performance_report/profit_loss_percent_chart.rb @@ -0,0 +1,36 @@ +class Spree::SalesPerformanceReport::ProfitLossPercentChart + def initialize(result) + time_dim = result.time_dimension + @time_series = result.observations.collect(&time_dim) + @data = result.observations.collect(&:profit_loss_percent) + end + + def to_h + { + id: 'profit-loss-percent', + json: { + title: { + useHTML: true, + text: "Profit/Loss %" + }, + xAxis: { categories: @time_series }, + yAxis: { + title: { text: 'Percentage(%)' } + }, + legend: { + layout: 'vertical', + align: 'right', + verticalAlign: 'middle', + borderWidth: 0 + }, + series: [ + { + name: 'Profit Loss Percent(%)', + tooltip: { valueSuffix: '%' }, + data: @data + } + ] + } + } + end +end diff --git a/app/reports/spree/sales_performance_report/sale_cost_price_chart.rb b/app/reports/spree/sales_performance_report/sale_cost_price_chart.rb new file mode 100644 index 0000000..dd2e63a --- /dev/null +++ b/app/reports/spree/sales_performance_report/sale_cost_price_chart.rb @@ -0,0 +1,48 @@ +class Spree::SalesPerformanceReport::SaleCostPriceChart + def initialize(result) + time_dim = result.time_dimension + @time_series = result.observations.collect(&time_dim) + @sale_price = result.observations.collect(&:sale_price) + @cost_price = result.observations.collect(&:cost_price) + @promotion_discount = result.observations.collect(&:promotion_discount) + end + + def to_h + { + id: 'sale-price', + json: { + chart: { type: 'column' }, + title: { + useHTML: true, + text: "Sales Performance %" + }, + xAxis: { categories: @time_series }, + yAxis: { + title: { text: 'Value($)' } + }, + tooltip: { valuePrefix: '$' }, + legend: { + layout: 'vertical', + align: 'right', + verticalAlign: 'middle', + borderWidth: 0 + }, + series: [ + { + name: 'Sale Price', + data: @sale_price + }, + { + name: 'Cost Price', + data: @cost_price + }, + { + name: 'Promotional Cost', + data: @promotion_discount + } + ] + } + } + + end +end diff --git a/app/reports/spree/sales_tax_report.rb b/app/reports/spree/sales_tax_report.rb index ec85dc0..9079d37 100644 --- a/app/reports/spree/sales_tax_report.rb +++ b/app/reports/spree/sales_tax_report.rb @@ -1,89 +1,64 @@ module Spree class SalesTaxReport < Spree::Report - HEADERS = { months_name: :string, zone_name: :string, sales_tax: :integer } - SEARCH_ATTRIBUTES = { start_date: :taxation_from, end_date: :taxation_till } + HEADERS = { zone_name: :string, sales_tax: :integer } + SEARCH_ATTRIBUTES = { start_date: :taxation_from, end_date: :taxation_till } SORTABLE_ATTRIBUTES = [] - def no_pagination? - true - end + class Result < Spree::Report::TimedResult + charts MonthlySalesTaxComparisonChart + + def build_empty_observations + super + @_zones = @results.collect { |r| r['zone_name'] }.uniq + @observations = @_zones.collect do |zone| + @observations.collect do |observation| + _d_observation = observation.dup + _d_observation.zone_name = zone + _d_observation.sales_tax = 0 + _d_observation + end + end.flatten + end - def generate(options = {}) - adjustments_with_month_name = SpreeReportify::ReportDb[:spree_adjustments___adjustments]. - join(:spree_tax_rates___tax_rates, id: :source_id). - join(:spree_zones___zones, id: :zone_id). - where(adjustments__source_type: "Spree::TaxRate", adjustments__adjustable_type: "Spree::LineItem"). - where(adjustments__created_at: @start_date..@end_date). #filter by params - select{[ - Sequel.as(abs(adjustments__amount), :sales_tax), - Sequel.as(:zones__id, :zone_id), - :zones__name___zone_name, - Sequel.as(MONTHNAME(:adjustments__created_at), :month_name), - Sequel.as(YEAR(:adjustments__created_at), :year), - Sequel.as(MONTH(:adjustments__created_at), :number) - ]} + class Observation < Spree::Report::TimedObservation + observation_fields [:zone_name, :sales_tax] - group_by_months = SpreeReportify::ReportDb[adjustments_with_month_name]. - group(:months_name, :zone_id). - order(:year, :number). - select{[ - number, - zone_name, - year, - Sequel.as(concat(month_name, ' ', year), :months_name), - Sequel.as(SUM(sales_tax), :sales_tax) - ]} - grouped_by_zone = group_by_months.all.group_by { |record| record[:zone_name] } - data = [] - grouped_by_zone.each_pair do |zone_name, collection| - data << fill_missing_values({ sales_tax: 0, zone_name: zone_name }, collection) + def describes?(result, time_scale) + (zone_name == result['zone_name']) && super + end + + def sales_tax + @sales_tax.to_f + end end - @data = data.flatten end - def group_by_zone_name - @grouped_by_zone_name ||= @data.group_by { |record| record[:zone_name] } - end - def chart_data - { - months_name: group_by_zone_name.first.try(:second).try(:map) { |record| record[:months_name] }, - collection: group_by_zone_name - } + def report_query + Spree::Report::QueryFragments + .from_subquery(tax_adjustments) + .group(*time_scale_columns_to_s, 'zone_name') + .order(*time_scale_columns) + .project( + 'zone_name', + *time_scale_columns, + 'SUM(sales_tax) as sales_tax' + ) end - def chart_json - { - chart: true, - charts: [ - { - id: 'sale-tax', - json: { - chart: { type: 'column' }, - title: { - useHTML: true, - text: "Monthly Sales Tax Comparison" - }, - xAxis: { categories: chart_data[:months_name] }, - yAxis: { - title: { text: 'Value($)' } - }, - tooltip: { valuePrefix: '$' }, - legend: { - layout: 'vertical', - align: 'right', - verticalAlign: 'middle', - borderWidth: 0 - }, - series: chart_data[:collection].map { |key, value| { type: 'column', name: key, data: value.map { |r| r[:sales_tax].to_f } } } - } - } - ] - } + private def tax_adjustments + Spree::TaxRate + .joins(:adjustments) + .joins(:zone) + .where(spree_adjustments: { adjustable_type: 'Spree::LineItem' } ) + .where(spree_adjustments: { created_at: reporting_period }) + .select( + 'spree_adjustments.amount as sales_tax', + 'spree_zones.id as zone_id', + 'spree_zones.name as zone_name', + *time_scale_selects('spree_adjustments') + ) end - def select_columns(dataset) - dataset - end end end diff --git a/app/reports/spree/sales_tax_report/monthly_sales_tax_comparison_chart.rb b/app/reports/spree/sales_tax_report/monthly_sales_tax_comparison_chart.rb new file mode 100644 index 0000000..a8a751b --- /dev/null +++ b/app/reports/spree/sales_tax_report/monthly_sales_tax_comparison_chart.rb @@ -0,0 +1,39 @@ +class Spree::SalesTaxReport::MonthlySalesTaxComparisonChart + def initialize(result) + @time_dimension = result.time_dimension + @grouped_by_zone_name = result.observations.group_by(&:zone_name) + @time_series = [] + if @grouped_by_zone_name.first.present? + @time_series = @grouped_by_zone_name.values.first.collect { |observation| observation.send(@time_dimension) } + end + @chart_series = @grouped_by_zone_name.map { |zone_name, observations| { type: 'column', name: zone_name, data: observations.collect(&:sales_tax) } } + end + + def to_h + { + id: 'sale-tax', + json: { + chart: { type: 'column' }, + title: { + useHTML: true, + text: "Monthly Sales Tax Comparison" + }, + xAxis: { categories: @time_series }, + yAxis: { + title: { text: 'Value($)' } + }, + tooltip: { valuePrefix: '$' }, + legend: { + layout: 'vertical', + align: 'right', + verticalAlign: 'middle', + borderWidth: 0 + }, + series: @chart_series + } + } + + end + + +end diff --git a/app/reports/spree/shipping_cost_report.rb b/app/reports/spree/shipping_cost_report.rb index d1f8b4b..7097e90 100644 --- a/app/reports/spree/shipping_cost_report.rb +++ b/app/reports/spree/shipping_cost_report.rb @@ -1,119 +1,89 @@ module Spree class ShippingCostReport < Spree::Report - HEADERS = { months_name: :string, name: :string, shipping_charge: :integer, revenue: :integer, shipping_cost_percentage: :integer } - SEARCH_ATTRIBUTES = { start_date: :start_date, end_date: :end_date } + HEADERS = { name: :string, shipping_charge: :integer, revenue: :integer, shipping_cost_percentage: :integer } + SEARCH_ATTRIBUTES = { start_date: :start_date, end_date: :end_date } SORTABLE_ATTRIBUTES = [] - def no_pagination? - true - end - - def generate(options = {}) - order_join_shipments = SpreeReportify::ReportDb[:spree_orders___orders]. - exclude(completed_at: nil). - join(:spree_shipments___shipments, order_id: :id). - where(orders__created_at: @start_date..@end_date). #filter by params - select{[ - Sequel.as(shipments__id, :shipment_id), - Sequel.as(orders__shipment_total, :shipping_charge), - Sequel.as(shipments__order_id, :order_id), - Sequel.as(orders__total, :order_total), - Sequel.as(MONTHNAME(:orders__created_at), :month_name), - Sequel.as(MONTH(:orders__created_at), :number), - Sequel.as(YEAR(:orders__created_at), :year) - ]}.as(:order_shipment) + class Result < Spree::Report::TimedResult + charts ShippingCostDistributionChart - order_shipment_join_shipment_rates = SpreeReportify::ReportDb[order_join_shipments]. - join(:spree_shipping_rates___shipping_rates, shipment_id: :order_shipment__shipment_id). - where(selected: true). - select{[ - order_id, - shipping_charge, - order_total, - shipping_method_id, - month_name, - number, - year, - Sequel.as(concat(month_name, ' ', IFNULL(year, 2016)), :months_name), - ]}.as(:order_shipment_rates) + def build_empty_observations + super + @_shipping_methods = @results.collect { |r| r['name'] }.uniq + @observations = @_shipping_methods.collect do |shipping_method| + @observations.collect do |observation| + _d_observation = observation.dup + _d_observation.name = shipping_method + _d_observation.revenue = 0 + _d_observation.shipping_charge = 0 + _d_observation.shipping_cost_percentage = 0 + _d_observation + end + end.flatten + end - revenue_table = SpreeReportify::ReportDb[order_shipment_join_shipment_rates]. - group(:months_name). - select{[ - Sequel.as(concat(month_name, ' ', IFNULL(year, 2016)), :months_name), - Sequel.as(SUM(order_total), :revenue), - order_id - ]} + class Observation < Spree::Report::TimedObservation + observation_fields [:name, :shipping_charge, :revenue, :shipping_cost_percentage] - group_by_months = SpreeReportify::ReportDb[order_shipment_join_shipment_rates]. - join(:spree_shipping_methods, id: :order_shipment_rates__shipping_method_id). - join(revenue_table, months_name: :order_shipment_rates__months_name). - group(:months_name, :spree_shipping_methods__id). - order(:year, :number). - select{[ - order_shipment_rates__order_id, - spree_shipping_methods__id, - Sequel.as(SUM(shipping_charge), :shipping_charge), - revenue, - shipping_method_id, - Sequel.as(concat(month_name, ' ', IFNULL(year, 2016)), :months_name), - Sequel.as(ROUND((SUM(shipping_charge) / revenue) * 100, 2), :shipping_cost_percentage), - number, - year, - name - ]} + def describes?(result, time_scale) + (name = result['name']) && super + end - grouped_by_method_name = group_by_months.all.group_by { |record| record[:name] } - data = [] - grouped_by_method_name.each_pair do |name, collection| - data << fill_missing_values({ shipping_charge: 0, revenue: 0, name: name, shipping_cost_percentage: 0 }, collection) + def shipping_cost_percentage + ((@shipping_charge.to_f * 100) / @revenue.to_f).round(2) + end end - @data = data.flatten end - def group_by_method_name - @grouped_by_method_name ||= @data.group_by { |record| record[:name] } - end + def report_query + ar_shipping_methods = Arel::Table.new(:spree_shipping_methods) + ar_subquery_with_rates = Arel::Table.new(:shipment_with_rates) - def chart_data - { - months_name: group_by_method_name.first.try(:second).try(:map) { |record| record[:months_name] }, - collection: group_by_method_name - } + Spree::Report::QueryFragments + .from_subquery(shipment_with_rates, as: 'shipment_with_rates') + .join(ar_shipping_methods) + .on(ar_shipping_methods[:id].eq(ar_subquery_with_rates[:shipping_method_id])) + .project( + *time_scale_columns, + ar_shipping_methods[:id], + 'revenue', + 'shipping_charge', + 'shipping_method_id', + 'name' + ) end - def chart_json - { - chart: true, - charts: [ - { - id: 'shipping-cost-percentage-comparison', - json: { - chart: { type: 'spline' }, - title: { - useHTML: true, - text: "Monthly Shipping Comparison" - }, - xAxis: { categories: chart_data[:months_name] }, - yAxis: { - title: { text: 'Percentage(%)' } - }, - tooltip: { valueSuffix: '%' }, - legend: { - layout: 'vertical', - align: 'right', - verticalAlign: 'middle', - borderWidth: 0 - }, - series: chart_data[:collection].map { |key, value| { name: key, data: value.map { |r| r[:shipping_cost_percentage].to_f } } } - } - } - ] - } + private def order_with_shipments + Spree::Order + .where.not(completed_at: nil) + .where(completed_at: reporting_period) + .joins(:shipments) + .select( + 'spree_shipments.id as shipment_id', + 'spree_orders.shipment_total as shipping_charge', + 'spree_orders.id as order_id', + 'spree_orders.total as order_total', + *time_scale_selects('spree_orders') + ) end - def select_columns(dataset) - dataset + private def shipment_with_rates + ar_shipping_rates = Arel::Table.new(:spree_shipping_rates) + ar_subquery = Arel::Table.new(:results) + + Spree::Report::QueryFragments.from_subquery(order_with_shipments) + .join(ar_shipping_rates) + .on(ar_shipping_rates[:shipment_id].eq(ar_subquery[:shipment_id])) + .where(ar_shipping_rates[:selected].eq(Arel::Nodes::Quoted.new(true))) + .group(*time_scale_columns, :shipping_method_id) + .order(*time_scale_columns) + .project( + *time_scale_columns, + 'shipping_method_id', + 'SUM(shipping_charge) as shipping_charge', + 'SUM(order_total) as revenue' + ) end + end end diff --git a/app/reports/spree/shipping_cost_report/shipping_cost_distribution_chart.rb b/app/reports/spree/shipping_cost_report/shipping_cost_distribution_chart.rb new file mode 100644 index 0000000..ab9c523 --- /dev/null +++ b/app/reports/spree/shipping_cost_report/shipping_cost_distribution_chart.rb @@ -0,0 +1,38 @@ +class Spree::ShippingCostReport::ShippingCostDistributionChart + + + def initialize(result) + time_dimension = result.time_dimension + @grouped_by_shipping_method = result.observations.group_by(&:name) + @time_series = [] + @time_series = @grouped_by_shipping_method.values.first.collect { |observation_value| observation_value.send(time_dimension) } if @grouped_by_shipping_method.first.present? + @result_series = @grouped_by_shipping_method.collect { |name, observations| { name: name, data: observations.collect(&:shipping_cost_percentage) } } + end + + def to_h + { + id: 'shipping-cost-percentage-comparison', + json: { + chart: { type: 'spline' }, + title: { + useHTML: true, + text: "Monthly Shipping Comparison" + }, + xAxis: { categories: @time_series }, + yAxis: { + title: { text: 'Percentage(%)' } + }, + tooltip: { valueSuffix: '%' }, + legend: { + layout: 'vertical', + align: 'right', + verticalAlign: 'middle', + borderWidth: 0 + }, + series: @result_series + } + } + + end + +end diff --git a/app/reports/spree/trending_search_report.rb b/app/reports/spree/trending_search_report.rb index beabcf7..af25b8d 100644 --- a/app/reports/spree/trending_search_report.rb +++ b/app/reports/spree/trending_search_report.rb @@ -1,79 +1,50 @@ module Spree class TrendingSearchReport < Spree::Report DEFAULT_SORTABLE_ATTRIBUTE = :occurrences - HEADERS = { searched_term: :string, occurrences: :integer } - SEARCH_ATTRIBUTES = { start_date: :start_date, end_date: :end_date, keywords_cont: :keyword } - SORTABLE_ATTRIBUTES = [] + HEADERS = { searched_term: :string, occurrences: :integer } + SEARCH_ATTRIBUTES = { start_date: :start_date, end_date: :end_date, keywords_cont: :keyword } + SORTABLE_ATTRIBUTES = [:occurrences] - def initialize(options) - super - @search_keywords_cont = @search[:keywords_cont].present? ? "%#{ @search[:keywords_cont] }%" : '%' - @sortable_type = :desc if options[:sort].blank? - set_sortable_attributes(options, DEFAULT_SORTABLE_ATTRIBUTE) + def paginated? + true end - def generate(options = {}) - top_searches = SpreeReportify::ReportDb[:spree_page_events___page_events]. - where(page_events__activity: 'search'). - where(page_events__created_at: @start_date..@end_date).where(Sequel.ilike(:page_events__search_keywords, @search_keywords_cont)). #filter by params - group(:searched_term). - order(Sequel.desc(:occurrences)). - limit(20) + class Result < Spree::Report::Result + charts FrequencyDistributionPieChart - top_searches + class Observation < Spree::Report::Observation + observation_fields [:searched_term, :occurrences] + end end - def select_columns(dataset) - dataset.select{[ - search_keywords.as(searched_term), - Sequel.as(count(:search_keywords), :occurrences) - ]} + deeplink searched_term: { template: %Q{{%# o['searched_term'] %}} } + + def paginated_report_query + report_query + .take(records_per_page) + .skip(current_page) + end + + def record_count_query + Spree::Report::QueryFragments.from_subquery(report_query).project(Arel.star.count) end - def chart_data - top_searches = select_columns(generate) - total_occurrences = SpreeReportify::ReportDb[top_searches].sum(:occurrences) - SpreeReportify::ReportDb[top_searches]. - select{[ - Sequel.as((occurrences / total_occurrences) * 100, :y), - Sequel.as(searched_term, :name) - ]}.all.map { |obj| obj.merge({ y: obj[:y].to_f })} # to convert percentage into float value from string + def report_query + Spree::Report::QueryFragments.from_subquery(searches) + .project("count(searched_term) as occurrences", "searched_term") + .group("searched_term") end - def chart_json - { - chart: true, - charts: [ - { - name: 'trending-search', - json: { - chart: { type: 'pie' }, - title: { - useHTML: true, - text: "Trending Search Keywords(Top 20)" - }, - tooltip: { - pointFormat: 'Search %: {point.percentage:.1f}%' - }, - plotOptions: { - pie: { - allowPointSelect: true, - cursor: 'pointer', - dataLabels: { - enabled: false - }, - showInLegend: true - } - }, - series: [{ - name: 'Hits', - data: chart_data - }] - } - } - ] - } + private def searches + Spree::PageEvent + .where(activity: 'search') + .where(created_at: reporting_period) + .where(Spree::PageEvent.arel_table[:search_keywords].matches(keyword_search)) + .select("search_keywords as searched_term") end + private def keyword_search + search[:keywords_cont].present? ? "%#{ search[:keywords_cont] }%" : '%' + end end end diff --git a/app/reports/spree/trending_search_report/frequency_distribution_pie_chart.rb b/app/reports/spree/trending_search_report/frequency_distribution_pie_chart.rb new file mode 100644 index 0000000..f4c0476 --- /dev/null +++ b/app/reports/spree/trending_search_report/frequency_distribution_pie_chart.rb @@ -0,0 +1,41 @@ +class Spree::TrendingSearchReport::FrequencyDistributionPieChart + attr_accessor :chart_data + + def initialize(result) + total_occurrences = result.observations.sum(&:occurrences).to_f + self.chart_data = result.observations.collect { |x| { name: x.searched_term, y: x.occurrences/total_occurrences } } + end + + def to_h + { + + name: 'trending-search', + json: { + chart: { type: 'pie' }, + title: { + useHTML: true, + text: "Trending Search Keywords(Top 20)" + }, + tooltip: { + pointFormat: 'Search %: {point.percentage:.1f}%' + }, + plotOptions: { + pie: { + allowPointSelect: true, + cursor: 'pointer', + dataLabels: { + enabled: false + }, + showInLegend: true + } + }, + series: [ + { + name: 'Hits', + data: chart_data + } + ] + } + } + end +end diff --git a/app/reports/spree/unique_purchases_report.rb b/app/reports/spree/unique_purchases_report.rb index 3c8dfc1..a934b32 100644 --- a/app/reports/spree/unique_purchases_report.rb +++ b/app/reports/spree/unique_purchases_report.rb @@ -1,33 +1,39 @@ module Spree class UniquePurchasesReport < Spree::Report DEFAULT_SORTABLE_ATTRIBUTE = :product_name - HEADERS = { sku: :string, product_name: :string, sold_count: :integer, users: :integer } - SEARCH_ATTRIBUTES = { start_date: :orders_completed_from, end_date: :orders_completed_till } - SORTABLE_ATTRIBUTES = [:product_name, :sku, :sold_count, :users] + HEADERS = { sku: :string, product_name: :string, sold_count: :integer, users: :integer } + SEARCH_ATTRIBUTES = { start_date: :orders_completed_from, end_date: :orders_completed_till } + SORTABLE_ATTRIBUTES = [:product_name, :sku, :sold_count, :users] - def initialize(options) - super - set_sortable_attributes(options, DEFAULT_SORTABLE_ATTRIBUTE) - end + deeplink product_name: { template: %Q{{%# o.product_name %}} } + + class Result < Spree::Report::Result + class Observation < Spree::Report::Observation + observation_fields [:product_name, :product_slug, :sku, :sold_count, :users] - def generate(options = {}) - ::SpreeReportify::ReportDb[:spree_line_items___line_items]. - join(:spree_orders___orders, id: :order_id). - join(:spree_variants___variants, variants__id: :line_items__variant_id). - join(:spree_products___products, products__id: :variants__product_id). - where(orders__state: 'complete'). - where(orders__completed_at: @start_date..@end_date). #filter by params - group(:variant_id). - order(sortable_sequel_expression) + def sku + @sku.presence || @product_name + end + end end - def select_columns(dataset) - dataset.select{[ - Sequel.as(IFNULL(variants__sku, products__name), :sku), - products__name.as(product_name), - sum(quantity).as(sold_count), - (count(distinct orders__user_id) + count(orders__id) - count(orders__user_id)).as(users) - ]} + def report_query + user_count_sql = '(COUNT(DISTINCT(spree_orders.user_id)) + COUNT(spree_orders.id) - COUNT(spree_orders.user_id))' + purchases_by_variant = + Spree::LineItem + .joins(:order) + .joins(:variant) + .joins(:product) + .where(spree_orders: { state: 'complete', completed_at: reporting_period }) + .group('variant_id', 'spree_variants.sku', 'spree_products.slug', 'spree_products.name') + .select( + 'spree_variants.sku as sku', + 'spree_products.slug as product_slug', + 'spree_products.name as product_name', + 'SUM(quantity) as sold_count', + "#{ user_count_sql } as users" + ) end + end end diff --git a/app/reports/spree/user_pool_report.rb b/app/reports/spree/user_pool_report.rb index 7f3b48b..3f7bebd 100644 --- a/app/reports/spree/user_pool_report.rb +++ b/app/reports/spree/user_pool_report.rb @@ -1,133 +1,66 @@ module Spree class UserPoolReport < Spree::Report - DEFAULT_SORTABLE_ATTRIBUTE = :orders__completed_at - HEADERS = { months_name: :string, guest_users: :integer, active_users: :integer, new_sign_ups: :integer } - SEARCH_ATTRIBUTES = { start_date: :users_created_from, end_date: :users_created_till } + HEADERS = { guest_users: :integer, active_users: :integer, new_sign_ups: :integer } + SEARCH_ATTRIBUTES = { start_date: :users_created_from, end_date: :users_created_till } SORTABLE_ATTRIBUTES = [] - def no_pagination? - true - end - - def generate(options = {}) - # order of column is important when we take union of two tables - new_sign_ups = SpreeReportify::ReportDb[:spree_users___users]. - where(users__created_at: @start_date..@end_date). - select{[ - id.as(:user_id), - Sequel.as(YEAR(:users__created_at), :year), - Sequel.as(MONTHNAME(:users__created_at), :month_name), - Sequel.as(MONTH(:users__created_at), :number) - ]} - - group_new_sign_ups_by_months = SpreeReportify::ReportDb[new_sign_ups]. - group(:months_name). - order(:year, :number). - select{[ - number, - year, - Sequel.as(concat(month_name, ' ', IFNULL(year, 2016)), :months_name), - Sequel.as(0, :guest_users), - Sequel.as(0, :active_users), - Sequel.as(IFNULL(COUNT(user_id), 0), :new_sign_ups) - ]} + class Result < Spree::Report::TimedResult + charts DistributionColumnChart - vistors = SpreeReportify::ReportDb[:spree_page_events___page_events]. - where(page_events__created_at: @start_date..@end_date). - select{[ - Sequel.as(YEAR(:page_events__created_at), :year), - Sequel.as(MONTHNAME(:page_events__created_at), :month_name), - Sequel.as(MONTH(:page_events__created_at), :number), - Sequel.as(actor_id, :user), - Sequel.as(session_id, :session) - ]} - - visitors_by_months = SpreeReportify::ReportDb[vistors]. - group(:months_name). - order(:year, :number). - select{[ - number, - year, - Sequel.as(concat(month_name, ' ', IFNULL(year, 2016)), :months_name), - Sequel.as((COUNT(DISTINCT session) - COUNT(DISTINCT user)), :guest_users), - Sequel.as(COUNT(DISTINCT user), :active_users), - Sequel.as(0, :new_sign_ups) - ]} + class Observation < Spree::Report::TimedObservation + observation_fields active_users: 0, guest_users: 0, new_sign_ups: 0 + end + end + def report_query + Report::QueryFragments + .from_union(grouped_sign_ups, grouped_visitors) + .group(*time_scale_columns) + .order(*time_scale_columns_to_s) + .project( + *time_scale_columns, + 'SUM(active_users) as active_users', + 'SUM(guest_users) as guest_users', + 'SUM(new_sign_ups) as new_sign_ups' + ) + end - union_of_stats = group_new_sign_ups_by_months.union(visitors_by_months) + private def grouped_sign_ups + sign_ups = Spree::User.where(created_at: reporting_period).select(:id, *time_scale_selects) - union_stats = SpreeReportify::ReportDb[union_of_stats]. - group(:months_name). - order(:year, :number). - select{[ - months_name, - year, - number, - Sequel.as(SUM(:guest_users), :guest_users), - Sequel.as(SUM(:active_users), :active_users), - Sequel.as(SUM(:new_sign_ups), :new_sign_ups) - ]} - fill_missing_values({guest_users: 0, active_users: 0, new_sign_ups: 0}, union_stats.all) + Report::QueryFragments.from_subquery(sign_ups) + .group(*time_scale_columns, 'guest_users', 'active_users') + .order(*time_scale_columns_to_s) + .project( + *time_scale_columns, + '0 as guest_users', + '0 as active_users', + 'COUNT(id) as new_sign_ups' + ) end - def select_columns(dataset) - dataset + private def grouped_visitors + guest_count_sql = '(COUNT(DISTINCT(session)) - COUNT(DISTINCT(user)))' + Report::QueryFragments.from_subquery(visitors) + .group(*time_scale_columns, 'new_sign_ups') + .order(*time_scale_columns_to_s) + .project( + *time_scale_columns, + "#{ guest_count_sql } as guest_users", + 'COUNT(DISTINCT(user)) as active_users', + '0 as new_sign_ups' + ) end - # extract it in report.rb - def chart_data - unless @data - @data = Hash.new {|h, k| h[k] = [] } - generate.each do |object| - object.each_pair do |key, value| - @data[key].push(value) - end - end - end - @data + private def visitors + Spree::PageEvent + .where(created_at: reporting_period) + .select( + *time_scale_selects, + 'actor_id as user', + 'session_id as session' + ) end - def chart_json - { - chart: true, - charts: [ - { - id: 'user-pool', - json: { - chart: { type: 'column' }, - title: { - useHTML: true, - text: 'User Pool' - }, - xAxis: { categories: chart_data[:months_name] }, - yAxis: { - title: { text: 'Count' } - }, - legend: { - layout: 'vertical', - align: 'right', - verticalAlign: 'middle', - borderWidth: 0 - }, - series: [ - { - name: Spree.t('user_pool.new_sign_ups'), - data: chart_data[:new_sign_ups].map(&:to_i) - }, - { - name: Spree.t('user_pool.active_users'), - data: chart_data[:active_users].map(&:to_i) - }, - { - name: Spree.t('user_pool.guest_users'), - data: chart_data[:guest_users].map(&:to_i) - } - ] - } - } - ] - } - end end end diff --git a/app/reports/spree/user_pool_report/distribution_column_chart.rb b/app/reports/spree/user_pool_report/distribution_column_chart.rb new file mode 100644 index 0000000..4e94a31 --- /dev/null +++ b/app/reports/spree/user_pool_report/distribution_column_chart.rb @@ -0,0 +1,65 @@ +class Spree::UserPoolReport::DistributionColumnChart + def initialize(result) + @chart_data = { + active_users: [], + guest_users: [], + new_sign_ups: [] + } + @time_dimension = result.time_dimension + @chart_data[@time_dimension] = [] + @result = result + process_chart_data + end + + def process_chart_data + chart_keys = @chart_data.keys + @result.observations.each do |observation| + chart_keys.each do |key| + @chart_data[key] << observation.public_send(key) + end + end + end + + def to_h + { + id: 'user-pool', + json: { + chart: { type: 'column' }, + title: { + useHTML: true, + text: %Q( + User Pool + + + ) + }, + xAxis: { categories: @chart_data[@time_dimension] }, + yAxis: { + title: { text: 'Count' } + }, + legend: { + layout: 'vertical', + align: 'right', + verticalAlign: 'middle', + borderWidth: 0 + }, + series: [ + { + name: Spree.t('user_pool.new_sign_ups'), + data: @chart_data[:new_sign_ups].map(&:to_i) + }, + { + name: Spree.t('user_pool.active_users'), + data: @chart_data[:active_users].map(&:to_i) + }, + { + name: Spree.t('user_pool.guest_users'), + data: @chart_data[:guest_users].map(&:to_i) + } + ] + } + } + end +end diff --git a/app/reports/spree/users_not_converted_report.rb b/app/reports/spree/users_not_converted_report.rb index c27c01e..ad5c4b9 100644 --- a/app/reports/spree/users_not_converted_report.rb +++ b/app/reports/spree/users_not_converted_report.rb @@ -1,29 +1,48 @@ module Spree class UsersNotConvertedReport < Spree::Report - DEFAULT_SORTABLE_ATTRIBUTE = :orders__completed_at - HEADERS = { user_email: :string, signup_date: :date } - SEARCH_ATTRIBUTES = { start_date: :users_created_from, end_date: :users_created_till, email_cont: :email } - SORTABLE_ATTRIBUTES = [:user_email, :signup_date] - - def initialize(options) - super - @sortable_type = :desc if options[:sort].blank? - @email_cont = @search[:email_cont].present? ? "%#{ @search[:email_cont] }%" : '%' + DEFAULT_SORTABLE_ATTRIBUTE = :user_email + HEADERS = { user_email: :string, signup_date: :date } + SEARCH_ATTRIBUTES = { start_date: :users_created_from, end_date: :users_created_till, email_cont: :email } + SORTABLE_ATTRIBUTES = [:user_email, :signup_date] + + def paginated? + true end - def generate(options = {}) - SpreeReportify::ReportDb[:spree_users___users]. - left_join(:spree_orders___orders, user_id: :id). - where(orders__completed_at: nil, orders__number: nil). - where(users__created_at: @start_date..@end_date).where(Sequel.ilike(:users__email, @email_cont)). #filter by params - order(sortable_sequel_expression) + class Result < Spree::Report::Result + class Observation < Spree::Report::Observation + observation_fields [:user_email, :signup_date] + + def signup_date + @signup_date.to_date.strftime("%B %d, %Y") + end + end end - def select_columns(dataset) - dataset.select{[ - users__email.as(user_email), - users__created_at.as(signup_date) - ]} + def paginated_report_query + report_query + .limit(records_per_page) + .offset(current_page) end + + def record_count_query + Spree::Report::QueryFragments.from_subquery(report_query).project(Arel.star.count) + end + + def report_query + Spree::User + .where(created_at: reporting_period) + .where(Spree::User.arel_table[:email].matches(email_search)) + .joins("LEFT JOIN spree_orders on spree_orders.user_id = spree_users.id") + .where(spree_orders: { completed_at: nil, number: nil }) + .select( + "spree_users.email as user_email", + "spree_users.created_at as signup_date") + end + + private def email_search + search[:email_cont].present? ? "%#{ search[:email_cont] }%" : '%' + end + end end diff --git a/app/reports/spree/users_who_have_not_recently_purchased_report.rb b/app/reports/spree/users_who_have_not_recently_purchased_report.rb deleted file mode 100644 index bb3d2b4..0000000 --- a/app/reports/spree/users_who_have_not_recently_purchased_report.rb +++ /dev/null @@ -1,35 +0,0 @@ -module Spree - class UsersWhoHaveNotRecentlyPurchasedReport < Spree::Report - DEFAULT_SORTABLE_ATTRIBUTE = :user_email - HEADERS = { user_email: :string, last_purchase_date: :date, last_purchased_order_number: :string } - SEARCH_ATTRIBUTES = { start_date: :start_date, end_date: :end_date, email_cont: :email } - SORTABLE_ATTRIBUTES = [:user_email, :last_purchase_date] - - def initialize(options) - super - @email_cont = @search[:email_cont].present? ? "%#{ @search[:email_cont] }%" : '%' - set_sortable_attributes(options, DEFAULT_SORTABLE_ATTRIBUTE) - end - - def generate(options = {}) - all_orders_with_users = SpreeReportify::ReportDb[:spree_users___users]. - left_join(:spree_orders___orders, user_id: :id). - where(Sequel.~(orders__completed_at: nil), Sequel.~(orders__completed_at: @start_date..@end_date)). - where(Sequel.ilike(:users__email, @email_cont)). - order(Sequel.desc(:orders__completed_at)). - select( - :users__email___user_email, - :orders__number___last_purchased_order_number, - :orders__completed_at___last_purchase_date - ).as(:all_orders_with_users) - - SpreeReportify::ReportDb[all_orders_with_users]. - group(:all_orders_with_users__user_email). - order(sortable_sequel_expression) - end - - def select_columns(dataset) - dataset.select_all - end - end -end diff --git a/app/reports/spree/users_who_recently_purchased_report.rb b/app/reports/spree/users_who_recently_purchased_report.rb index f85168a..cd0b705 100644 --- a/app/reports/spree/users_who_recently_purchased_report.rb +++ b/app/reports/spree/users_who_recently_purchased_report.rb @@ -1,40 +1,69 @@ module Spree class UsersWhoRecentlyPurchasedReport < Spree::Report DEFAULT_SORTABLE_ATTRIBUTE = :user_email - HEADERS = { user_email: :string, purchase_count: :integer, last_purchase_date: :date, last_purchased_order_number: :string } - SEARCH_ATTRIBUTES = { start_date: :start_date, end_date: :end_date, email_cont: :email } - SORTABLE_ATTRIBUTES = [:user_email, :purchase_count, :last_purchase_date] - - def initialize(options) - super - @email_cont = @search[:email_cont].present? ? "%#{ @search[:email_cont] }%" : '%' - set_sortable_attributes(options, DEFAULT_SORTABLE_ATTRIBUTE) + HEADERS = { user_email: :string, purchase_count: :integer, last_purchase_date: :date, last_purchased_order_number: :string } + SEARCH_ATTRIBUTES = { start_date: :start_date, end_date: :end_date, email_cont: :email } + SORTABLE_ATTRIBUTES = [:user_email, :purchase_count, :last_purchase_date] + + def paginated? + true + end + + class Result < Spree::Report::Result + class Observation < Spree::Report::Observation + observation_fields [:user_email, :last_purchased_order_number, :last_purchase_date, :purchase_count] + + def last_purchase_date + @last_purchase_date.to_date.strftime("%B %d, %Y") + end + end + end + + def record_count_query + Spree::Report::QueryFragments.from_subquery(report_query).project(Arel.star.count) + end + + def report_query + Spree::Report::QueryFragments + .from_subquery(all_orders_with_users) + .project( + "user_email", + "last_purchased_order_number", + "last_purchase_date", + "COUNT(user_email) as purchase_count") + .group( + "user_email", + "last_purchased_order_number", + "last_purchase_date" + ) + end + + + def paginated_report_query + report_query + .take(records_per_page) + .skip(current_page) end - def generate(options = {}) - all_orders_with_users = SpreeReportify::ReportDb[:spree_users___users]. - left_join(:spree_orders___orders, user_id: :id). - where(orders__completed_at: @start_date..@end_date). - where(Sequel.ilike(:users__email, @email_cont)). - order(Sequel.desc(:orders__completed_at)). - select( - :users__email___user_email, - :orders__number___last_purchased_order_number, - :orders__completed_at___last_purchase_date, - ).as(:all_orders_with_users) - - SpreeReportify::ReportDb[all_orders_with_users]. - group(:all_orders_with_users__user_email). - order(sortable_sequel_expression) + private def all_orders_with_users + Spree::User + .where(Spree::User.arel_table[:email].matches(email_search)) + .joins("LEFT JOIN spree_orders on spree_orders.user_id = spree_users.id") + .where(spree_orders: { completed_at: reporting_period }) + .order("spree_orders.completed_at desc") + .select( + "spree_users.email as user_email", + "spree_orders.number as last_purchased_order_number", + "spree_orders.completed_at as last_purchase_date" + ).group( + 'user_email', + "spree_orders.number", + "spree_orders.completed_at" + ) end - def select_columns(dataset) - dataset.select{[ - all_orders_with_users__user_email, - all_orders_with_users__last_purchased_order_number, - all_orders_with_users__last_purchase_date, - count(all_orders_with_users__user_email).as(purchase_count) - ]} + private def email_search + search[:email_cont].present? ? "%#{ search[:email_cont] }%" : '%' end end end diff --git a/app/services/spree/report_generation_service.rb b/app/services/spree/report_generation_service.rb index 8580e0d..2439db8 100644 --- a/app/services/spree/report_generation_service.rb +++ b/app/services/spree/report_generation_service.rb @@ -1,72 +1,25 @@ module Spree class ReportGenerationService - REPORTS = { - finance_analysis: [ - :payment_method_transactions, :payment_method_transactions_conversion_rate, - :sales_performance, :shipping_cost, :sales_tax - ], - product_analysis: [ - :cart_additions, :cart_removals, :cart_updations, - :product_views, :product_views_to_cart_additions, - :product_views_to_purchases, :unique_purchases, - :best_selling_products, :returned_products - ], - promotion_analysis: [:promotional_cost, :annual_promotional_cost], - trending_search_analysis: [:trending_search], - user_analysis: [:user_pool, :users_not_converted, :users_who_recently_purchased] - } + class << self + delegate :reports, :report_exists?, :reports_for_category, :default_report_category, to: :configuration + delegate :configuration, to: SpreeAdminInsights::Config + end def self.generate_report(report_name, options) klass = Spree.const_get((report_name.to_s + '_report').classify) resource = klass.new(options) dataset = resource.generate - total_records = resource.select_columns(dataset).count - if resource.no_pagination? - result_set = dataset - else - result_set = resource.select_columns(dataset.limit(options['records_per_page'], options['offset'])).all - end - options['no_pagination'] = resource.no_pagination?.to_s unless options['no_pagination'] == 'true' - [headers(klass, resource, report_name), result_set, total_pages(total_records, options['records_per_page'], options['no_pagination']), search_attributes(klass), resource.chart_json, resource] end - def self.download(options = {}, headers, stats) + def self.download(report, options = {}) + headers = report.headers + stats = report.observations ::CSV.generate(options) do |csv| csv << headers.map { |head| head[:name] } stats.each do |record| - csv << headers.map { |head| record[head[:value]] } - end - end - end - - def self.search_attributes(klass) - search_attributes = {} - klass::SEARCH_ATTRIBUTES.each do |key, value| - search_attributes[key] = value.to_s.humanize - end - search_attributes - end - - def self.total_pages(total_records, records_per_page, no_pagination) - if no_pagination != 'true' - total_pages = total_records / records_per_page - if total_records % records_per_page == 0 - total_pages -= 1 + csv << headers.map { |head| record.public_send(head[:value]) } end - total_pages - end - end - - def self.headers(klass, resource, report_name) - klass::HEADERS.keys.map do |header| - { - name: Spree.t(header.to_sym, scope: [:insight, report_name]), - value: header, - sorted: resource.try(:header_sorted?, header) ? resource.sortable_type.to_s : nil, - type: klass::HEADERS[header], - sortable: header.in?(klass::SORTABLE_ATTRIBUTES) - } end end diff --git a/app/views/spree/admin/insights/download.pdf.erb b/app/views/spree/admin/insights/download.pdf.erb index 3d3a3f2..ea0f973 100644 --- a/app/views/spree/admin/insights/download.pdf.erb +++ b/app/views/spree/admin/insights/download.pdf.erb @@ -10,16 +10,16 @@
<%= Spree.t(_header_[:value], scope: [:insight, @report_name], default: _header_[:name]) %> | <% end %>|
---|---|
<%= stat[head[:value]] %> | + <% @report.headers.each do |head|%> +<%= stat.public_send(head[:value]) %> | <% end %>{% if(o['headers'][i].sortable) { %} - {%= o['headers'][i].name %} + {%= o['headers'][i].name %} {% if(o['headers'][i].sorted != undefined) { %} - + {% } %} {% } else { %} {%= o['headers'][i].name %} @@ -22,18 +22,22 @@ |
- {% if (o['headers'][j].value == 'profit_loss') { %} + {% if (o['headers'][j].value == 'profit_loss' || o['headers'][j].value == 'profit_loss_percent') { %} {%= Math.abs(o['stats'][i][o['headers'][j].value] || 0) %} {% if (o['stats'][i][o['headers'][j].value] > 0) { %} - + {% } %} {% if (o['stats'][i][o['headers'][j].value] < 0) { %} - + {% } %} {% } else if (o['headers'][j].type == 'integer') { %} {%= o['stats'][i][o['headers'][j].value] || 0 %} {% } else { %} - {%= o['stats'][i][o['headers'][j].value] || '-' %} + {% if (o['deeplink']['deeplinked'] && o['deeplink'][o['headers'][j].value]) { %} + {%# tmpl(o['deeplink'][o['headers'][j].value]['template'], o['stats'][i]) %} + {% } else { %} + {%= o['stats'][i][o['headers'][j].value] || '-' %} + {% } %} {% } %} | {% } %} @@ -41,5 +45,4 @@ {% } %}