brisk-bills 0.6.0 → 0.7.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 (34) hide show
  1. data/CHANGELOG +6 -1
  2. data/TODO.txt +60 -38
  3. data/app/controllers/admin/employees_controller.rb +1 -1
  4. data/app/controllers/admin/invoices_controller.rb +38 -2
  5. data/app/controllers/admin/payments_controller.rb +13 -6
  6. data/app/helpers/admin/activity_tax_field_helper.rb +1 -1
  7. data/app/helpers/admin/activity_type_field_helper.rb +2 -2
  8. data/app/helpers/admin/payments_helper.rb +8 -2
  9. data/app/helpers/application_helper.rb +9 -2
  10. data/app/model_views/invoices_with_total.rb +5 -0
  11. data/app/models/activity.rb +7 -3
  12. data/app/models/activity/labor.rb +1 -1
  13. data/app/models/activity/labor/slimtimer.rb +2 -0
  14. data/app/models/client.rb +93 -3
  15. data/app/models/client_representative.rb +9 -1
  16. data/app/models/employee.rb +21 -1
  17. data/app/models/employee/slimtimer.rb +11 -0
  18. data/app/models/invoice.rb +93 -129
  19. data/app/models/invoice_payment.rb +54 -0
  20. data/app/models/payment.rb +25 -48
  21. data/config/boot.rb +1 -1
  22. data/db/migrate/029_invoices_with_totals_view_adjustment.rb +29 -0
  23. data/db/schema.rb +1 -1
  24. data/lib/brisk-bills.rb +1 -1
  25. data/lib/tasks/create_last_months_invoices.rake +8 -3
  26. data/public/javascripts/prototype.js +1573 -1019
  27. data/public/javascripts/prototype.js-1.6.0.3 +4320 -0
  28. data/test/test_unit_factory_helper.rb +16 -9
  29. data/test/unit/client_test.rb +298 -2
  30. data/test/unit/invoice_payment_test.rb +313 -3
  31. data/test/unit/invoice_test.rb +49 -36
  32. data/test/unit/payment_test.rb +35 -31
  33. data/vendor/plugins/active_scaffold_full_refresh/lib/active_scaffold_full_refresh.rb +4 -2
  34. metadata +69 -33
@@ -18,5 +18,13 @@ class ClientRepresentative < ActiveRecord::Base
18
18
 
19
19
  ret
20
20
  end
21
-
21
+
22
+ # This fixes (I guess its a bug?) in _add_existing_form.html when ClientReps are being shown
23
+ # as a sublist in Clients, and the user chooses to "Add Existing". Without this - the order
24
+ # Is totally effed.
25
+ def self.find(*args)
26
+ (args == [:all]) ? super(:all, :order => 'first_name ASC, last_name ASC') : super(*args)
27
+ end
28
+
29
+
22
30
  end
@@ -44,6 +44,26 @@ class Employee < ActiveRecord::Base
44
44
  true
45
45
  end
46
46
  end
47
-
47
+
48
+ # There were some issues in rails 2.3.2 that caused associations (slimtimer/credential/etc) to not save without this hack
49
+ # we may have to do it for all active record objects in the project ...
50
+ def with_unsaved_associated
51
+ associations_for_update.all? do |association|
52
+ association_proxy = instance_variable_get("@#{association.name}")
53
+
54
+ if association_proxy
55
+ records = association_proxy
56
+
57
+ records = [records] unless records.is_a? Array # convert singular associations into collections for ease of use
58
+
59
+ records.select {|r| r.changed? and not r.readonly?}.all?{|r| yield r} # must use select instead of find_all, which Rails overrides on association proxies for db access
60
+ else
61
+ true
62
+ end
63
+
64
+ association_proxy
65
+ end
66
+ end
67
+
48
68
  handle_extensions
49
69
  end
