brisk-bills 0.6.0 → 0.7.0

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