double_entry-reporting 0.1.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fb30fb81a5ac9f48c82bb5619f5a1f08ea039c98fe65851ec7faf0bdf7cafabb
4
+ data.tar.gz: 39922494fa79df8186046675e8cc797d97b7d3a4fcec59c720ae67cd053f62cc
5
+ SHA512:
6
+ metadata.gz: 8974397dc11de77e803a23f70bc30a9fb3bca382fc3027bc1725d8b193aa0f8e7f56332e9286a56e5679260b4a01e7f4c7e1cc72a0182df80eac9e20e18156d5
7
+ data.tar.gz: 16a3fd534dcc5b13dabf191a39d9934735ccc1f255b9c09114722478bf7ac28ccea55c217b9930e87c55de3c8800341cc40f24dce079ff48fad17e83933517e9
@@ -0,0 +1,75 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6
+ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2019-01-27
9
+
10
+ ### Added
11
+
12
+ - `DoubleEntry::Reporting` module extracted from the `double_entry` gem. Please see
13
+ the [Double Entry changelog](https://github.com/envato/double_entry/blob/master/CHANGELOG.md)
14
+ for changes prior to this release.
15
+
16
+ - Added support for Ruby 2.3, 2.4, 2.5 and 2.6.
17
+
18
+ - Added support for Rails 5.0, 5.1 and 5.2
19
+
20
+ - Allow filtering aggregates by multiple metadata key/value pairs.
21
+
22
+ ### Changed
23
+
24
+ - These methods now use keyword arguments. This is a breaking change.
25
+ - `DoubleEntry::Reporting::aggregate`
26
+ - `DoubleEntry::Reporting::aggregate_array`
27
+ - `DoubleEntry::Reporting::Aggregate::new`
28
+ - `DoubleEntry::Reporting::Aggregate::formatted_amount`
29
+ - `DoubleEntry::Reporting::AggregateArray::new`
30
+ - `DoubleEntry::Reporting::LineAggregateFilter::new`
31
+
32
+ - Allow partner account to be specified for aggregates. This changes the DB
33
+ schema. Apply this change with the migration:
34
+
35
+ ```ruby
36
+ add_column :double_entry_line_aggregates, :partner_account, :string, after: :code
37
+ remove_index :double_entry_line_aggregates, name: :line_aggregate_idx
38
+ add_index :double_entry_line_aggregates, %i[function account code partner_account year month week day], name: :line_aggregate_idx
39
+ ```
40
+
41
+ - Replaced Machinist with Factory Bot in test suite.
42
+
43
+ - Changed the `double_entry_line_aggregates.amount` column to be of type `bigint`.
44
+ Apply this change with the migration:
45
+
46
+ ```ruby
47
+ change_column :double_entry_line_aggregates, :amount, :bigint, null: false
48
+ ```
49
+
50
+ - Changed the maximum length of the `account`, `code` and `scope` columns.
51
+ Apply this change with the migration:
52
+
53
+ ```ruby
54
+ change_column :double_entry_line_aggregates, :account, :string, null: false
55
+ change_column :double_entry_line_aggregates, :code, :string, null: true
56
+ change_column :double_entry_line_aggregates, :scope, :string, null: true
57
+ ```
58
+
59
+ ### Removed
60
+
61
+ - Removed support for Ruby 1.9, 2.0, 2.1 and 2.2.
62
+
63
+ - Removed support for Rails 3.2, 4.0, and 4.1.
64
+
65
+ - Removed `DoubleEntry::Reporting.scopes_with_minimum_balance_for_account`
66
+ method. This is now available on the `DoubleEntry::AccountBalance` class.
67
+
68
+ ### Fixed
69
+
70
+ - Fixed Ruby warnings.
71
+
72
+ - Fixed problem of Rails version number not being set in migration template for apps using Rails 5 or higher.
73
+
74
+ [Unreleased]: https://github.com/envato/double_entry/compare/v0.1.0...HEAD
75
+ [0.1.0]: https://github.com/envato/double_entry-reporting/compare/double-entry-v1.0.0...v0.1.0
@@ -0,0 +1,19 @@
1
+ Copyright © 2014 Envato Pty Ltd
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,71 @@
1
+ # DoubleEntry Reporting
2
+
3
+ [![License MIT](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/envato/double_entry-reporting/blob/master/LICENSE.md)
4
+ [![Gem Version](https://badge.fury.io/rb/double_entry-reporting.svg)](http://badge.fury.io/rb/double_entry-reporting)
5
+ [![Build Status](https://travis-ci.org/envato/double_entry-reporting.svg?branch=master)](https://travis-ci.org/envato/double_entry-reporting)
6
+
7
+ ## Installation
8
+
9
+ In your application's `Gemfile`, add:
10
+
11
+ ```ruby
12
+ gem 'double_entry-reporting'
13
+ ```
14
+
15
+ Download and install the gem with Bundler:
16
+
17
+ ```sh
18
+ bundle
19
+ ```
20
+
21
+ Generate Rails schema migrations for the required tables:
22
+
23
+ ```sh
24
+ rails generate double_entry:reporting:install
25
+ ```
26
+
27
+ Update the local database:
28
+
29
+ ```sh
30
+ rake db:migrate
31
+ ```
32
+
33
+ ## Development Environment Setup
34
+
35
+ 1. Clone this repo.
36
+
37
+ ```sh
38
+ git clone git@github.com:envato/double_entry-reporting.git && cd double_entry-reporting
39
+ ```
40
+
41
+ 2. Run the included setup script to install the gem dependencies.
42
+
43
+ ```sh
44
+ ./script/setup.sh
45
+ ```
46
+
47
+ 3. Install MySQL, PostgreSQL and SQLite. We run tests against all three databases.
48
+ 4. Create a database in MySQL.
49
+
50
+ ```sh
51
+ mysql -u root -e 'create database double_entry_reporting_test;'
52
+ ```
53
+
54
+ 5. Create a database in PostgreSQL.
55
+
56
+ ```sh
57
+ psql -c 'create database double_entry_reporting_test;' -U postgres
58
+ ```
59
+
60
+ 6. Specify how the tests should connect to the database
61
+
62
+ ```sh
63
+ cp spec/support/{database.example.yml,database.yml}
64
+ vim spec/support/database.yml
65
+ ```
66
+
67
+ 7. Run the tests
68
+
69
+ ```sh
70
+ bundle exec rake
71
+ ```
@@ -0,0 +1,154 @@
1
+ # encoding: utf-8
2
+ require 'double_entry'
3
+ require 'double_entry/reporting/aggregate'
4
+ require 'double_entry/reporting/aggregate_array'
5
+ require 'double_entry/reporting/time_range'
6
+ require 'double_entry/reporting/day_range'
7
+ require 'double_entry/reporting/hour_range'
8
+ require 'double_entry/reporting/week_range'
9
+ require 'double_entry/reporting/month_range'
10
+ require 'double_entry/reporting/year_range'
11
+ require 'double_entry/reporting/line_aggregate'
12
+ require 'double_entry/reporting/line_aggregate_filter'
13
+ require 'double_entry/reporting/line_metadata_filter'
14
+ require 'double_entry/reporting/time_range_array'
15
+
16
+ module DoubleEntry
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
+ private
146
+
147
+ delegate :connection, :to => ActiveRecord::Base
148
+ delegate :select_values, :to => :connection
149
+
150
+ def sanitize_sql_array(sql_array)
151
+ ActiveRecord::Base.send(:sanitize_sql_array, sql_array)
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,130 @@
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