@@ -5,11 +5,22 @@ class Employee::Slimtimer < ActiveRecord::Base
5
5
  has_many :time_entries, :class_name => 'SlimtimerTimeEntry', :dependent => :destroy, :foreign_key => :employee_slimtimer_id
6
6
 
7
7
  validates_presence_of [:employee_id, :api_key, :username, :password]
8
+
8
9
  end
9
10
 
10
11
  Employee.class_eval do
11
12
  has_one :slimtimer, :class_name => 'Employee::Slimtimer', :dependent => :destroy, :foreign_key => :employee_id
13
+
14
+ # This ensures validation and save in the employee ActiveScaffold do_cupdate & do_create
15
+ def scaffold_update_follow_with_slimtimer
16
+ (scaffold_update_follow_without_slimtimer || []) << :slimtimer
17
+ end
12
18
 
19
+ # We kind of need for there to be a scaffold_update_follow if alias_method_chain is to work:
20
+ def scaffold_update_follow; end unless self.respond_to? :scaffold_update_follow
21
+
22
+ alias_method_chain :scaffold_update_follow, :slimtimer
23
+
13
24
  def slimtimer_api_key
14
25
  slimtimer.api_key unless slimtimer.nil?
15
26
  end
@@ -3,20 +3,16 @@ class Invoice < ActiveRecord::Base
3
3
 
4
4
  # NOTE: this has to be above the has_many, otherwise activities would get nullified before this callback had a chance to return fals
5
5
  before_destroy :ensure_not_published_on_destroy
6
- before_destroy :ensure_were_the_most_recent
7
6
 
8
7
  before_update :ensure_not_published_on_update
9
-
10
- after_create :reattach_activities
11
- after_update :reattach_activities
12
-
13
- after_update :mark_invoice_payments
14
- after_create :mark_invoice_payments
15
- after_destroy :remove_invoice_payments
8
+
9
+ # NOTE: If we ever try removing this - we have a problem with invoice_payments exceeding the invoice price when activities are added/removed
10
+ before_save :clear_invoice_payments_if_unpublished
16
11
 
17
12
  belongs_to :client
18
13
  has_many :activities, :dependent => :nullify
19
- has_many :payments, :through => 'invoice_payments'
14
+ has_many :payments, :through => :assigned_payments
15
+ has_many :payment_assignments, :class_name => 'InvoicePayment', :dependent => :delete_all
20
16
 
21
17
  has_and_belongs_to_many(
22
18
  :activity_types,
@@ -27,6 +23,8 @@ class Invoice < ActiveRecord::Base
27
23
  )
28
24
 
29
25
  validates_presence_of :client_id, :issued_on
26
+ validate :validate_invoice_payments_not_greater_than_amount
27
+ validate :validate_payment_assignments_only_if_published
30
28
 
31
29
  # This just ends up being useful in a couple places
32
30
  ACTIVITY_TOTAL_SQL = '(IF(activities.cost_in_cents IS NULL, 0, activities.cost_in_cents)+IF(activities.tax_in_cents IS NULL, 0, activities.tax_in_cents))'
@@ -36,53 +34,13 @@ class Invoice < ActiveRecord::Base
36
34
  end_of_last_month = Time.utc(*Time.now.to_a).last_month.end_of_month
37
35
  self.issued_on = end_of_last_month unless self.issued_on
38
36
  end
39
-
40
- def invalid_if_published(collection_record = nil)
41
- raise "Can't adjust an already-published invoice." if !new_record? and is_published
37
+
38
+ def validate_payment_assignments_only_if_published
39
+ errors.add :payment_assignments, "can only be set for published invoices" if !is_published and payment_assignments and payment_assignments.length > 0
42
40
  end
43
41
 
