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.
Files changed (79) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +497 -0
  3. data/README.md +107 -44
  4. data/double_entry.gemspec +22 -49
  5. data/lib/active_record/locking_extensions.rb +3 -3
  6. data/lib/active_record/locking_extensions/log_subscriber.rb +1 -1
  7. data/lib/double_entry.rb +29 -21
  8. data/lib/double_entry/account.rb +39 -46
  9. data/lib/double_entry/account_balance.rb +20 -3
  10. data/lib/double_entry/balance_calculator.rb +5 -5
  11. data/lib/double_entry/configurable.rb +1 -0
  12. data/lib/double_entry/configuration.rb +8 -2
  13. data/lib/double_entry/errors.rb +13 -13
  14. data/lib/double_entry/line.rb +7 -6
  15. data/lib/double_entry/locking.rb +5 -5
  16. data/lib/double_entry/transfer.rb +37 -30
  17. data/lib/double_entry/validation.rb +1 -0
  18. data/lib/double_entry/validation/account_fixer.rb +36 -0
  19. data/lib/double_entry/validation/line_check.rb +25 -43
  20. data/lib/double_entry/version.rb +1 -1
  21. data/lib/generators/double_entry/install/install_generator.rb +22 -1
  22. data/lib/generators/double_entry/install/templates/initializer.rb +20 -0
  23. data/lib/generators/double_entry/install/templates/migration.rb +45 -55
  24. metadata +35 -256
  25. data/.gitignore +0 -32
  26. data/.rspec +0 -2
  27. data/.travis.yml +0 -29
  28. data/.yardopts +0 -2
  29. data/Gemfile +0 -2
  30. data/Rakefile +0 -15
  31. data/lib/double_entry/reporting.rb +0 -181
  32. data/lib/double_entry/reporting/aggregate.rb +0 -110
  33. data/lib/double_entry/reporting/aggregate_array.rb +0 -76
  34. data/lib/double_entry/reporting/day_range.rb +0 -42
  35. data/lib/double_entry/reporting/hour_range.rb +0 -45
  36. data/lib/double_entry/reporting/line_aggregate.rb +0 -16
  37. data/lib/double_entry/reporting/line_aggregate_filter.rb +0 -79
  38. data/lib/double_entry/reporting/month_range.rb +0 -94
  39. data/lib/double_entry/reporting/time_range.rb +0 -59
  40. data/lib/double_entry/reporting/time_range_array.rb +0 -49
  41. data/lib/double_entry/reporting/week_range.rb +0 -107
  42. data/lib/double_entry/reporting/year_range.rb +0 -40
  43. data/script/jack_hammer +0 -210
  44. data/script/setup.sh +0 -8
  45. data/spec/active_record/locking_extensions_spec.rb +0 -110
  46. data/spec/double_entry/account_balance_spec.rb +0 -7
  47. data/spec/double_entry/account_spec.rb +0 -130
  48. data/spec/double_entry/balance_calculator_spec.rb +0 -88
  49. data/spec/double_entry/configuration_spec.rb +0 -50
  50. data/spec/double_entry/line_spec.rb +0 -80
  51. data/spec/double_entry/locking_spec.rb +0 -214
  52. data/spec/double_entry/performance/double_entry_performance_spec.rb +0 -32
  53. data/spec/double_entry/performance/reporting/aggregate_performance_spec.rb +0 -50
  54. data/spec/double_entry/reporting/aggregate_array_spec.rb +0 -123
  55. data/spec/double_entry/reporting/aggregate_spec.rb +0 -205
  56. data/spec/double_entry/reporting/line_aggregate_filter_spec.rb +0 -90
  57. data/spec/double_entry/reporting/line_aggregate_spec.rb +0 -39
  58. data/spec/double_entry/reporting/month_range_spec.rb +0 -139
  59. data/spec/double_entry/reporting/time_range_array_spec.rb +0 -169
  60. data/spec/double_entry/reporting/time_range_spec.rb +0 -45
  61. data/spec/double_entry/reporting/week_range_spec.rb +0 -103
  62. data/spec/double_entry/reporting_spec.rb +0 -181
  63. data/spec/double_entry/transfer_spec.rb +0 -93
  64. data/spec/double_entry/validation/line_check_spec.rb +0 -99
  65. data/spec/double_entry_spec.rb +0 -428
  66. data/spec/generators/double_entry/install/install_generator_spec.rb +0 -30
  67. data/spec/spec_helper.rb +0 -118
  68. data/spec/support/accounts.rb +0 -21
  69. data/spec/support/blueprints.rb +0 -43
  70. data/spec/support/database.example.yml +0 -21
  71. data/spec/support/database.travis.yml +0 -24
  72. data/spec/support/double_entry_spec_helper.rb +0 -27
  73. data/spec/support/gemfiles/Gemfile.rails-3.2.x +0 -8
  74. data/spec/support/gemfiles/Gemfile.rails-4.1.x +0 -6
  75. data/spec/support/gemfiles/Gemfile.rails-4.2.x +0 -5
  76. data/spec/support/gemfiles/Gemfile.rails-5.0.x +0 -5
  77. data/spec/support/performance_helper.rb +0 -26
  78. data/spec/support/reporting_configuration.rb +0 -6
  79. data/spec/support/schema.rb +0 -74
