event_sourced_accounting 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.
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