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,57 +1,59 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
module DoubleEntry
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
module Reporting
|
4
|
+
class TimeRange
|
5
|
+
attr_reader :start, :finish
|
6
|
+
attr_reader :year, :month, :week, :day, :hour, :range_type
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
when
|
8
|
+
def self.make(options = {})
|
9
|
+
@options = options
|
10
|
+
case
|
11
|
+
when options[:year] && options[:week] && options[:day] && options[:hour]
|
12
12
|
HourRange.new(options)
|
13
|
-
when
|
13
|
+
when options[:year] && options[:week] && options[:day]
|
14
14
|
DayRange.new(options)
|
15
|
-
when
|
15
|
+
when options[:year] && options[:week]
|
16
16
|
WeekRange.new(options)
|
17
|
-
when
|
17
|
+
when options[:year] && options[:month]
|
18
18
|
MonthRange.new(options)
|
19
19
|
when options[:year]
|
20
20
|
YearRange.new(options)
|
21
21
|
else
|
22
|
-
|
22
|
+
fail "Invalid range information #{options}"
|
23
|
+
end
|
23
24
|
end
|
24
|
-
end
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
26
|
+
def self.range_from_time_for_period(start_time, period_name)
|
27
|
+
case period_name
|
28
|
+
when 'month'
|
29
|
+
YearRange.from_time(start_time)
|
30
|
+
when 'week'
|
31
|
+
YearRange.from_time(start_time)
|
32
|
+
when 'day'
|
33
|
+
MonthRange.from_time(start_time)
|
34
|
+
when 'hour'
|
35
|
+
DayRange.from_time(start_time)
|
36
|
+
end
|
37
|
+
end
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
def include?(time)
|
40
|
+
time >= @start &&
|
41
|
+
time <= @finish
|
42
|
+
end
|
42
43
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
44
|
+
def initialize(options)
|
45
|
+
@year = options[:year]
|
46
|
+
@range_type = options[:range_type] || :normal
|
47
|
+
@month = @week = @day = @hour = nil
|
48
|
+
end
|
47
49
|
|
48
|
-
|
49
|
-
|
50
|
-
|
50
|
+
def key
|
51
|
+
"#{@year}:#{@month}:#{@week}:#{@day}:#{@hour}"
|
52
|
+
end
|
51
53
|
|
52
|
-
|
53
|
-
|
54
|
+
def human_readable_name
|
55
|
+
self.class.name.gsub('DoubleEntry::Reporting::', '').gsub('Range', '')
|
56
|
+
end
|
54
57
|
end
|
55
58
|
end
|
56
|
-
end
|
57
59
|
end
|
@@ -2,7 +2,6 @@
|
|
2
2
|
module DoubleEntry
|
3
3
|
module Reporting
|
4
4
|
class TimeRangeArray
|
5
|
-
|
6
5
|
attr_reader :type, :require_start
|
7
6
|
alias_method :require_start?, :require_start
|
8
7
|
|
@@ -14,7 +13,7 @@ module DoubleEntry
|
|
14
13
|
def make(start = nil, finish = nil)
|
15
14
|
start = start_range(start)
|
16
15
|
finish = finish_range(finish)
|
17
|
-
[
|
16
|
+
[start].tap do |array|
|
18
17
|
while start != finish
|
19
18
|
start = start.next
|
20
19
|
array << start
|
@@ -23,7 +22,7 @@ module DoubleEntry
|
|
23
22
|
end
|
24
23
|
|
25
24
|
def start_range(start = nil)
|
26
|
-
|
25
|
+
fail 'Must specify start of range' if start.blank? && require_start?
|
27
26
|
start_time = start ? Time.parse(start) : Reporting.configuration.start_of_business
|
28
27
|
type.from_time(start_time)
|
29
28
|
end
|
@@ -42,10 +41,9 @@ module DoubleEntry
|
|
42
41
|
|
43
42
|
def self.make(range_type, start = nil, finish = nil)
|
44
43
|
factory = FACTORIES[range_type]
|
45
|
-
|
44
|
+
fail ArgumentError, "Invalid range type '#{range_type}'" unless factory
|
46
45
|
factory.make(start, finish)
|
47
46
|
end
|
48
|
-
|
49
47
|
end
|
50
48
|
end
|
51
49
|
end
|
@@ -1,108 +1,107 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
module DoubleEntry
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
3
|
+
module Reporting
|
4
|
+
# We use a particularly crazy week numbering system: week 1 of any given year
|
5
|
+
# is the first week with any days that fall into that year.
|
6
|
+
#
|
7
|
+
# So, for example, week 1 of 2011 starts on 27 Dec 2010.
|
8
|
+
class WeekRange < TimeRange
|
9
|
+
class << self
|
10
|
+
def from_time(time)
|
11
|
+
date = time.to_date
|
12
|
+
week = date.cweek
|
13
|
+
year = date.end_of_week.year
|
14
|
+
|
15
|
+
if date.beginning_of_week.year != year
|
16
|
+
week = 1
|
17
|
+
elsif date.beginning_of_year.cwday > Date::DAYNAMES.index('Thursday')
|
18
|
+
week += 1
|
19
|
+
end
|
11
20
|
|
12
|
-
|
13
|
-
date = time.to_date
|
14
|
-
week = date.cweek
|
15
|
-
year = date.end_of_week.year
|
16
|
-
|
17
|
-
if date.beginning_of_week.year != year
|
18
|
-
week = 1
|
19
|
-
elsif date.beginning_of_year.cwday > Date::DAYNAMES.index('Thursday')
|
20
|
-
week += 1
|
21
|
+
new(:year => year, :week => week)
|
21
22
|
end
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
def current
|
27
|
-
from_time(Time.now)
|
28
|
-
end
|
24
|
+
def current
|
25
|
+
from_time(Time.now)
|
26
|
+
end
|
29
27
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
28
|
+
# Obtain a sequence of WeekRanges from the given start to the current
|
29
|
+
# week.
|
30
|
+
#
|
31
|
+
# @option options :from [Time] Time of the first in the returned sequence
|
32
|
+
# of WeekRanges.
|
33
|
+
# @return [Array<WeekRange>]
|
34
|
+
def reportable_weeks(options = {})
|
35
|
+
week = options[:from] ? from_time(options[:from]) : earliest_week
|
36
|
+
last_in_sequence = current
|
37
|
+
[week].tap do |weeks|
|
38
|
+
while week != last_in_sequence
|
39
|
+
week = week.next
|
40
|
+
weeks << week
|
41
|
+
end
|
43
42
|
end
|
44
43
|
end
|
45
|
-
end
|
46
44
|
|
47
|
-
|
45
|
+
private
|
48
46
|
|
49
|
-
|
50
|
-
|
51
|
-
|
47
|
+
def start_of_year(year)
|
48
|
+
Time.local(year, 1, 1).beginning_of_week
|
49
|
+
end
|
52
50
|
|
53
|
-
|
54
|
-
|
51
|
+
def earliest_week
|
52
|
+
from_time(Reporting.configuration.start_of_business)
|
53
|
+
end
|
55
54
|
end
|
56
|
-
end
|
57
55
|
|
58
|
-
|
56
|
+
attr_reader :year, :week
|
59
57
|
|
60
|
-
|
61
|
-
|
58
|
+
def initialize(options = {})
|
59
|
+
super options
|
62
60
|
|
63
|
-
|
64
|
-
|
61
|
+
if options.present?
|
62
|
+
@week = options[:week]
|
65
63
|
|
66
|
-
|
67
|
-
|
64
|
+
@start = week_and_year_to_time(@week, @year)
|
65
|
+
@finish = @start.end_of_week
|
68
66
|
|
69
|
-
|
67
|
+
@start = earliest_week.start if options[:range_type] == :all_time
|
68
|
+
end
|
70
69
|
end
|
71
|
-
end
|
72
70
|
|
73
|
-
|
74
|
-
|
75
|
-
|
71
|
+
def previous
|
72
|
+
from_time(@start - 1.week)
|
73
|
+
end
|
76
74
|
|
77
|
-
|
78
|
-
|
79
|
-
|
75
|
+
def next
|
76
|
+
from_time(@start + 1.week)
|
77
|
+
end
|
80
78
|
|
81
|
-
|
82
|
-
|
83
|
-
|
79
|
+
def ==(other)
|
80
|
+
week == other.week &&
|
81
|
+
year == other.year
|
82
|
+
end
|
84
83
|
|
85
|
-
|
86
|
-
|
87
|
-
|
84
|
+
def all_time
|
85
|
+
self.class.new(:year => year, :week => week, :range_type => :all_time)
|
86
|
+
end
|
88
87
|
|
89
|
-
|
90
|
-
|
91
|
-
|
88
|
+
def to_s
|
89
|
+
"#{year}, Week #{week}"
|
90
|
+
end
|
92
91
|
|
93
|
-
|
92
|
+
private
|
94
93
|
|
95
|
-
|
96
|
-
|
97
|
-
|
94
|
+
def from_time(time)
|
95
|
+
self.class.from_time(time)
|
96
|
+
end
|
98
97
|
|
99
|
-
|
100
|
-
|
101
|
-
|
98
|
+
def earliest_week
|
99
|
+
self.class.send(:earliest_week)
|
100
|
+
end
|
102
101
|
|
103
|
-
|
104
|
-
|
102
|
+
def week_and_year_to_time(week, year)
|
103
|
+
self.class.send(:start_of_year, year) + (week - 1).weeks
|
104
|
+
end
|
105
105
|
end
|
106
106
|
end
|
107
|
-
end
|
108
107
|
end
|
@@ -1,40 +1,40 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
module DoubleEntry
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
module Reporting
|
4
|
+
class YearRange < TimeRange
|
5
|
+
attr_reader :year
|
6
6
|
|
7
|
-
|
8
|
-
|
7
|
+
def initialize(options)
|
8
|
+
super options
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
year_start = Time.local(@year, 1, 1)
|
11
|
+
@start = year_start
|
12
|
+
@finish = year_start.end_of_year
|
13
|
+
end
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
def self.current
|
16
|
+
new(:year => Time.now.year)
|
17
|
+
end
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
def self.from_time(time)
|
20
|
+
new(:year => time.year)
|
21
|
+
end
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
def ==(other)
|
24
|
+
year == other.year
|
25
|
+
end
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
def previous
|
28
|
+
YearRange.new(:year => year - 1)
|
29
|
+
end
|
30
30
|
|
31
|
-
|
32
|
-
|
33
|
-
|
31
|
+
def next
|
32
|
+
YearRange.new(:year => year + 1)
|
33
|
+
end
|
34
34
|
|
35
|
-
|
36
|
-
|
35
|
+
def to_s
|
36
|
+
year.to_s
|
37
|
+
end
|
37
38
|
end
|
38
39
|
end
|
39
|
-
end
|
40
40
|
end
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
module DoubleEntry
|
3
3
|
class Transfer
|
4
|
-
|
5
4
|
class << self
|
6
5
|
attr_writer :transfers, :code_max_length
|
7
6
|
|
@@ -17,7 +16,7 @@ module DoubleEntry
|
|
17
16
|
|
18
17
|
# @api private
|
19
18
|
def transfer(amount, options = {})
|
20
|
-
|
19
|
+
fail TransferIsNegative if amount < Money.zero
|
21
20
|
from = options[:from]
|
22
21
|
to = options[:to]
|
23
22
|
code = options[:code]
|
@@ -37,14 +36,14 @@ module DoubleEntry
|
|
37
36
|
end
|
38
37
|
|
39
38
|
def find!(from, to, code)
|
40
|
-
|
41
|
-
|
42
|
-
|
39
|
+
find(from, to, code).tap do |transfer|
|
40
|
+
fail TransferNotAllowed, [from.identifier, to.identifier, code].inspect unless transfer
|
41
|
+
end
|
43
42
|
end
|
44
43
|
|
45
44
|
def <<(transfer)
|
46
45
|
if _find(transfer.from, transfer.to, transfer.code)
|
47
|
-
|
46
|
+
fail DuplicateTransfer
|
48
47
|
else
|
49
48
|
super(transfer)
|
50
49
|
end
|
@@ -54,7 +53,9 @@ module DoubleEntry
|
|
54
53
|
|
55
54
|
def _find(from, to, code)
|
56
55
|
detect do |transfer|
|
57
|
-
transfer.from == from
|
56
|
+
transfer.from == from &&
|
57
|
+
transfer.to == to &&
|
58
|
+
transfer.code == code
|
58
59
|
end
|
59
60
|
end
|
60
61
|
end
|
@@ -66,18 +67,17 @@ module DoubleEntry
|
|
66
67
|
@from = attributes[:from]
|
67
68
|
@to = attributes[:to]
|
68
69
|
if code.length > Transfer.code_max_length
|
69
|
-
|
70
|
-
|
71
|
-
)
|
70
|
+
fail TransferCodeTooLongError,
|
71
|
+
"transfer code '#{code}' is too long. Please limit it to #{Transfer.code_max_length} characters."
|
72
72
|
end
|
73
73
|
end
|
74
74
|
|
75
75
|
def process(amount, from, to, code, detail)
|
76
|
-
if from.scope_identity == to.scope_identity
|
77
|
-
|
76
|
+
if from.scope_identity == to.scope_identity && from.identifier == to.identifier
|
77
|
+
fail TransferNotAllowed, 'from and to are identical'
|
78
78
|
end
|
79
79
|
if to.currency != from.currency
|
80
|
-
|
80
|
+
fail MismatchedCurrencies, "Missmatched currency (#{to.currency} <> #{from.currency})"
|
81
81
|
end
|
82
82
|
Locking.lock_accounts(from, to) do
|
83
83
|
credit, debit = Line.new, Line.new
|
@@ -86,7 +86,7 @@ module DoubleEntry
|
|
86
86
|
debit_balance = Locking.balance_for_locked_account(to)
|
87
87
|
|
88
88
|
credit_balance.update_attribute :balance, credit_balance.balance - amount
|
89
|
-
debit_balance.update_attribute
|
89
|
+
debit_balance.update_attribute :balance, debit_balance.balance + amount
|
90
90
|
|
91
91
|
credit.amount, debit.amount = -amount, amount
|
92
92
|
credit.account, debit.account = from, to
|
@@ -102,6 +102,5 @@ module DoubleEntry
|
|
102
102
|
credit.update_attribute :partner_id, debit.id
|
103
103
|
end
|
104
104
|
end
|
105
|
-
|
106
105
|
end
|
107
106
|
end
|