event_sourced_accounting 0.1.6 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.markdown +179 -2
- data/app/models/esa/account.rb +2 -2
- data/app/models/esa/chart.rb +2 -2
- data/app/models/esa/context.rb +21 -12
- data/app/models/esa/contexts/account_context.rb +2 -2
- data/app/models/esa/contexts/accountable_context.rb +1 -1
- data/app/models/esa/contexts/accountable_type_context.rb +1 -1
- data/app/models/esa/contexts/date_context.rb +22 -2
- data/app/models/esa/contexts/empty_context.rb +1 -1
- data/app/models/esa/event.rb +2 -2
- data/app/models/esa/flag.rb +2 -2
- data/app/models/esa/ruleset.rb +95 -29
- data/app/models/esa/transaction.rb +2 -2
- data/lib/esa/balance_checker.rb +1 -1
- data/lib/esa/blocking_processor.rb +38 -23
- data/lib/esa/configuration.rb +22 -8
- data/lib/esa/context_provider.rb +7 -2
- data/lib/esa/filters/accountable_filter.rb +10 -0
- data/lib/esa/filters/accountable_type_filter.rb +2 -0
- data/lib/esa/subcontext_checker.rb +17 -5
- data/lib/esa/version.rb +1 -1
- metadata +111 -110
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 725485c3fb1eb0802b7e7f3553a0fdb598f0c927
|
4
|
+
data.tar.gz: 9a5c5aef5200ebd25e0a1aadc0e791d213bac61a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b7229fe63379704b0388f1a97971a630c24f0bd2faa28fa9b89d1aca1a3887c92a9d76068cd1aa8ee0759c0e25d0c24f0a6351d82619d41593a40539bd63795b
|
7
|
+
data.tar.gz: 5c177694686252945b7823a5163e7f7e6023827118e2797f82918affcec077ae5aefad2710349239c0e00c94d172c4900a53fe3a35a12ad4d0fd7384ef1c751f
|
data/README.markdown
CHANGED
@@ -27,10 +27,187 @@ Installation
|
|
27
27
|
|
28
28
|
- run migrations `rake db:migrate`
|
29
29
|
|
30
|
-
- add `include ESA::Traits::Accountable` to relevant models
|
31
30
|
|
32
|
-
|
31
|
+
Integration
|
32
|
+
============
|
33
|
+
|
34
|
+
First, configure the gem by creating `config/initializers/accounting.rb`.
|
35
|
+
```
|
36
|
+
require 'esa'
|
37
|
+
|
38
|
+
ESA.configure do |config|
|
39
|
+
config.processor = ESA::BlockingProcessor # default
|
40
|
+
config.extension_namespace = 'Accounting' # default
|
41
|
+
config.register('BankTransaction')
|
42
|
+
...
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
Then add `include ESA::Traits::Accountable` to the registered models.
|
47
|
+
```
|
48
|
+
class BankTransaction < ActiveRecord::Base
|
49
|
+
include ESA::Traits::Accountable
|
50
|
+
...
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
Implement the corresponding Event, Flag, Ruleset and Transaction classes for the registered models.
|
55
|
+
```
|
56
|
+
# app/models/accounting/events/bank_transaction_event.rb
|
57
|
+
module Accounting
|
58
|
+
module Events
|
59
|
+
class BankTransactionEvent < ESA::Event
|
60
|
+
enumerize :nature, in: [
|
61
|
+
:adjustment, # mandatory
|
62
|
+
:confirm, # example
|
63
|
+
:revoke, # example
|
64
|
+
]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
```
|
71
|
+
# app/models/accounting/flags/bank_transaction_flag.rb
|
72
|
+
module Accounting
|
73
|
+
module Flags
|
74
|
+
class BankTransactionFlag < ESA::Flag
|
75
|
+
enumerize :nature, in: [
|
76
|
+
:complete, # example
|
77
|
+
]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
```
|
84
|
+
# app/models/accounting/transactions/bank_transaction_transaction.rb
|
85
|
+
module Accounting
|
86
|
+
module Transactions
|
87
|
+
class BankTransactionTransaction < ESA::Transaction
|
88
|
+
# this relation definition is optional
|
89
|
+
has_one :bank_transaction, :through => :flag, :source => :accountable, :source_type => "BankTransaction"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
```
|
96
|
+
# app/models/accounting/rulesets/bank_transaction_ruleset.rb
|
97
|
+
module Accounting
|
98
|
+
module Rulesets
|
99
|
+
class BankTransactionRuleset < ESA::Ruleset
|
100
|
+
# events that have happened according to the current state
|
101
|
+
def event_times(bank_transaction)
|
102
|
+
{
|
103
|
+
confirm: bank_transaction.confirm_time,
|
104
|
+
revoke: bank_transaction.revoke_time,
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
# flags to be changed when events occur
|
109
|
+
def event_nature_flags
|
110
|
+
{
|
111
|
+
confirm: {complete: true},
|
112
|
+
revoke: {complete: false},
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
# transaction for when the :complete flag is switched to true
|
117
|
+
def flag_complete_transactions(bank_transaction)
|
118
|
+
{
|
119
|
+
:description => 'BankTransaction completed',
|
120
|
+
:debits => [
|
121
|
+
{
|
122
|
+
:account => find_account('Asset', 'Bank'),
|
123
|
+
:amount => bank_transaction.transferred_amount
|
124
|
+
}
|
125
|
+
],
|
126
|
+
:credits => [
|
127
|
+
{
|
128
|
+
:account => find_account('Asset', 'Bank Transit'),
|
129
|
+
:amount => bank_transaction.transferred_amount
|
130
|
+
}
|
131
|
+
],
|
132
|
+
}
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
Usage
|
140
|
+
============
|
141
|
+
|
142
|
+
In order to create events and transactions, the accountable objects
|
143
|
+
have to pass through a processor, which will register the necessary
|
144
|
+
Events, Flags & Transactions in the database.
|
145
|
+
|
146
|
+
You can use the provided processor implementation, or inherit from
|
147
|
+
the base implementation and provide your own class (e.g. to implement
|
148
|
+
delayed or scheduled processing).
|
149
|
+
|
150
|
+
```
|
151
|
+
>> bank_transaction = BankTransaction.find(..)
|
152
|
+
>> bank_transaction.confirm_time = Time.now
|
153
|
+
>> bank_transaction.save
|
154
|
+
true
|
155
|
+
|
156
|
+
>> ESA.configuration.processor.enqueue(bank_transaction)
|
157
|
+
|
158
|
+
>> bank_transaction.esa_events.count
|
159
|
+
1
|
160
|
+
|
161
|
+
>> bank_transaction.esa_flags.count
|
162
|
+
1
|
163
|
+
|
164
|
+
>> bank_transaction.esa_transactions.count
|
165
|
+
1
|
166
|
+
```
|
167
|
+
|
168
|
+
Reporting
|
169
|
+
============
|
33
170
|
|
171
|
+
There are many different reporting and filtering implementations available.
|
172
|
+
For a simple example, let's look at a report that only involves the transaction.
|
173
|
+
|
174
|
+
The following commands initialize the report and update the persisted values
|
175
|
+
to the depth of 1, which includes the creation of sub-reports per each account
|
176
|
+
involved in the transactions of that BankAccount.
|
177
|
+
|
178
|
+
```
|
179
|
+
>> report = ESA::Contexts::AccountableContext.create(chart: ESA::Chart.first, accountable: bank_transaction)
|
180
|
+
>> report.check_freshness(1)
|
181
|
+
```
|
182
|
+
|
183
|
+
Complex reports can be constructed automatically using the context provider
|
184
|
+
functionality. Reports, filters and context providers are available for:
|
185
|
+
|
186
|
+
- account
|
187
|
+
- accountable object (e.g. a single BankTransaction)
|
188
|
+
- accountable type (e.g. all known BankTransactions)
|
189
|
+
- date periods (year, month, date, custom)
|
190
|
+
|
191
|
+
Please refer to the source code for examples.
|
192
|
+
|
193
|
+
Subreport structure and context providers need to be configured:
|
194
|
+
|
195
|
+
```
|
196
|
+
ESA.configure do |config|
|
197
|
+
...
|
198
|
+
config.context_providers['bank_account'] = Accounting::ContextProviders::BankAccountContextProvider
|
199
|
+
|
200
|
+
config.context_tree = {
|
201
|
+
'month' => {
|
202
|
+
'account' => {
|
203
|
+
'bank_account' => {},
|
204
|
+
'date' => {},
|
205
|
+
},
|
206
|
+
},
|
207
|
+
}
|
208
|
+
...
|
209
|
+
end
|
210
|
+
```
|
34
211
|
|
35
212
|
Development
|
36
213
|
============
|
data/app/models/esa/account.rb
CHANGED
@@ -31,7 +31,7 @@ module ESA
|
|
31
31
|
|
32
32
|
enumerize :normal_balance, in: [:none, :debit, :credit]
|
33
33
|
|
34
|
-
after_initialize :
|
34
|
+
after_initialize :initialize_defaults
|
35
35
|
|
36
36
|
before_validation :update_normal_balance
|
37
37
|
validates_presence_of :type, :name, :chart, :normal_balance
|
@@ -69,7 +69,7 @@ module ESA
|
|
69
69
|
|
70
70
|
private
|
71
71
|
|
72
|
-
def
|
72
|
+
def initialize_defaults
|
73
73
|
self.chart ||= Chart.where(:name => 'Chart of Accounts').first_or_create if self.chart_id.nil?
|
74
74
|
self.normal_balance ||= :none
|
75
75
|
end
|
data/app/models/esa/chart.rb
CHANGED
@@ -18,7 +18,7 @@ module ESA
|
|
18
18
|
has_many :transactions, :through => :accounts, :uniq => true
|
19
19
|
has_many :amounts, :through => :accounts, :uniq => true, :extend => ESA::Associations::AmountsExtension
|
20
20
|
|
21
|
-
after_initialize :
|
21
|
+
after_initialize :initialize_defaults
|
22
22
|
|
23
23
|
validates_presence_of :name
|
24
24
|
validates_uniqueness_of :name
|
@@ -37,7 +37,7 @@ module ESA
|
|
37
37
|
|
38
38
|
private
|
39
39
|
|
40
|
-
def
|
40
|
+
def initialize_defaults
|
41
41
|
self.name ||= "Chart of Accounts"
|
42
42
|
end
|
43
43
|
end
|
data/app/models/esa/context.rb
CHANGED
@@ -21,7 +21,7 @@ module ESA
|
|
21
21
|
belongs_to :parent, :class_name => "Context"
|
22
22
|
has_many :subcontexts, :class_name => "Context", :foreign_key => "parent_id", :dependent => :destroy
|
23
23
|
|
24
|
-
after_initialize :
|
24
|
+
after_initialize :initialize_defaults, :initialize_filters
|
25
25
|
before_validation :update_name, :update_position
|
26
26
|
validates_presence_of :chart, :name
|
27
27
|
validate :validate_parent
|
@@ -75,9 +75,13 @@ module ESA
|
|
75
75
|
self.freshness.present? and (self.freshness + ESA.configuration.context_freshness_threshold) > time
|
76
76
|
end
|
77
77
|
|
78
|
-
def
|
79
|
-
|
80
|
-
|
78
|
+
def has_subcontext_namespaces?(*namespaces)
|
79
|
+
(namespaces.flatten.compact - subcontext_namespaces).none?
|
80
|
+
end
|
81
|
+
|
82
|
+
def check_freshness(depth=0, options = {})
|
83
|
+
if self.is_update_needed? or not has_subcontext_namespaces?(options[:namespace])
|
84
|
+
self.update!(options)
|
81
85
|
else
|
82
86
|
self.update_freshness_timestamp!
|
83
87
|
end
|
@@ -103,12 +107,12 @@ module ESA
|
|
103
107
|
end
|
104
108
|
end
|
105
109
|
|
106
|
-
def update!
|
110
|
+
def update!(options = {})
|
107
111
|
self.freshness = Time.zone.now
|
108
112
|
|
109
113
|
ESA.configuration.context_checkers.each do |checker|
|
110
114
|
if checker.respond_to? :check
|
111
|
-
checker.check(self)
|
115
|
+
checker.check(self, options)
|
112
116
|
end
|
113
117
|
end
|
114
118
|
|
@@ -178,16 +182,21 @@ module ESA
|
|
178
182
|
end
|
179
183
|
end
|
180
184
|
|
181
|
-
def
|
185
|
+
def initialize_defaults
|
182
186
|
self.chart ||= self.parent.chart if self.chart_id.nil? and not self.parent_id.nil?
|
183
|
-
self.namespace ||=
|
187
|
+
self.namespace ||= self.default_namespace
|
188
|
+
self.position ||= self.default_position
|
189
|
+
end
|
190
|
+
|
191
|
+
def default_namespace
|
192
|
+
(self.type || self.class.name).demodulize.underscore.gsub(/_context$/, '')
|
184
193
|
end
|
185
194
|
|
186
195
|
def update_name
|
187
|
-
self.name = self.
|
196
|
+
self.name = self.default_name
|
188
197
|
end
|
189
198
|
|
190
|
-
def
|
199
|
+
def default_name
|
191
200
|
if self.type.nil?
|
192
201
|
self.chart.name unless self.chart.nil?
|
193
202
|
else
|
@@ -196,10 +205,10 @@ module ESA
|
|
196
205
|
end
|
197
206
|
|
198
207
|
def update_position
|
199
|
-
self.position = self.
|
208
|
+
self.position = self.default_position
|
200
209
|
end
|
201
210
|
|
202
|
-
def
|
211
|
+
def default_position
|
203
212
|
nil
|
204
213
|
end
|
205
214
|
|
@@ -10,11 +10,11 @@ module ESA
|
|
10
10
|
|
11
11
|
protected
|
12
12
|
|
13
|
-
def
|
13
|
+
def default_name
|
14
14
|
self.account.name unless self.account.nil?
|
15
15
|
end
|
16
16
|
|
17
|
-
def
|
17
|
+
def default_position
|
18
18
|
self.account.code.gsub(/[^0-9]/, '').to_i unless self.account.nil? or self.account.code.nil?
|
19
19
|
end
|
20
20
|
|
@@ -33,7 +33,27 @@ module ESA
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
-
def
|
36
|
+
def default_namespace
|
37
|
+
if self.start_date.present? and self.end_date.present?
|
38
|
+
if self.start_date == self.end_date
|
39
|
+
"date"
|
40
|
+
elsif self.start_date == self.start_date.beginning_of_month and
|
41
|
+
self.end_date == self.end_date.end_of_month and
|
42
|
+
self.end_date == self.start_date.end_of_month then
|
43
|
+
"month"
|
44
|
+
elsif self.start_date == self.start_date.beginning_of_year and
|
45
|
+
self.end_date == self.end_date.end_of_year and
|
46
|
+
self.end_date == self.start_date.end_of_year then
|
47
|
+
"year"
|
48
|
+
else
|
49
|
+
"period"
|
50
|
+
end
|
51
|
+
else
|
52
|
+
"period"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def default_name
|
37
57
|
if self.start_date.present? and self.end_date.present?
|
38
58
|
if self.start_date == self.end_date
|
39
59
|
"#{self.start_date.to_s}"
|
@@ -47,7 +67,7 @@ module ESA
|
|
47
67
|
end
|
48
68
|
end
|
49
69
|
|
50
|
-
def
|
70
|
+
def default_position
|
51
71
|
if self.start_date.present?
|
52
72
|
self.start_date.to_time.to_i
|
53
73
|
elsif self.end_date.present?
|
data/app/models/esa/event.rb
CHANGED
@@ -21,13 +21,13 @@ module ESA
|
|
21
21
|
|
22
22
|
enumerize :nature, in: [:unknown, :adjustment]
|
23
23
|
|
24
|
-
after_initialize :
|
24
|
+
after_initialize :initialize_defaults
|
25
25
|
validates_presence_of :time, :nature, :accountable, :ruleset
|
26
26
|
validates_inclusion_of :processed, :in => [true, false]
|
27
27
|
|
28
28
|
private
|
29
29
|
|
30
|
-
def
|
30
|
+
def initialize_defaults
|
31
31
|
self.time ||= Time.zone.now if self.time.nil?
|
32
32
|
self.ruleset ||= Ruleset.extension_instance(self.accountable) if self.ruleset_id.nil? and not self.accountable_id.nil?
|
33
33
|
self.processed ||= false
|
data/app/models/esa/flag.rb
CHANGED
@@ -27,7 +27,7 @@ module ESA
|
|
27
27
|
|
28
28
|
enumerize :nature, in: [:unknown]
|
29
29
|
|
30
|
-
after_initialize :
|
30
|
+
after_initialize :initialize_defaults
|
31
31
|
validates_presence_of :nature, :event, :time, :accountable, :ruleset
|
32
32
|
validates_inclusion_of :state, :in => [true, false]
|
33
33
|
validates_inclusion_of :processed, :in => [true, false]
|
@@ -77,7 +77,7 @@ module ESA
|
|
77
77
|
end
|
78
78
|
end
|
79
79
|
|
80
|
-
def
|
80
|
+
def initialize_defaults
|
81
81
|
if not self.event_id.nil?
|
82
82
|
self.time ||= self.event.time if self.time.nil?
|
83
83
|
self.accountable ||= self.event.accountable if self.accountable_id.nil?
|
data/app/models/esa/ruleset.rb
CHANGED
@@ -14,7 +14,7 @@ module ESA
|
|
14
14
|
has_many :events
|
15
15
|
has_many :flags
|
16
16
|
|
17
|
-
after_initialize :
|
17
|
+
after_initialize :initialize_defaults
|
18
18
|
validates_presence_of :type, :chart
|
19
19
|
|
20
20
|
# accountable
|
@@ -25,18 +25,36 @@ module ESA
|
|
25
25
|
|
26
26
|
# events
|
27
27
|
|
28
|
+
def event_times(accountable)
|
29
|
+
{}
|
30
|
+
end
|
31
|
+
|
28
32
|
def stateful_events(accountable)
|
29
|
-
|
33
|
+
self.event_times(accountable).map do |nature,times|
|
34
|
+
if times.present? and times.is_a? Time
|
35
|
+
{nature: nature, time: times}
|
36
|
+
elsif times.present? and times.respond_to? :each
|
37
|
+
times.map do |t|
|
38
|
+
if t.is_a? Time
|
39
|
+
{nature: nature, time: t}
|
40
|
+
else
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
end.compact
|
44
|
+
else
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end.flatten.compact
|
30
48
|
end
|
31
49
|
|
32
50
|
def stateful_events_as_attributes(accountable)
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
51
|
+
defaults = {
|
52
|
+
accountable: accountable,
|
53
|
+
ruleset: self,
|
54
|
+
}
|
55
|
+
stateful_events(accountable).map do |event|
|
56
|
+
defaults.merge(event)
|
57
|
+
end
|
40
58
|
end
|
41
59
|
|
42
60
|
def unrecorded_events_as_attributes(accountable)
|
@@ -48,18 +66,32 @@ module ESA
|
|
48
66
|
stateful.reject{|s| [s[:nature].to_s, s[:time].to_i].in? recorded}
|
49
67
|
end
|
50
68
|
|
69
|
+
def addable_unrecorded_events_as_attributes(accountable)
|
70
|
+
flag_times_max = accountable.esa_flags.group(:nature).maximum(:time)
|
71
|
+
|
72
|
+
unrecorded_events_as_attributes(accountable).select do |event|
|
73
|
+
event_flags = event_nature_flags[event[:nature]] || {}
|
74
|
+
flag_times = flag_times_max.slice(*event_flags.keys.map(&:to_s))
|
75
|
+
|
76
|
+
# allow when the event flags have not been used before or
|
77
|
+
# when all the currently used flag times are before the new event
|
78
|
+
flag_times.values.none? || flag_times.values.max <= event[:time]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
51
82
|
def is_adjustment_event_needed?(accountable)
|
52
83
|
flags_needing_adjustment(accountable).count > 0
|
53
84
|
end
|
54
85
|
|
55
86
|
# flags
|
56
87
|
|
57
|
-
def
|
88
|
+
def event_nature_flags
|
58
89
|
{}
|
59
90
|
end
|
60
91
|
|
61
92
|
def event_flags_as_attributes(event)
|
62
|
-
|
93
|
+
flags = self.event_nature_flags[event.nature.to_sym] || {}
|
94
|
+
flags.map do |nature,state|
|
63
95
|
{
|
64
96
|
:accountable => event.accountable,
|
65
97
|
:nature => nature,
|
@@ -76,24 +108,42 @@ module ESA
|
|
76
108
|
accountable.esa_flags.transitioning.most_recent(nature)
|
77
109
|
end.compact
|
78
110
|
|
79
|
-
|
80
|
-
|
81
|
-
set_flags.reject do |flag|
|
82
|
-
specs = flag_transactions_as_attributes(flag)
|
83
|
-
flag.transactions_match_specs?(specs)
|
111
|
+
most_recent_flags.select do |flag|
|
112
|
+
flag.is_set? and not flag_transactions_match_specs?(flag)
|
84
113
|
end
|
85
114
|
end
|
86
115
|
|
87
116
|
# transactions
|
88
117
|
|
118
|
+
def flag_transactions_spec(accountable, flag_nature)
|
119
|
+
function_name = "flag_#{flag_nature}_transactions"
|
120
|
+
|
121
|
+
if self.respond_to? function_name
|
122
|
+
transactions = self.send(function_name, accountable)
|
123
|
+
|
124
|
+
if transactions.is_a? Hash
|
125
|
+
[transactions]
|
126
|
+
elsif transactions.is_a? Array
|
127
|
+
transactions
|
128
|
+
else
|
129
|
+
[]
|
130
|
+
end
|
131
|
+
else
|
132
|
+
[]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
89
136
|
def flag_transactions_when_set(flag)
|
90
|
-
|
137
|
+
flag_transactions_spec(flag.accountable, flag.nature)
|
91
138
|
end
|
92
139
|
|
93
140
|
def flag_transactions_when_unset(flag)
|
94
|
-
self.flag_transactions_when_set(flag).
|
95
|
-
tx
|
96
|
-
|
141
|
+
self.flag_transactions_when_set(flag).map do |tx|
|
142
|
+
tx.merge({
|
143
|
+
description: "#{tx[:description]} / reversed",
|
144
|
+
debits: tx[:credits],
|
145
|
+
credits: tx[:debits]
|
146
|
+
})
|
97
147
|
end
|
98
148
|
end
|
99
149
|
|
@@ -129,17 +179,33 @@ module ESA
|
|
129
179
|
end
|
130
180
|
|
131
181
|
def flag_transactions_as_attributes(flag)
|
182
|
+
defaults = {
|
183
|
+
time: flag.time,
|
184
|
+
accountable: flag.accountable,
|
185
|
+
flag: flag,
|
186
|
+
}
|
132
187
|
flag_transactions(flag).map do |tx|
|
133
|
-
|
134
|
-
|
135
|
-
|
188
|
+
attrs = defaults.merge(tx)
|
189
|
+
ensure_positive_amounts(attrs)
|
190
|
+
end
|
191
|
+
end
|
136
192
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
193
|
+
def flag_transactions_match_specs?(flag)
|
194
|
+
specs = flag_transactions_as_attributes(flag)
|
195
|
+
flag.transactions_match_specs?(specs)
|
196
|
+
end
|
197
|
+
|
198
|
+
def ensure_positive_amounts(attrs)
|
199
|
+
amounts = attrs[:debits] + attrs[:credits]
|
200
|
+
nonpositives = amounts.map{|a| a[:amount] <= BigDecimal(0)}
|
141
201
|
|
142
|
-
|
202
|
+
if nonpositives.all?
|
203
|
+
attrs.merge({
|
204
|
+
debits: inverted(attrs[:credits]),
|
205
|
+
credits: inverted(attrs[:debits]),
|
206
|
+
})
|
207
|
+
else
|
208
|
+
attrs
|
143
209
|
end
|
144
210
|
end
|
145
211
|
|
@@ -157,7 +223,7 @@ module ESA
|
|
157
223
|
|
158
224
|
private
|
159
225
|
|
160
|
-
def
|
226
|
+
def initialize_defaults
|
161
227
|
self.chart ||= Chart.extension_instance(self) if self.chart_id.nil?
|
162
228
|
self.name ||= "#{self.chart.name} #{self.class.name.demodulize}" if self.name.nil? and self.chart_id.present?
|
163
229
|
end
|
@@ -19,7 +19,7 @@ module ESA
|
|
19
19
|
has_many :amounts, :extend => ESA::Associations::AmountsExtension
|
20
20
|
has_many :accounts, :through => :amounts, :source => :account, :uniq => true
|
21
21
|
|
22
|
-
after_initialize :
|
22
|
+
after_initialize :initialize_defaults
|
23
23
|
|
24
24
|
validates_presence_of :time, :description
|
25
25
|
validate :has_credit_amounts?
|
@@ -71,7 +71,7 @@ module ESA
|
|
71
71
|
|
72
72
|
private
|
73
73
|
|
74
|
-
def
|
74
|
+
def initialize_defaults
|
75
75
|
self.time ||= Time.zone.now
|
76
76
|
end
|
77
77
|
|
data/lib/esa/balance_checker.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module ESA
|
2
2
|
class BalanceChecker
|
3
|
-
def self.check(context)
|
3
|
+
def self.check(context, options = {})
|
4
4
|
if context.can_be_persisted? and not context.freshness.nil?
|
5
5
|
#context.event_count = context.events.created_before(context.freshness).count
|
6
6
|
#context.flag_count = context.flags.created_before(context.freshness).count
|