Class: Spree::TaxRate

Inherits:
ActiveRecord::Base
  • Object
show all
Includes:
Core::AdjustmentSource, Core::CalculatedAdjustments
Defined in:
app/models/spree/tax_rate.rb

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Core::AdjustmentSource

included

Methods included from Core::CalculatedAdjustments

included

Class Method Details

.adjust(order, items) ⇒ Object

This method is best described by the documentation on #potentially_applicable?


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'app/models/spree/tax_rate.rb', line 75

def self.adjust(order, items)
  rates = self.match(order)
  tax_categories = rates.map(&:tax_category)
  relevant_items, non_relevant_items = items.partition { |item| tax_categories.include?(item.tax_category) }
  relevant_items.each do |item|
    item.adjustments.tax.delete_all
    relevant_rates = rates.select { |rate| rate.tax_category == item.tax_category }
    store_pre_tax_amount(item, relevant_rates)
    relevant_rates.each do |rate|
      rate.adjust(order, item)
    end
  end
  non_relevant_items.each do |item|
    if item.adjustments.tax.present?
      item.adjustments.tax.delete_all
      item.update_column(:pre_tax_amount, nil)
      Spree::ItemAdjustments.new(item).update
    end
  end
end

.defaultObject

For Vat the default rate is the rate that is configured for the default category It is needed for every price calculation (as all customer facing prices include vat ) The function returns the actual amount, which may be 0 in case of wrong setup, but is never nil


99
100
101
102
103
104
105
106
107
# File 'app/models/spree/tax_rate.rb', line 99

def self.default
  category = TaxCategory.includes(:tax_rates).where(is_default: true).first
  return 0 unless category

  address ||= Address.new(country_id: Spree::Config[:default_country_id])
  rate = category.tax_rates.detect { |rate| rate.zone.include? address }.try(:amount)

  rate || 0
end

.match(order) ⇒ Object

Gets the array of TaxRates appropriate for the specified order


30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'app/models/spree/tax_rate.rb', line 30

def self.match(order)
  order_zone = order.tax_zone
  return [] unless order_zone
  rates = includes(zone: { zone_members: :zoneable }).load.select do |rate|
    # Why "potentially"?
    # Go see the documentation for that method.
    rate.potentially_applicable?(order)
  end

  # Imagine with me this scenario:
  # You are living in Spain and you have a store which ships to France.
  # Spain is therefore your default tax rate.
  # When you ship to Spain, you want the Spanish rate to apply.
  # When you ship to France, you want the French rate to apply.
  #
  # Normally, Spree would notice that you have two potentially applicable
  # tax rates for one particular item.
  # When you ship to Spain, only the Spanish one will apply.
  # When you ship to France, you'll see a Spanish refund AND a French tax.
  # This little bit of code at the end stops the Spanish refund from appearing.
  #
  # For further discussion, see #4397 and #4327.
  rates.delete_if do |rate|
    rate.included_in_price? &&
    (rates - [rate]).map(&:tax_category).include?(rate.tax_category)
  end
end

.store_pre_tax_amount(item, rates) ⇒ Object

Pre-tax amounts must be stored so that we can calculate correct rate amounts in the future. For example: github.com/spree/spree/issues/4318#issuecomment-34723428


61
62
63
64
65
66
67
68
69
70
71
72
# File 'app/models/spree/tax_rate.rb', line 61

def self.store_pre_tax_amount(item, rates)
  if rates.any? { |r| r.included_in_price }
    case item
    when Spree::LineItem
      item_amount = item.discounted_amount
    when Spree::Shipment
      item_amount = item.discounted_cost
    end
    pre_tax_amount = item_amount / (1 + rates.map(&:amount).sum)
    item.update_column(:pre_tax_amount, pre_tax_amount)
  end
end

Instance Method Details

#adjust(order, item) ⇒ Object

Creates necessary tax adjustments for the order.


160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'app/models/spree/tax_rate.rb', line 160

def adjust(order, item)
  amount = compute_amount(item)
  return if amount == 0

  included = included_in_price && default_zone_or_zone_match?(item)

  if amount < 0
    label = Spree.t(:refund) + ' ' + create_label
  end

  self.adjustments.create!({
    :adjustable => item,
    :amount => amount,
    :order => order,
    :label => label || create_label,
    :included => included
  })
end

#compute_amount(item) ⇒ Object

This method is used by Adjustment#update to recalculate the cost.


180
181
182
183
184
185
186
187
188
189
190
191
# File 'app/models/spree/tax_rate.rb', line 180

def compute_amount(item)
  if included_in_price
    if default_zone_or_zone_match?(item)
      calculator.compute(item)
    else
      # In this case, it's a refund.
      calculator.compute(item) * - 1
    end
  else
    calculator.compute(item)
  end
end

#default_zone_or_zone_match?(item) ⇒ Boolean

Returns:

  • (Boolean)

193
194
195
196
# File 'app/models/spree/tax_rate.rb', line 193

def default_zone_or_zone_match?(item)
  Zone.default_tax.contains?(item.order.tax_zone) ||
  item.order.tax_zone == self.zone
end

#potentially_applicable?(order) ⇒ Boolean

Tax rates can potentially be applicable to an order. We do not know if they are/aren't until we attempt to apply these rates to the items contained within the Order itself. For instance, if a rate passes the criteria outlined in this method, but then has a tax category that doesn't match against any of the line items inside of the order, then that tax rate will not be applicable to anything. For instance:

Zones:

- Spain (default tax zone)
- France

Tax rates: (note: amounts below do not actually reflect real VAT rates)

21% inclusive - "Clothing" - Spain
18% inclusive - "Clothing" - France
10% inclusive - "Food" - Spain
8% inclusive - "Food" - France
5% inclusive - "Hotels" - Spain
2% inclusive - "Hotels" - France

Order has:

Line Item #1 - Tax Category: Clothing
Line Item #2 - Tax Category: Food

Tax rates that should be selected:

21% inclusive - "Clothing" - Spain
10% inclusive - "Food" - Spain

If the order's address changes to one in France, then the tax will be recalculated:

18% inclusive - "Clothing" - France
8% inclusive - "Food" - France

Note here that the “Hotels” tax rates will not be used at all. This is because there are no items which have the tax category of “Hotels”.

Under no circumstances should negative adjustments be applied for the Spanish tax rates.

Those rates should never come into play at all and only the French rates should apply.

Returns:

  • (Boolean)

150
151
152
153
154
155
156
157
# File 'app/models/spree/tax_rate.rb', line 150

def potentially_applicable?(order)
  # If the rate's zone matches the order's tax zone, then it's applicable.
  self.zone == order.tax_zone ||
  # If the rate's zone *contains* the order's tax zone, then it's applicable.
  self.zone.contains?(order.tax_zone) ||
  # 1) The rate's zone is the default zone, then it's always applicable.
  (self.included_in_price? && self.zone.default_tax)
end