accountant_clerk 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d645f395733dd7f4880484d7f52ae4a578d3294e
4
- data.tar.gz: 6d5d978bbc33a4cc4d63f9435536196759f3b793
3
+ metadata.gz: 37383fb29bcc2e600f53b951f1dd41efd1834836
4
+ data.tar.gz: af52e05489b0d545f9fddc98640667deb8c45c38
5
5
  SHA512:
6
- metadata.gz: 02b08efb8da6420349ba229bfc5fe295044348f4a646bb64cf3b510138acc34aeca3404fe6cfb25068555c040226a66ddd6e0bdadcb2a758644442009238d760
7
- data.tar.gz: 3d62f395476f66102fb213ffa238fc4d0a83263a0d793d86475014feef1c0d6fec94fc886e995bf326b2008d8f203b774a7da0fa28a5fe968cf820c3bcc924a2
6
+ metadata.gz: 85ebcbe816f35d2488cdf09e6d94ec972aac7b21ede5968b4cbf2ffd4a8b3193432534fbc1409baeb6642c2f9cf24244167f20c3316ff92d06ac571997286ba2
7
+ data.tar.gz: 046f5960f37b7e78f09bb9f94c082848ebce5430d25d09e46a0312b2c8daffafb3a9ee1e0b618c610ca706aa46efcbe2be9ffa84f9c07092dbe4121a28134e0a
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.rbc
2
+ *.sassc
3
+ .sass-cache
4
+ capybara-*.html
5
+ .rspec
6
+ .rvmrc
7
+ /.bundle
8
+ /vendor/bundle
9
+ /log/*
10
+ /tmp/*
11
+ /db/*.sqlite3
12
+ /public/system/*
13
+ /coverage/
14
+ /spec/tmp/*
15
+ **.orig
16
+ rerun.txt
17
+ pickle-email-*.html
18
+ .project
19
+ config/initializers/secret_token.rb
data/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ Copyright (c) 2014 [Torsten Ruger]
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+ * Neither the name Clerk nor the names of its contributors may be used to
13
+ endorse or promote products derived from this software without specific
14
+ prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
20
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
21
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
22
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
23
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
24
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
25
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,75 @@
1
+ Accountant Clerk
2
+ ================
3
+
4
+ This is a tool to give shop owners the ability to find out how their products are performing. As such it concentrates on the quantity of products sold, ie Items.
5
+
6
+ What started as a very simple reporting system (visually it still is), is now able to provide quite a host of useful functionality.
7
+
8
+ Search by (any combination of):
9
+
10
+ - Product name contains
11
+ - Category name contains
12
+ - Within a date range
13
+
14
+ You can group results, resulting in a stacked bar-graph, but you can also get numeric sums for the group. Group results by:
15
+
16
+ - nothing (just a summary)
17
+ - Category
18
+ - Product
19
+
20
+
21
+ And show a bar graph for the following time intervals:
22
+
23
+ - Day
24
+ - Week
25
+ - Month
26
+
27
+ The resulting number may be:
28
+
29
+ - Price
30
+ - Amount
31
+
32
+ Usage scenarios
33
+ ===============
34
+
35
+ The general idea is to start with an overview and drill down into interesting weak/strong spots using one of the tools, with the ultimate goal of understanding your sales better and possibly changing the offer as a result or creating promotions.
36
+
37
+ For example, start with a year view by month, and group by Category. As a result you see which of your Categorys sells best and when it is selling the most. This may help you to create promotions at the right time for example.
38
+
39
+ Say you have already found your strongest Categorys but want to break it down by whatever properties you use. We have e.g. Supplier. So enter the category name into the category field, and group by the property: Thus you find the best selling supplier in that category and you may want to add a cross-sell for it, or an up-sell for similar products by other suppliers.
40
+
41
+ Then you could add the supplier name to the property search field, and then group by Product. You then see the best selling Products of that Supplier in that Category, or if you remove the category name from the search, the best selling Products of that supplier.
42
+
43
+ In fact I often alternate between two properties. Search by one property, group by another and back and forth.
44
+
45
+ Installation
46
+ ===========
47
+
48
+ I'm not releasing gems now. It's not really beta yet, but may still be useful to some. So the well known gem line is:
49
+
50
+ gem 'report_clerk', :git => 'git://github.com/dancinglightning/clerk_simple_reports.git'
51
+
52
+ There are no external dependencies and the only javascript file is referenced from the one template. So no further actions should be needed.
53
+
54
+ Warning: Do not do silly queries as they will slow down your production environment. For intensive work I suggest to copy your database, ie with yaml_db, to your local machine first.
55
+
56
+ Issues
57
+ =======
58
+
59
+ The metasearch with subsequent ruby code approach has served well to get the project up quick. For larger datasets a more hand crafted sql approach may be needed.
60
+
61
+ As the search searches Items , items that are in non completed orders are included. Quite trivial fix, just never got around to it as we don't have that problem.
62
+
63
+ Also it is quite simple to grind your database and server to a halt by grouping by variant, and reporting a year by day.
64
+
65
+ Plans
66
+ =====
67
+
68
+ Vague Plans exist to introduce also:
69
+
70
+ - Reports about inventory
71
+ - Reports about Order numbers
72
+ - Grouping by customer
73
+
74
+
75
+ Copyright (c) 2014 [Torsten Ruger], released under the New BSD License
@@ -0,0 +1,21 @@
1
+ # encoding: UTF-8
2
+ Gem::Specification.new do |s|
3
+ s.platform = Gem::Platform::RUBY
4
+ s.name = 'accountant_clerk'
5
+ s.version = '0.3'
6
+ s.summary = 'Simple reports that are not so simple anymore'
7
+ s.required_ruby_version = '>= 1.9.3'
8
+
9
+ s.author = 'Torsten Ruger'
10
+ s.email = 'torsten@villataika.fi'
11
+
12
+ s.files = `git ls-files`.split("\n")
13
+ s.test_files = `git ls-files -- spec/*`.split("\n")
14
+
15
+ s.require_path = 'lib'
16
+ s.requirements << 'none'
17
+
18
+ s.add_runtime_dependency 'office_clerk', '~> 0.1'
19
+ s.add_runtime_dependency 'flot-rails', '~> 0.0.6'
20
+ end
21
+
File without changes
@@ -0,0 +1,177 @@
1
+ function Flotomatic(placeholder, data, options) {
2
+ this.placeholder = '#' + placeholder;
3
+ this.tooltip = '#flot_tooltip';
4
+ this.overview = '#flot_overview';
5
+ this.choices = '#flot_choices';
6
+ this.data = data;
7
+ this.options = options;
8
+ this.plot = null;
9
+ this.overviewPlot = null;
10
+ }
11
+
12
+ Flotomatic.prototype = {
13
+ createLink: function() {
14
+ var placeholder = jQuery(this.placeholder);
15
+
16
+ placeholder.bind("plotclick", function(event, pos, item) {
17
+ var series = item.series,
18
+ dataIndex = item.dataIndex;
19
+
20
+ window.open(series.data[dataIndex][3]);
21
+ });
22
+ },
23
+
24
+ createTooltip: function() {
25
+ var placeholder = jQuery(this.placeholder),
26
+ tooltip = jQuery(this.tooltip),
27
+ previousPoint = null;
28
+
29
+
30
+ function showTooltip(x, y, contents) {
31
+ jQuery('<div id="flot_tooltip" class="flotomatic_tooltip">' + contents + '</div>').css(
32
+ {
33
+ top: y + 5,
34
+ left: x + 5
35
+ }).appendTo("body").fadeIn(200);
36
+ }
37
+
38
+ function tooltipFormatter(item) {
39
+ var date = new Date(item.datapoint[0]),
40
+ label = item.series.label,
41
+ series = item.series,
42
+ dataIndex = item.dataIndex,
43
+ content = "";
44
+
45
+ if (series.data[dataIndex][2] == null){
46
+ content = label + ": " + item.datapoint[1] + " on " + (date.getMonth() + 1) + "/" + date.getDate() + "</a>";
47
+ }
48
+ else {
49
+ content = series.data[dataIndex][2];
50
+ }
51
+
52
+
53
+ return content;
54
+ }
55
+
56
+ placeholder.bind("plothover", this.tooltip, function(event, pos, item) {
57
+ var tooltip = jQuery(event.data);
58
+
59
+ if (item) {
60
+ if (previousPoint != item.datapoint) {
61
+ previousPoint = item.datapoint;
62
+
63
+ tooltip.remove();
64
+ var x = item.datapoint[0],//.toFixed(2),
65
+ y = item.datapoint[1]
66
+
67
+ showTooltip(item.pageX, item.pageY, tooltipFormatter(item));
68
+ }
69
+ }
70
+ else {
71
+ tooltip.remove();
72
+ previousPoint = null;
73
+ }
74
+ });
75
+ },
76
+
77
+ draw: function(placeholder, data, initialOptions, ranges, dynamic, zoom) {
78
+ var options = initialOptions;
79
+
80
+ if (zoom)
81
+ options = jQuery.extend(true, {}, options, {
82
+ selection: {
83
+ mode: "x"
84
+ },
85
+ xaxis: {
86
+ min: ranges.xaxis.from,
87
+ max: ranges.xaxis.to
88
+ }
89
+ });
90
+
91
+ return jQuery.plot(placeholder, data, options);
92
+ },
93
+
94
+ graph: function(overview, dynamic) {
95
+ var placeholder = jQuery(this.placeholder);
96
+
97
+ this.plot = this.draw(placeholder, this.data, this.options);
98
+ },
99
+
100
+ graphDynamic: function() {
101
+ var placeholder = jQuery(this.placeholder),
102
+ choices = jQuery(this.choices),
103
+ options = this.options,
104
+ data = this.data,
105
+ i = 0;
106
+
107
+ jQuery.each(data, function(key, val) {
108
+ if (val.color == null) {
109
+ val.color = i;
110
+ }
111
+ ++i;
112
+ });
113
+
114
+ jQuery.each(data, function(key, val) {
115
+ choices.append(choiceFormatter(key, val));
116
+ });
117
+
118
+ choices.find("input").click(graphChoices);
119
+
120
+ function graphChoices() {
121
+ var set = [];
122
+
123
+ choices.find("input:checked").each(function () {
124
+ var key = jQuery(this).attr("name");
125
+
126
+ if (key && data[key])
127
+ set.push(data[key]);
128
+ });
129
+
130
+ if (set.length > 0)
131
+ this.plot = jQuery.plot(placeholder, set, options);
132
+ }
133
+
134
+ function choiceFormatter(key, val) {
135
+ return '<input type="checkbox" name="' + key + '" checked="checked" > <span class="flot_choice_label">' + val.label + '</span></input> ';
136
+ }
137
+
138
+ graphChoices();
139
+ },
140
+
141
+ graphOverview: function() {
142
+ var overview = jQuery(this.overview),
143
+ placeholder = jQuery(this.placeholder),
144
+ plot = this.plot;
145
+
146
+ this.overviewPlot = jQuery.plot(overview, this.data, {
147
+ legend: false,
148
+ shadowSize: 0,
149
+ xaxis: {
150
+ ticks: [],
151
+ mode: "time"
152
+ },
153
+ yaxis: {
154
+ ticks: []
155
+ },
156
+ selection: {
157
+ mode: "x"
158
+ }
159
+ });
160
+
161
+ placeholder.bind("plotselected", {
162
+ that:this
163
+ }, function (event, ranges) {
164
+ var that = event.data.that,
165
+ placeholder = jQuery(that.placeholder);
166
+
167
+ that.plot = that.draw(placeholder, that.data, that.options, ranges, false, true);
168
+ that.overviewPlot.setSelection(ranges, true);
169
+ });
170
+
171
+ overview.bind("plotselected", {
172
+ that:this
173
+ }, function (event, ranges) {
174
+ event.data.that.plot.setSelection(ranges);
175
+ });
176
+ }
177
+ }
@@ -0,0 +1,7 @@
1
+ /*
2
+ */
3
+
4
+ div.flot_choice_label { font-variant: small-caps; font-weight: bold;}
5
+ div.flot_canvas {width:600px; height:300px}
6
+ div.flot_overview {width:600px; height:50px}
7
+ div.flotomatic_tooltip {position: absolute; display: none; border: 1px solid #fc9; padding: 2px; background-color: #ffc; opacity: 0.90}
@@ -0,0 +1,4 @@
1
+ div.flot_choice_label { font-variant: small-caps; font-weight: bold;}
2
+ div.flot_canvas {width:600px; height:300px}
3
+ div.flot_overview {width:600px; height:50px}
4
+ div.flotomatic_tooltip {position: absolute; display: none; border: 1px solid #fc9; padding: 2px; background-color: #ffc; opacity: 0.90}
@@ -0,0 +1,114 @@
1
+ class AccountantController < AdminController
2
+
3
+ def report
4
+ search = params[:q] || {}
5
+ search[:meta_sort] = "created_at asc"
6
+ if search[:created_at_gt].blank?
7
+ search[:created_at_gt] = Time.now - 3.months
8
+ else
9
+ search[:created_at_gt] = Time.zone.parse(search[:created_at_gt]).beginning_of_day rescue Time.zone.now.beginning_of_month
10
+ end
11
+ unless search[:created_at_lt].blank?
12
+ search[:created_at_lt] =
13
+ Time.zone.parse(search[:created_at_lt]).end_of_day rescue search[:created_at_lt]
14
+ end
15
+ @type = params[:type] || "Order"
16
+ search[:basket_kori_type_eq] = @type
17
+ @period = params[:period] || "week"
18
+ @days = 1
19
+ @days = 7 if @period == "week"
20
+ @days = 30.5 if @period == "month"
21
+ @price_or = (params[:price_or] || "total").to_sym
22
+ search[:order_completed_at_present] = true
23
+ search_on = case @group_by
24
+ when "all"
25
+ Item
26
+ when "by_category"
27
+ Item.includes(:category)
28
+ when "by_product"
29
+ Item
30
+ when "by_variant"
31
+ Item
32
+ else
33
+ Item
34
+ end
35
+ @search = search_on.includes(:product).ransack(search)
36
+ @flot_options = { :series => { :bars => { :show => true , :barWidth => @days * 24*60*60*1000 } , :stack => 0 } ,
37
+ :legend => { :container => "#legend"} ,
38
+ :xaxis => { :mode => "time" }
39
+ }
40
+ group_data
41
+ # csv ? send_data( render_to_string( :csv , :layout => false) , :type => "application/csv" , :filename => "tilaukset.csv")
42
+ end
43
+
44
+ def group_data
45
+ @group_by = (params[:group_by] || "all" )
46
+ all = @search.result(:distinct => true )
47
+ flot = {}
48
+ smallest = all.first ? all.first.created_at : Time.now - 1.week
49
+ largest = all.first ? all.last.created_at : Time.now
50
+ if( @group_by == "all" )
51
+ flot["all"] = all
52
+ else
53
+ all.each do |item|
54
+ bucket = get_bucket(item)
55
+ flot[ bucket ] = [] unless flot[bucket]
56
+ flot[ bucket ] << item
57
+ end
58
+ end
59
+ @flot_data = flot.collect do |label , data |
60
+ buck = bucket_array( data , smallest , largest )
61
+ sum = buck.inject(0.0){|total , val | total + val[1] }.round(2)
62
+ { :label => "#{label} =#{sum}" , :data => buck }
63
+ end
64
+ @flot_data.sort!{ |a,b| b[:label].split("=")[1].to_f <=> a[:label].split("=")[1].to_f }
65
+ end
66
+
67
+ def get_bucket item
68
+ return "all" if @group_by == "all"
69
+ case @group_by
70
+ when "by_category"
71
+ item.product.category.blank? ? "blank" : item.product.category.name
72
+ when "by_supplier"
73
+ item.product.supplier.blank? ? "blank" : item.product.supplier.supplier_name
74
+ when "by_product"
75
+ item.product.name
76
+ when "by_product_line"
77
+ return "Basket #{item.basket.id}" if item.product.line_item? and not item.product.product
78
+ return "Basket #{item.basket.id}" unless item.product
79
+ item.product.full_name
80
+ # item.product.line_item? ? item.product.product.name : item.product.name
81
+ else
82
+ pps = item.product.properties.detect{|p,v| p == @group_by}
83
+ pps ? pps.value : "blank"
84
+ end
85
+ end
86
+
87
+ # a new bucketet array version is returned
88
+ # a value is creted for every tick between from and two (so all arrays have same length)
89
+ # ticks int he returned array are javascsript times ie milliseconds since 1970
90
+ def bucket_array( array , from , to )
91
+ rb_tick = (@days * 24 * 60 * 60).to_i
92
+ js_tick = rb_tick * 1000
93
+ from = (from.to_i / rb_tick) * js_tick
94
+ to = (to.to_i / rb_tick)* js_tick
95
+ ret = {}
96
+ while from <= to
97
+ ret[from] = 0
98
+ from += js_tick
99
+ end
100
+ array.each do |item|
101
+ value = item.send(@price_or)
102
+ index = (item.created_at.to_i / rb_tick)*js_tick
103
+ if ret[index] == nil
104
+ puts "No index #{index} in array (for bucketing) #{ret.to_json}" if Rails.env == "development"
105
+ ret[index] = 0
106
+ end
107
+ ret[index] = ret[index] + value
108
+ end
109
+ ret.sort
110
+ end
111
+
112
+ end
113
+
114
+
@@ -0,0 +1,21 @@
1
+ module ReportsHelper
2
+
3
+
4
+ # assume the array to contains hashes with :data option to bucket
5
+ # second arg is the function to call, ie :day , :week (on a time object created fro the integer)
6
+ # the hash stays, but the data values are replaced
7
+ def bucket_data( array , by )
8
+ by = by.to_sym
9
+ array.each do |has|
10
+ has[:data] = bucket_array( has[:data] , by )
11
+ end
12
+ end
13
+
14
+ def group_options
15
+ opt = { t("all") => :all , t("category") => :by_category , t("supplier") => :by_supplier ,
16
+ t("product") => :by_product , t("product_line") => :by_product_line}
17
+ # Property.all.each { |p| opt[p.name] = p.name }
18
+ opt
19
+ end
20
+
21
+ end
@@ -0,0 +1,11 @@
1
+ BigDecimal.class_eval do
2
+ def to_json(options = {})
3
+ self.to_f.to_json(options)
4
+ end
5
+ end
6
+
7
+ Time.class_eval do
8
+ def week
9
+ self.day / 7
10
+ end
11
+ end
@@ -0,0 +1,79 @@
1
+ - content_for :head do
2
+ %script#source{:type => "text/javascript"}
3
+ var d = #{@flot_data.to_json.html_safe} ;
4
+ = javascript_include_tag 'jquery.flot'
5
+ = javascript_include_tag 'jquery.flot.resize'
6
+ = javascript_include_tag 'jquery.flot.time'
7
+ .row
8
+ .col-md-9
9
+ #placeholder{:style => "width:800px;height:400px;"}
10
+ %script#source{:type => "text/javascript"}
11
+ $(function () {
12
+ $.plot($("#placeholder"), d , #{@flot_options.to_json.html_safe} );
13
+ });
14
+ #legend
15
+ .col-md-3
16
+ = search_form_for @search , :url => admin_reports_url , :html => { :class => "well well-small" } do |f|
17
+ .form-group.row
18
+ = f.label :type
19
+ %br/
20
+ = select_tag :type, options_for_select( { t("order") => "Order" , t("purchase") => "Purchase" } , @type)
21
+ .form-group.row
22
+ .col-md-4
23
+ = f.label :product_name_cont, t("name")
24
+ .col-md-8
25
+ = f.text_field :product_name_cont, :size => 15
26
+ .col-md-4
27
+ = f.label :supplier
28
+ .col-md-8
29
+ = f.text_field :product_supplier_supplier_name_cont, :size => 15
30
+ .form-group.row
31
+ .col-md-4
32
+ = f.label :category
33
+ .col-md-8
34
+ = f.text_field :product_category_name_cont, :size => 15
35
+ .col-md-4
36
+ = f.label :property
37
+ .col-md-8
38
+ = f.text_field :product_properties_cont, :size => 15
39
+ .form-group.row
40
+ .col-md-3
41
+ = f.label :price
42
+ .col-md-3
43
+ = f.text_field :price_gt , :size => 6
44
+ .col-md-1
45
+
46
+ .col-md-3
47
+ = f.text_field :price_lt , :size => 6
48
+ .form-group.row
49
+ %label= t("date_range")
50
+ .date-range-filter
51
+ .col-md-4
52
+ = f.label :start
53
+ .col-md-8
54
+ = f.text_field :created_at_gt, :class => 'datepicker'
55
+ .col-md-4
56
+ = f.label :stop
57
+ .col-md-8
58
+ = f.text_field :created_at_lt, :class => 'datepicker'
59
+ .form-group.row
60
+ %label= t("group_by")
61
+ %br/
62
+ = select_tag :group_by, options_for_select( group_options , @group_by)
63
+ .form-group.row
64
+ .col-md-6
65
+ = t(:price)
66
+ = radio_button_tag :price_or, "total" , :total == @price_or
67
+ .col-md-6
68
+ = t(:quantity)
69
+ = radio_button_tag :price_or, "quantity" , :quantity == @price_or
70
+ .form-group.row
71
+ .col-md-4
72
+ = button_tag( :name => 'period' , :value => :day ) do
73
+ %span Day
74
+ .col-md-4
75
+ = button_tag( :name => 'period' , :value => :week ) do
76
+ %span Week
77
+ .col-md-4
78
+ = button_tag( :name => 'period' , :value => :month ) do
79
+ %span Month
@@ -0,0 +1,2 @@
1
+ en:
2
+ some: ho
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ OfficeClerk::Application.routes.draw do
2
+ match '/accountant/report' => 'accountant#report' ,
3
+ :as => "admin_reports", :via => [:get, :post]
4
+ end
5
+
@@ -0,0 +1,27 @@
1
+ module AccountantClerk
2
+ class Engine < Rails::Engine
3
+ engine_name 'accountant_clerk'
4
+
5
+ config.autoload_paths += %W(#{config.root}/lib)
6
+
7
+ # use rspec for tests
8
+ config.generators do |g|
9
+ g.test_framework :rspec
10
+ end
11
+
12
+ def self.activate
13
+ Dir.glob(File.join(File.dirname(__FILE__), "../../app/**/*_decorator*.rb")) do |c|
14
+ Rails.application.config.cache_classes ? require(c) : load(c)
15
+ end
16
+ Dir.glob(File.join(File.dirname(__FILE__), "../../app/helpers**/*.rb")) do |c|
17
+ Rails.application.config.cache_classes ? require(c) : load(c)
18
+ end
19
+
20
+ Dir.glob(File.join(File.dirname(__FILE__), "../../app/overrides/*.rb")) do |c|
21
+ Rails.application.config.cache_classes ? require(c) : load(c)
22
+ end
23
+ end
24
+
25
+ config.to_prepare &method(:activate).to_proc
26
+ end
27
+ end
@@ -0,0 +1,2 @@
1
+ require "flot-rails"
2
+ require 'accountant_clerk/engine'
data/lib/time_flot.rb ADDED
@@ -0,0 +1,84 @@
1
+ # Author:: Michael Cowden
2
+ # Copyright:: MigraineLiving.com
3
+ # License:: Distributed under the same terms as Ruby
4
+
5
+ =begin rdoc
6
+ == TimeFlot
7
+
8
+ The TimeFlot class provides for a graph of values over time. See Flot for more details.
9
+
10
+ Usage:
11
+ TimeFlot.new('graph') do |f|
12
+ f.bars
13
+ f.grid :hoverable => true
14
+ f.selection :mode => "xy"
15
+ f.filter {|collection| collection.select {|j| j.entry_date < Date.parse("7/8/2007") }}
16
+ f.series_for("Stress", @journals, :x => :entry_date, :y => :stress_rating)
17
+ f.series_for("Hours of Sleep", @journals, :x => :entry_date, :y => :hours_of_sleep)
18
+ f.series_for("Restful Night?", @journals, :x => :entry_date, :y => lambda {|record| record.restful_night ? 5 : 0 }, :options => {:points => {:show => true}, :bars => {:show => false}})
19
+ end
20
+
21
+ =end
22
+ class TimeFlot < Flot
23
+ JS_TIME_MULTIPLIER = 1000
24
+ BAR_WIDTH = 1.day * JS_TIME_MULTIPLIER
25
+
26
+ # TODO: need a way to replot, do hover overs, etc.
27
+ # TODO: don't like the way it overrides the initialize method signature
28
+
29
+ # Create a new TimeFlot object with a default time_axis of :xaxis
30
+ # TimeFlot.new do |tf|
31
+ # tf.bars # default width is equal to 1 day
32
+ # tf.series_for("Temperature", @temps, :x => :created_on, :y => :temperature)
33
+ # end
34
+ #
35
+ def initialize(time_axis = :xaxis, &block)
36
+ @options ||= {}
37
+ time_axis(time_axis)
38
+ super(nil, {}, &block)
39
+ end
40
+
41
+ # Sets the default width to one day... different set of defaults from Flot#bar
42
+ #
43
+ def bars(opts = {:show => true, :barWidth => BAR_WIDTH, :align => "center"})
44
+ @options[:bars] = opts
45
+ end
46
+
47
+ def series(label, d, opts = {})
48
+ super label, d.map {|data| is_time_axis?(:yaxis) ? [data[0], TimeFlot.js_time_from(data[1]), data[2], data[3]] : [TimeFlot.js_time_from(data[0]), data[1], data[2], data[3]]}, opts
49
+ end
50
+
51
+ # Sets up a time series based on a collection:
52
+ # tf.series_for("Temperature", @temps, :x => :created_on, :y => :temperature, :color => '#ff0')
53
+ #
54
+ private
55
+
56
+ def time_axis(axis = :xaxis)
57
+ [:xaxis, :yaxis].each {|ax| return if is_time_axis?(ax)}
58
+ merge_options axis, {:mode => "time"}
59
+ end
60
+
61
+ def is_time_axis?(axis)
62
+ options[axis] && (options[axis][:mode] == "time")
63
+ end
64
+
65
+ def convert_to_js_time(method)
66
+ return method if method.is_a?(Proc)
67
+ lambda {|model| TimeFlot.js_time_from model.send(method) }
68
+ end
69
+
70
+ def self.js_time_from(date)
71
+ date.to_time.to_i * JS_TIME_MULTIPLIER
72
+ end
73
+
74
+ def build_time_series(collection, x, y, x_transform, y_transform)
75
+ collection.map do |model|
76
+ [transform(model, x, x_transform), transform(model, y, y_transform)]
77
+ end
78
+ end
79
+
80
+ def transform(model, method, transformation)
81
+ transformation ? transformation.call(model.send(method)) : model.send(method)
82
+ end
83
+
84
+ end
@@ -0,0 +1,31 @@
1
+ # Configure Rails Environment
2
+ ENV["RAILS_ENV"] = "test"
3
+
4
+
5
+ require File.expand_path("../../../config/environment.rb", __FILE__)
6
+
7
+
8
+ require 'rspec/rails'
9
+
10
+ # Requires supporting ruby files with custom matchers and macros, etc,
11
+ # in spec/support/ and its subdirectories.
12
+ Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
13
+
14
+ RSpec.configure do |config|
15
+ # == Mock Framework
16
+ #
17
+ # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
18
+ #
19
+ # config.mock_with :mocha
20
+ # config.mock_with :flexmock
21
+ # config.mock_with :rr
22
+ config.mock_with :rspec
23
+
24
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
25
+ config.fixture_path = "#{::Rails.root}/spec/fixtures"
26
+
27
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
28
+ # examples within a transaction, remove the following line or assign false
29
+ # instead of true.
30
+ config.use_transactional_fixtures = true
31
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: accountant_clerk
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.2'
4
+ version: '0.3'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Torsten Ruger
@@ -43,7 +43,25 @@ email: torsten@villataika.fi
43
43
  executables: []
44
44
  extensions: []
45
45
  extra_rdoc_files: []
46
- files: []
46
+ files:
47
+ - ".gitignore"
48
+ - LICENSE
49
+ - README.md
50
+ - accountant_clerk.gemspec
51
+ - app/assets/javascripts/accountant_clerk.js
52
+ - app/assets/javascripts/flotomatic.js
53
+ - app/assets/stylesheets/accountant_clerk.css
54
+ - app/assets/stylesheets/flotomatic.css
55
+ - app/controllers/accountant_controller.rb
56
+ - app/helpers/reports_helper.rb
57
+ - app/models/array_decorator.rb
58
+ - app/views/accountant/report.html.haml
59
+ - config/locales/en.yml
60
+ - config/routes.rb
61
+ - lib/accountant_clerk.rb
62
+ - lib/accountant_clerk/engine.rb
63
+ - lib/time_flot.rb
64
+ - spec/spec_helper.rb
47
65
  homepage:
48
66
  licenses: []
49
67
  metadata: {}
@@ -68,4 +86,5 @@ rubygems_version: 2.2.2
68
86
  signing_key:
69
87
  specification_version: 4
70
88
  summary: Simple reports that are not so simple anymore
71
- test_files: []
89
+ test_files:
90
+ - spec/spec_helper.rb