solidus_admin_insights 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +54 -0
- data/Gemfile +2 -0
- data/LICENSE +26 -0
- data/README.md +71 -0
- data/Rakefile +21 -0
- data/app/assets/javascripts/spree/backend/jquery.tablesorter.min.js +4 -0
- data/app/assets/javascripts/spree/backend/solidus_admin_insights.js +2 -0
- data/app/assets/javascripts/spree/backend/solidus_admin_insights/paginator.js +128 -0
- data/app/assets/javascripts/spree/backend/solidus_admin_insights/report_loader.js +265 -0
- data/app/assets/javascripts/spree/backend/solidus_admin_insights/searcher.js +72 -0
- data/app/assets/javascripts/spree/backend/solidus_admin_insights/table_sorter.js +47 -0
- data/app/assets/javascripts/spree/backend/tmpl.js +87 -0
- data/app/assets/javascripts/spree/frontend/solidus_admin_insights.js +2 -0
- data/app/assets/stylesheets/spree/backend/override_pdf.css +71 -0
- data/app/assets/stylesheets/spree/backend/solidus_admin_insights.css +132 -0
- data/app/assets/stylesheets/spree/frontend/solidus_admin_insights.css +4 -0
- data/app/controllers/spree/admin/insights_controller.rb +103 -0
- data/app/helpers/spree/admin/base_helper_decorator.rb +17 -0
- data/app/models/spree/app_configuration_decorator.rb +3 -0
- data/app/models/spree/product_decorator.rb +3 -0
- data/app/models/spree/promotion_action_decorator.rb +3 -0
- data/app/models/spree/return_authorization_decorator.rb +4 -0
- data/app/permissions/spree/permission_sets/insight_display.rb +9 -0
- data/app/reports/spree/best_selling_products_report.rb +42 -0
- data/app/reports/spree/cart_additions_report.rb +36 -0
- data/app/reports/spree/cart_removals_report.rb +36 -0
- data/app/reports/spree/cart_updations_report.rb +40 -0
- data/app/reports/spree/payment_method_transactions_conversion_rate_report.rb +74 -0
- data/app/reports/spree/payment_method_transactions_conversion_rate_report/payment_method_state_distribution_chart.rb +39 -0
- data/app/reports/spree/payment_method_transactions_report.rb +60 -0
- data/app/reports/spree/payment_method_transactions_report/payment_method_revenue_distribution_chart.rb +36 -0
- data/app/reports/spree/product_views_report.rb +46 -0
- data/app/reports/spree/product_views_to_cart_additions_report.rb +53 -0
- data/app/reports/spree/product_views_to_purchases_report.rb +54 -0
- data/app/reports/spree/promotional_cost_report.rb +84 -0
- data/app/reports/spree/promotional_cost_report/promotional_cost_chart.rb +37 -0
- data/app/reports/spree/promotional_cost_report/usage_count_chart.rb +41 -0
- data/app/reports/spree/report.rb +131 -0
- data/app/reports/spree/report/chart.rb +11 -0
- data/app/reports/spree/report/configuration.rb +40 -0
- data/app/reports/spree/report/date_slicer.rb +61 -0
- data/app/reports/spree/report/observation.rb +49 -0
- data/app/reports/spree/report/query_fragments.rb +45 -0
- data/app/reports/spree/report/query_time_scale.rb +19 -0
- data/app/reports/spree/report/result.rb +100 -0
- data/app/reports/spree/report/timed_observation.rb +47 -0
- data/app/reports/spree/report/timed_result.rb +48 -0
- data/app/reports/spree/returned_products_report.rb +37 -0
- data/app/reports/spree/sales_performance_report.rb +107 -0
- data/app/reports/spree/sales_performance_report/profit_loss_chart.rb +37 -0
- data/app/reports/spree/sales_performance_report/profit_loss_percent_chart.rb +36 -0
- data/app/reports/spree/sales_performance_report/sale_cost_price_chart.rb +48 -0
- data/app/reports/spree/sales_tax_report.rb +64 -0
- data/app/reports/spree/sales_tax_report/monthly_sales_tax_comparison_chart.rb +39 -0
- data/app/reports/spree/shipping_cost_report.rb +89 -0
- data/app/reports/spree/shipping_cost_report/shipping_cost_distribution_chart.rb +38 -0
- data/app/reports/spree/trending_search_report.rb +50 -0
- data/app/reports/spree/trending_search_report/frequency_distribution_pie_chart.rb +41 -0
- data/app/reports/spree/unique_purchases_report.rb +39 -0
- data/app/reports/spree/user_pool_report.rb +66 -0
- data/app/reports/spree/user_pool_report/distribution_column_chart.rb +65 -0
- data/app/reports/spree/users_not_converted_report.rb +48 -0
- data/app/reports/spree/users_who_recently_purchased_report.rb +69 -0
- data/app/services/spree/report_generation_service.rb +27 -0
- data/app/views/spree/admin/insights/_chart.html.erb +4 -0
- data/app/views/spree/admin/insights/download.pdf.erb +27 -0
- data/app/views/spree/admin/insights/index.html.erb +82 -0
- data/app/views/spree/admin/insights/search/_product_views_search.html.erb +13 -0
- data/app/views/spree/admin/insights/search/_search_form.html.erb +39 -0
- data/app/views/spree/admin/insights/search/_trending_searches_search.html.erb +13 -0
- data/app/views/spree/admin/insights/search/_users_not_converted_search.html.erb +13 -0
- data/app/views/spree/admin/insights/search/_users_who_have_not_purchased_recently_search.html.erb +13 -0
- data/app/views/spree/admin/insights/search/_users_who_recently_purchased_search.html.erb +13 -0
- data/app/views/spree/admin/shared/_insights_side_menu.html.erb +5 -0
- data/app/views/spree/admin/shared/sub_menu/_insight.html.erb +7 -0
- data/app/views/spree/admin/templates/insights/_paginator.template +11 -0
- data/app/views/spree/admin/templates/insights/_search.template +76 -0
- data/app/views/spree/admin/templates/insights/_show.template +49 -0
- data/app/views/spree/layouts/pdf.html.erb +9 -0
- data/bin/rails +7 -0
- data/config/initializers/add_to_sidebar.rb +14 -0
- data/config/initializers/assets.rb +1 -0
- data/config/initializers/mime_types.rb +2 -0
- data/config/initializers/wicked_pdf.rb +21 -0
- data/config/locales/en.yml +167 -0
- data/config/routes.rb +6 -0
- data/lib/generators/solidus_admin_insights/install/install_generator.rb +36 -0
- data/lib/generators/solidus_admin_insights/install/solidus_admin_insights.rb +22 -0
- data/lib/solidus_admin_insights.rb +14 -0
- data/lib/solidus_admin_insights/engine.rb +27 -0
- data/lib/solidus_admin_insights/factories.rb +6 -0
- data/solidus_admin_insights.gemspec +42 -0
- data/spec/spec_helper.rb +93 -0
- metadata +419 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
module Spree::Report::DateSlicer
|
2
|
+
def self.slice_into(start_date, end_date, time_scale, klass)
|
3
|
+
case time_scale
|
4
|
+
when :hourly
|
5
|
+
slice_hours_into(start_date, end_date, klass)
|
6
|
+
when :daily
|
7
|
+
slice_days_into(start_date, end_date, klass)
|
8
|
+
when :monthly
|
9
|
+
slice_months_into(start_date, end_date, klass)
|
10
|
+
when :yearly
|
11
|
+
slice_years_into(start_date, end_date, klass)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.slice_hours_into(start_date, end_date, klass)
|
16
|
+
current_date = start_date
|
17
|
+
slices = []
|
18
|
+
while current_date < end_date
|
19
|
+
slices << (0..23).collect do |hour|
|
20
|
+
obj = klass.new
|
21
|
+
obj.date = current_date
|
22
|
+
obj.hour = hour
|
23
|
+
obj
|
24
|
+
end
|
25
|
+
current_date = current_date.next_day
|
26
|
+
end
|
27
|
+
slices.flatten
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.slice_days_into(start_date, end_date, klass)
|
31
|
+
current_date = start_date
|
32
|
+
slices = []
|
33
|
+
while current_date < end_date
|
34
|
+
obj = klass.new
|
35
|
+
obj.date = current_date
|
36
|
+
slices << obj
|
37
|
+
current_date = current_date.next_day
|
38
|
+
end
|
39
|
+
slices
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.slice_months_into(start_date, end_date, klass)
|
43
|
+
current_date = start_date
|
44
|
+
slices = []
|
45
|
+
while current_date < end_date
|
46
|
+
obj = klass.new
|
47
|
+
obj.date = current_date
|
48
|
+
slices << obj
|
49
|
+
current_date = current_date.end_of_month.next_day
|
50
|
+
end
|
51
|
+
slices
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.slice_years_into(start_date, end_date, klass)
|
55
|
+
(start_date.year..end_date.year).collect do |year|
|
56
|
+
obj = klass.new
|
57
|
+
obj.date = Date.new(year).end_of_year
|
58
|
+
obj
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class Spree::Report::Observation
|
2
|
+
|
3
|
+
def initialize
|
4
|
+
set_defaults
|
5
|
+
end
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def observation_fields(records)
|
9
|
+
case records
|
10
|
+
when Hash
|
11
|
+
build_from_hash(records)
|
12
|
+
else
|
13
|
+
build_from_list(records)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def build_from_hash(records)
|
18
|
+
build_from_list(records.keys)
|
19
|
+
|
20
|
+
define_method :set_defaults do
|
21
|
+
records.keys.each do |key|
|
22
|
+
self.send("#{ key }=", records[key])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def build_from_list(records)
|
28
|
+
attr_accessor *records
|
29
|
+
|
30
|
+
define_method :populate do |result|
|
31
|
+
records.each do |record|
|
32
|
+
record_name = record.to_s
|
33
|
+
self.send("#{ record }=", result[record_name]) if result[record_name]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
define_method :set_defaults do
|
38
|
+
end
|
39
|
+
|
40
|
+
define_method :observations_to_h do
|
41
|
+
records.inject({}) { |acc, record| acc[record] = self.send(record); acc }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_h
|
47
|
+
observations_to_h
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Spree::Report::QueryFragments
|
2
|
+
def self.from_subquery(subquery, as: 'results')
|
3
|
+
Arel::SelectManager.new(Arel.sql("(#{subquery.to_sql}) as #{ as }"))
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.from_join(subquery1, subquery2, join_expr)
|
7
|
+
Arel::SelectManager.new(Arel.sql("((#{ subquery1.to_sql }) as q1 JOIN (#{ subquery2.to_sql }) as q2 ON #{ join_expr })"))
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.from_union(subquery1, subquery2, as: 'results')
|
11
|
+
Arel::SelectManager.new(Arel.sql("((#{ subquery1.to_sql }) UNION (#{ subquery2.to_sql })) as #{ as }"))
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.year(column, as='year')
|
15
|
+
extract_from_date(:year, column, as)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.month(column, as='month')
|
19
|
+
extract_from_date(:month, column, as)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.week(column, as='week')
|
23
|
+
extract_from_date(:week, column, as)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.day(column, as='day')
|
27
|
+
extract_from_date(:day, column, as)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.hour(column, as='hour')
|
31
|
+
extract_from_date(:hour, column, as)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.extract_from_date(part, column, as)
|
35
|
+
"EXTRACT(#{ part } from #{ column }) AS #{ as }"
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.if_null(val, default_val)
|
39
|
+
Arel::Nodes::NamedFunction.new('COALESCE', [val, default_val])
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.sum(node)
|
43
|
+
Arel::Nodes::NamedFunction.new('SUM', [node])
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Spree::Report::QueryTimeScale
|
2
|
+
def self.select(time_scale, time_scale_on)
|
3
|
+
db_col_name = time_scale_on.present? ? "#{ time_scale_on }.created_at" : "created_at"
|
4
|
+
time_scale_columns(time_scale).collect { |time_scale_column| ::Spree::Report::QueryFragments.public_send(time_scale_column, db_col_name) }
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.time_scale_columns(time_scale)
|
8
|
+
case time_scale
|
9
|
+
when :hourly
|
10
|
+
[:day, :hour]
|
11
|
+
when :daily
|
12
|
+
[:month, :day]
|
13
|
+
when :monthly
|
14
|
+
[:year, :month]
|
15
|
+
when :yearly
|
16
|
+
[:year]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Spree
|
2
|
+
class Report
|
3
|
+
class Result
|
4
|
+
attr_accessor :start_date, :end_date, :time_scale, :report
|
5
|
+
attr_reader :observations
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
yield self
|
9
|
+
build_report_observations
|
10
|
+
end
|
11
|
+
|
12
|
+
def build_report_observations
|
13
|
+
query_results
|
14
|
+
populate_observations
|
15
|
+
end
|
16
|
+
|
17
|
+
def query_results
|
18
|
+
@results = report.get_results
|
19
|
+
end
|
20
|
+
|
21
|
+
def populate_observations
|
22
|
+
@observations = @results.collect do |result|
|
23
|
+
_observation = self.class::Observation.new
|
24
|
+
_observation.populate(result)
|
25
|
+
_observation
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def to_h
|
31
|
+
{
|
32
|
+
deeplink: report.deeplink_properties,
|
33
|
+
total_pages: report.total_pages,
|
34
|
+
per_page: report.records_per_page,
|
35
|
+
pagination_required: report.pagination_required?,
|
36
|
+
headers: headers,
|
37
|
+
search_attributes: search_attributes,
|
38
|
+
stats: observations.collect(&:to_h),
|
39
|
+
chart_json: chart_json
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def chart_json
|
44
|
+
{
|
45
|
+
chart: false,
|
46
|
+
charts: []
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.charts(*report_charts)
|
51
|
+
define_method :chart_json do
|
52
|
+
{
|
53
|
+
chart: true,
|
54
|
+
charts: report_charts.collect { |report_chart| report_chart.new(self).to_h }.flatten
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def search_attributes
|
60
|
+
report.class::SEARCH_ATTRIBUTES.transform_values { |value| value.to_s.humanize }
|
61
|
+
end
|
62
|
+
|
63
|
+
def total_pages # O indexed
|
64
|
+
if report.pagination_required?
|
65
|
+
total_pages = report.total_records / report.records_per_page
|
66
|
+
if report.total_records % report.records_per_page == 0
|
67
|
+
total_pages -= 1
|
68
|
+
end
|
69
|
+
total_pages
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def headers
|
74
|
+
report.class::HEADERS.keys.collect do |header|
|
75
|
+
header_description = {
|
76
|
+
name: Spree.t(header.to_sym, scope: [:insight, report.name]),
|
77
|
+
value: header,
|
78
|
+
type: report.class::HEADERS[header],
|
79
|
+
sortable: header.in?(report.class::SORTABLE_ATTRIBUTES)
|
80
|
+
}
|
81
|
+
header_description[:sorted] = report.sort_direction if report.header_sorted?(header)
|
82
|
+
header_description
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def time_dimension
|
87
|
+
case time_scale
|
88
|
+
when :hourly
|
89
|
+
:hour_name
|
90
|
+
when :daily
|
91
|
+
:day_name
|
92
|
+
when :monthly
|
93
|
+
:month_name
|
94
|
+
when :yearly
|
95
|
+
:year
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class Spree::Report::TimedObservation < Spree::Report::Observation
|
2
|
+
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
attr_accessor :date, :hour, :reportable_keys
|
6
|
+
|
7
|
+
def_delegators :date, :day, :month, :year
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
super
|
11
|
+
self.hour = 0
|
12
|
+
end
|
13
|
+
|
14
|
+
def describes?(result, time_scale)
|
15
|
+
case time_scale
|
16
|
+
when :hourly
|
17
|
+
result['hour'] == hour && result['day'] == day
|
18
|
+
when :daily
|
19
|
+
result['day'] == day && result['month'] == month
|
20
|
+
when :monthly
|
21
|
+
result['month'] == month && result['year'] == year
|
22
|
+
when :yearly
|
23
|
+
result['year'] == year
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def month_name
|
28
|
+
Date::MONTHNAMES[month]
|
29
|
+
end
|
30
|
+
|
31
|
+
def hour_name
|
32
|
+
if hour == 23
|
33
|
+
return "23:00 - 00:00"
|
34
|
+
else
|
35
|
+
return "#{ hour }:00 - #{ hour + 1 }:00"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def day_name
|
40
|
+
"#{ day } #{ month_name }"
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_h
|
44
|
+
super.merge({day_name: day_name, month_name: month_name, year: year, hour_name: hour_name})
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Spree
|
2
|
+
class Report
|
3
|
+
class TimedResult < Result
|
4
|
+
|
5
|
+
def build_report_observations
|
6
|
+
query_results
|
7
|
+
build_empty_observations
|
8
|
+
populate_observations
|
9
|
+
end
|
10
|
+
|
11
|
+
def build_empty_observations
|
12
|
+
@observations = Spree::Report::DateSlicer.slice_into(start_date, end_date, time_scale, self.class::Observation)
|
13
|
+
end
|
14
|
+
|
15
|
+
def populate_observations
|
16
|
+
observation_iter = @observations.each
|
17
|
+
current_observation = @observations.present? ? observation_iter.next : nil
|
18
|
+
@results.each do |result|
|
19
|
+
if current_observation.present?
|
20
|
+
begin
|
21
|
+
until current_observation.describes? result, time_scale
|
22
|
+
current_observation = observation_iter.next
|
23
|
+
end
|
24
|
+
|
25
|
+
current_observation.populate(result)
|
26
|
+
current_observation = observation_iter.next
|
27
|
+
rescue StopIteration
|
28
|
+
break
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def headers
|
35
|
+
[time_headers] + super
|
36
|
+
end
|
37
|
+
|
38
|
+
private def time_headers
|
39
|
+
{
|
40
|
+
name: Spree.t(time_dimension, scope: [:admin]),
|
41
|
+
value: time_dimension,
|
42
|
+
type: :string,
|
43
|
+
sortable: false
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Spree
|
2
|
+
class ReturnedProductsReport < Spree::Report
|
3
|
+
DEFAULT_SORTABLE_ATTRIBUTE = :product_name
|
4
|
+
HEADERS = { sku: :string, product_name: :string, return_count: :integer }
|
5
|
+
SEARCH_ATTRIBUTES = { start_date: :product_returned_from, end_date: :product_returned_till }
|
6
|
+
SORTABLE_ATTRIBUTES = [:product_name, :sku, :return_count]
|
7
|
+
|
8
|
+
deeplink product_name: { template: %Q{<a href="/admin/products/{%# o.product_slug %}" target="_blank">{%# o.product_name %}</a>} }
|
9
|
+
|
10
|
+
class Result < Spree::Report::Result
|
11
|
+
class Observation < Spree::Report::Observation
|
12
|
+
observation_fields [:sku, :product_name, :return_count, :product_slug]
|
13
|
+
|
14
|
+
def sku
|
15
|
+
@sku.presence || @product_name
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def report_query
|
21
|
+
Spree::ReturnAuthorization
|
22
|
+
.joins(:return_items)
|
23
|
+
.joins(:inventory_units)
|
24
|
+
.joins(:variants)
|
25
|
+
.joins(:products)
|
26
|
+
.where(spree_return_items: { created_at: reporting_period })
|
27
|
+
.group('spree_variants.id', 'spree_products.name', 'spree_products.slug', 'spree_variants.sku')
|
28
|
+
.select(
|
29
|
+
'spree_products.name as product_name',
|
30
|
+
'spree_products.slug as product_slug',
|
31
|
+
'spree_variants.sku as sku',
|
32
|
+
'COUNT(spree_variants.id) as return_count'
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Spree
|
2
|
+
class SalesPerformanceReport < Spree::Report
|
3
|
+
HEADERS = { sale_price: :integer, cost_price: :integer, promotion_discount: :integer, profit_loss: :integer, profit_loss_percent: :integer }
|
4
|
+
SEARCH_ATTRIBUTES = { start_date: :orders_created_from, end_date: :orders_created_till }
|
5
|
+
SORTABLE_ATTRIBUTES = []
|
6
|
+
|
7
|
+
class Result < Spree::Report::TimedResult
|
8
|
+
charts ProfitLossChart, ProfitLossPercentChart, SaleCostPriceChart
|
9
|
+
|
10
|
+
class Observation < Spree::Report::TimedObservation
|
11
|
+
observation_fields cost_price: 0, sale_price: 0, profit_loss: 0, profit_loss_percent: 0, promotion_discount: 0
|
12
|
+
|
13
|
+
def cost_price
|
14
|
+
@cost_price.to_f
|
15
|
+
end
|
16
|
+
|
17
|
+
def sale_price
|
18
|
+
@sale_price.to_f
|
19
|
+
end
|
20
|
+
|
21
|
+
def profit_loss
|
22
|
+
@profit_loss.to_f
|
23
|
+
end
|
24
|
+
|
25
|
+
def profit_loss_percent
|
26
|
+
return (profit_loss * 100 / cost_price).round(2) unless cost_price.zero?
|
27
|
+
0.0
|
28
|
+
end
|
29
|
+
|
30
|
+
def promotion_discount
|
31
|
+
@promotion_discount.to_f
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
private def report_query
|
38
|
+
Spree::Report::QueryFragments
|
39
|
+
.from_union(order_with_line_items_grouped_by_time, promotions_grouped_by_time)
|
40
|
+
.group(*time_scale_columns_to_s)
|
41
|
+
.order(*time_scale_columns)
|
42
|
+
.project(
|
43
|
+
*time_scale_columns,
|
44
|
+
'SUM(sale_price) as sale_price',
|
45
|
+
'SUM(cost_price) as cost_price',
|
46
|
+
'SUM(profit_loss) as profit_loss',
|
47
|
+
'SUM(promotion_discount) as promotion_discount'
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
private def promotions_grouped_by_time
|
52
|
+
Spree::Report::QueryFragments
|
53
|
+
.from_subquery(promotion_adjustments_with_time)
|
54
|
+
.group(*time_scale_columns_to_s, 'sale_price', 'cost_price')
|
55
|
+
.order(*time_scale_columns)
|
56
|
+
.project(
|
57
|
+
*time_scale_columns,
|
58
|
+
'0 as sale_price',
|
59
|
+
'0 as cost_price',
|
60
|
+
'SUM(promotion_discount) * -1 as profit_loss',
|
61
|
+
'SUM(promotion_discount) as promotion_discount'
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
private def promotion_adjustments_with_time
|
66
|
+
Spree::Adjustment
|
67
|
+
.promotion
|
68
|
+
.where(created_at: reporting_period)
|
69
|
+
.select(
|
70
|
+
'abs(amount) as promotion_discount',
|
71
|
+
*time_scale_selects('spree_adjustments')
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
private def order_with_line_items_grouped_by_time
|
76
|
+
order_with_line_items_ar = Arel::Table.new(:order_with_line_items)
|
77
|
+
zero = Arel::Nodes.build_quoted(0.0)
|
78
|
+
Spree::Report::QueryFragments
|
79
|
+
.from_subquery(order_with_line_items, as: :order_with_line_items)
|
80
|
+
.group(*time_scale_columns_to_s)
|
81
|
+
.order(*time_scale_columns)
|
82
|
+
.project(
|
83
|
+
*time_scale_columns,
|
84
|
+
Spree::Report::QueryFragments.if_null(Spree::Report::QueryFragments.sum(order_with_line_items_ar[:sale_price]), zero).as('sale_price'),
|
85
|
+
Spree::Report::QueryFragments.if_null(Spree::Report::QueryFragments.sum(order_with_line_items_ar[:cost_price]), zero).as('cost_price'),
|
86
|
+
Spree::Report::QueryFragments.if_null(Spree::Report::QueryFragments.sum(order_with_line_items_ar[:profit_loss]), zero).as('profit_loss'),
|
87
|
+
'0 as promotion_discount'
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
private def order_with_line_items
|
92
|
+
line_item_ar = Spree::LineItem.arel_table
|
93
|
+
Spree::Order
|
94
|
+
.where.not(completed_at: nil)
|
95
|
+
.where(created_at: reporting_period)
|
96
|
+
.joins(:line_items)
|
97
|
+
.group('spree_orders.id', *time_scale_columns_to_s)
|
98
|
+
.select(
|
99
|
+
*time_scale_selects('spree_orders'),
|
100
|
+
"spree_orders.item_total as sale_price",
|
101
|
+
"SUM(#{ Spree::Report::QueryFragments.if_null(line_item_ar[:cost_price], line_item_ar[:price]).to_sql } * spree_line_items.quantity) as cost_price",
|
102
|
+
"(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"
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|