Class: Invoice

Inherits:
ActiveRecord::Base
  • Object
show all
Includes:
ExtensibleObjectHelper
Defined in:
app/models/invoice.rb

Direct Known Subclasses

InvoicesWithTotal

Constant Summary collapse

ACTIVITY_TOTAL_SQL =

This just ends up being useful in a couple places

'(IF(activities.cost_in_cents IS NULL, 0, activities.cost_in_cents)+IF(activities.tax_in_cents IS NULL, 0, activities.tax_in_cents))'

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ExtensibleObjectHelper

append_features

Constructor Details

#initialize(*args) ⇒ Invoice



34
35
36
37
38
# File 'app/models/invoice.rb', line 34

def initialize(*args)
  super(*args)
  end_of_last_month = Time.utc(*Time.now.to_a).last_month.end_of_month
  self.issued_on = end_of_last_month unless self.issued_on
end

Class Method Details

.find_with_totals(how_many = :all, options = {}) ⇒ Object



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'app/models/invoice.rb', line 227

def self.find_with_totals( how_many = :all, options = {} )
  joins = []
  joins << 'LEFT JOIN ('+
    "SELECT invoices.id AS invoice_id, SUM(#{ACTIVITY_TOTAL_SQL}) AS total_in_cents"+
    ' FROM invoices'+
    ' LEFT JOIN activities ON activities.invoice_id = invoices.id'+
    ' GROUP BY invoices.id'+
  ') AS activities_total ON activities_total.invoice_id = invoices.id'
  
  joins << 'LEFT JOIN ('+
    'SELECT invoices.id AS invoice_id, SUM(invoice_payments.amount_in_cents) AS total_in_cents'+
    ' FROM invoices'+
    ' LEFT JOIN invoice_payments ON invoice_payments.invoice_id = invoices.id'+
    ' GROUP BY invoices.id'+
  ') AS invoices_total ON invoices_total.invoice_id = invoices.id'

  cast_amount = 'IF(activities_total.total_in_cents IS NULL, 0,activities_total.total_in_cents)'
  cast_amount_paid = 'IF(invoices_total.total_in_cents IS NULL, 0,invoices_total.total_in_cents)'

  Invoice.find( 
    how_many,
    {
      :select => [
        'invoices.id',
        'invoices.client_id',
        'invoices.comments',
        'invoices.issued_on',
        "#{cast_amount} AS amount_in_cents",
        "#{cast_amount_paid} AS amount_paid_in_cents",
        "#{cast_amount} - #{cast_amount_paid} AS amount_outstanding_in_cents"
      ].join(', '),
      :order => 'issued_on ASC',
      :joins => joins.join(' ')
    }.merge(options)
  )
end

Instance Method Details

#amount(force_reload = false) ⇒ Object



132
133
134
135
136
# File 'app/models/invoice.rb', line 132

def amount( force_reload = false )
  (attribute_present? :amount_in_cents  and !force_reload) ?
    Money.new(read_attribute(:amount_in_cents).to_i) :
    process_total( :amount, ACTIVITY_TOTAL_SQL )
end

#amount_outstandingObject



223
224
225
# File 'app/models/invoice.rb', line 223

def amount_outstanding
  amount - amount_paid
end

#amount_paid(force_reload = false) ⇒ Object



215
216
217
218
219
220
221
# File 'app/models/invoice.rb', line 215

def amount_paid( force_reload = false )
  Money.new( 
    (attribute_present? :amount_paid_in_cents and !force_reload) ? 
    read_attribute(:amount_paid_in_cents).to_i :
    InvoicePayment.sum( :amount_in_cents, :conditions => ['invoice_id = ?', id] ).to_i
  )
end

#authorized_for?(options) ⇒ Boolean



264
265
266
267
268
269
270
271
# File 'app/models/invoice.rb', line 264

def authorized_for?(options)
  case options[:action].to_s
    when /^(update|destroy)$/
      (is_published and !is_most_recent_invoice?) ? false : true
    else
      true
  end
end

#ensure_not_published_on_destroyObject



101
102
103
104
105
106
# File 'app/models/invoice.rb', line 101

def ensure_not_published_on_destroy
  if is_published and !changes.has_key? :is_published
    errors.add_to_base "Can't destroy a published invoice"
    return false
  end
end

#ensure_not_published_on_updateObject



108
109
110
# File 'app/models/invoice.rb', line 108

def ensure_not_published_on_update
  errors.add_to_base "Can't update a published invoice" if is_published and !changes.has_key? :is_published
end

#ensure_were_the_most_recentObject



94
95
96
97
98
99
# File 'app/models/invoice.rb', line 94

def ensure_were_the_most_recent
  unless is_most_recent_invoice?
    errors.add_to_base "Can't destroy an invoice if its not the client's most recent invoice"
    return false
  end
end

#grand_totalObject



138
139
140
# File 'app/models/invoice.rb', line 138

def grand_total
  process_total :grand_total, ACTIVITY_TOTAL_SQL
end

#invalid_if_published(collection_record = nil) ⇒ Object



40
41
42
# File 'app/models/invoice.rb', line 40

def invalid_if_published(collection_record = nil)
  raise "Can't adjust an already-published invoice." if !new_record? and is_published
end

#is_most_recent_invoice?Boolean



88
89
90
91
92
# File 'app/models/invoice.rb', line 88

def is_most_recent_invoice?
  newest_invoice = Invoice.find :first, :select => 'id', :order => 'issued_on DESC', :conditions => ['client_id = ?', client_id]

  (newest_invoice.nil? or newest_invoice.id == id) ? true : false
end

