double_entry 2.0.0.beta1 → 2.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bd62c2e42e510db804162a1de625cc123d04c65c6183eb1e1004061eef376e46
4
- data.tar.gz: ffde728e5768509bd391e4056971dbb63128ad46ef02a9780497facaaa31415e
3
+ metadata.gz: 278a1d1188105e311363ff13e598a96bd3b2264fd4f2a0ce90ba75044b319f31
4
+ data.tar.gz: '0915d86b5e811c80920ece162e6901e8dcdafd2f3b170d3b41382302bc53b209'
5
5
  SHA512:
6
- metadata.gz: 3c30e7b10ebeac99660c122ecedfafccbb115f36ed309e9a7c0b41bc122ea5a86d51baac92c28c0a5bcff4850b752fa7ffd356a94267812cb79fa4c7ef029bfd
7
- data.tar.gz: 145104fb048131b5f49d075f9a8e0b94d6e5fd73c5450cad0b4bdf522ecc9f6c311571e809457017383fa4b0d4576859a3a3e4aa991ff45d672189fd9434ff46
6
+ metadata.gz: 411912d2abe22aa8ac019764cc61ddb001363c01e43dbfe10919081dd0526e7bc6d4c48bbac32316fc0e2bde19a608384b4e1a9675b2c9ec6dcfb9ebc71ef30a
7
+ data.tar.gz: 957dd450536552f43910024f48421b5af6ccda4ac905773efa447ec8e1e1f7b69231356621b691cab5af564d0472fa0037a4833f26d5bcd7539e71145e92fa06
data/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.0.0.beta2] - 2019-01-27
11
+
12
+ ### Removed
13
+
14
+ - Extract `DoubleEntry::Reporting` module to a separate gem:
15
+ [`double_entry-reporting`](https://github.com/envato/double_entry-reporting).
16
+
17
+ If this module is in use in your project add the `double_entry-reporting` gem
18
+ and checkout the
19
+ [changelog](https://github.com/envato/double_entry-reporting/blob/master/CHANGELOG.md)
20
+ for more updates.
21
+
22
+ If not in use, one can delete the `double_entry_line_aggregates` table using
23
+ the following migration:
24
+
25
+ ```ruby
26
+ drop_table :double_entry_line_aggregates
27
+ ```
28
+
10
29
  ## [2.0.0.beta1] - 2018-12-31
11
30
 
12
31
  ### Added
data/double_entry.gemspec CHANGED
@@ -11,12 +11,13 @@ Gem::Specification.new do |gem|
11
11
  gem.email = ['rubygems@envato.com']
12
12
  gem.summary = 'Tools to build your double entry financial ledger'
13
13
  gem.homepage = 'https://github.com/envato/double_entry'
14
+ gem.license = 'MIT'
14
15
 
15
16
  gem.metadata = {
16
17
  'bug_tracker_uri' => 'https://github.com/envato/double_entry/issues',
17
- 'changelog_uri' => 'https://github.com/envato/double_entry/blob/master/CHANGELOG.md',
18
- 'documentation_uri' => 'https://www.rubydoc.info/github/envato/double_entry/',
19
- 'source_code_uri' => 'https://github.com/envato/double_entry',
18
+ 'changelog_uri' => "https://github.com/envato/double_entry/blob/v#{gem.version}/CHANGELOG.md",
19
+ 'documentation_uri' => "https://www.rubydoc.info/gems/double_entry/#{gem.version}",
20
+ 'source_code_uri' => "https://github.com/envato/double_entry/tree/v#{gem.version}",
20
21
  }
21
22
 
22
23
  gem.files = `git ls-files -z`.split("\x0").select do |f|
data/lib/double_entry.rb CHANGED
@@ -16,7 +16,6 @@ require 'double_entry/locking'
16
16
  require 'double_entry/transfer'
17
17
  require 'double_entry/line'
18
18
  require 'double_entry/line_metadata'
19
- require 'double_entry/reporting'
20
19
  require 'double_entry/validation'
21
20
 
22
21
  # Keep track of all the monies!
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module DoubleEntry
4
- VERSION = '2.0.0.beta1'
4
+ VERSION = '2.0.0.beta2'
5
5
  end
@@ -30,24 +30,6 @@ class CreateDoubleEntryTables < ActiveRecord::Migration<%= migration_version %>
30
30
  add_index "double_entry_lines", ["scope", "account", "created_at"], :name => "lines_scope_account_created_at_idx"
31
31
  add_index "double_entry_lines", ["scope", "account", "id"], :name => "lines_scope_account_id_idx"
32
32
 
33
- create_table "double_entry_line_aggregates", :force => true do |t|
34
- t.string "function", :limit => 15, :null => false
35
- t.string "account", :null => false
36
- t.string "code"
37
- t.string "scope"
38
- t.integer "year"
39
- t.integer "month"
40
- t.integer "week"
41
- t.integer "day"
42
- t.integer "hour"
43
- t.bigint "amount", :null => false
44
- t.string "filter"
45
- t.string "range_type", :limit => 15, :null => false
46
- t.timestamps :null => false
47
- end
48
-
49
- add_index "double_entry_line_aggregates", ["function", "account", "code", "year", "month", "week", "day"], :name => "line_aggregate_idx"
50
-
51
33
  create_table "double_entry_line_checks", :force => true do |t|
52
34
  t.references "last_line", :null => false, :index => false
53
35
  t.boolean "errors_found", :null => false
@@ -70,7 +52,6 @@ class CreateDoubleEntryTables < ActiveRecord::Migration<%= migration_version %>
70
52
  def self.down
71
53
  drop_table "double_entry_line_metadata"
72
54
  drop_table "double_entry_line_checks"
73
- drop_table "double_entry_line_aggregates"
74
55
  drop_table "double_entry_lines"
75
56
  drop_table "double_entry_account_balances"
76
57
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: double_entry
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.beta1
4
+ version: 2.0.0.beta2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Envato
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-12-31 00:00:00.000000000 Z
11
+ date: 2019-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -243,19 +243,6 @@ files:
243
243
  - lib/double_entry/line.rb
244
244
  - lib/double_entry/line_metadata.rb
245
245
  - lib/double_entry/locking.rb
246
- - lib/double_entry/reporting.rb
247
- - lib/double_entry/reporting/aggregate.rb
248
- - lib/double_entry/reporting/aggregate_array.rb
249
- - lib/double_entry/reporting/day_range.rb
250
- - lib/double_entry/reporting/hour_range.rb
251
- - lib/double_entry/reporting/line_aggregate.rb
252
- - lib/double_entry/reporting/line_aggregate_filter.rb
253
- - lib/double_entry/reporting/line_metadata_filter.rb
254
- - lib/double_entry/reporting/month_range.rb
255
- - lib/double_entry/reporting/time_range.rb
256
- - lib/double_entry/reporting/time_range_array.rb
257
- - lib/double_entry/reporting/week_range.rb
258
- - lib/double_entry/reporting/year_range.rb
259
246
  - lib/double_entry/transfer.rb
260
247
  - lib/double_entry/validation.rb
261
248
  - lib/double_entry/validation/account_fixer.rb
@@ -264,12 +251,13 @@ files:
264
251
  - lib/generators/double_entry/install/install_generator.rb
265
252
  - lib/generators/double_entry/install/templates/migration.rb
266
253
  homepage: https://github.com/envato/double_entry
267
- licenses: []
254
+ licenses:
255
+ - MIT
268
256
  metadata:
269
257
  bug_tracker_uri: https://github.com/envato/double_entry/issues
270
- changelog_uri: https://github.com/envato/double_entry/blob/master/CHANGELOG.md
271
- documentation_uri: https://www.rubydoc.info/github/envato/double_entry/
272
- source_code_uri: https://github.com/envato/double_entry
258
+ changelog_uri: https://github.com/envato/double_entry/blob/v2.0.0.beta2/CHANGELOG.md
259
+ documentation_uri: https://www.rubydoc.info/gems/double_entry/2.0.0.beta2
260
+ source_code_uri: https://github.com/envato/double_entry/tree/v2.0.0.beta2
273
261
  post_install_message:
274
262
  rdoc_options: []
275
263
  require_paths:
@@ -285,8 +273,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
285
273
  - !ruby/object:Gem::Version
286
274
  version: 1.3.1
287
275
  requirements: []
288
- rubyforge_project:
289
- rubygems_version: 2.7.6
276
+ rubygems_version: 3.0.2
290
277
  signing_key:
291
278
  specification_version: 4
292
279
  summary: Tools to build your double entry financial ledger
@@ -1,169 +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/line_metadata_filter'
13
- require 'double_entry/reporting/time_range_array'
14
-
15
- module DoubleEntry
16
- # @api private
17
- module Reporting
18
- include Configurable
19
- extend self
20
-
21
- class Configuration
22
- attr_accessor :start_of_business, :first_month_of_financial_year
23
-
24
- def initialize #:nodoc:
25
- @start_of_business = Time.new(1970, 1, 1)
26
- @first_month_of_financial_year = 7
27
- end
28
- end
29
-
30
- class AggregateFunctionNotSupported < RuntimeError; end
31
-
32
- # Perform an aggregate calculation on a set of transfers for an account.
33
- #
34
- # The transfers included in the calculation can be limited by time range
35
- # and provided custom filters.
36
- #
37
- # @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).
38
- # time_range = DoubleEntry::Reporting::TimeRange.make(2014, 1)
39
- #
40
- # DoubleEntry::Line.class_eval do
41
- # scope :specific_transfer_amount, ->(amount) { where(:amount => amount.fractional) }
42
- # end
43
- #
44
- # DoubleEntry::Reporting.aggregate(
45
- # :sum,
46
- # :checking,
47
- # :save,
48
- # time_range,
49
- # :filter => [
50
- # :scope => {
51
- # :name => :specific_transfer_amount,
52
- # :arguments => [Money.new(10_00)]
53
- # },
54
- # :metadata => {
55
- # :user_location => 'AU'
56
- # },
57
- # ]
58
- # )
59
- # @param [Symbol] function The function to perform on the set of transfers.
60
- # Valid functions are :sum, :count, and :average
61
- # @param [Symbol] account The symbol identifying the account to perform
62
- # the aggregate calculation on. As specified in the account configuration.
63
- # @param [Symbol] code The application specific code for the type of
64
- # transfer to perform an aggregate calculation on. As specified in the
65
- # transfer configuration.
66
- # @param [DoubleEntry::Reporting::TimeRange] range Only include transfers in
67
- # the given time range in the calculation.
68
- # @param [Symbol] partner_account The symbol identifying the partner account
69
- # to perform the aggregate calculatoin on. As specified in the account
70
- # configuration.
71
- # @param [Array<Hash<Symbol,Hash<Symbol,Object>>>] filter
72
- # An array of custom filter to apply before performing the aggregate
73
- # calculation. Filters can be either scope filters, where the name must be
74
- # specified, or they can be metadata filters, where the key/value pair to
75
- # match on must be specified.
76
- # Scope filters must be monkey patched as scopes into the DoubleEntry::Line
77
- # class, as the example above shows. Scope filters may also take a list of
78
- # arguments to pass into the monkey patched scope, and, if provided, must
79
- # be contained within an array.
80
- # @return [Money, Integer] Returns a Money object for :sum and :average
81
- # calculations, or a Integer for :count calculations.
82
- # @raise [Reporting::AggregateFunctionNotSupported] The provided function
83
- # is not supported.
84
- #
85
- def aggregate(function:, account:, code:, range:, partner_account: nil, filter: nil)
86
- Aggregate.formatted_amount(function: function, account: account, code: code, range: range,
87
- partner_account: partner_account, filter: filter)
88
- end
89
-
90
- # Perform an aggregate calculation on a set of transfers for an account
91
- # and return the results in an array partitioned by a time range type.
92
- #
93
- # The transfers included in the calculation can be limited by a time range
94
- # and provided custom filters.
95
- #
96
- # @example Find the number of all $10 :save transfers in all :checking accounts per month for the entire year (Assume the year is 2014).
97
- # DoubleEntry::Reporting.aggregate_array(
98
- # :sum,
99
- # :checking,
100
- # :save,
101
- # :range_type => 'month',
102
- # :start => '2014-01-01',
103
- # :finish => '2014-12-31',
104
- # )
105
- # @param [Symbol] function The function to perform on the set of transfers.
106
- # Valid functions are :sum, :count, and :average
107
- # @param [Symbol] account The symbol identifying the account to perform
108
- # the aggregate calculation on. As specified in the account configuration.
109
- # @param [Symbol] code The application specific code for the type of
110
- # transfer to perform an aggregate calculation on. As specified in the
111
- # transfer configuration.
112
- # @param [Symbol] partner_account The symbol identifying the partner account
113
- # to perform the aggregative calculation on. As specified in the account
114
- # configuration.
115
- # @param [Array<Symbol>, Array<Hash<Symbol, Object>>] filter
116
- # A custom filter to apply before performing the aggregate calculation.
117
- # Currently, filters must be monkey patched as scopes into the
118
- # DoubleEntry::Line class in order to be used as filters, as the example
119
- # shows. If the filter requires a parameter, it must be given in a Hash,
120
- # otherwise pass an array with the symbol names for the defined scopes.
121
- # @param [String] range_type The type of time range to return data
122
- # for. For example, specifying 'month' will return an array of the resulting
123
- # aggregate calculation for each month.
124
- # Valid range_types are 'hour', 'day', 'week', 'month', and 'year'
125
- # @param [String] start The start date for the time range to perform
126
- # calculations in. The default start date is the start_of_business (can
127
- # be specified in configuration).
128
- # The format of the string must be as follows: 'YYYY-mm-dd'
129
- # @param [String] finish The finish (or end) date for the time range
130
- # to perform calculations in. The default finish date is the current date.
131
- # The format of the string must be as follows: 'YYYY-mm-dd'
132
- # @return [Array<Money, Integer>] Returns an array of Money objects for :sum
133
- # and :average calculations, or an array of Integer for :count calculations.
134
- # The array is indexed by the range_type. For example, if range_type is
135
- # specified as 'month', each index in the array will represent a month.
136
- # @raise [Reporting::AggregateFunctionNotSupported] The provided function
137
- # is not supported.
138
- #
139
- def aggregate_array(function:, account:, code:, partner_account: nil, filter: nil,
140
- range_type: nil, start: nil, finish: nil)
141
- AggregateArray.new(function: function, account: account, code: code, partner_account: partner_account,
142
- filter: filter, range_type: range_type, start: start, finish: finish)
143
- end
144
-
145
- # This is used by the concurrency test script.
146
- #
147
- # @api private
148
- # @return [Boolean] true if all the amounts for an account add up to the final balance,
149
- # which they always should.
150
- #
151
- def reconciled?(account)
152
- scoped_lines = Line.where(:account => "#{account.identifier}")
153
- scoped_lines = scoped_lines.where(:scope => "#{account.scope_identity}") if account.scoped?
154
- sum_of_amounts = scoped_lines.sum(:amount)
155
- final_balance = scoped_lines.order(:id).last[:balance]
156
- cached_balance = AccountBalance.find_by_account(account)[:balance]
157
- final_balance == sum_of_amounts && final_balance == cached_balance
158
- end
159
-
160
- private
161
-
162
- delegate :connection, :to => ActiveRecord::Base
163
- delegate :select_values, :to => :connection
164
-
165
- def sanitize_sql_array(sql_array)
166
- ActiveRecord::Base.send(:sanitize_sql_array, sql_array)
167
- end
168
- end
169
- end
@@ -1,130 +0,0 @@
1
- # encoding: utf-8
2
- module DoubleEntry
3
- module Reporting
4
- class Aggregate
5
- attr_reader :function, :account, :partner_account, :code, :range, :filter, :currency
6
-
7
- def self.formatted_amount(function:, account:, code:, range:, partner_account: nil, filter: nil)
8
- new(
9
- function: function,
10
- account: account,
11
- code: code,
12
- range: range,
13
- partner_account: partner_account,
14
- filter: filter,
15
- ).formatted_amount
16
- end
17
-
18
- def initialize(function:, account:, code:, range:, partner_account: nil, filter: nil)
19
- @function = function.to_s
20
- fail AggregateFunctionNotSupported unless %w(sum count average).include?(@function)
21
-
22
- @account = account
23
- @code = code.try(:to_s)
24
- @range = range
25
- @partner_account = partner_account
26
- @filter = filter
27
- @currency = DoubleEntry::Account.currency(account)
28
- end
29
-
30
- def amount(force_recalculation = false)
31
- if force_recalculation
32
- clear_old_aggregates
33
- calculate
34
- else
35
- retrieve || calculate
36
- end
37
- end
38
-
39
- def formatted_amount(value = amount)
40
- value ||= 0
41
- if function == 'count'
42
- value
43
- else
44
- Money.new(value, currency)
45
- end
46
- end
47
-
48
- private
49
-
50
- def retrieve
51
- aggregate = LineAggregate.where(field_hash).first
52
- aggregate.amount if aggregate
53
- end
54
-
55
- def clear_old_aggregates
56
- LineAggregate.delete_all(field_hash)
57
- end
58
-
59
- def calculate
60
- if range.class == YearRange
61
- aggregate = calculate_yearly_aggregate
62
- else
63
- aggregate = LineAggregate.aggregate(
64
- function: function,
65
- account: account,
66
- partner_account: partner_account,
67
- code: code,
68
- range: range,
69
- named_scopes: filter
70
- )
71
- end
72
-
73
- if range_is_complete?
74
- fields = field_hash
75
- fields[:amount] = aggregate || 0
76
- LineAggregate.create! fields
77
- end
78
-
79
- aggregate
80
- end
81
-
82
- def calculate_yearly_aggregate
83
- # We calculate yearly aggregates by combining monthly aggregates
84
- # otherwise they will get excruciatingly slow to calculate
85
- # as the year progresses. (I am thinking mainly of the 'current' year.)
86
- # Combining monthly aggregates will mean that the figure will be partially memoized
87
- if function == 'average'
88
- calculate_yearly_average
89
- else
90
- result = (1..12).inject(formatted_amount(0)) do |total, month|
91
- total + Aggregate.new(function: function, account: account, code: code,
92
- range: MonthRange.new(:year => range.year, :month => month),
93
- partner_account: partner_account, filter: filter).formatted_amount
94
- end
95
- result.is_a?(Money) ? result.cents : result
96
- end
97
- end
98
-
99
- def calculate_yearly_average
100
- # need this seperate function, because an average of averages is not the correct average
101
- year_range = YearRange.new(:year => range.year)
102
- sum = Aggregate.new(function: :sum, account: account, code: code, range: year_range,
103
- partner_account: partner_account, filter: filter).formatted_amount
104
- count = Aggregate.new(function: :count, account: account, code: code, range: year_range,
105
- partner_account: partner_account, filter: filter).formatted_amount
106
- (count == 0) ? 0 : (sum / count).cents
107
- end
108
-
109
- def range_is_complete?
110
- Time.now > range.finish
111
- end
112
-
113
- def field_hash
114
- {
115
- :function => function,
116
- :account => account,
117
- :partner_account => partner_account,
118
- :code => code,
119
- :year => range.year,
120
- :month => range.month,
121
- :week => range.week,
122
- :day => range.day,
123
- :hour => range.hour,
124
- :filter => filter.inspect,
125
- :range_type => range.range_type.to_s,
126
- }
127
- end
128
- end
129
- end
130
- end
@@ -1,79 +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, :partner_account, :code, :filter, :range_type, :start, :finish, :currency
13
-
14
- def initialize(function:, account:, code:, partner_account: nil, filter: nil, range_type: nil, start: nil, finish: nil)
15
- @function = function.to_s
16
- @account = account
17
- @code = code
18
- @partner_account = partner_account
19
- @filter = filter
20
- @range_type = range_type
21
- @start = start
22
- @finish = finish
23
- @currency = DoubleEntry::Account.currency(account)
24
-
25
- retrieve_aggregates
26
- fill_in_missing_aggregates
27
- populate_self
28
- end
29
-
30
- private
31
-
32
- def populate_self
33
- all_periods.each do |period|
34
- self << @aggregates[period.key]
35
- end
36
- end
37
-
38
- def fill_in_missing_aggregates
39
- # some aggregates may not have been previously calculated, so we can request them now
40
- # (this includes aggregates for the still-running period)
41
- all_periods.each do |period|
42
- unless @aggregates[period.key]
43
- @aggregates[period.key] = Aggregate.formatted_amount(function: function, account: account, code: code,
44
- range: period, partner_account: partner_account, filter: filter)
45
- end
46
- end
47
- end
48
-
49
- # get any previously calculated aggregates
50
- def retrieve_aggregates
51
- fail ArgumentError, "Invalid range type '#{range_type}'" unless %w(year month week day hour).include? range_type
52
- scope = LineAggregate.
53
- where(:function => function).
54
- where(:range_type => 'normal').
55
- where(:account => account.try(:to_s)).
56
- where(:partner_account => partner_account.try(:to_s)).
57
- where(:code => code.try(:to_s)).
58
- where(:filter => filter.inspect).
59
- where(LineAggregate.arel_table[range_type].not_eq(nil))
60
- @aggregates = scope.each_with_object({}) do |result, hash|
61
- hash[result.key] = formatted_amount(result.amount)
62
- end
63
- end
64
-
65
- def all_periods
66
- TimeRangeArray.make(range_type, start, finish)
67
- end
68
-
69
- def formatted_amount(amount)
70
- amount ||= 0
71
- if function == 'count'
72
- amount
73
- else
74
- Money.new(amount, currency)
75
- end
76
- end
77
- end
78
- end
79
- end
@@ -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,17 +0,0 @@
1
- # encoding: utf-8
2
- module DoubleEntry
3
- module Reporting
4
- class LineAggregate < ActiveRecord::Base
5
- def self.aggregate(function:, account:, partner_account:, code:, range:, named_scopes:)
6
- collection_filter = LineAggregateFilter.new(account: account, partner_account: partner_account,
7
- code: code, range: range, filter_criteria: named_scopes)
8
- collection = collection_filter.filter
9
- collection.send(function, :amount)
10
- end
11
-
12
- def key
13
- "#{year}:#{month}:#{week}:#{day}:#{hour}"
14
- end
15
- end
16
- end
17
- end
@@ -1,77 +0,0 @@
1
- # encoding: utf-8
2
- module DoubleEntry
3
- module Reporting
4
- class LineAggregateFilter
5
-
6
- def initialize(account:, partner_account:, code:, range:, filter_criteria:)
7
- @account = account
8
- @partner_account = partner_account
9
- @code = code
10
- @range = range
11
- @filter_criteria = filter_criteria || []
12
- end
13
-
14
- def filter
15
- @collection ||= apply_filters
16
- end
17
-
18
- private
19
-
20
- def apply_filters
21
- collection = apply_filter_criteria.
22
- where(:account => @account).
23
- where(:created_at => @range.start..@range.finish)
24
- collection = collection.where(:code => @code) if @code
25
- collection = collection.where(:partner_account => @partner_account) if @partner_account
26
-
27
- collection
28
- end
29
-
30
- # a lot of the trickier reports will use filters defined
31
- # in filter_criteria to bring in data from other tables.
32
- # For example:
33
- #
34
- # filter_criteria = [
35
- # # an example of calling a named scope called with arguments
36
- # {
37
- # :scope => {
38
- # :name => :ten_dollar_purchases_by_category,
39
- # :arguments => [:cat_videos, :cat_pictures]
40
- # }
41
- # },
42
- # # an example of calling a named scope with no arguments
43
- # {
44
- # :scope => {
45
- # :name => :ten_dollar_purchases
46
- # }
47
- # },
48
- # # an example of providing multiple metadatum criteria to filter on
49
- # {
50
- # :metadata => {
51
- # :meme => :business_cat,
52
- # :category => :fun_times,
53
- # }
54
- # }
55
- # ]
56
- def apply_filter_criteria
57
- @filter_criteria.reduce(DoubleEntry::Line) do |collection, filter|
58
- if filter[:scope].present?
59
- filter_by_scope(collection, filter[:scope])
60
- elsif filter[:metadata].present?
61
- filter_by_metadata(collection, filter[:metadata])
62
- else
63
- collection
64
- end
65
- end
66
- end
67
-
68
- def filter_by_scope(collection, scope)
69
- collection.public_send(scope[:name], *scope[:arguments])
70
- end
71
-
72
- def filter_by_metadata(collection, metadata)
73
- DoubleEntry::Reporting::LineMetadataFilter.filter(collection: collection, metadata: metadata)
74
- end
75
- end
76
- end
77
- end
@@ -1,33 +0,0 @@
1
- # encoding: utf-8
2
- module DoubleEntry
3
- module Reporting
4
- class LineMetadataFilter
5
-
6
- def self.filter(collection:, metadata:)
7
- table_alias_index = 0
8
-
9
- metadata.reduce(collection) do |filtered_collection, (key, value)|
10
- table_alias = "m#{table_alias_index}"
11
- table_alias_index += 1
12
-
13
- filtered_collection.
14
- joins("INNER JOIN #{line_metadata_table} as #{table_alias} ON #{table_alias}.line_id = #{lines_table}.id").
15
- where("#{table_alias}.key = ? AND #{table_alias}.value = ?", key, value)
16
- end
17
- end
18
-
19
- private
20
-
21
- def self.line_metadata_table
22
- DoubleEntry::LineMetadata.table_name
23
- end
24
- private_class_method :line_metadata_table
25
-
26
- def self.lines_table
27
- DoubleEntry::Line.table_name
28
- end
29
- private_class_method :lines_table
30
-
31
- end
32
- end
33
- 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
@@ -1,40 +0,0 @@
1
- # encoding: utf-8
2
- module DoubleEntry
3
- module Reporting
4
- class YearRange < TimeRange
5
- attr_reader :year
6
-
7
- def initialize(options)
8
- super options
9
-
10
- year_start = Time.local(@year, 1, 1)
11
- @start = year_start
12
- @finish = year_start.end_of_year
13
- end
14
-
15
- def self.current
16
- new(:year => Time.now.year)
17
- end
18
-
19
- def self.from_time(time)
20
- new(:year => time.year)
21
- end
22
-
23
- def ==(other)
24
- year == other.year
25
- end
26
-
27
- def previous
28
- YearRange.new(:year => year - 1)
29
- end
30
-
31
- def next
32
- YearRange.new(:year => year + 1)
33
- end
34
-
35
- def to_s
36
- year.to_s
37
- end
38
- end
39
- end
40
- end