Skip to content

Commit

Permalink
Add currency support to models.
Browse files Browse the repository at this point in the history
Fixes #2197

Add currency parameter support to Spree::Money.

Spree::Money now allows an optional currency parameter to be passed in,
which will be used if specified.  In the case where it is not specified,
the global spree configuration value will be used instead.

The foundation of multi-currency support is going to be modifying the
code to support storing the currency in the various objects used by
Spree.

The goal is to modify the code to allow:

- adjustments
- line items
- orders
- payments
- return authorizations
- shipments
- shipping rates
- variants

to eventually store and track their own currency.

Currently these objects mirror the global currency value.  As code
support improves, this will be stored in the database for these models,
or inferred from dependent models (for example, determining the line
item currency from the order currency).

Flagged money helper as deprecated

The money helper is based around having a single site wide currency.
This is instead being moved to a series of display_* calls on the models
which have both the monetary amount and currency to produce its own
Spree::Money object, which then allows us to display a properly
formatted currency on the output.

Split prices from variants & store currency in DB.

This will allow a variant to have many prices in many different
currencies.

Store currency in database.

Add currency to line items.

Line items now support currency, which can be tracked on a line
item basis.

Add currency support to orders.

Infer payment currency from order.

Support currency on shipping.

Base currencies off of order currency.

Adjustment, Return Auth, and Shipment now infer their currency from the
currency of their order.
  • Loading branch information
Gregor MacDougall authored and radar committed Nov 22, 2012
1 parent 7ddf7fe commit 58ec5cd
Show file tree
Hide file tree
Showing 80 changed files with 910 additions and 211 deletions.
6 changes: 3 additions & 3 deletions core/app/controllers/spree/admin/products_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ def collection
page(params[:page]).
per(Spree::Config[:admin_products_per_page])

if params[:q][:s].include?("master_price")
if params[:q][:s].include?("master_default_price_amount")
# PostgreSQL compatibility
@collection = @collection.group("spree_variants.price")
@collection = @collection.group("spree_prices.amount")
end
@collection
end
Expand All @@ -97,7 +97,7 @@ def update_before
end

def product_includes
[{:variants => [:images, {:option_values => :option_type}]}, {:master => :images}]
[{:variants => [:images, {:option_values => :option_type}]}, {:master => [:images, :default_price]}]
end

end
Expand Down
11 changes: 7 additions & 4 deletions core/app/controllers/spree/admin/reports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,14 @@ def sales_total

@search = Order.complete.ransack(params[:q])
@orders = @search.result
@item_total = @orders.sum(:item_total)
@adjustment_total = @orders.sum(:adjustment_total)
@sales_total = @orders.sum(:total)

respond_with
@totals = {}
@orders.each do |order|
@totals[order.currency] = { :item_total => ::Money.new(0, order.currency), :adjustment_total => ::Money.new(0, order.currency), :sales_total => ::Money.new(0, order.currency) } unless @totals[order.currency]
@totals[order.currency][:item_total] += order.display_item_total.money
@totals[order.currency][:adjustment_total] += order.display_adjustment_total.money
@totals[order.currency][:sales_total] += order.display_total.money
end
end

end
Expand Down
2 changes: 2 additions & 0 deletions core/app/controllers/spree/admin/variants_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ def create_before
def new_before
@object.attributes = @object.product.master.attributes.except('id', 'created_at', 'deleted_at',
'sku', 'is_master', 'count_on_hand')
# Shallow Clone of the default price to populate the price field.
@object.default_price = @object.product.master.default_price.clone
end

def collection
Expand Down
4 changes: 2 additions & 2 deletions core/app/controllers/spree/orders_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ def populate
params[:products].each do |product_id,variant_id|
quantity = params[:quantity].to_i if !params[:quantity].is_a?(Hash)
quantity = params[:quantity][variant_id].to_i if params[:quantity].is_a?(Hash)
@order.add_variant(Variant.find(variant_id), quantity) if quantity > 0
@order.add_variant(Variant.find(variant_id), quantity, selected_currency) if quantity > 0
end if params[:products]

params[:variants].each do |variant_id, quantity|
quantity = quantity.to_i
@order.add_variant(Variant.find(variant_id), quantity) if quantity > 0
@order.add_variant(Variant.find(variant_id), quantity, selected_currency) if quantity > 0
end if params[:variants]

fire_event('spree.cart.add')
Expand Down
1 change: 1 addition & 0 deletions core/app/helpers/spree/base_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ def gem_available?(name)
end

def money(amount)
ActiveSupport::Deprecation.warn("[SPREE] Spree::BaseHelper#money will be deprecated. It relies upon a single master currency. You can instead create a Spree::Money.new(amount, { :currency => your_currency}) or see if the object you're working with returns a Spree::Money object to use.")
Spree::Money.new(amount)
end

