2

Clients have many Invoices. Invoices have a number attribute that I want to initialize by incrementing the client's previous invoice number.

For example:

@client = Client.find(1)
@client.last_invoice_number
> 14
@invoice = @client.invoices.build
@invoice.number
> 15

I want to get this functionality into my Invoice model, but I'm not sure how to. Here's what I'm imagining the code to be like:

class Invoice < ActiveRecord::Base
  ...
  def initialize(attributes = {})
    client = Client.find(attributes[:client_id])
    attributes[:number] = client.last_invoice_number + 1
    client.update_attributes(:last_invoice_number => client.last_invoice_number + 1)
  end
end

However, attributes[:client_id] isn't set when I call @client.invoices.build.

How and when is the invoice's client_id initialized, and when can I use it to initialize the invoice's number? Can I get this logic into the model, or will I have to put it in the controller?

2
  • I don't follow. Do you want last invoice's number (like a serial number) or you want last_invoice_number to reflect how many invoices user has? Commented Jun 17, 2010 at 4:36
  • last_invoice_number is a serial number, per client. So if I just sent an invoice to client=4, and it had number=42, the next invoice I send to that client will have number=43. Commented Jun 17, 2010 at 5:11

4 Answers 4

7

Generate a migration that adds invoices_number column to users table. Then in Invoice model write this:

class Invoice < ActiveRecord::Base
  belongs_to :user, :counter_cache => true
  ...
end

This will automatically increase invoices_count attribute for user once the invoice is created.

Sign up to request clarification or add additional context in comments.

1 Comment

Wow, I didn't know you could do this! This is very useful.
6

how about this:

class Invoice < ActiveRecord::Base
  ...
  def initialize(attributes = {})
    super
    self.number = self.client.invoices.size + 1 unless self.client.nil?
  end
end

8 Comments

Ah... facepalm moment. Eimantas takes the cake but this is still very useful to me!
I should add that this method doesn't seem to work when I call @client.invoices.build - it only works with Invoice.new(:client_id => @client.id). Not sure why :S
I'd love to know why it doesn't work when building an object instance through a relationship. I'd also like to know how to ensure a particular line of code is executed no matter how a rails AR object comes to life.
after_initialize is a callback that isn't widely documented or used that is extremely relevant to my previous comment!
Probably more importantly, I also read that overriding initialize is a bad idea in general: blog.dalethatcher.com/2008/03/…
|
2

Here is some useful discussion on after_initialize per Jonathan R. Wallace's comment above:

Rails: Don't override initialize on ActiveRecord objects

ActiveRecord::Base doesn't always use new to create objects, so initialize might not be called. I wanted to use a Hash on an ActiveRecord::Base subclass to store some calculated values, so I naively did this:

class User < ActiveRecord::Base
 def initialize(args = nil)
   super
   @my_cache = {}
 end
end

However I quickly ran into some "You have a nil object when you didn't expect it!" issues. Some debugger investigation revealed that the @my_cache variable wasn't being set when I called find_or_create_ if the object already existed in the database. Digging in the source revealed that the instantiate method in active_record/base.rb uses allocate to create classes rather than new. This means the initialize method is being neatly sidestepped when creating objects from the database. The solution is to use the 'after_initialize' callback:

class User < ActiveRecord::Base
 def after_initialize
   @my_cache = {}
 end
end

One further note of caution, When passing parameters into a new or create method the after_initialize is called after the parameters have been set. So you can't rely on the initialization being done before overridden accessors are called.

From http://blog.dalethatcher.com/2008/03/rails-dont-override-initialize-on.html

Comments

1

first of all, you don't need to use the attributes collection, you can just do self.client_id. Better yet, as long as you have a belongs_to :client in your Invoice, you could just do self.client.last_invoice_number. Lastly, you almost always want to raise an exception if an update or create fails, so get used to using update_attributes!, which is a better default choice. (if you have any questions about those points, ask, and I'll go into more detail)

Now that that is out of the way, you ran into a bit of a gotcha with ActiveRecord, initializer methods are almost never the right choice. AR gives you a bunch of methods to hook into whatever point of the lifecycle you need to. These are

after_create
after_destroy
after_save
after_update
after_validation
after_validation_on_create
after_validation_on_update
before_create
before_destroy
before_save
before_update
before_validation
before_validation_on_create
before_validation_on_update

What you probably want is to hook into before_create. Something like this

def before_create
  self.number ||= self.client.last_invoice_number + 1 unless self.client
end

What that will do is it will hit up the database for your client, get the last invoice number, increment it by one, and set it as its new number, but only if you haven't already set a number (||= will assign, but only if the left side is nil), and only if you have set a client (or client_id) before the save.

1 Comment

Neither attributes[:client_id] nor self.client_id were initialized when I was trying to access them (before calling super; see Patrick Klingemann's answer below). The before_create callback wasn't suitable in this case as @invoice.number won't be visible in the form_for @invoice in the view - it isn't assigned until @invoice.save is called. Thanks for the answer though!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.