double_entry 0.10.0 → 0.10.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +2 -1
- data/.rubocop.yml +55 -0
- data/.travis.yml +23 -12
- data/README.md +5 -1
- data/Rakefile +8 -3
- data/double_entry.gemspec +4 -3
- data/lib/active_record/locking_extensions.rb +28 -40
- data/lib/active_record/locking_extensions/log_subscriber.rb +4 -4
- data/lib/double_entry.rb +0 -2
- data/lib/double_entry/account.rb +13 -16
- data/lib/double_entry/account_balance.rb +0 -4
- data/lib/double_entry/balance_calculator.rb +4 -5
- data/lib/double_entry/configurable.rb +0 -2
- data/lib/double_entry/configuration.rb +2 -3
- data/lib/double_entry/errors.rb +2 -2
- data/lib/double_entry/line.rb +13 -16
- data/lib/double_entry/locking.rb +13 -18
- data/lib/double_entry/reporting.rb +2 -3
- data/lib/double_entry/reporting/aggregate.rb +90 -88
- data/lib/double_entry/reporting/aggregate_array.rb +58 -58
- data/lib/double_entry/reporting/day_range.rb +37 -35
- data/lib/double_entry/reporting/hour_range.rb +40 -37
- data/lib/double_entry/reporting/line_aggregate.rb +27 -28
- data/lib/double_entry/reporting/month_range.rb +67 -67
- data/lib/double_entry/reporting/time_range.rb +40 -38
- data/lib/double_entry/reporting/time_range_array.rb +3 -5
- data/lib/double_entry/reporting/week_range.rb +77 -78
- data/lib/double_entry/reporting/year_range.rb +27 -27
- data/lib/double_entry/transfer.rb +14 -15
- data/lib/double_entry/validation/line_check.rb +92 -86
- data/lib/double_entry/version.rb +1 -1
- data/lib/generators/double_entry/install/install_generator.rb +1 -2
- data/lib/generators/double_entry/install/templates/migration.rb +0 -2
- data/script/jack_hammer +1 -1
- data/spec/active_record/locking_extensions_spec.rb +45 -38
- data/spec/double_entry/account_balance_spec.rb +4 -5
- data/spec/double_entry/account_spec.rb +43 -44
- data/spec/double_entry/balance_calculator_spec.rb +6 -8
- data/spec/double_entry/configuration_spec.rb +14 -16
- data/spec/double_entry/line_spec.rb +25 -26
- data/spec/double_entry/locking_spec.rb +34 -39
- data/spec/double_entry/reporting/aggregate_array_spec.rb +8 -10
- data/spec/double_entry/reporting/aggregate_spec.rb +84 -44
- data/spec/double_entry/reporting/line_aggregate_spec.rb +7 -6
- data/spec/double_entry/reporting/month_range_spec.rb +109 -103
- data/spec/double_entry/reporting/time_range_array_spec.rb +145 -135
- data/spec/double_entry/reporting/time_range_spec.rb +36 -35
- data/spec/double_entry/reporting/week_range_spec.rb +82 -76
- data/spec/double_entry/reporting_spec.rb +9 -13
- data/spec/double_entry/transfer_spec.rb +13 -15
- data/spec/double_entry/validation/line_check_spec.rb +73 -79
- data/spec/double_entry_spec.rb +65 -68
- data/spec/generators/double_entry/install/install_generator_spec.rb +7 -10
- data/spec/spec_helper.rb +68 -10
- data/spec/support/accounts.rb +2 -4
- data/spec/support/double_entry_spec_helper.rb +4 -4
- data/spec/support/gemfiles/Gemfile.rails-3.2.x +1 -0
- metadata +31 -2
@@ -1,76 +1,76 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
module DoubleEntry
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
3
|
+
module Reporting
|
4
|
+
class AggregateArray < Array
|
5
|
+
# An AggregateArray is awesome
|
6
|
+
# It is useful for making reports
|
7
|
+
# It is basically an array of aggregate results,
|
8
|
+
# representing a column of data in a report.
|
9
|
+
#
|
10
|
+
# For example, you could request all sales
|
11
|
+
# broken down by month and it would return an array of values
|
12
|
+
attr_reader :function, :account, :code, :filter, :range_type, :start, :finish, :currency
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
14
|
+
def initialize(function, account, code, options)
|
15
|
+
@function = function.to_s
|
16
|
+
@account = account
|
17
|
+
@code = code
|
18
|
+
@filter = options[:filter]
|
19
|
+
@range_type = options[:range_type]
|
20
|
+
@start = options[:start]
|
21
|
+
@finish = options[:finish]
|
22
|
+
@currency = DoubleEntry::Account.currency(account)
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
24
|
+
retrieve_aggregates
|
25
|
+
fill_in_missing_aggregates
|
26
|
+
populate_self
|
27
|
+
end
|
28
28
|
|
29
29
|
private
|
30
30
|
|
31
|
-
|
32
|
-
|
33
|
-
|
31
|
+
def populate_self
|
32
|
+
all_periods.each do |period|
|
33
|
+
self << @aggregates[period.key]
|
34
|
+
end
|
34
35
|
end
|
35
|
-
end
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
37
|
+
def fill_in_missing_aggregates
|
38
|
+
# some aggregates may not have been previously calculated, so we can request them now
|
39
|
+
# (this includes aggregates for the still-running period)
|
40
|
+
all_periods.each do |period|
|
41
|
+
unless @aggregates[period.key]
|
42
|
+
@aggregates[period.key] = Reporting.aggregate(function, account, code, :filter => filter, :range => period)
|
43
|
+
end
|
43
44
|
end
|
44
45
|
end
|
45
|
-
end
|
46
46
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
47
|
+
# get any previously calculated aggregates
|
48
|
+
def retrieve_aggregates
|
49
|
+
fail ArgumentError, "Invalid range type '#{range_type}'" unless %w(year month week day hour).include? range_type
|
50
|
+
@aggregates = LineAggregate.
|
51
|
+
where(:function => function).
|
52
|
+
where(:range_type => 'normal').
|
53
|
+
where(:account => account.to_s).
|
54
|
+
where(:code => code.to_s).
|
55
|
+
where(:filter => filter.inspect).
|
56
|
+
where(LineAggregate.arel_table[range_type].not_eq(nil)).
|
57
|
+
each_with_object({}) do |hash, result|
|
58
|
+
hash[result.key] = formatted_amount(result.amount)
|
59
|
+
end
|
60
|
+
end
|
61
61
|
|
62
|
-
|
63
|
-
|
64
|
-
|
62
|
+
def all_periods
|
63
|
+
TimeRangeArray.make(range_type, start, finish)
|
64
|
+
end
|
65
65
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
66
|
+
def formatted_amount(amount)
|
67
|
+
amount ||= 0
|
68
|
+
if function == 'count'
|
69
|
+
amount
|
70
|
+
else
|
71
|
+
Money.new(amount, currency)
|
72
|
+
end
|
72
73
|
end
|
73
74
|
end
|
74
75
|
end
|
75
|
-
end
|
76
76
|
end
|
@@ -1,40 +1,42 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
module DoubleEntry
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
3
|
+
module Reporting
|
4
|
+
class DayRange < TimeRange
|
5
|
+
attr_reader :year, :week, :day
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
super options
|
9
|
+
|
10
|
+
@week = options[:week]
|
11
|
+
@day = options[:day]
|
12
|
+
week_range = WeekRange.new(options)
|
13
|
+
|
14
|
+
@start = week_range.start + (options[:day] - 1).days
|
15
|
+
@finish = @start.end_of_day
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.from_time(time)
|
19
|
+
week_range = WeekRange.from_time(time)
|
20
|
+
DayRange.new(:year => week_range.year, :week => week_range.week, :day => time.wday == 0 ? 7 : time.wday)
|
21
|
+
end
|
22
|
+
|
23
|
+
def previous
|
24
|
+
DayRange.from_time(@start - 1.day)
|
25
|
+
end
|
26
|
+
|
27
|
+
def next
|
28
|
+
DayRange.from_time(@start + 1.day)
|
29
|
+
end
|
30
|
+
|
31
|
+
def ==(other)
|
32
|
+
week == other.week &&
|
33
|
+
year == other.year &&
|
34
|
+
day == other.day
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
start.strftime('%Y, %a %b %d')
|
39
|
+
end
|
37
40
|
end
|
38
41
|
end
|
39
|
-
end
|
40
42
|
end
|
@@ -1,42 +1,45 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
module DoubleEntry
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
3
|
+
module Reporting
|
4
|
+
class HourRange < TimeRange
|
5
|
+
attr_reader :year, :week, :day, :hour
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
super options
|
9
|
+
|
10
|
+
@week = options[:week]
|
11
|
+
@day = options[:day]
|
12
|
+
@hour = options[:hour]
|
13
|
+
|
14
|
+
day_range = DayRange.new(options)
|
15
|
+
|
16
|
+
@start = day_range.start + options[:hour].hours
|
17
|
+
@finish = @start.end_of_hour
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.from_time(time)
|
21
|
+
day = DayRange.from_time(time)
|
22
|
+
HourRange.new :year => day.year, :week => day.week, :day => day.day, :hour => time.hour
|
23
|
+
end
|
24
|
+
|
25
|
+
def previous
|
26
|
+
HourRange.from_time(@start - 1.hour)
|
27
|
+
end
|
28
|
+
|
29
|
+
def next
|
30
|
+
HourRange.from_time(@start + 1.hour)
|
31
|
+
end
|
32
|
+
|
33
|
+
def ==(other)
|
34
|
+
week == other.week &&
|
35
|
+
year == other.year &&
|
36
|
+
day == other.day &&
|
37
|
+
hour == other.hour
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
"#{start.hour}:00:00 - #{start.hour}:59:59"
|
42
|
+
end
|
39
43
|
end
|
40
44
|
end
|
41
|
-
end
|
42
45
|
end
|
@@ -1,38 +1,37 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
module DoubleEntry
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
end
|
3
|
+
module Reporting
|
4
|
+
class LineAggregate < ActiveRecord::Base
|
5
|
+
def self.aggregate(function, account, code, range, named_scopes)
|
6
|
+
collection = aggregate_collection(named_scopes)
|
7
|
+
collection = collection.where(:account => account)
|
8
|
+
collection = collection.where(:created_at => range.start..range.finish)
|
9
|
+
collection = collection.where(:code => code) if code
|
10
|
+
collection.send(function, :amount)
|
11
|
+
end
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
13
|
+
# a lot of the trickier reports will use filters defined
|
14
|
+
# in named_scopes to bring in data from other tables.
|
15
|
+
def self.aggregate_collection(named_scopes)
|
16
|
+
if named_scopes
|
17
|
+
collection = DoubleEntry::Line
|
18
|
+
named_scopes.each do |named_scope|
|
19
|
+
if named_scope.is_a?(Hash)
|
20
|
+
method_name = named_scope.keys[0]
|
21
|
+
collection = collection.send(method_name, named_scope[method_name])
|
22
|
+
else
|
23
|
+
collection = collection.send(named_scope)
|
24
|
+
end
|
25
25
|
end
|
26
|
+
collection
|
27
|
+
else
|
28
|
+
DoubleEntry::Line
|
26
29
|
end
|
27
|
-
collection
|
28
|
-
else
|
29
|
-
DoubleEntry::Line
|
30
30
|
end
|
31
|
-
end
|
32
31
|
|
33
|
-
|
34
|
-
|
32
|
+
def key
|
33
|
+
"#{year}:#{month}:#{week}:#{day}:#{hour}"
|
34
|
+
end
|
35
35
|
end
|
36
36
|
end
|
37
|
-
end
|
38
37
|
end
|
@@ -1,94 +1,94 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
module DoubleEntry
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
3
|
+
module Reporting
|
4
|
+
class MonthRange < TimeRange
|
5
|
+
class << self
|
6
|
+
def from_time(time)
|
7
|
+
new(:year => time.year, :month => time.month)
|
8
|
+
end
|
10
9
|
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
def current
|
11
|
+
from_time(Time.now)
|
12
|
+
end
|
14
13
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
14
|
+
# Obtain a sequence of MonthRanges from the given start to the current
|
15
|
+
# month.
|
16
|
+
#
|
17
|
+
# @option options :from [Time] Time of the first in the returned sequence
|
18
|
+
# of MonthRanges.
|
19
|
+
# @return [Array<MonthRange>]
|
20
|
+
def reportable_months(options = {})
|
21
|
+
month = options[:from] ? from_time(options[:from]) : earliest_month
|
22
|
+
last = current
|
23
|
+
[month].tap do |months|
|
24
|
+
while month != last
|
25
|
+
month = month.next
|
26
|
+
months << month
|
27
|
+
end
|
28
28
|
end
|
29
29
|
end
|
30
|
-
end
|
31
30
|
|
32
|
-
|
33
|
-
|
31
|
+
def earliest_month
|
32
|
+
from_time(Reporting.configuration.start_of_business)
|
33
|
+
end
|
34
34
|
end
|
35
|
-
end
|
36
35
|
|
37
|
-
|
36
|
+
attr_reader :year, :month
|
38
37
|
|
39
|
-
|
40
|
-
|
38
|
+
def initialize(options = {})
|
39
|
+
super options
|
41
40
|
|
42
|
-
|
43
|
-
|
41
|
+
if options.present?
|
42
|
+
@month = options[:month]
|
44
43
|
|
45
|
-
|
46
|
-
|
47
|
-
|
44
|
+
month_start = Time.local(year, options[:month], 1)
|
45
|
+
@start = month_start
|
46
|
+
@finish = month_start.end_of_month
|
48
47
|
|
49
|
-
|
48
|
+
@start = MonthRange.earliest_month.start if options[:range_type] == :all_time
|
49
|
+
end
|
50
50
|
end
|
51
|
-
end
|
52
51
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
52
|
+
def previous
|
53
|
+
if month <= 1
|
54
|
+
MonthRange.new :year => year - 1, :month => 12
|
55
|
+
else
|
56
|
+
MonthRange.new :year => year, :month => month - 1
|
57
|
+
end
|
58
58
|
end
|
59
|
-
end
|
60
59
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
60
|
+
def next
|
61
|
+
if month >= 12
|
62
|
+
MonthRange.new :year => year + 1, :month => 1
|
63
|
+
else
|
64
|
+
MonthRange.new :year => year, :month => month + 1
|
65
|
+
end
|
66
66
|
end
|
67
|
-
end
|
68
67
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
68
|
+
def beginning_of_financial_year
|
69
|
+
first_month_of_financial_year = Reporting.configuration.first_month_of_financial_year
|
70
|
+
year = (month >= first_month_of_financial_year) ? @year : (@year - 1)
|
71
|
+
MonthRange.new(:year => year, :month => first_month_of_financial_year)
|
72
|
+
end
|
74
73
|
|
75
|
-
|
74
|
+
alias_method :succ, :next
|
76
75
|
|
77
|
-
|
78
|
-
|
79
|
-
|
76
|
+
def <=>(other)
|
77
|
+
start <=> other.start
|
78
|
+
end
|
80
79
|
|
81
|
-
|
82
|
-
|
83
|
-
|
80
|
+
def ==(other)
|
81
|
+
month == other.month &&
|
82
|
+
year == other.year
|
83
|
+
end
|
84
84
|
|
85
|
-
|
86
|
-
|
87
|
-
|
85
|
+
def all_time
|
86
|
+
MonthRange.new(:year => year, :month => month, :range_type => :all_time)
|
87
|
+
end
|
88
88
|
|
89
|
-
|
90
|
-
|
89
|
+
def to_s
|
90
|
+
start.strftime('%Y, %b')
|
91
|
+
end
|
91
92
|
end
|
92
93
|
end
|
93
|
-
end
|
94
94
|
end
|