Expand Down
6 changes: 3 additions & 3 deletions core/app/helpers/spree/products_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ def variant_price_diff(variant)
diff = variant.price - variant.product.price
return nil if diff == 0
if diff > 0
"(#{t(:add)}: #{Spree::Money.new(diff.abs)})"
"(#{t(:add)}: #{Spree::Money.new(diff.abs, { :currency => variant.currency })})"
else
"(#{t(:subtract)}: #{Spree::Money.new(diff.abs)})"
"(#{t(:subtract)}: #{Spree::Money.new(diff.abs, { :currency => variant.currency })})"
end
end

# returns the formatted full price for the variant, if at least one variant price differs from product price
def variant_full_price(variant)
product = variant.product
unless product.variants.active.all? { |v| v.price == product.price }
Spree::Money.new(variant.price).to_s
Spree::Money.new(variant.price, { :currency => variant.currency }).to_s
end
end

Expand Down
6 changes: 5 additions & 1 deletion core/app/models/spree/adjustment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,12 @@ def update!(src = nil)
set_eligibility
end

def currency
adjustable.nil? ? Spree::Config[:currency] : adjustable.currency
end

def display_amount
Spree::Money.new(amount).to_s
Spree::Money.new(amount, { :currency => currency }).to_s
end

private
Expand Down
1 change: 1 addition & 0 deletions core/app/models/spree/app_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class AppConfiguration < Preferences::Configuration
preference :orders_per_page, :integer, :default => 15
preference :prices_inc_tax, :boolean, :default => false
preference :products_per_page, :integer, :default => 12
preference :require_master_price, :boolean, :default => true
preference :shipment_inc_vat, :boolean, :default => false
preference :shipping_instructions, :boolean, :default => false # Request instructions/info for shipping
preference :show_descendents, :boolean, :default => true
Expand Down
4 changes: 3 additions & 1 deletion core/app/models/spree/calculator/flat_rate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
module Spree
class Calculator::FlatRate < Calculator
preference :amount, :decimal, :default => 0
attr_accessible :preferred_amount
preference :currency, :string, :default => Spree::Config[:currency]

attr_accessible :preferred_amount, :preferred_currency

def self.description
I18n.t(:flat_rate_per_order)
Expand Down
3 changes: 2 additions & 1 deletion core/app/models/spree/calculator/flexi_rate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ class Calculator::FlexiRate < Calculator
preference :first_item, :decimal, :default => 0.0
preference :additional_item, :decimal, :default => 0.0
preference :max_items, :integer, :default => 0
preference :currency, :string, :default => Spree::Config[:currency]

attr_accessible :preferred_first_item, :preferred_additional_item, :preferred_max_items
attr_accessible :preferred_first_item, :preferred_additional_item, :preferred_max_items, :preferred_currency

def self.description
I18n.t(:flexible_rate)
Expand Down
3 changes: 2 additions & 1 deletion core/app/models/spree/calculator/per_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
module Spree
class Calculator::PerItem < Calculator
preference :amount, :decimal, :default => 0
preference :currency, :string, :default => Spree::Config[:currency]

attr_accessible :preferred_amount
attr_accessible :preferred_amount, :preferred_currency

def self.description
I18n.t(:flat_rate_per_item)
Expand Down
4 changes: 3 additions & 1 deletion core/app/models/spree/calculator/price_sack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ class Calculator::PriceSack < Calculator
preference :minimal_amount, :decimal, :default => 0
preference :normal_amount, :decimal, :default => 0
preference :discount_amount, :decimal, :default => 0
preference :currency, :string, :default => Spree::Config[:currency]

attr_accessible :preferred_minimal_amount,
:preferred_normal_amount,
:preferred_discount_amount
:preferred_discount_amount,
:preferred_currency

def self.description
I18n.t(:price_sack)
Expand Down
16 changes: 15 additions & 1 deletion core/app/models/spree/line_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ class LineItem < ActiveRecord::Base
after_destroy :update_order

def copy_price
self.price = variant.price if variant && price.nil?
if variant
self.price = variant.price if price.nil?
self.currency = variant.currency if currency.nil?
end
end

def increment_quantity
Expand All @@ -40,6 +43,17 @@ def amount
end
alias total amount

def single_money
Spree::Money.new(price, { :currency => currency })
end
alias single_display_amount single_money

def money
Spree::Money.new(amount, { :currency => currency })
end
alias display_total money
alias display_amount money

def adjust_quantity
self.quantity = 0 if quantity.nil? || quantity < 0
end
Expand Down
40 changes: 34 additions & 6 deletions core/app/models/spree/order.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class Order < ActiveRecord::Base