44
- def reattach_activities
45
- included_activity_types = activity_types.collect{ |a| a.label.downcase }
46
- unincluded_activity_types = ActivityType.find(:all).collect{ |a| a.label.downcase } - included_activity_types
47
-
48
- # First we NULL'ify (remove) existing attachments that no longer should be:
49
- nullify_conditions = []
50
- nullify_parameters = []
51
-
52
- # Conditions for occurance adjutments
53
- nullify_conditions << '(DATEDIFF(occurred_on, DATE(?)) > 0)'
54
- nullify_parameters << issued_on
55
-
56
- # For the ActivityType Adjustments:
57
- unless unincluded_activity_types.empty?
58
- nullify_conditions << '(%s)' % ( ['activity_type = ?'] * unincluded_activity_types.size).join(' OR ')
59
- nullify_parameters += unincluded_activity_types
60
- end
61
-
62
- Activity.update_all(
63
- 'invoice_id = NULL',
64
- [ ['invoice_id = ?', 'is_published = ?', ('(%s)' % nullify_conditions.join(' OR ')) ].join(' AND ') ]+
65
- [id, true]+nullify_parameters
66
- ) unless new_record?
67
-
68
- # Now we attach the new records :
69
- update_where = [
70
- ['invoice_id IS NULL'],
71
- ['is_published = ?', true],
72
- ['client_id = ?', client_id],
73
- ['DATEDIFF(occurred_on, DATE(?)) <= 0', issued_on],
74
-
75
- # Slightly more complicated, for the type includes:
76
- ( (included_activity_types.size > 0) ?
77
- [ '('+(['activity_type = ?'] * included_activity_types.size).join(' OR ')+')', included_activity_types ] :
78
- [ 'activity_type IS NULL' ] )
79
- ]
80
-
81
- Activity.update_all(
82
- ['invoice_id = ?', id ],
83
- # This is what ActiveRecord actually expects...
84
- update_where.collect{|c| c[0]}.join(' AND ').to_a + update_where.reject{|c| c.length < 2 }.collect{|c| c[1]}.flatten
85
- )
42
+ def invalid_if_published(collection_record = nil)
43
+ raise "Can't adjust an already-published invoice." if !new_record? and is_published
86
44
  end
87
45
 
88
46
  def is_most_recent_invoice?
@@ -91,13 +49,6 @@ class Invoice < ActiveRecord::Base
91
49
  (newest_invoice.nil? or newest_invoice.id == id) ? true : false
92
50
  end
93
51
 
94
- def ensure_were_the_most_recent
95
- unless is_most_recent_invoice?
96
- errors.add_to_base "Can't destroy an invoice if its not the client's most recent invoice"
97
- return false
98
- end
99
- end
100
-
101
52
  def ensure_not_published_on_destroy
102
53
  if is_published and !changes.has_key? :is_published
103
54
  errors.add_to_base "Can't destroy a published invoice"
@@ -114,30 +65,46 @@ class Invoice < ActiveRecord::Base
114
65
 
115
66
  errors.add_to_base(
116
67
  "Invoice can't be updated once published."
117
- ) if is_published and changes.reject{|k,v| k == 'is_published'}.length > 0
68
+ ) if is_published and changes.reject{|k,v| /(?:is_published|payment_assignments)/.match k}.length > 0
118
69
 
119
- errors.add_to_base(
120
- "Invoice can't be unpublished, unless its the newest invoice in the client's queue."
121
- ) if changes.has_key?('is_published') and is_published_was and !is_most_recent_invoice?
122
70
  end
123
71
 
124
- def taxes_total
125
- process_total :taxes_total, :tax_in_cents
72
+ def validate_invoice_payments_not_greater_than_amount
73
+ inv_amount = self.amount
74
+ assignment_amount = self.payment_assignments.inject(Money.new(0)){|sum,ip| ip.amount+sum }
75
+
76
+ # We use the funky :> /:< to differentiate between the case of a credit invoice and a (normal?) invoice
77
+ errors.add :payment_assignments, "exceeds invoice amount" if inv_amount >= 0 and self.amount < assignment_amount
78
+ end
79
+
80
+ def authorized_for?(options)
81
+ case options[:action].to_s
82
+ when /^(destroy)$/
83
+ !is_published
84
+ else
85
+ true
86
+ end
87
+ end
88
+
89
+ def taxes_total( force_reload = false )
90
+ (attribute_present? :tax_in_cents and !force_reload) ?
91
+ Money.new(read_attribute(:tax_in_cents).to_i) :
92
+ self.activities.inject(Money.new(0)){|sum,a| sum + ((a.tax) ? a.tax : Money.new(0)) }
126
93
  end
