event_sourced_accounting 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +23 -0
  3. data/README.markdown +37 -0
  4. data/Rakefile +11 -0
  5. data/app/assets/javascripts/esa/application.js +15 -0
  6. data/app/assets/stylesheets/esa/application.css +13 -0
  7. data/app/assets/stylesheets/esa/main.css.scss +86 -0
  8. data/app/models/esa/account.rb +80 -0
  9. data/app/models/esa/accounts/asset.rb +22 -0
  10. data/app/models/esa/accounts/equity.rb +22 -0
  11. data/app/models/esa/accounts/expense.rb +22 -0
  12. data/app/models/esa/accounts/liability.rb +22 -0
  13. data/app/models/esa/accounts/revenue.rb +22 -0
  14. data/app/models/esa/amount.rb +27 -0
  15. data/app/models/esa/amounts/credit.rb +12 -0
  16. data/app/models/esa/amounts/debit.rb +12 -0
  17. data/app/models/esa/associations/amounts_extension.rb +79 -0
  18. data/app/models/esa/associations/events_extension.rb +33 -0
  19. data/app/models/esa/associations/flags_extension.rb +35 -0
  20. data/app/models/esa/associations/transactions_extension.rb +7 -0
  21. data/app/models/esa/chart.rb +41 -0
  22. data/app/models/esa/context.rb +218 -0
  23. data/app/models/esa/context_provider.rb +52 -0
  24. data/app/models/esa/context_providers/account_context_provider.rb +21 -0
  25. data/app/models/esa/context_providers/accountable_context_provider.rb +22 -0
  26. data/app/models/esa/context_providers/accountable_type_context_provider.rb +21 -0
  27. data/app/models/esa/context_providers/date_context_provider.rb +33 -0
  28. data/app/models/esa/contexts/account_context.rb +26 -0
  29. data/app/models/esa/contexts/accountable_context.rb +26 -0
  30. data/app/models/esa/contexts/accountable_type_context.rb +24 -0
  31. data/app/models/esa/contexts/created_at_context.rb +26 -0
  32. data/app/models/esa/contexts/date_context.rb +71 -0
  33. data/app/models/esa/contexts/empty_context.rb +19 -0
  34. data/app/models/esa/contexts/filter_context.rb +11 -0
  35. data/app/models/esa/contexts/open_close_context.rb +15 -0
  36. data/app/models/esa/event.rb +33 -0
  37. data/app/models/esa/filters/account_filter.rb +42 -0
  38. data/app/models/esa/filters/accountable_filter.rb +58 -0
  39. data/app/models/esa/filters/accountable_type_filter.rb +26 -0
  40. data/app/models/esa/filters/chart_filter.rb +17 -0
  41. data/app/models/esa/filters/context_filter.rb +35 -0
  42. data/app/models/esa/filters/date_time_filter.rb +52 -0
  43. data/app/models/esa/flag.rb +70 -0
  44. data/app/models/esa/ruleset.rb +175 -0
  45. data/app/models/esa/traits/accountable.rb +21 -0
  46. data/app/models/esa/traits/extendable.rb +93 -0
  47. data/app/models/esa/traits/or_scope.rb +35 -0
  48. data/app/models/esa/traits/union_scope.rb +24 -0
  49. data/app/models/esa/transaction.rb +74 -0
  50. data/config/backtrace_silencers.rb +7 -0
  51. data/config/database.yml +5 -0
  52. data/config/inflections.rb +10 -0
  53. data/config/mime_types.rb +5 -0
  54. data/config/routes.rb +6 -0
  55. data/config/secret_token.rb +7 -0
  56. data/config/session_store.rb +8 -0
  57. data/lib/esa/balance_checker.rb +17 -0
  58. data/lib/esa/blocking_processor.rb +158 -0
  59. data/lib/esa/config.rb +55 -0
  60. data/lib/esa/subcontext_checker.rb +15 -0
  61. data/lib/esa/version.rb +3 -0
  62. data/lib/esa.rb +8 -0
  63. data/lib/generators/esa/USAGE +11 -0
  64. data/lib/generators/esa/esa_generator.rb +26 -0
  65. data/lib/generators/esa/templates/migration.rb +142 -0
  66. data/spec/factories/account_factory.rb +51 -0
  67. data/spec/factories/amount_factory.rb +19 -0
  68. data/spec/factories/chart_factory.rb +7 -0
  69. data/spec/factories/transaction_factory.rb +11 -0
  70. data/spec/lib/esa_spec.rb +0 -0
  71. data/spec/models/account_spec.rb +31 -0
  72. data/spec/models/amount_spec.rb +8 -0
  73. data/spec/models/asset_spec.rb +9 -0
  74. data/spec/models/chart_spec.rb +52 -0
  75. data/spec/models/credit_amount_spec.rb +9 -0
  76. data/spec/models/debit_amount_spec.rb +9 -0
  77. data/spec/models/equity_spec.rb +9 -0
  78. data/spec/models/expense_spec.rb +9 -0
  79. data/spec/models/liability_spec.rb +9 -0
  80. data/spec/models/revenue_spec.rb +9 -0
  81. data/spec/models/transaction_spec.rb +118 -0
  82. data/spec/rcov.opts +2 -0
  83. data/spec/spec.opts +4 -0
  84. data/spec/spec_helper.rb +16 -0
  85. data/spec/support/account_shared_examples.rb +57 -0
  86. data/spec/support/amount_shared_examples.rb +21 -0
  87. metadata +306 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 50060f8c9f1f6d7255e40b89573a8f8b57557b0a
