double_entry 1.0.1 → 2.0.0.beta5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +497 -0
- data/README.md +107 -44
- data/double_entry.gemspec +22 -49
- data/lib/active_record/locking_extensions.rb +3 -3
- data/lib/active_record/locking_extensions/log_subscriber.rb +1 -1
- data/lib/double_entry.rb +29 -21
- data/lib/double_entry/account.rb +39 -46
- data/lib/double_entry/account_balance.rb +20 -3
- data/lib/double_entry/balance_calculator.rb +5 -5
- data/lib/double_entry/configurable.rb +1 -0
- data/lib/double_entry/configuration.rb +8 -2
- data/lib/double_entry/errors.rb +13 -13
- data/lib/double_entry/line.rb +7 -6
- data/lib/double_entry/locking.rb +5 -5
- data/lib/double_entry/transfer.rb +37 -30
- data/lib/double_entry/validation.rb +1 -0
- data/lib/double_entry/validation/account_fixer.rb +36 -0
- data/lib/double_entry/validation/line_check.rb +25 -43
- data/lib/double_entry/version.rb +1 -1
- data/lib/generators/double_entry/install/install_generator.rb +22 -1
- data/lib/generators/double_entry/install/templates/initializer.rb +20 -0
- data/lib/generators/double_entry/install/templates/migration.rb +45 -55
- metadata +35 -256
- data/.gitignore +0 -32
- data/.rspec +0 -2
- data/.travis.yml +0 -29
- data/.yardopts +0 -2
- data/Gemfile +0 -2
- data/Rakefile +0 -15
- data/lib/double_entry/reporting.rb +0 -181
- data/lib/double_entry/reporting/aggregate.rb +0 -110
- data/lib/double_entry/reporting/aggregate_array.rb +0 -76
- data/lib/double_entry/reporting/day_range.rb +0 -42
- data/lib/double_entry/reporting/hour_range.rb +0 -45
- data/lib/double_entry/reporting/line_aggregate.rb +0 -16
- data/lib/double_entry/reporting/line_aggregate_filter.rb +0 -79
- data/lib/double_entry/reporting/month_range.rb +0 -94
- data/lib/double_entry/reporting/time_range.rb +0 -59
- data/lib/double_entry/reporting/time_range_array.rb +0 -49
- data/lib/double_entry/reporting/week_range.rb +0 -107
- data/lib/double_entry/reporting/year_range.rb +0 -40
- data/script/jack_hammer +0 -210
- data/script/setup.sh +0 -8
- data/spec/active_record/locking_extensions_spec.rb +0 -110
- data/spec/double_entry/account_balance_spec.rb +0 -7
- data/spec/double_entry/account_spec.rb +0 -130
- data/spec/double_entry/balance_calculator_spec.rb +0 -88
- data/spec/double_entry/configuration_spec.rb +0 -50
- data/spec/double_entry/line_spec.rb +0 -80
- data/spec/double_entry/locking_spec.rb +0 -214
- data/spec/double_entry/performance/double_entry_performance_spec.rb +0 -32
- data/spec/double_entry/performance/reporting/aggregate_performance_spec.rb +0 -50
- data/spec/double_entry/reporting/aggregate_array_spec.rb +0 -123
- data/spec/double_entry/reporting/aggregate_spec.rb +0 -205
- data/spec/double_entry/reporting/line_aggregate_filter_spec.rb +0 -90
- data/spec/double_entry/reporting/line_aggregate_spec.rb +0 -39
- data/spec/double_entry/reporting/month_range_spec.rb +0 -139
- data/spec/double_entry/reporting/time_range_array_spec.rb +0 -169
- data/spec/double_entry/reporting/time_range_spec.rb +0 -45
- data/spec/double_entry/reporting/week_range_spec.rb +0 -103
- data/spec/double_entry/reporting_spec.rb +0 -181
- data/spec/double_entry/transfer_spec.rb +0 -93
- data/spec/double_entry/validation/line_check_spec.rb +0 -99
- data/spec/double_entry_spec.rb +0 -428
- data/spec/generators/double_entry/install/install_generator_spec.rb +0 -30
- data/spec/spec_helper.rb +0 -118
- data/spec/support/accounts.rb +0 -21
- data/spec/support/blueprints.rb +0 -43
- data/spec/support/database.example.yml +0 -21
- data/spec/support/database.travis.yml +0 -24
- data/spec/support/double_entry_spec_helper.rb +0 -27
- data/spec/support/gemfiles/Gemfile.rails-3.2.x +0 -8
- data/spec/support/gemfiles/Gemfile.rails-4.1.x +0 -6
- data/spec/support/gemfiles/Gemfile.rails-4.2.x +0 -5
- data/spec/support/gemfiles/Gemfile.rails-5.0.x +0 -5
- data/spec/support/performance_helper.rb +0 -26
- data/spec/support/reporting_configuration.rb +0 -6
- data/spec/support/schema.rb +0 -74
@@ -1,42 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
module DoubleEntry
|
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
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
@@ -1,45 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
module DoubleEntry
|
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
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
@@ -1,16 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
module DoubleEntry
|
3
|
-
module Reporting
|
4
|
-
class LineAggregate < ActiveRecord::Base
|
5
|
-
def self.aggregate(function, account, code, range, named_scopes)
|
6
|
-
collection_filter = LineAggregateFilter.new(account, code, range, named_scopes)
|
7
|
-
collection = collection_filter.filter
|
8
|
-
collection.send(function, :amount)
|
9
|
-
end
|
10
|
-
|
11
|
-
def key
|
12
|
-
"#{year}:#{month}:#{week}:#{day}:#{hour}"
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
@@ -1,79 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
module DoubleEntry
|
3
|
-
module Reporting
|
4
|
-
class LineAggregateFilter
|
5
|
-
def initialize(account, code, range, filter_criteria)
|
6
|
-
@account = account
|
7
|
-
@code = code
|
8
|
-
@range = range
|
9
|
-
@filter_criteria = filter_criteria || []
|
10
|
-
end
|
11
|
-
|
12
|
-
def filter
|
13
|
-
@collection ||= apply_filters
|
14
|
-
end
|
15
|
-
|
16
|
-
private
|
17
|
-
|
18
|
-
def apply_filters
|
19
|
-
collection = apply_filter_criteria.
|
20
|
-
where(:account => @account).
|
21
|
-
where(:created_at => @range.start..@range.finish)
|
22
|
-
collection = collection.where(:code => @code) if @code
|
23
|
-
|
24
|
-
collection
|
25
|
-
end
|
26
|
-
|
27
|
-
# a lot of the trickier reports will use filters defined
|
28
|
-
# in filter_criteria to bring in data from other tables.
|
29
|
-
# For example:
|
30
|
-
#
|
31
|
-
# filter_criteria = [
|
32
|
-
# # an example of calling a named scope called with arguments
|
33
|
-
# {
|
34
|
-
# :scope => {
|
35
|
-
# :name => :ten_dollar_purchases_by_category,
|
36
|
-
# :arguments => [:cat_videos, :cat_pictures]
|
37
|
-
# }
|
38
|
-
# },
|
39
|
-
# # an example of calling a named scope with no arguments
|
40
|
-
# {
|
41
|
-
# :scope => {
|
42
|
-
# :name => :ten_dollar_purchases
|
43
|
-
# }
|
44
|
-
# },
|
45
|
-
# # an example of providing a single metadatum criteria to filter on
|
46
|
-
# {
|
47
|
-
# :metadata => {
|
48
|
-
# :meme => :business_cat
|
49
|
-
# }
|
50
|
-
# }
|
51
|
-
# ]
|
52
|
-
def apply_filter_criteria
|
53
|
-
@filter_criteria.reduce(DoubleEntry::Line) do |collection, filter|
|
54
|
-
if filter[:scope].present?
|
55
|
-
filter_by_scope(collection, filter[:scope])
|
56
|
-
elsif filter[:metadata].present?
|
57
|
-
filter_by_metadata(collection, filter[:metadata])
|
58
|
-
else
|
59
|
-
collection
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def filter_by_scope(collection, scope)
|
65
|
-
collection.public_send(scope[:name], *scope[:arguments])
|
66
|
-
end
|
67
|
-
|
68
|
-
def filter_by_metadata(collection, metadata)
|
69
|
-
metadata.reduce(collection.joins(:metadata)) do |filtered_collection, (key, value)|
|
70
|
-
filtered_collection.where(metadata_table => { :key => key, :value => value })
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def metadata_table
|
75
|
-
DoubleEntry::LineMetadata.table_name.to_sym
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
@@ -1,94 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
module DoubleEntry
|
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
|
9
|
-
|
10
|
-
def current
|
11
|
-
from_time(Time.now)
|
12
|
-
end
|
13
|
-
|
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
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def earliest_month
|
32
|
-
from_time(Reporting.configuration.start_of_business)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
attr_reader :year, :month
|
37
|
-
|
38
|
-
def initialize(options = {})
|
39
|
-
super options
|
40
|
-
|
41
|
-
if options.present?
|
42
|
-
@month = options[:month]
|
43
|
-
|
44
|
-
month_start = Time.local(year, options[:month], 1)
|
45
|
-
@start = month_start
|
46
|
-
@finish = month_start.end_of_month
|
47
|
-
|
48
|
-
@start = MonthRange.earliest_month.start if options[:range_type] == :all_time
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
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
|
-
end
|
59
|
-
|
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
|
-
end
|
67
|
-
|
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
|
73
|
-
|
74
|
-
alias_method :succ, :next
|
75
|
-
|
76
|
-
def <=>(other)
|
77
|
-
start <=> other.start
|
78
|
-
end
|
79
|
-
|
80
|
-
def ==(other)
|
81
|
-
month == other.month &&
|
82
|
-
year == other.year
|
83
|
-
end
|
84
|
-
|
85
|
-
def all_time
|
86
|
-
MonthRange.new(:year => year, :month => month, :range_type => :all_time)
|
87
|
-
end
|
88
|
-
|
89
|
-
def to_s
|
90
|
-
start.strftime('%Y, %b')
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
@@ -1,59 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
module DoubleEntry
|
3
|
-
module Reporting
|
4
|
-
class TimeRange
|
5
|
-
attr_reader :start, :finish
|
6
|
-
attr_reader :year, :month, :week, :day, :hour, :range_type
|
7
|
-
|
8
|
-
def self.make(options = {})
|
9
|
-
@options = options
|
10
|
-
case
|
11
|
-
when options[:year] && options[:week] && options[:day] && options[:hour]
|
12
|
-
HourRange.new(options)
|
13
|
-
when options[:year] && options[:week] && options[:day]
|
14
|
-
DayRange.new(options)
|
15
|
-
when options[:year] && options[:week]
|
16
|
-
WeekRange.new(options)
|
17
|
-
when options[:year] && options[:month]
|
18
|
-
MonthRange.new(options)
|
19
|
-
when options[:year]
|
20
|
-
YearRange.new(options)
|
21
|
-
else
|
22
|
-
fail "Invalid range information #{options}"
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
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
|
-
|
39
|
-
def include?(time)
|
40
|
-
time >= @start &&
|
41
|
-
time <= @finish
|
42
|
-
end
|
43
|
-
|
44
|
-
def initialize(options)
|
45
|
-
@year = options[:year]
|
46
|
-
@range_type = options[:range_type] || :normal
|
47
|
-
@month = @week = @day = @hour = nil
|
48
|
-
end
|
49
|
-
|
50
|
-
def key
|
51
|
-
"#{@year}:#{@month}:#{@week}:#{@day}:#{@hour}"
|
52
|
-
end
|
53
|
-
|
54
|
-
def human_readable_name
|
55
|
-
self.class.name.gsub('DoubleEntry::Reporting::', '').gsub('Range', '')
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
@@ -1,49 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
module DoubleEntry
|
3
|
-
module Reporting
|
4
|
-
class TimeRangeArray
|
5
|
-
attr_reader :type, :require_start
|
6
|
-
alias_method :require_start?, :require_start
|
7
|
-
|
8
|
-
def initialize(options = {})
|
9
|
-
@type = options[:type]
|
10
|
-
@require_start = options[:require_start]
|
11
|
-
end
|
12
|
-
|
13
|
-
def make(start = nil, finish = nil)
|
14
|
-
start = start_range(start)
|
15
|
-
finish = finish_range(finish)
|
16
|
-
[start].tap do |array|
|
17
|
-
while start != finish
|
18
|
-
start = start.next
|
19
|
-
array << start
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def start_range(start = nil)
|
25
|
-
fail 'Must specify start of range' if start.blank? && require_start?
|
26
|
-
start_time = start ? Time.parse(start) : Reporting.configuration.start_of_business
|
27
|
-
type.from_time(start_time)
|
28
|
-
end
|
29
|
-
|
30
|
-
def finish_range(finish = nil)
|
31
|
-
finish ? type.from_time(Time.parse(finish)) : type.current
|
32
|
-
end
|
33
|
-
|
34
|
-
FACTORIES = {
|
35
|
-
'hour' => new(:type => HourRange, :require_start => true),
|
36
|
-
'day' => new(:type => DayRange, :require_start => true),
|
37
|
-
'week' => new(:type => WeekRange, :require_start => true),
|
38
|
-
'month' => new(:type => MonthRange, :require_start => false),
|
39
|
-
'year' => new(:type => YearRange, :require_start => false),
|
40
|
-
}
|
41
|
-
|
42
|
-
def self.make(range_type, start = nil, finish = nil)
|
43
|
-
factory = FACTORIES[range_type]
|
44
|
-
fail ArgumentError, "Invalid range type '#{range_type}'" unless factory
|
45
|
-
factory.make(start, finish)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
@@ -1,107 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
module DoubleEntry
|
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
|
20
|
-
|
21
|
-
new(:year => year, :week => week)
|
22
|
-
end
|
23
|
-
|
24
|
-
def current
|
25
|
-
from_time(Time.now)
|
26
|
-
end
|
27
|
-
|
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
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
private
|
46
|
-
|
47
|
-
def start_of_year(year)
|
48
|
-
Time.local(year, 1, 1).beginning_of_week
|
49
|
-
end
|
50
|
-
|
51
|
-
def earliest_week
|
52
|
-
from_time(Reporting.configuration.start_of_business)
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
attr_reader :year, :week
|
57
|
-
|
58
|
-
def initialize(options = {})
|
59
|
-
super options
|
60
|
-
|
61
|
-
if options.present?
|
62
|
-
@week = options[:week]
|
63
|
-
|
64
|
-
@start = week_and_year_to_time(@week, @year)
|
65
|
-
@finish = @start.end_of_week
|
66
|
-
|
67
|
-
@start = earliest_week.start if options[:range_type] == :all_time
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def previous
|
72
|
-
from_time(@start - 1.week)
|
73
|
-
end
|
74
|
-
|
75
|
-
def next
|
76
|
-
from_time(@start + 1.week)
|
77
|
-
end
|
78
|
-
|
79
|
-
def ==(other)
|
80
|
-
week == other.week &&
|
81
|
-
year == other.year
|
82
|
-
end
|
83
|
-
|
84
|
-
def all_time
|
85
|
-
self.class.new(:year => year, :week => week, :range_type => :all_time)
|
86
|
-
end
|
87
|
-
|
88
|
-
def to_s
|
89
|
-
"#{year}, Week #{week}"
|
90
|
-
end
|
91
|
-
|
92
|
-
private
|
93
|
-
|
94
|
-
def from_time(time)
|
95
|
-
self.class.from_time(time)
|
96
|
-
end
|
97
|
-
|
98
|
-
def earliest_week
|
99
|
-
self.class.send(:earliest_week)
|
100
|
-
end
|
101
|
-
|
102
|
-
def week_and_year_to_time(week, year)
|
103
|
-
self.class.send(:start_of_year, year) + (week - 1).weeks
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|