127
94
 
128
- def sub_total
129
- process_total :sub_total, :cost_in_cents
95
+ def sub_total( force_reload = false )
96
+ (attribute_present? :cost_in_cents and !force_reload) ?
97
+ Money.new(read_attribute(:cost_in_cents).to_i) :
98
+ self.activities.inject(Money.new(0)){|sum,a| sum + ((a.cost) ? a.cost : Money.new(0)) }
130
99
  end
131
100
 
132
101
  def amount( force_reload = false )
133
- (attribute_present? :amount_in_cents and !force_reload) ?
102
+ (attribute_present? :amount_in_cents and !force_reload) ?
134
103
  Money.new(read_attribute(:amount_in_cents).to_i) :
135
- process_total( :amount, ACTIVITY_TOTAL_SQL )
104
+ self.activities.inject(Money.new(0)){|sum,a| sum + ((a.cost) ? a.cost : Money.new(0)) + ((a.tax) ? a.tax : Money.new(0)) }
136
105
  end
137
106
 
138
- def grand_total
139
- process_total :grand_total, ACTIVITY_TOTAL_SQL
140
- end
107
+ alias :grand_total :amount
141
108
 
142
109
  def name
143
110
  '"%s" Invoice on %s' % [ (client) ? client.company_name : '(Unknown Client)', issued_on.strftime("%m/%d/%Y %I:%M %p") ]
@@ -151,46 +118,6 @@ class Invoice < ActiveRecord::Base
151
118
  ('$%.2f' % amount.to_s).gsub(/(\d)(?=\d{3}+(\.\d*)?$)/, '\1,')
152
119
  ]
153
120
  end
154
-
155
- def remove_invoice_payments
156
- InvoicePayment.destroy_all ['invoice_id = ?', id]
157
- end
158
-
159
- def mark_invoice_payments
160
- if changes.has_key? "is_published"
161
- remove_invoice_payments
162
-
163
- if is_published
164
- unallocated_payments = Payment.find_with_totals(
165
- :all,
166
- :conditions => [
167
- 'client_id = ? AND (payments.amount_in_cents - IF(payments_total.amount_allocated_in_cents IS NULL, 0, payments_total.amount_allocated_in_cents) ) > ?',
168
- client_id,
169
- 0
170
- ]
171
- )
172
-
173
- current_client_balance = 0.0.to_money
174
- unallocated_payments.each { |pmnt| current_client_balance -= pmnt.amount_unallocated }
175
-
176
- invoice_balance = amount
177
-
178
- unallocated_payments.each do |unallocated_pmnt|
179
- break if invoice_balance == 0 or current_client_balance >= 0
180
-
181
- payment_allocation = (unallocated_pmnt.amount_unallocated > invoice_balance) ?
182
- invoice_balance :
183
- unallocated_pmnt.amount_unallocated
184
-
185
- InvoicePayment.create! :invoice => self, :payment => unallocated_pmnt, :amount => payment_allocation
186
-
187
- invoice_balance -= payment_allocation
188
- current_client_balance += payment_allocation
189
- end
190
- end
191
- end
192
-
193
- end
194
121
 
195
122
  def paid_on
196
123
  raise StandardError unless is_paid?
@@ -209,7 +136,7 @@ class Invoice < ActiveRecord::Base
209
136
  def is_paid?( force_reload = false )
210
137
  (attribute_present? :is_paid and !force_reload) ?
211
138
  (read_attribute(:is_paid).to_i == 1) :
