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