Search code examples
validationruby-on-rails-4nested-attributes

Before validation on nested model


I have a nested model items and I am trying to multiply two columns together cost and quantity to set the last column price. I need to set the column price before the form is saved and i need to also validate the model. The before_validation call back obviously breaks if the cost and quantity is not set before saving. Any ideas of how I can multiply these columns together and also validate the model?

Here is my code item.rb

class Item < ActiveRecord::Base
belongs_to :invoice

 validates :invoice_id, presence: true
 validates :name, presence: true
 validates_presence_of :quantity, :cost, :price

 before_validation :set_price

 def set_price
    self.price = cost * quantity.round(2)
 end
end

and here is my parent model invoice.rb

class Invoice < ActiveRecord::Base
belongs_to :user
has_many :items

accepts_nested_attributes_for :items, :reject_if => :all_blank, :allow_destroy => true

validates :sender, presence: true
validates_associated :items

before_save :increment_invoice_number, :set_amount

private

def increment_invoice_number
    if published == true
        self.invoice_number = user.invoices.where(:published => true).count + 1
    end
end

def set_amount
    self.amount = items.map(&:price).sum             
end

end


Solution

  • If you're always going to update the price on every save, you don't need to validate it's presence, so just remove that from your validations and add a before_save callback, which will get called after the validations, so you'll be guaranteed to have valid cost and quantity fields. Here's a link to the place in the Rails Guides where this is discussed: http://guides.rubyonrails.org/active_record_callbacks.html#available-callbacks

    Here's an example:

    class Item < ActiveRecord::Base
      belongs_to :invoice
    
      validates :invoice_id, presence: true
      validates :name, presence: true
      validates_presence_of :quantity, :cost
    
      before_save :set_price
    
      def set_price
        self.price = cost * quantity.round(2)
      end
    end
    

    If you want to set the price only on creation or update, you can use the before_create or before_update callback instead. Alternatively, you can optionally set the price if it isn't already set:

    self.price ||= cost * quantity.round(2)

    Is this what you are trying to achieve?

    Update: After some discussion (see comments), it seems like the best way to handle this might be to update all child items in a before_save callback on the parent invoice, since this will be called every time an invoice is saved. Below is an example implementation - I also removed a few validates statements from your original code and combined everything using the new syntax. Also, the items will be validated regardless when updated through a nested_attributes association.

    class Item < ActiveRecord::Base
      belongs_to :invoice
    
      validates :invoice_id, :name, :quantity, :cost, presence: true
    
      def set_price
        self.price = cost * quantity.round(2)
      end
    end
    
    class Invoice < ActiveRecord::Base
      belongs_to :user
      has_many :items
    
      accepts_nested_attributes_for :items, :reject_if => :all_blank, :allow_destroy => true
    
      validates :sender, presence: true
    
      before_save :increment_invoice_number, :update_prices, :set_amount
    
      private
    
      def increment_invoice_number
        if published == true
          self.invoice_number = user.invoices.where(:published => true).count + 1
        end
      end
    
      def update_prices
        items.each(&:set_price)
      end
    
      def set_amount
        self.amount = items.map(&:price).sum             
      end
    end