double_entry 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|