#is_paid?(force_reload = false) ⇒ Boolean



209
210
211
212
213
# File 'app/models/invoice.rb', line 209

def is_paid?( force_reload = false )
  (attribute_present? :is_paid  and !force_reload) ? 
    (read_attribute(:is_paid).to_i == 1) :
    amount_outstanding.zero?
end

#long_nameObject



146
147
148
149
150
151
152
153
# File 'app/models/invoice.rb', line 146

def long_name
  "Invoice #%d (%s) - %s (%s)" % [
    id,
    issued_on.strftime("%m/%d/%Y %I:%M %p"),
    client.company_name,
    ('$%.2f' % amount.to_s).gsub(/(\d)(?=\d{3}+(\.\d*)?$)/, '\1,')
  ]
end

#mark_invoice_paymentsObject



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'app/models/invoice.rb', line 159

def mark_invoice_payments   
  if changes.has_key? "is_published"
    remove_invoice_payments
    
    if is_published
      unallocated_payments = Payment.find_with_totals( 
        :all, 
        :conditions => [
          'client_id = ? AND (payments.amount_in_cents - IF(payments_total.amount_allocated_in_cents IS NULL, 0, payments_total.amount_allocated_in_cents) ) > ?', 
          client_id, 
          0
        ] 
      )
  
      current_client_balance = 0.0.to_money
      unallocated_payments.each { |pmnt| current_client_balance -= pmnt.amount_unallocated }
      
      invoice_balance = amount
  
      unallocated_payments.each do |unallocated_pmnt|
        break if invoice_balance == 0 or current_client_balance >= 0
        
        payment_allocation = (unallocated_pmnt.amount_unallocated > invoice_balance) ?
          invoice_balance :
          unallocated_pmnt.amount_unallocated
        
        InvoicePayment.create! :invoice => self, :payment => unallocated_pmnt, :amount => payment_allocation
        
        invoice_balance -= payment_allocation
        current_client_balance += payment_allocation
      end        
    end
  end
  
end

#nameObject



142
143
144
# File 'app/models/invoice.rb', line 142

def name  
  '"%s" Invoice on %s'  % [ (client) ? client.company_name : '(Unknown Client)', issued_on.strftime("%m/%d/%Y %I:%M %p") ]
end


195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'app/models/invoice.rb', line 195

def paid_on
  raise StandardError unless is_paid?

  InvoicePayment.find(
    :first,
    :order => 'payments.paid_on DESC', 
    :include => [:payment],
    :conditions => ['invoice_id = ?', id]
  ).payment.paid_on
  
  rescue
    nil
end

#reattach_activitiesObject



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'app/models/invoice.rb', line 44

def reattach_activities
  included_activity_types = activity_types.collect{ |a| a.label.downcase }
  unincluded_activity_types = ActivityType.find(:all).collect{ |a| a.label.downcase } - included_activity_types

  # First we NULL'ify (remove) existing attachments that no longer should be:
  nullify_conditions = []
  nullify_parameters = [] 
      
  # Conditions for occurance adjutments
  nullify_conditions << '(DATEDIFF(occurred_on, DATE(?)) > 0)'
  nullify_parameters << issued_on
  
  # For the ActivityType Adjustments:
  unless unincluded_activity_types.empty?
    nullify_conditions << '(%s)' % ( ['activity_type = ?'] * unincluded_activity_types.size).join(' OR ')
    nullify_parameters += unincluded_activity_types
  end

  Activity.update_all( 
    'invoice_id = NULL', 
    [ ['invoice_id = ?', 'is_published = ?', ('(%s)' % nullify_conditions.join(' OR ')) ].join(' AND ') ]+
    [id, true]+nullify_parameters
  ) unless new_record?
  
  # Now we attach the new records :
  update_where = [
    ['invoice_id IS NULL'],
    ['is_published = ?', true],
    ['client_id = ?', client_id],
    ['DATEDIFF(occurred_on, DATE(?)) <= 0', issued_on],
    
    # Slightly more complicated, for the type includes:
    ( (included_activity_types.size > 0) ? 
      [ '('+(['activity_type = ?'] * included_activity_types.size).join(' OR ')+')', included_activity_types ] :
      [ 'activity_type IS NULL' ] )
  ]
  
  Activity.update_all(
    ['invoice_id = ?', id ],
    # This is what ActiveRecord actually expects...
    update_where.collect{|c| c[0]}.join(' AND ').to_a + update_where.reject{|c| c.length < 2 }.collect{|c| c[1]}.flatten
  )
end

#remove_invoice_paymentsObject



155
156
157
# File 'app/models/invoice.rb', line 155

def remove_invoice_payments
  InvoicePayment.destroy_all ['invoice_id = ?', id]    
end

#sub_totalObject



128
129
130
# File 'app/models/invoice.rb', line 128

def sub_total
  process_total :sub_total, :cost_in_cents
end

#taxes_totalObject



124
125
126
# File 'app/models/invoice.rb', line 124

def taxes_total
  process_total :taxes_total, :tax_in_cents
end

#validate_on_updateObject



112
113
114
115
116
117
118
119
120
121
122
# File 'app/models/invoice.rb', line 112

def validate_on_update
  errors.add :client, "can't be updated after creation" if changes.has_key? "client_id"

  errors.add_to_base(
    "Invoice can't be updated once published."
  ) if is_published and changes.reject{|k,v| k == 'is_published'}.length > 0

  errors.add_to_base(
    "Invoice can't be unpublished, unless its the newest invoice in the client's queue."
  ) if changes.has_key?('is_published') and is_published_was and !is_most_recent_invoice?
end