I consider myself still fairly new to Rails after about a year of learning and I still seem to get caught up on some things that I shouldn't. I'm working on an application that needs to care about money to some extent and I was hoping to avoid the pain of dealing with floating point issues. I've been there numerous times in the past and it is never enjoyable and sometimes results in embarrassing situations as you try to explain why somethings don't add up correctly or compare well. These experiences have me leaning towards storing money as integer values in the database and working with them as pennies. So I was happy to see someone has already done most of the work in Ruby and there is a nice plug-in for Rails to simplify the code in the model.
If you're dealing with money in your Rails application the first thing you'll need is the Money gem and then you'll likely want the act_as_money plug-in. It seems that most things in the Ruby and Rails communities these days have forked all over God's green earth and it is often a challenge trying to figure out what I should use. (Am I the only one seeing and thinking this?) I dug for an hour or so and read through numerous posts before making my decision. Because I'm using Ruby 1.8.7 and Rails 2.3.5, I decided to go with the original Money gem (currently 2.1.5 updated November 2009) instead of the collectiveidea Money gem. For those of you with Rails 2.1 or earlier you might want to consider using the collectiveidea-money gem as it appears from my reading that you'll have more success there. It's that or bite the bullet and move to a more recent version of Rails. I also opted to use the act_as_money plug-in from collectiveidea (updated 11-19-2008). Again, for anyone on Rails 2.1 you will want to pull that plug-in from the repository using the 2.1 tag.
The play by play
- First install the Money gem.
gem install money
- Then from inside your Rails project install the act_as_money plug-in
script/plugin install git://github.com/collectiveidea/acts_as_money.git
- The next thing you'll want to do is create a migration if your existing models with non-integer money values. In my case I was using decimal instead of integer.
def self.up change_column(:invoice_items, :rate, :integer) change_column(:invoice_items, :amount, :integer) change_column(:invoices, :taxable_amount, :integer) change_column(:invoices, :non_taxable_amount, :integer) change_column(:invoices, :taxes, :integer) change_column(:invoices, :total, :integer) rename_column(:invoice_items, :rate, :rate_in_cents) rename_column(:invoice_items, :amount, :amount_in_cents) rename_column(:invoices, :taxable_amount, :taxable_amount_in_cents ) rename_column(:invoices, :non_taxable_amount,:non_taxable_amount_in_cents ) rename_column(:invoices, :taxes, :taxes_in_cents) rename_column(:invoices, :total, :total_in_cents) end
class ModifyMoneyFields < ActiveRecord::Migration def ModifyMoneyFields::fixDollarsToCents(model, fields) records = model.find(:all) records.each do |rec| fields.each do |field| d = rec.send(field) d = d * 100 rec.update_attribute(field,d) end end end def ModifyMoneyFields::fixCentsToDollars(model, fields) records = model.find(:all) records.each do |rec| fields.each do |field| d = rec.send(field) d = d / 100.0 rec.update_attribute(field,d) end end end def self.up fixDollarsToCents(Invoice, [:taxable_amount, :non_taxable_amount, :taxes, :total]) fixDollarsToCents(InvoiceItem, [:rate, :amount])
class Invoice < Blameable belongs_to :term has_many :invoice_items # Note: The :currency => false means NO currency field in the model, if not supplied the plugin defaults to US dollars and you need a field in your model # Note: You can also provide :cents => :total_pennies if you opted for different field names money :taxable_amount, :currency => false money :non_taxable_amount, :currency => false money :taxes, :currency => false money :total, :currency => false
Something that tripped me up for a few minutes...
In some of my validation code for the models I was comparing the "new" Money fields to a fixed number such as:
def validate if rate == 0 errors.add(:rate, "must not be zero.") end endThis caused my code to freak out tossing the following exception:
undefined method `cents' for 0:Fixnum
To fix this just change your code to:
def validate if rate == 0.to_money errors.add(:rate, "must not be zero.") end endThe Money gem has core extensions for Number and String which adds the to_money() method.
Once I decided to make the switch I was able to make changes related 22 fields and 14 models in about 1 hour. All my model tests confirmed things were working correctly. Hopefully you found this helpful. Good luck!
The documentation for the Money gem
The Act_As_Money plugin
post on act_as_money
An good simple post on composed_of also referencing Money and act_as_money