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,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
|