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