212
- amount_outstanding.zero?
139
+ amount_outstanding(true) <= 0
213
140
  end
214
141
 
215
142
  def amount_paid( force_reload = false )
@@ -220,8 +147,49 @@ class Invoice < ActiveRecord::Base
220
147
  )
221
148
  end
222
149
 
223
- def amount_outstanding
224
- amount - amount_paid
150
+ def amount_outstanding( force_reload = false )
151
+ (attribute_present? :amount_outstanding_in_cents and !force_reload) ?
152
+ Money.new(read_attribute(:amount_outstanding_in_cents).to_i) :
153
+ (amount(true) - amount_paid(true))
154
+ end
155
+
156
+ # This is a shortcut to the self.recommended_activities_for , and is provided as a shortcut when its necessary to update an existing invoice's
157
+ # activities inclusion
158
+ def recommended_activities
159
+ Invoice.recommended_activities_for client_id, issued_on, self.activity_types, self.id
160
+ end
161
+
162
+ # Given a client_id, cut_at_or_before date, and (optionally) an array of types, we'll return the activities that should go into a corresponding invoice.
163
+ # THis was placed here, b/c its conceivable that in the future, we may support an array for the client_id parameter...
164
+ def self.recommended_activities_for(for_client_id, occurred_on_or_before, included_activity_types, for_invoice_id = nil)
165
+ for_client_id = for_client_id.id if for_client_id.class == Client
166
+ for_invoice_id = for_invoice_id.id if for_invoice_id.class == Invoice
167
+
168
+ included_activity_types = included_activity_types.collect{|a| a.label.downcase}
169
+
170
+ conditions = [
171
+ 'is_published = ? AND client_id = ? AND DATEDIFF(occurred_on, DATE(?)) <= 0',
172
+ true,
173
+ for_client_id,
174
+ occurred_on_or_before
175
+ ]
176
+
177
+ # Slightly more complicated, for the type includes:
178
+ if included_activity_types and included_activity_types.size > 0
179
+ conditions[0] += ' AND ('+(['activity_type = ?'] * included_activity_types.size).join(' OR ')+')'
180
+ conditions.push *included_activity_types
181
+ else
182
+ conditions[0] += ' AND activity_type IS NULL'
183
+ end
184
+
185
+ if for_invoice_id
186
+ conditions[0] += ' AND ( invoice_id IS NULL OR invoice_id = ? )'
187
+ conditions << for_invoice_id
188
+ else
189
+ conditions[0] += ' AND invoice_id IS NULL'
190
+ end
191
+
192
+ Activity.find :all, :conditions => conditions
225
193
  end
226
194
 
227
195
  def self.find_with_totals( how_many = :all, options = {} )
@@ -251,6 +219,9 @@ class Invoice < ActiveRecord::Base
251
219
  'invoices.client_id',
252
220
  'invoices.comments',
253
221
  'invoices.issued_on',
222
+ 'invoices.is_published',
223
+ 'invoices.created_at',
224
+ 'invoices.updated_at',
254
225
  "#{cast_amount} AS amount_in_cents",
255
226
  "#{cast_amount_paid} AS amount_paid_in_cents",
256
227
  "#{cast_amount} - #{cast_amount_paid} AS amount_outstanding_in_cents"
@@ -261,19 +232,12 @@ class Invoice < ActiveRecord::Base
261
232
  )
262
233
  end
263
234
 
264
- def authorized_for?(options)
265
- case options[:action].to_s
266
- when /^(update|destroy)$/
267
- (is_published and !is_most_recent_invoice?) ? false : true
268
- else
269
- true
270
- end
271
- end
272
-
273
235
  private
274
-
275
- def process_total(name, field_sql)
276
- Money.new Activity.sum(field_sql, :conditions => ['invoice_id = ?', id]).to_i
236
+
237
+ # When/if we save an invoice, and we determine that its changed or created as unpublished, we need to ensure that no payments are assigned to the invoice.
238
+ # This means deleting any existing assignments should there be any.
239
+ def clear_invoice_payments_if_unpublished
240
+ payment_assignments.clear if changes.has_key? "is_published" and !is_published
277
241
  end
