double_entry 2.0.0.beta1 → 2.0.0.beta2

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