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 +4 -4
- data/CHANGELOG.md +19 -0
- data/double_entry.gemspec +4 -3
- data/lib/double_entry.rb +0 -1
- data/lib/double_entry/version.rb +1 -1
- data/lib/generators/double_entry/install/templates/migration.rb +0 -19
- metadata +8 -21
- data/lib/double_entry/reporting.rb +0 -169
- data/lib/double_entry/reporting/aggregate.rb +0 -130
- data/lib/double_entry/reporting/aggregate_array.rb +0 -79
- data/lib/double_entry/reporting/day_range.rb +0 -42
- data/lib/double_entry/reporting/hour_range.rb +0 -45
- data/lib/double_entry/reporting/line_aggregate.rb +0 -17
- data/lib/double_entry/reporting/line_aggregate_filter.rb +0 -77
- data/lib/double_entry/reporting/line_metadata_filter.rb +0 -33
- data/lib/double_entry/reporting/month_range.rb +0 -94
- data/lib/double_entry/reporting/time_range.rb +0 -59
- data/lib/double_entry/reporting/time_range_array.rb +0 -49
- data/lib/double_entry/reporting/week_range.rb +0 -107
- data/lib/double_entry/reporting/year_range.rb +0 -40
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 278a1d1188105e311363ff13e598a96bd3b2264fd4f2a0ce90ba75044b319f31
|
|
4
|
+
data.tar.gz: '0915d86b5e811c80920ece162e6901e8dcdafd2f3b170d3b41382302bc53b209'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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' =>
|
|
18
|
-
'documentation_uri' =>
|
|
19
|
-
'source_code_uri' =>
|
|
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
data/lib/double_entry/version.rb
CHANGED
|
@@ -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.
|
|
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:
|
|
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/
|
|
271
|
-
documentation_uri: https://www.rubydoc.info/
|
|
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
|
-
|
|
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
|