double_entry-reporting 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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