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 +4 -4
- data/.gitignore +19 -0
- data/LICENSE +26 -0
- data/README.md +75 -0
- data/accountant_clerk.gemspec +21 -0
- data/app/assets/javascripts/accountant_clerk.js +0 -0
- data/app/assets/javascripts/flotomatic.js +177 -0
- data/app/assets/stylesheets/accountant_clerk.css +7 -0
- data/app/assets/stylesheets/flotomatic.css +4 -0
- data/app/controllers/accountant_controller.rb +114 -0
- data/app/helpers/reports_helper.rb +21 -0
- data/app/models/array_decorator.rb +11 -0
- data/app/views/accountant/report.html.haml +79 -0
- data/config/locales/en.yml +2 -0
- data/config/routes.rb +5 -0
- data/lib/accountant_clerk/engine.rb +27 -0
- data/lib/accountant_clerk.rb +2 -0
- data/lib/time_flot.rb +84 -0
- data/spec/spec_helper.rb +31 -0
- metadata +22 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 37383fb29bcc2e600f53b951f1dd41efd1834836
|
4
|
+
data.tar.gz: af52e05489b0d545f9fddc98640667deb8c45c38
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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,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
|
data/config/routes.rb
ADDED
@@ -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
|
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
|
data/spec/spec_helper.rb
ADDED
@@ -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.
|
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
|