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.
- data/CHANGELOG +6 -1
- data/TODO.txt +60 -38
- data/app/controllers/admin/employees_controller.rb +1 -1
- data/app/controllers/admin/invoices_controller.rb +38 -2
- data/app/controllers/admin/payments_controller.rb +13 -6
- data/app/helpers/admin/activity_tax_field_helper.rb +1 -1
- data/app/helpers/admin/activity_type_field_helper.rb +2 -2
- data/app/helpers/admin/payments_helper.rb +8 -2
- data/app/helpers/application_helper.rb +9 -2
- data/app/model_views/invoices_with_total.rb +5 -0
- data/app/models/activity.rb +7 -3
- data/app/models/activity/labor.rb +1 -1
- data/app/models/activity/labor/slimtimer.rb +2 -0
- data/app/models/client.rb +93 -3
- data/app/models/client_representative.rb +9 -1
- data/app/models/employee.rb +21 -1
- data/app/models/employee/slimtimer.rb +11 -0
- data/app/models/invoice.rb +93 -129
- data/app/models/invoice_payment.rb +54 -0
- data/app/models/payment.rb +25 -48
- data/config/boot.rb +1 -1
- data/db/migrate/029_invoices_with_totals_view_adjustment.rb +29 -0
- data/db/schema.rb +1 -1
- data/lib/brisk-bills.rb +1 -1
- data/lib/tasks/create_last_months_invoices.rake +8 -3
- data/public/javascripts/prototype.js +1573 -1019
- data/public/javascripts/prototype.js-1.6.0.3 +4320 -0
- data/test/test_unit_factory_helper.rb +16 -9
- data/test/unit/client_test.rb +298 -2
- data/test/unit/invoice_payment_test.rb +313 -3
- data/test/unit/invoice_test.rb +49 -36
- data/test/unit/payment_test.rb +35 -31
- data/vendor/plugins/active_scaffold_full_refresh/lib/active_scaffold_full_refresh.rb +4 -2
- 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
|
data/app/models/employee.rb
CHANGED
@@ -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
|
data/app/models/invoice.rb
CHANGED
@@ -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
|
-
|
11
|
-
|
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 =>
|
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
|
41
|
-
|
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
|
45
|
-
|
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
|
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
|
125
|
-
|
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
|
-
|
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
|
102
|
+
(attribute_present? :amount_in_cents and !force_reload) ?
|
134
103
|
Money.new(read_attribute(:amount_in_cents).to_i) :
|
135
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
276
|
-
|
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
|
data/app/models/payment.rb
CHANGED
@@ -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 =>
|
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
|
25
|
-
|
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
|