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