Class: Invoice

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

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

Returns a new instance of Invoice.



32
33
34
35
36
# File 'app/models/invoice.rb', line 32

def initialize(*args)
  super(*args)
  end_of_last_month = Time.utc(*Time.now.to_a).prev_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



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'app/models/invoice.rb', line 199

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',
        'invoices.is_published',
        'invoices.created_at',
        'invoices.updated_at',
        "#{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

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. THis was placed here, b/c its conceivable that in the future, we may support an array for the client_id parameter…



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
194
195
196
197
# File 'app/models/invoice.rb', line 168

def self.recommended_activities_for(for_client_id, occurred_on_or_before, included_activity_types, for_invoice_id = nil)
  for_client_id = for_client_id.id if for_client_id.class == Client
  for_invoice_id = for_invoice_id.id if for_invoice_id.class == Invoice
  
  included_activity_types = included_activity_types.collect{|a| a.label.downcase}
  
  conditions = [
    'is_published = ? AND client_id = ? AND DATEDIFF(occurred_on, DATE(?)) <= 0',
    true,
    for_client_id,
    occurred_on_or_before
  ]
  
  # Slightly more complicated, for the type includes:
  if included_activity_types and included_activity_types.size > 0
    conditions[0] += ' AND ('+(['activity_type = ?'] * included_activity_types.size).join(' OR ')+')'
    conditions.push *included_activity_types
  else
    conditions[0] += ' AND activity_type IS NULL'
  end
  
  if for_invoice_id
    conditions[0] += ' AND ( invoice_id IS NULL OR invoice_id = ? )'
    conditions << for_invoice_id
  else
    conditions[0] += ' AND invoice_id IS NULL'
  end

  Activity.find :all, :conditions => conditions
end

Instance Method Details

#amount(force_reload = false) ⇒ Object Also known as: grand_total



105
106
107
108
109
# File 'app/models/invoice.rb', line 105

def amount( force_reload = false )
  (attribute_present? :amount_in_cents and !force_reload) ?
    Money.new(read_attribute(:amount_in_cents).to_i) :
    self.activities.inject(Money.new(0)){|sum,a| sum + ((a.cost) ? a.cost : Money.new(0)) + ((a.tax) ? a.tax : Money.new(0)) }
end

#amount_outstanding(force_reload = false) ⇒ Object



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

def amount_outstanding( force_reload = false )
  (attribute_present? :amount_outstanding_in_cents and !force_reload) ? 
    Money.new(read_attribute(:amount_outstanding_in_cents).to_i) :
    (amount(force_reload) - amount_paid(force_reload))
end

#amount_paid(force_reload = false) ⇒ Object



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

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 :
    (payment_assignments(force_reload).collect(&:amount_in_cents).sum  || 0)
  )
end

#authorized_for?(options) ⇒ Boolean

Returns:

  • (Boolean)


80
81
82
83
84
85
86
87
88
89
90
91
# File 'app/models/invoice.rb', line 80

def authorized_for?(options)
  return true unless options.try(:[],:action)
  
  case options[:action].to_sym
    when :delete
      !is_published
    when :edit
      !is_published
    else
      true
  end
end

#ensure_not_published_on_destroyObject



52
53
54
55
56
57
# File 'app/models/invoice.rb', line 52

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



59
60
61
# File 'app/models/invoice.rb', line 59

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

#invalid_if_published(collection_record = nil) ⇒ Object



42
43
44
# File 'app/models/invoice.rb', line 42

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

Returns:

  • (Boolean)


46
47
48
49
50
# File 'app/models/invoice.rb', line 46

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

Returns:

  • (Boolean)


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

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

#long_nameObject



117
118
119
120
121
122
123
124
# File 'app/models/invoice.rb', line 117

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

#nameObject



113
114
115
# File 'app/models/invoice.rb', line 113

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


126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'app/models/invoice.rb', line 126

def paid_on
  raise StandardError unless is_paid?(true)

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

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 activities inclusion



162
163
164
# File 'app/models/invoice.rb', line 162

def recommended_activities
  Invoice.recommended_activities_for client_id, issued_on, self.activity_types, self.id
end

#sub_total(force_reload = false) ⇒ Object



99
100
101
102
103
# File 'app/models/invoice.rb', line 99

def sub_total( force_reload = false )
  (attribute_present? :cost_in_cents and !force_reload) ?
    Money.new(read_attribute(:cost_in_cents).to_i) :
    self.activities.inject(Money.new(0)){|sum,a| sum + ((a.cost) ? a.cost : Money.new(0)) }
end

#taxes_total(force_reload = false) ⇒ Object



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

def taxes_total( force_reload = false )
  (attribute_present? :tax_in_cents and !force_reload) ?
    Money.new(read_attribute(:tax_in_cents).to_i) :
    self.activities.inject(Money.new(0)){|sum,a| sum + ((a.tax) ? a.tax : Money.new(0)) }
end

#validate_invoice_payments_not_greater_than_amountObject



72
73
74
75
76
77
78
# File 'app/models/invoice.rb', line 72

def validate_invoice_payments_not_greater_than_amount
  inv_amount = self.amount
  assignment_amount = self.payment_assignments.inject(Money.new(0)){|sum,ip| ip.amount+sum }
  
  # We use the funky :> /:< to differentiate between the case of a credit invoice and a (normal?) invoice
  errors.add :payment_assignments, "exceeds invoice amount" if inv_amount >= 0 and self.amount < assignment_amount
end

#validate_on_updateObject



63
64
65
66
67
68
69
70
# File 'app/models/invoice.rb', line 63

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| /(?:is_published|payment_assignments)/.match k}.length > 0

end

#validate_payment_assignments_only_if_publishedObject



38
39
40
# File 'app/models/invoice.rb', line 38

def validate_payment_assignments_only_if_published
  errors.add :payment_assignments, "can only be set for published invoices" if !is_published and payment_assignments and payment_assignments.length > 0 
end