278
242
 
279
243
  handle_extensions
@@ -5,4 +5,58 @@ class InvoicePayment < ActiveRecord::Base
5
5
  belongs_to :invoice
6
6
 
7
7
  money :amount, :currency => false
8
+
9
+ validates_numericality_of :amount, :greater_than_or_equal_to => 0
10
+ validate :amount_not_greater_than_payment_or_invoice_totals
11
+ validate :validate_invoice_is_published
12
+
13
+ # Ensure the assigned invoice is_published, otherwise, we shouldn't be able to mark it paid
14
+ def validate_invoice_is_published
15
+ errors.add :invoice, "can't be assigned to an unpublished invoice" if invoice and !invoice.is_published
16
+ end
17
+
18
+ def label
19
+ '%s @ (Invoice %d, Payment %d)' % [amount.format, invoice.id,payment.id, ]
20
+ end
21
+
22
+ # This is just to make the code a little easier to type/read. Its a create!, just without all the option verbosity.
23
+ # Note: We accept either and invoice object or invoice_id, and either a payment object or payment_id
24
+ def self.quick_create!(invoice_id, payment_id, amount)
25
+ InvoicePayment.create!(
26
+ :invoice_id => (invoice_id.class == Invoice) ? invoice_id.id : invoice_id,
27
+ :payment_id => (payment_id.class == Payment) ? payment_id.id : payment_id,
28
+ :amount => amount.to_money
29
+ )
30
+ end
31
+
32
+ # Here, we verify that newly created and/or updated InvoicePayments, won't have an amount which adds up to a greater value
33
+ # than would be possible for the associated invoice or payment
34
+ def amount_not_greater_than_payment_or_invoice_totals
35
+ conditions_fields = []
36
+ conditions_values = []
37
+
38
+ # If we're updating an existing payment, it gets a little more complicated:
39
+ if id
40
+ conditions_fields << 'id != ?'
41
+ conditions_values << id
42
+ end
43
+
44
+ errors.add :amount, "exceeds the payment's remainder amount" if payment_id and payment.amount < (
45
+ Money.new(
46
+ InvoicePayment.sum(
47
+ :amount_in_cents,
48
+ :conditions => [(conditions_fields+['payment_id = ?']).join(' AND ')]+conditions_values+[payment_id]
49
+ ).to_i
50
+ ) + amount)
51
+
52
+ # This could act flaky on you if you didn't specify activities for your invoice at creation time (and did specify invoice_payments)
53
+ # this , b/c we're checking the invoice amount below and unlike payments, invoices have no amount field
54
+ errors.add :amount, "exceeds the invoice's remainder balance" if invoice_id and invoice.amount < (
55
+ Money.new(
56
+ InvoicePayment.sum(
57
+ :amount_in_cents,
58
+ :conditions => [(conditions_fields+['invoice_id = ?']).join(' AND ')]+conditions_values+[invoice_id]
59
+ ).to_i
60
+ ) + amount)
61
+ end
8
62
  end
@@ -2,18 +2,17 @@ class Payment < ActiveRecord::Base
2
2
  include ExtensibleObjectHelper
3
3
  include MoneyModelHelper
4
4
 
5
- after_create :create_invoice_payments
6
- after_destroy :destroy_invoice_payments
7
- # NOTE: after_update is not needed , because payment's can't be updated...
8
-
9
5
  belongs_to :client
10
6
  belongs_to :payment_method
11
7
 
12
- has_many :invoices, :through => 'invoice_payments'
8
+ has_many :invoices, :through => :assigned_payments
9
+ has_many :invoice_assignments, :class_name => 'InvoicePayment', :dependent => :delete_all
13
10
 
