double_entry 1.0.1 → 2.0.0.beta5
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 +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
|