attr_accessible :line_items, :bill_address_attributes, :ship_address_attributes, :payments_attributes,
:ship_address, :bill_address, :line_items_attributes, :number,
:shipping_method_id, :email, :use_billing, :special_instructions
:shipping_method_id, :email, :use_billing, :special_instructions, :currency

if Spree.user_class
belongs_to :user, :class_name => Spree.user_class.to_s
Expand Down Expand Up @@ -56,6 +56,7 @@ class Order < ActiveRecord::Base
accepts_nested_attributes_for :shipments

# Needs to happen before save_permalink is called
before_validation :set_currency
before_validation :generate_order_number, :on => :create
before_validation :clone_billing_address, :if => :use_billing?
attr_accessor :use_billing
Expand Down Expand Up @@ -107,8 +108,24 @@ def amount
line_items.sum(&:amount)
end

def currency
self[:currency] || Spree::Config[:currency]
end

def display_outstanding_balance
Spree::Money.new(outstanding_balance, { :currency => currency })
end

def display_item_total
Spree::Money.new(item_total, { :currency => currency })
end

def display_adjustment_total
Spree::Money.new(adjustment_total, { :currency => currency })
end

def display_total
Spree::Money.new(total)
Spree::Money.new(total, { :currency => currency })
end

def to_param
Expand Down Expand Up @@ -216,15 +233,21 @@ def awaiting_returns?
return_authorizations.any? { |return_authorization| return_authorization.authorized? }
end

def add_variant(variant, quantity = 1)
def add_variant(variant, quantity = 1, currency = nil)
current_item = find_line_item_by_variant(variant)
if current_item
current_item.quantity += quantity
current_item.currency = currency unless currency.nil?
current_item.save
else
current_item = LineItem.new(:quantity => quantity)
current_item.variant = variant
current_item.price = variant.price
if currency
current_item.currency = currency unless currency.nil?
current_item.price = variant.price_in(currency).amount
else
current_item.price = variant.price
end
self.line_items << current_item
end

Expand Down Expand Up @@ -385,7 +408,8 @@ def rate_hash
ShippingRate.new( :id => ship_method.id,
:shipping_method => ship_method,
:name => ship_method.name,
:cost => cost)
:cost => cost,
:currency => currency)
end.compact.sort_by { |r| r.cost }
end

Expand Down Expand Up @@ -451,7 +475,7 @@ def insufficient_stock_lines

def merge!(order)
order.line_items.each do |line_item|
self.add_variant(line_item.variant, line_item.quantity)
self.add_variant(line_item.variant, line_item.quantity) if line_item.currency == currency
end
order.destroy
end
Expand Down Expand Up @@ -536,5 +560,9 @@ def unstock_items!
def use_billing?
@use_billing == true || @use_billing == "true" || @use_billing == "1"
end

def set_currency
self.currency = Spree::Config[:currency] if self[:currency].nil?
end
end
end
8 changes: 8 additions & 0 deletions core/app/models/spree/payment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ class Payment < ActiveRecord::Base
end
end

def currency
order.currency
end

def display_amount
Spree::Money.new(amount, { :currency => currency })
end

def offsets_total
offsets.map(&:amount).sum
end
Expand Down
61 changes: 61 additions & 0 deletions core/app/models/spree/price.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
module Spree
class Price < ActiveRecord::Base
belongs_to :variant, :class_name => 'Spree::Variant'

validate :check_price
validates :amount, :numericality => { :greater_than_or_equal_to => 0 }, :allow_nil => true

attr_accessible :variant_id, :currency, :amount

def display_amount
return nil if amount.nil?
money.to_s
end
alias :display_price :display_amount

def money
Spree::Money.new(amount, { :currency => currency })
end

def price
amount
end

def price=(price)
self[:amount] = parse_price(price) if price.present?
end

private
def check_price
raise "Price must belong to a variant" if variant.nil?
if amount.nil?
if variant.is_master? || variant.product.master.nil? || variant.product.master.default_price.nil?
self.amount = nil
else
self.amount = variant.product.master.default_price.amount
end
end
if currency.nil?
if variant.product.master.default_price.nil?
self.currency = Spree::Config[:currency]
else
self.currency = variant.product.master.default_price.currency
end
end
end

# strips all non-price-like characters from the price, taking into account locale settings
def parse_price(price)
return price unless price.is_a?(String)

separator, delimiter = I18n.t([:'number.currency.format.separator', :'number.currency.format.delimiter'])
non_price_characters = /[^0-9\-#{separator}]/
price.gsub!(non_price_characters, '') # strip everything else first
price.gsub!(separator, '.') unless separator == '.' # then replace the locale-specific decimal separator with the standard separator if necessary

price.to_d
end

end
end

Loading

0 comments on commit 58ec5cd

Please sign in to comment.