14
11
  validates_presence_of :client_id, :payment_method_id
15
12
  validates_numericality_of :amount, :allow_nil => false
16
-
13
+ validates_numericality_of :amount, :greater_than_or_equal_to => 0
14
+ validate :validate_invoice_payments_not_greater_than_amount
15
+
17
16
  money :amount, :currency => false
18
17
 
19
18
  def initialize(*args)
@@ -21,58 +20,36 @@ class Payment < ActiveRecord::Base
21
20
  self.paid_on = Time.now.beginning_of_day if paid_on.nil?
22
21
  end
23
22
 
24
- def create_invoice_payments
25
- # NOTE: Orders by oldest outstanding date first:
26
- unpaid_invoices = Invoice.find_with_totals(
27
- :all,
28
- :conditions => [
29
- [
30
- 'client_id = ?',
31
- 'IF(activities_total.total_in_cents IS NULL, 0,activities_total.total_in_cents) - '+
32
- 'IF(invoices_total.total_in_cents IS NULL, 0,invoices_total.total_in_cents) > ?'
33
- ].join(' AND '),
34
- client_id,
35
- 0
36
- ]
37
- )
38
-
39
- current_client_balance = Money.new(0)
40
- unpaid_invoices.each { |inv| current_client_balance += inv.amount_outstanding }
41
-
42
- currently_unallocated = amount_unallocated
43
-
44
- unpaid_invoices.each do |unpaid_invoice|
45
- break if currently_unallocated <= 0 or current_client_balance <= 0
46
-
47
- payment_allocation = (currently_unallocated >= unpaid_invoice.amount_outstanding) ?
48
- unpaid_invoice.amount_outstanding :
49
- currently_unallocated
50
-
51
- InvoicePayment.create! :payment => self, :invoice => unpaid_invoice, :amount => payment_allocation
52
-
53
- current_client_balance -= payment_allocation
54
- currently_unallocated -= payment_allocation
55
- end
56
- end
57
-
58
- def destroy_invoice_payments
59
- InvoicePayment.destroy_all ['payment_id = ?', id]
60
- end
61
-
62
- def amount_unallocated
63
- (attribute_present? :amount_unallocated_in_cents) ?
23
+ def amount_unallocated( force_reload = false )
24
+ (attribute_present? :amount_unallocated_in_cents and !force_reload) ?
64
25
  Money.new(read_attribute(:amount_unallocated_in_cents).to_i) :
65
26
  (amount - amount_allocated)
66
27
  end
67
28
 
68
- def amount_allocated
29
+ def amount_allocated( force_reload = false )
69
30
  Money.new(
70
- (attribute_present? :amount_allocated_in_cents) ?
31
+ (attribute_present? :amount_allocated_in_cents and !force_reload) ?
71
32
  read_attribute(:amount_allocated_in_cents).to_i :
72
33
  ( InvoicePayment.sum(:amount_in_cents, :conditions => ['payment_id = ?', id]) || 0 )
73
34
  )
74
35
  end
75
36
 
37
+ def is_allocated?( force_reload = false )
38
+ (attribute_present? :is_allocated and !force_reload) ?
39
+ (read_attribute(:is_allocated).to_i == 1) :
40
+ amount_unallocated(true).zero?
41
+ end
42
+
43
+ def validate_invoice_payments_not_greater_than_amount
44
+ my_amount = self.amount
45
+ assignment_amount = self.invoice_assignments.inject(Money.new(0)){|sum,ip| ip.amount+sum }
46
+
47
+ # We use the funky :> /:< to differentiate between the case of a credit invoice and a (normal?) invoice
48
+ errors.add :invoice_assignments, "exceeds payment amount" if assignment_amount.send(
49
+ (my_amount >= 0) ? :> : :<, my_amount
50
+ )
51
+ end
52
+
76
53
  def validate_on_update
77
54
  errors.add_to_base "Payments can't be updated after creation"
78
55
  end