4
+ data.tar.gz: baf5ccaa16ad6488d6a94997692e780932bdd35e
5
+ SHA512:
6
+ metadata.gz: 6c6c898e8105dfa1bf731b6985108e0110413d6cad12d4dd2cfdb8b0710e4ce585a2f7a88a15d2c290b6f69d0118d9e20782035fb37168596a221d8ef2120f2f
7
+ data.tar.gz: 6543bd307114e95356464413db9633a0de666576c19a14541a7eb3f84d4719052bc09b222a6431caca23406ac860e58a026e5a0b9dc921d7efda57c1d9808cd6
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2013-2014 Lenno Nagel, 2010 Michael Bulat
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
+ Except as contained in this notice, the name(s) of the above copyright holders
14
+ shall not be used in advertising or otherwise to promote the sale, use or
15
+ other dealings in this Software without prior written authorization.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,37 @@
1
+ Event-Sourced Accounting
2
+ =================
3
+
4
+ The Event-Sourced Accounting plugin provides an event-sourced double entry accounting system.
5
+ It uses the data models of a Rails application as a data source and automatically
6
+ generates accounting transactions based on defined accounting rules.
7
+
8
+ This plugin began life as a fork of the [Plutus](https://github.com/mbulat/plutus) plugin with
9
+ many added features and refactored compontents. As the aims of the ESA plug-in have completely
10
+ changed compared to the original project, it warrants a release under its own name.
11
+
12
+ The API is not yet declared frozen and may change, as some refactoring is still due.
13
+ The documentation and test coverage is expected to be completed within April-May 2014.
14
+
15
+
16
+ Installation
17
+ ============
18
+
19
+ - Add `gem "event-sourced-accounting"` to your Gemfile
20
+
21
+ - generate migration files with `rails g esa`
22
+
23
+ - run migrations `rake db:migrate`
24
+
25
+ - add `include ESA::Traits::Accountable` to relevant models
26
+
27
+ - implement the corresponding Event, Flag, Ruleset and Transaction classes for relevant models
28
+
29
+
30
+ Development
31
+ ============
32
+
33
+ Any comments and contributions are welcome. Will gladly accept patches sent via pull requests.
34
+
35
+ - run rspec tests simply with `rake`
36
+
37
+ - update documentation with `yard`
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'bundler/setup'
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ desc "run specs"
6
+ RSpec::Core::RakeTask.new do |t|
7
+ t.rspec_opts = ["-c", "-f d", "-r ./spec/spec_helper.rb"]
8
+ t.pattern = 'spec/**/*_spec.rb'
9
+ end
10
+
11
+ task :default => :spec
@@ -0,0 +1,15 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // the compiled file.
9
+ //
10
+ // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
11
+ // GO AFTER THE REQUIRES BELOW.
12
+ //
13
+ //= require jquery
14
+ //= require jquery_ujs
15
+ //= require_tree .
@@ -0,0 +1,13 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the top of the
9
+ * compiled file, but it's generally better to create a new file per style scope.
10
+ *
11
+ *= require_self
12
+ *= require_tree .
13
+ */
@@ -0,0 +1,86 @@
1
+ .esa_container {
2
+
3
+ /* CSS from http://wptheming.com/2011/01/simple-table-css3/ */
4
+
5
+ margin:0;
6
+ padding:0;
7
+ border:0;
8
+ font-family: "Helvetica Neue",Arial, Helvetica, sans-serif;
9
+
10
+ /*
11
+ Pretty Table Styling
12
+ CSS Tricks also has a nice writeup: http://css-tricks.com/feature-table-design/
13
+ */
14
+
15
+ table {
16
+ overflow:hidden;
17
+ border:1px solid #d3d3d3;
18
+ background:#fefefe;
19
+ width:70%;
20
+ margin:5% auto 0;
21
+ -moz-border-radius:5px; /* FF1+ */
22
+ -webkit-border-radius:5px; /* Saf3-4 */
23
+ border-radius:5px;
24
+ -moz-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
25
+ -webkit-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
26
+ }
27
+
28
+ th, td {padding:10px 18px 10px; }
29
+
30
+ th {padding-top:22px; text-shadow: 1px 1px 1px #fff; background:#e8eaeb;}
31
+
32
+ td {border-top:1px solid #e0e0e0; border-right:1px solid #e0e0e0;}
33
+
34
+ tr.even td {background:#f6f6f6;}
35
+
36
+ td.first, th.first {text-align:left}
37
+
38
+ td.last {border-right:none;}
39
+
40
+ /*
41
+ Background gradients are completely unnessary but a neat effect.
42
+ */
43
+
44
+ td {
45
+ background: -moz-linear-gradient(100% 25% 90deg, #fefefe, #f9f9f9);
46
+ background: -webkit-gradient(linear, 0% 0%, 0% 25%, from(#f9f9f9), to(#fefefe));
47
+ }
48
+
49
+ tr.even td {
50
+ background: -moz-linear-gradient(100% 25% 90deg, #f6f6f6, #f1f1f1);
51
+ background: -webkit-gradient(linear, 0% 0%, 0% 25%, from(#f1f1f1), to(#f6f6f6));
52
+ }
53
+
54
+ th {
55
+ background: -moz-linear-gradient(100% 20% 90deg, #e8eaeb, #ededed);
56
+ background: -webkit-gradient(linear, 0% 0%, 0% 20%, from(#ededed), to(#e8eaeb));
57
+ }
58
+
59
+ /*
60
+ I know this is annoying, but we need dditional styling so webkit will recognize rounded corners on background elements.
61
+ Nice write up of this issue: http://www.onenaught.com/posts/266/css-inner-elements-breaking-border-radius
62
+
63
+ And, since we've applied the background colors to td/th element because of IE, Gecko browsers also need it.
64
+ */
65
+
66
+ tr:first-child th.first {
67
+ -moz-border-radius-topleft:5px;
68
+ -webkit-border-top-left-radius:5px; /* Saf3-4 */
69
+ }
70
+
71
+ tr:first-child th.last {
72
+ -moz-border-radius-topright:5px;
73
+ -webkit-border-top-right-radius:5px; /* Saf3-4 */
74
+ }
75
+
76
+ tr:last-child td.first {
77
+ -moz-border-radius-bottomleft:5px;
78
+ -webkit-border-bottom-left-radius:5px; /* Saf3-4 */
79
+ }
80
+
81
+ tr:last-child td.last {
82
+ -moz-border-radius-bottomright:5px;
83
+ -webkit-border-bottom-right-radius:5px; /* Saf3-4 */
84
+ }
85
+
86
+ }
@@ -0,0 +1,80 @@
1
+ module ESA
2
+ # The Account class represents accounts in the system. Each account must be subclassed as one of the following types:
3
+ #
4
+ # TYPE | NORMAL BALANCE | DESCRIPTION
5
+ # --------------------------------------------------------------------------
6
+ # Asset | Debit | Resources owned by the Business Entity
7
+ # Liability | Credit | Debts owed to outsiders
8
+ # Equity | Credit | Owners rights to the Assets
9
+ # Revenue | Credit | Increases in owners equity
10
+ # Expense | Debit | Assets or services consumed in the generation of revenue
11
+ #
12
+ # Each account can also be marked as a "Contra Account". A contra account will have it's
13
+ # normal balance swapped. For example, to remove equity, a "Drawing" account may be created
14
+ # as a contra equity account as follows:
15
+ #
16
+ # ESA::Equity.create(:name => "Drawing", contra => true)
17
+ #
18
+ # @author Lenno Nagel, Michael Bulat
19
+ class Account < ActiveRecord::Base
20
+ extend ::Enumerize
21
+
22
+ attr_accessible :chart, :type, :code, :name, :contra
23
+ attr_accessible :chart, :type, :code, :name, :contra, :as => :admin
24
+ attr_readonly :chart
25
+
26
+ belongs_to :chart
27
+ has_many :amounts, :extend => Associations::AmountsExtension
28
+ has_many :transactions, :through => :amounts, :source => :transaction
29
+
30
+ enumerize :normal_balance, in: [:none, :debit, :credit]
31
+
32
+ after_initialize :default_values
33
+
34
+ before_validation :update_normal_balance
35
+ validates_presence_of :type, :name, :chart, :normal_balance
36
+ validates_uniqueness_of :code, :scope => :chart_id
37
+ validates_uniqueness_of :name, :scope => :chart_id
38
+
39
+ # The balance of the account.
40
+ #
41
+ # @example
42
+ # >> account.balance
43
+ # => #<BigDecimal:103259bb8,'0.2E4',4(12)>
44
+ #
45
+ # @return [BigDecimal] The decimal value balance
46
+ def balance
47
+ if self.normal_balance.debit?
48
+ self.amounts.balance
49
+ elsif self.normal_balance.credit?
50
+ - self.amounts.balance
51
+ else
52
+ nil
53
+ end
54
+ end
55
+
56
+ def self.valid_type?(type)
57
+ type.in? ["Asset", "Liability", "Equity", "Revenue", "Expense"]
58
+ end
59
+
60
+ def self.namespaced_type(type)
61
+ if valid_type?(type)
62
+ "ESA::Accounts::#{type}"
63
+ else
64
+ type
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def default_values
71
+ self.chart ||= Chart.where(:name => 'Chart of Accounts').first_or_create if self.chart_id.nil?
72
+ self.normal_balance ||= :none
73
+ end
74
+
75
+ # The normal balance for the account. Must be overridden in implementations.
76
+ def update_normal_balance
77
+ self.normal_balance = :none
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,22 @@
1
+ module ESA
2
+ module Accounts
3
+ # The Asset class is an account type used to represents resources owned by the business entity.
4
+ #
5
+ # === Normal Balance
6
+ # The normal balance on Asset accounts is a *Debit*.
7
+ #
8
+ # @see http://en.wikipedia.org/wiki/Asset Assets
9
+ #
10
+ # @author Lenno Nagel
11
+ class Asset < ESA::Account
12
+ # The normal balance for the account. Must be overridden in implementations.
13
+ def update_normal_balance
14
+ unless self.contra
15
+ self.normal_balance = :debit
16
+ else
17
+ self.normal_balance = :credit
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ module ESA
2
+ module Accounts
3
+ # The Equity class is an account type used to represents owners rights to the assets.
4
+ #
5
+ # === Normal Balance
6
+ # The normal balance on Equity accounts is a *Credit*.
7
+ #
8
+ # @see http://en.wikipedia.org/wiki/Equity_(finance) Equity
9
+ #
10
+ # @author Lenno Nagel
11
+ class Equity < ESA::Account
12
+ # The normal balance for the account. Must be overridden in implementations.
13
+ def update_normal_balance
14
+ unless self.contra
15
+ self.normal_balance = :credit
16
+ else
17
+ self.normal_balance = :debit
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ module ESA
2
+ module Accounts
3
+ # The Expense class is an account type used to represents assets or services consumed in the generation of revenue.
4
+ #
5
+ # === Normal Balance
6
+ # The normal balance on Expense accounts is a *Debit*.
7
+ #
8
+ # @see http://en.wikipedia.org/wiki/Expense Expenses
9
+ #
10
+ # @author Lenno Nagel
11
+ class Expense < ESA::Account
12
+ # The normal balance for the account. Must be overridden in implementations.
13
+ def update_normal_balance
14
+ unless self.contra
15
+ self.normal_balance = :debit
16
+ else
17
+ self.normal_balance = :credit
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ module ESA
2
+ module Accounts
3
+ # The Liability class is an account type used to represents debts owed to outsiders.
4
+ #
5
+ # === Normal Balance
6
+ # The normal balance on Liability accounts is a *Credit*.
7
+ #
8
+ # @see http://en.wikipedia.org/wiki/Liability_(financial_accounting) Liability
9
+ #
10
+ # @author Lenno Nagel
11
+ class Liability < ESA::Account
12
+ # The normal balance for the account. Must be overridden in implementations.
13
+ def update_normal_balance
14
+ unless self.contra
15
+ self.normal_balance = :credit
16
+ else
17
+ self.normal_balance = :debit
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ module ESA
2
+ module Accounts
3
+ # The Revenue class is an account type used to represents increases in owners equity.
4
+ #
5
+ # === Normal Balance
6
+ # The normal balance on Revenue accounts is a *Credit*.
7
+ #
8
+ # @see http://en.wikipedia.org/wiki/Revenue Revenue
9
+ #
10
+ # @author Lenno Nagel
11
+ class Revenue < ESA::Account
12
+ # The normal balance for the account. Must be overridden in implementations.
13
+ def update_normal_balance
14
+ unless self.contra
15
+ self.normal_balance = :credit
16
+ else
17
+ self.normal_balance = :debit
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ module ESA
2
+ # The Amount class represents debit and credit amounts in the system.
3
+ #
4
+ # @abstract
5
+ # An amount must be a subclass as either a debit or a credit to be saved to the database.
6
+ #
7
+ # @author Lenno Nagel, Michael Bulat
8
+ class Amount < ActiveRecord::Base
9
+ attr_accessible :type, :account, :amount, :transaction
10
+
11
+ belongs_to :transaction
12
+ belongs_to :account
13
+
14
+ validates_presence_of :type, :amount, :transaction, :account
15
+
16
+ scope :credits, lambda { where(type: Amounts::Credit) }
17
+ scope :debits, lambda { where(type: Amounts::Debit) }
18
+
19
+ def is_credit?
20
+ self.is_a? Amounts::Credit
21
+ end
22
+
23
+ def is_debit?
24
+ self.is_a? Amounts::Debit
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,12 @@
1
+ module ESA
2
+ module Amounts
3
+ # The Credit class represents credit entries in the transaction journal.
4
+ #
5
+ # @example
6
+ # credit_amount = ESA::Amounts::Credit.new(:account => revenue, :amount => 1000)
7
+ #
8
+ # @author Lenno Nagel
9
+ class Credit < ESA::Amount
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module ESA
2
+ module Amounts
3
+ # The Debit class represents debit entries in the transaction journal.
4
+ #
5
+ # @example
6
+ # debit_amount = ESA::Amounts::Debit.new(:account => cash, :amount => 1000)
7
+ #
8
+ # @author Lenno Nagel
9
+ class Debit < ESA::Amount
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,79 @@
1
+ module ESA
2
+ module Associations
3
+ # Association extension for has_many :amounts relations. Internal.
4
+ module AmountsExtension
5
+ # Returns a total sum of the referenced Amount objects
6
+ def total
7
+ sums = group('esa_amounts.amount is null').
8
+ sum(:amount)
9
+
10
+ checked = sums.map do |s|
11
+ amount_is_nil, amount = s.flatten
12
+ amount
13
+ end
14
+
15
+ if checked.all?
16
+ return checked.inject(BigDecimal(0)){|x,y| x + y}
17
+ else
18
+ return nil
19
+ end
20
+ end
21
+
22
+ # Returns a sum of the referenced Amount objects.
23
+ def balance
24
+ sums = group('esa_amounts.amount is null').
25
+ group('esa_amounts.type').
26
+ sum(:amount)
27
+
28
+ signed = sums.map do |s|
29
+ amount_is_nil, amount_type, amount = s.flatten
30
+ amount_type = amount_type.demodulize.downcase
31
+
32
+ if amount_type == "debit"
33
+ amount
34
+ elsif amount_type == "credit"
35
+ - amount
36
+ else
37
+ nil
38
+ end
39
+ end
40
+
41
+ if signed.all?
42
+ return signed.inject(BigDecimal(0)){|x,y| x + y}
43
+ else
44
+ return nil
45
+ end
46
+ end
47
+
48
+ # Returns a sum of the referenced Amount objects.
49
+ def iterated_total
50
+ amounts = map(&:amount)
51
+
52
+ if amounts.all?
53
+ amounts.inject(BigDecimal(0)){|x,y| x + y}
54
+ else
55
+ return nil
56
+ end
57
+ end
58
+
59
+ # Returns a sum of the referenced Amount objects.
60
+ def iterated_balance
61
+ amounts = map do |a|
62
+ if a.is_debit?
63
+ a.amount
64
+ elsif a.is_credit? and not a.amount.nil?
65
+ - a.amount
66
+ else
67
+ nil
68
+ end
69
+ end
70
+
71
+ if amounts.all?
72
+ amounts.inject(BigDecimal(0)){|x,y| x + y}
73
+ else
74
+ return nil
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,33 @@
1
+ module ESA
2
+ module Associations
3
+ module EventsExtension
4
+ # This function adds an event only if there were no previous events,
5
+ # or the previous event was a different one, avoiding duplicates.
6
+ def maybe(attrs)
7
+ events = proxy_association.owner.esa_events
8
+ last_event = events.order('time DESC, created_at DESC').first
9
+
10
+ # make sure that the previous event was not of the same event type
11
+ if last_event.nil? or last_event.nature != attrs[:nature]
12
+ if attrs[:time].present? and last_event.present? and last_event.time.present? and last_event.time > attrs[:time]
13
+ # we cannot input past events, so let's make it most recent
14
+ attrs[:time] = last_event.time
15
+ e = events.new(attrs)
16
+ else
17
+ e = events.new(attrs)
18
+ end
19
+ e.save
20
+ else
21
+ # we didn't need to add anything
22
+ true
23
+ end
24
+ end
25
+
26
+ def hashes
27
+ proxy_association.owner.esa_events.
28
+ map{|e| {:nature => e.nature, :time => e.time}}.
29
+ sort_by{|e| e[:time]}
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,35 @@
1
+ module ESA
2
+ module Associations
3
+ module FlagsExtension
4
+ def most_recent(nature, time=Time.zone.now, exclude=nil)
5
+ query = where(nature: nature).
6
+ where('time <= ?', time)
7
+
8
+ if exclude.present?
9
+ query = query.where('esa_flags.id not in (?)', exclude)
10
+ end
11
+
12
+ query.order('time DESC, created_at DESC').first
13
+ end
14
+
15
+ def is_set?(nature, time=Time.zone.now, exclude=nil)
16
+ most_recent = most_recent(nature, time, exclude)
17
+ if most_recent.present?
18
+ most_recent.is_set? # return the set bit of this flag
19
+ else
20
+ false
21
+ end
22
+ end
23
+
24
+ def transition_for(flag)
25
+ if flag.state and not is_set?(flag.nature, flag.time, flag.id)
26
+ 1
27
+ elsif not flag.state and is_set?(flag.nature, flag.time, flag.id)
28
+ -1
29
+ else
30
+ 0
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,7 @@
1
+ module ESA
2
+ module Associations
3
+ module TransactionsExtension
4
+ # for future usage
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,41 @@
1
+ module ESA
2
+ # The Chart class represents an organized set of accounts in the system.
3
+ #
4
+ # @author Lenno Nagel
5
+ class Chart < ActiveRecord::Base
6
+ include Traits::Extendable
7
+
8
+ attr_accessible :name
9
+
10
+ has_many :accounts
11
+ has_many :rulesets
12
+
13
+ has_many :events, :through => :rulesets, :uniq => true
14
+ has_many :flags, :through => :rulesets, :uniq => true
15
+ has_many :transactions, :through => :accounts, :uniq => true
16
+ has_many :amounts, :through => :accounts, :uniq => true, :extend => Associations::AmountsExtension
17
+
18
+ after_initialize :default_values
19
+
20
+ validates_presence_of :name
21
+ validates_uniqueness_of :name
22
+
23
+ # The trial balance of all accounts in the system. This should always equal zero,
24
+ # otherwise there is an error in the system.
25
+ #
26
+ # @example
27
+ # >> chart.trial_balance.to_i
28
+ # => 0
29
+ #
30
+ # @return [BigDecimal] The decimal value balance of all accounts
31
+ def trial_balance
32
+ self.amounts.balance
33
+ end
34
+
35
+ private
36
+
37
+ def default_values
38
+ self.name ||= "Chart of Accounts"
39
+ end
40
+ end
41
+ end