data/.gitignore DELETED
@@ -1,32 +0,0 @@
1
- *.gem
2
- *.rbc
3
- /.config
4
- /coverage/
5
- /InstalledFiles
6
- /pkg/
7
- /spec/reports/
8
- /test/tmp/
9
- /test/version_tmp/
10
- /tmp/
11
-
12
- ## Performance profiling
13
- /profiles/
14
-
15
- ## Documentation cache and generated files:
16
- /.yardoc/
17
- /_yardoc/
18
- /doc/
19
- /rdoc/
20
-
21
- ## Environment normalisation:
22
- /.bundle/
23
- /lib/bundler/man/
24
- /Gemfile.lock
25
- /.ruby-version
26
- /.ruby-gemset
27
-
28
- # DoubleEntry specific
29
- /bin/
30
- /log/
31
- /spec/reports/
32
- /spec/support/database.yml
data/.rspec DELETED
@@ -1,2 +0,0 @@
1
- --color
2
- --require spec_helper
data/.travis.yml DELETED
@@ -1,29 +0,0 @@
1
- language: ruby
2
- sudo: false
3
- before_script:
4
- - cp spec/support/database.travis.yml spec/support/database.yml
5
- - mysql -e 'create database double_entry_test;'
6
- - psql -c 'create database double_entry_test;' -U postgres
7
- script:
8
- - bundle exec rake spec
9
- - ruby script/jack_hammer -t 2000
10
- matrix:
11
- include:
12
- - rvm: 2.1
13
- gemfile: spec/support/gemfiles/Gemfile.rails-3.2.x
14
- env: DB=mysql
15
- - rvm: 2.2
16
- gemfile: spec/support/gemfiles/Gemfile.rails-4.1.x
17
- env: DB=mysql
18
- - rvm: 2.2
19
- gemfile: spec/support/gemfiles/Gemfile.rails-4.2.x
20
- env: DB=mysql
21
- - rvm: 2.3
22
- gemfile: spec/support/gemfiles/Gemfile.rails-5.0.x
23
- env: DB=mysql
24
- - rvm: 2.3
25
- gemfile: spec/support/gemfiles/Gemfile.rails-5.0.x
26
- env: DB=sqlite
27
- - rvm: 2.3
28
- gemfile: spec/support/gemfiles/Gemfile.rails-5.0.x
29
- env: DB=postgres
data/.yardopts DELETED
@@ -1,2 +0,0 @@
1
- --markup markdown
2
- - LICENSE.md
data/Gemfile DELETED
@@ -1,2 +0,0 @@
1
- source 'https://rubygems.org'
2
- gemspec
data/Rakefile DELETED
@@ -1,15 +0,0 @@
1
- require 'rspec/core/rake_task'
2
- require 'bundler/gem_tasks'
3
-
4
- RSpec::Core::RakeTask.new(:spec) do |t|
5
- t.verbose = false
6
- t.ruby_opts = '-w'
7
- end
8
-
9
- task :default do
10
- %w(mysql postgres sqlite).each do |db|
11
- puts "Running tests with `DB=#{db}`"
12
- ENV['DB'] = db
13
- Rake::Task['spec'].execute
14
- end
15
- end
@@ -1,181 +0,0 @@
1
- # encoding: utf-8
2
- require 'double_entry/reporting/aggregate'
3
- require 'double_entry/reporting/aggregate_array'
4
- require 'double_entry/reporting/time_range'
5
- require 'double_entry/reporting/day_range'
6
- require 'double_entry/reporting/hour_range'
7
- require 'double_entry/reporting/week_range'
8
- require 'double_entry/reporting/month_range'
9
- require 'double_entry/reporting/year_range'
10
- require 'double_entry/reporting/line_aggregate'
11
- require 'double_entry/reporting/line_aggregate_filter'
12
- require 'double_entry/reporting/time_range_array'
13
-
14
- module DoubleEntry
15
- # @api private
16
- module Reporting
17
- include Configurable
18
- extend self
19
-
20
- class Configuration
21
- attr_accessor :start_of_business, :first_month_of_financial_year
22
-
23
- def initialize #:nodoc:
24
- @start_of_business = Time.new(1970, 1, 1)
25
- @first_month_of_financial_year = 7
26
- end
27
- end
28
-
29
- class AggregateFunctionNotSupported < RuntimeError; end
30
-
31
- # Perform an aggregate calculation on a set of transfers for an account.
32
- #
33
- # The transfers included in the calculation can be limited by time range
34
- # and provided custom filters.
35
- #
36
- # @example Find the sum for all $10 :save transfers in all :checking accounts in the current month, made by Australian users (assume the date is January 30, 2014).
37
- # time_range = DoubleEntry::Reporting::TimeRange.make(2014, 1)
38
- #
39
- # DoubleEntry::Line.class_eval do
40
- # scope :specific_transfer_amount, ->(amount) { where(:amount => amount.fractional) }
41
- # end
42
- #
43
- # DoubleEntry::Reporting.aggregate(
44
- # :sum,
45
- # :checking,
46
- # :save,
47
- # time_range,
48
- # :filter => [
49
- # :scope => {
50
- # :name => :specific_transfer_amount,
51
- # :arguments => [Money.new(10_00)]
52
- # },
53
- # :metadata => {
54
- # :user_location => 'AU'
55
- # },
56
- # ]
57
- # )
58
- # @param [Symbol] function The function to perform on the set of transfers.
59
- # Valid functions are :sum, :count, and :average
60
- # @param [Symbol] account The symbol identifying the account to perform
61
- # the aggregate calculation on. As specified in the account configuration.
62
- # @param [Symbol] code The application specific code for the type of
63
- # transfer to perform an aggregate calculation on. As specified in the
64
- # transfer configuration.
65
- # @param [DoubleEntry::Reporting::TimeRange] Only include transfers in the
66
- # given time range in the calculation.
67
- # @option options :filter [Array<Hash<Symbol,Hash<Symbol,Object>>>]
68
- # An array of custom filter to apply before performing the aggregate
69
- # calculation. Filters can be either scope filters, where the name must be
70
- # specified, or they can be metadata filters, where the key/value pair to
71
- # match on must be specified.
72
- # Scope filters must be monkey patched as scopes into the DoubleEntry::Line
73
- # class, as the example above shows. Scope filters may also take a list of
74
- # arguments to pass into the monkey patched scope, and, if provided, must
75
- # be contained within an array.
76
- # @return [Money, Fixnum] Returns a Money object for :sum and :average
77
- # calculations, or a Fixnum for :count calculations.
78
- # @raise [Reporting::AggregateFunctionNotSupported] The provided function
79
- # is not supported.
80
- #
81
- def aggregate(function, account, code, range, options = {})
82
- Aggregate.formatted_amount(function, account, code, range, options)
83
- end
84
-
85
- # Perform an aggregate calculation on a set of transfers for an account
86
- # and return the results in an array partitioned by a time range type.
87
- #
88
- # The transfers included in the calculation can be limited by a time range
89
- # and provided custom filters.
90
- #
91
- # @example Find the number of all $10 :save transfers in all :checking accounts per month for the entire year (Assume the year is 2014).
92
- # DoubleEntry::Reporting.aggregate_array(
93
- # :sum,
94
- # :checking,
95
- # :save,
96
- # :range_type => 'month',
97
- # :start => '2014-01-01',
98
- # :finish => '2014-12-31',
99
- # )
100
- # @param [Symbol] function The function to perform on the set of transfers.
101
- # Valid functions are :sum, :count, and :average
102
- # @param [Symbol] account The symbol identifying the account to perform
103
- # the aggregate calculation on. As specified in the account configuration.
104
- # @param [Symbol] code The application specific code for the type of
105
- # transfer to perform an aggregate calculation on. As specified in the
106
- # transfer configuration.
107
- # @option options :filter [Array<Symbol>, Array<Hash<Symbol, Object>>]
108
- # A custom filter to apply before performing the aggregate calculation.
109
- # Currently, filters must be monkey patched as scopes into the
110
- # DoubleEntry::Line class in order to be used as filters, as the example
111
- # shows. If the filter requires a parameter, it must be given in a Hash,
112
- # otherwise pass an array with the symbol names for the defined scopes.
113
- # @option options :range_type [String] The type of time range to return data
114
- # for. For example, specifying 'month' will return an array of the resulting
115
- # aggregate calculation for each month.
116
- # Valid range_types are 'hour', 'day', 'week', 'month', and 'year'
117
- # @option options :start [String] The start date for the time range to perform
118
- # calculations in. The default start date is the start_of_business (can
119
- # be specified in configuration).
120
- # The format of the string must be as follows: 'YYYY-mm-dd'
121
- # @option options :finish [String] The finish (or end) date for the time range
122
- # to perform calculations in. The default finish date is the current date.
123
- # The format of the string must be as follows: 'YYYY-mm-dd'
124
- # @return [Array<Money, Fixnum>] Returns an array of Money objects for :sum
125
- # and :average calculations, or an array of Fixnum for :count calculations.
126
- # The array is indexed by the range_type. For example, if range_type is
127
- # specified as 'month', each index in the array will represent a month.
128
- # @raise [Reporting::AggregateFunctionNotSupported] The provided function
129
- # is not supported.
130
- #
131
- def aggregate_array(function, account, code, options = {})
132
- AggregateArray.new(function, account, code, options)
133
- end
134
-
135
- # Identify the scopes with the given account identifier holding at least
136
- # the provided minimum balance.
137
- #
138
- # @example Find users with at least $1,000,000 in their savings accounts
139
- # DoubleEntry::Reporting.scopes_with_minimum_balance_for_account(
140
- # 1_000_000.dollars,
141
- # :savings,
142
- # ) # might return the user ids: [ 1423, 12232, 34729 ]
143
- # @param [Money] minimum_balance Minimum account balance a scope must have
144
- # to be included in the result set.
145
- # @param [Symbol] account_identifier
146
- # @return [Array<Fixnum>] Scopes
147
- #
148
- def scopes_with_minimum_balance_for_account(minimum_balance, account_identifier)
149
- select_values(sanitize_sql_array([<<-SQL, account_identifier, minimum_balance.cents])).map(&:to_i)
150
- SELECT scope
151
- FROM #{AccountBalance.table_name}
152
- WHERE account = ?
153
- AND balance >= ?
154
- SQL
155
- end
156
-
157
- # This is used by the concurrency test script.
158
- #
159
- # @api private
160
- # @return [Boolean] true if all the amounts for an account add up to the final balance,
161
- # which they always should.
162
- #
163
- def reconciled?(account)
164
- scoped_lines = Line.where(:account => "#{account.identifier}")
165
- scoped_lines = scoped_lines.where(:scope => "#{account.scope_identity}") if account.scoped?
166
- sum_of_amounts = scoped_lines.sum(:amount)
167
- final_balance = scoped_lines.order(:id).last[:balance]
168
- cached_balance = AccountBalance.find_by_account(account)[:balance]
169
- final_balance == sum_of_amounts && final_balance == cached_balance
170
- end
171
-
172
- private
173
-
174
- delegate :connection, :to => ActiveRecord::Base
175
- delegate :select_values, :to => :connection
176
-
177
- def sanitize_sql_array(sql_array)
178
- ActiveRecord::Base.send(:sanitize_sql_array, sql_array)
179
- end
180
- end
181
- end
@@ -1,110 +0,0 @@
1
- # encoding: utf-8
2
- module DoubleEntry
3
- module Reporting
4
- class Aggregate
5
- attr_reader :function, :account, :code, :range, :filter, :currency
6
-
7
- def self.formatted_amount(function, account, code, range, options = {})
8
- new(function, account, code, range, options).formatted_amount
9
- end
10
-
11
- def initialize(function, account, code, range, options = {})
12
- @function = function.to_s
13
- fail AggregateFunctionNotSupported unless %w(sum count average).include?(@function)
14
-
15
- @account = account
16
- @code = code ? code.to_s : nil
17
- @range = range
18
- @filter = options[:filter]
19
- @currency = DoubleEntry::Account.currency(account)
20
- end
21
-
22
- def amount(force_recalculation = false)
23
- if force_recalculation
24
- clear_old_aggregates
25
- calculate
26
- else
27
- retrieve || calculate
28
- end
29
- end
30
-
31
- def formatted_amount(value = amount)
32
- value ||= 0
33
- if function == 'count'
34
- value
35
- else
36
- Money.new(value, currency)
37
- end
38
- end
39
-
40
- private
41
-
42
- def retrieve
43
- aggregate = LineAggregate.where(field_hash).first
44
- aggregate.amount if aggregate
45
- end
46
-
47
- def clear_old_aggregates
48
- LineAggregate.delete_all(field_hash)
49
- end
50
-
51
- def calculate
52
- if range.class == YearRange
53
- aggregate = calculate_yearly_aggregate
54
- else
55
- aggregate = LineAggregate.aggregate(function, account, code, range, filter)
56
- end
57
-
58
- if range_is_complete?
59
- fields = field_hash
60
- fields[:amount] = aggregate || 0
61
- LineAggregate.create! fields
62
- end
63
-
64
- aggregate
65
- end
66
-
67
- def calculate_yearly_aggregate
68
- # We calculate yearly aggregates by combining monthly aggregates
69
- # otherwise they will get excruciatingly slow to calculate
70
- # as the year progresses. (I am thinking mainly of the 'current' year.)
71
- # Combining monthly aggregates will mean that the figure will be partially memoized
72
- if function == 'average'
73
- calculate_yearly_average
74
- else
75
- result = (1..12).inject(formatted_amount(0)) do |total, month|
76
- total + Aggregate.new(function, account, code, MonthRange.new(:year => range.year, :month => month), :filter => filter).formatted_amount
77
- end
78
- result.is_a?(Money) ? result.cents : result
79
- end
80
- end
81
-
82
- def calculate_yearly_average
83
- # need this seperate function, because an average of averages is not the correct average
84
- year_range = YearRange.new(:year => range.year)
85
- sum = Aggregate.new(:sum, account, code, year_range, :filter => filter).formatted_amount
86
- count = Aggregate.new(:count, account, code, year_range, :filter => filter).formatted_amount
87
- (count == 0) ? 0 : (sum / count).cents
88
- end
89
-
90
- def range_is_complete?
91
- Time.now > range.finish
92
- end
93
-
94
- def field_hash
95
- {
96
- :function => function,
97
- :account => account,
98
- :code => code,
99
- :year => range.year,
100
- :month => range.month,
101
- :week => range.week,
102
- :day => range.day,
103
- :hour => range.hour,
104
- :filter => filter.inspect,
105
- :range_type => range.range_type.to_s,
106
- }
107
- end
108
- end
109
- end
110
- end
@@ -1,76 +0,0 @@
1
- # encoding: utf-8
2
- module DoubleEntry
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
-
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
-
24
- retrieve_aggregates
25
- fill_in_missing_aggregates
26
- populate_self
27
- end
28
-
29
- private
30
-
31
- def populate_self
32
- all_periods.each do |period|
33
- self << @aggregates[period.key]
34
- end
35
- end
36
-
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] = Aggregate.formatted_amount(function, account, code, period, :filter => filter)
43
- end
44
- end
45
- end
46
-
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
- scope = 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
- @aggregates = scope.each_with_object({}) do |result, hash|
58
- hash[result.key] = formatted_amount(result.amount)
59
- end
60
- end
61
-
62
- def all_periods
63
- TimeRangeArray.make(range_type, start, finish)
64
- end
65
-
66
- def formatted_amount(amount)
67
- amount ||= 0
68
- if function == 'count'
69
- amount
70
- else
71
- Money.new(amount, currency)
72
- end
73
- end
74
- end
75
- end
76
- end