Class: Spree::Variant

Inherits:
Base
  • Object
show all
Includes:
DefaultPrice, SoftDeletable, Scopes
Defined in:
app/models/spree/variant.rb,
app/models/spree/variant/scopes.rb,
app/models/spree/variant/price_selector.rb,
app/models/spree/variant/pricing_options.rb,
app/models/spree/variant/vat_price_generator.rb

Overview

Master Variant

Every product has one master variant, which stores master price and SKU, size and weight, etc. The master variant does not have option values associated with it. Contains on_hand inventory levels only when there are no variants for the product.

Variants

All variants can access the product properties directly (via reverse delegation). Inventory units are tied to Variant. The master variant can have inventory units, but not option values. All other variants have option values and may have inventory units. Sum of on_hand each variant’s inventory level determine “on_hand” level for the product.

Defined Under Namespace

Modules: Scopes Classes: PriceSelector, PricingOptions, VatPriceGenerator

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from DefaultPrice

#default_price, #default_price_or_build, #has_default_price?

Methods included from Scopes

prepended

Methods inherited from Base

display_includes

Methods included from Core::Permalinks

#generate_permalink, #save_permalink

Instance Attribute Details

#rebuild_vat_prices=(value) ⇒ Object (writeonly)

Sets the attribute rebuild_vat_prices

Parameters:

  • value

    the value to set the attribute rebuild_vat_prices to.



28
29
30
# File 'app/models/spree/variant.rb', line 28

def rebuild_vat_prices=(value)
  @rebuild_vat_prices = value
end

Class Method Details

.in_stock(stock_locations = nil) ⇒ ActiveRecord::Relation

Returns variants that are in stock. When stock locations are provided as a parameter, the scope is limited to variants that are in stock in the provided stock locations.

If you want to also include backorderable variants see suppliable

Parameters:

Returns:

  • (ActiveRecord::Relation)


92
93
94
95
96
97
98
99
# File 'app/models/spree/variant.rb', line 92

def self.in_stock(stock_locations = nil)
  return all unless Spree::Config.track_inventory_levels
  in_stock_variants = joins(:stock_items).where(Spree::StockItem.arel_table[:count_on_hand].gt(0).or(arel_table[:track_inventory].eq(false)))
  if stock_locations.present?
    in_stock_variants = in_stock_variants.where(spree_stock_items: { stock_location_id: stock_locations.map(&:id) })
  end
  in_stock_variants
end

.suppliableActiveRecord::Relation

Returns a scope of Variants which are suppliable. This includes:

  • in_stock variants

  • backorderable variants

  • variants which do not track stock

Returns:

  • (ActiveRecord::Relation)


107
108
109
110
111
112
113
114
115
# File 'app/models/spree/variant.rb', line 107

def self.suppliable
  return all unless Spree::Config.track_inventory_levels
  arel_conditions = [
    arel_table[:track_inventory].eq(false),
    Spree::StockItem.arel_table[:count_on_hand].gt(0),
    Spree::StockItem.arel_table[:backorderable].eq(true)
  ]
  joins(:stock_items).where(arel_conditions.inject(:or)).distinct
end

.with_prices(pricing_options = Spree::Config.default_pricing_options) ⇒ ActiveRecord::Relation

Returns variants that have a price for the given pricing options If you have modified the pricing options class, you might want to modify this scope too.

Parameters:

  • pricing_options (defaults to: Spree::Config.default_pricing_options)

    A Pricing Options object as defined on the price selector class

Returns:

  • (ActiveRecord::Relation)


125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'app/models/spree/variant.rb', line 125

def self.with_prices(pricing_options = Spree::Config.default_pricing_options)
  where(
    Spree::Price.
      where(Spree::Variant.arel_table[:id].eq(Spree::Price.arel_table[:variant_id])).
      # This next clause should just be `where(pricing_options.search_arguments)`, but ActiveRecord
      # generates invalid SQL, so the SQL here is written manually.
      where(
        "spree_prices.currency = ? AND (spree_prices.country_iso IS NULL OR spree_prices.country_iso = ?)",
        pricing_options.search_arguments[:currency],
        pricing_options.search_arguments[:country_iso].compact
      ).
      arel.exists
  )
end

Instance Method Details

#can_supply?(quantity = 1, stock_location = nil) ⇒ Boolean

Returns true if the desired quantity can be supplied.

Parameters:

  • quantity (Fixnum) (defaults to: 1)

    how many are desired

  • stock_location (Spree::StockLocation) (defaults to: nil)

    Optionally restrict stock quantity check to a specific stock location. If unspecified it will check inventory in all available StockLocations.

Returns:

  • (Boolean)

    true if the desired quantity can be supplied



330
331
332
# File 'app/models/spree/variant.rb', line 330

def can_supply?(quantity = 1, stock_location = nil)
  Spree::Stock::Quantifier.new(self, stock_location).can_supply?(quantity)
end

#cost_price=(price) ⇒ Bignum

Sets the cost_price for the variant.

Parameters:

  • price (Any)

    the price to set

Returns:

  • (Bignum)


170
171
172
# File 'app/models/spree/variant.rb', line 170

def cost_price=(price)
  self[:cost_price] = Spree::LocalizedNumber.parse(price) if price.present?
end

#deleted?Boolean

Returns whether this variant has been deleted. Provided as a method of overriding the logic for determining if a variant is deleted.

Returns:

  • (Boolean)

    true if this variant has been deleted



228
229
230
# File 'app/models/spree/variant.rb', line 228

def deleted?
  !!deleted_at
end

#descriptive_nameString

Generates a verbose name for the variant, appending ‘Master’ if it is a master variant, otherwise a list of its option values.

Returns:

  • (String)

    the generated name



220
221
222
# File 'app/models/spree/variant.rb', line 220

def descriptive_name
  is_master? ? name + ' - Master' : name + ' - ' + options_text
end

#exchange_nameString

Determines the name of an Exchange variant.

Returns:

  • (String)

    the master variant name, if it is a master; or a comma-separated list of all option values.



212
213
214
# File 'app/models/spree/variant.rb', line 212

def exchange_name
  is_master? ? name : options_text
end

The gallery for the variant, which represents all the images associated with it

Returns:



367
368
369
# File 'app/models/spree/variant.rb', line 367

def gallery
  @gallery ||= Spree::Config.variant_gallery_class.new(self)
end

#in_stock?Boolean

Returns true if there is stock on-hand for the variant.

Returns:

  • (Boolean)

    true if there is stock on-hand for the variant.



319
320
321
322
323
# File 'app/models/spree/variant.rb', line 319

def in_stock?
  Rails.cache.fetch(in_stock_cache_key) do
    total_on_hand > 0
  end
end

#is_backorderable?Boolean

Returns true if this variant can be backordered.

Returns:

  • (Boolean)

    true if this variant can be backordered



190
191
192
# File 'app/models/spree/variant.rb', line 190

def is_backorderable?
  Spree::Stock::Quantifier.new(self).backorderable?
end

#name_and_skuString

Generates a friendly name and sku string.

Returns:

  • (String)


307
308
309
# File 'app/models/spree/variant.rb', line 307

def name_and_sku
  "#{name} - #{sku}"
end

#on_backorderFixnum

Counts the number of units currently on backorder for this variant.

Returns:

  • (Fixnum)


185
186
187
# File 'app/models/spree/variant.rb', line 185

def on_backorder
  inventory_units.with_state('backordered').size
end

#option_value(opt_name) ⇒ String

Fetches the option value for the given option name.

Parameters:

  • opt_name (String)

    the name of the option whose value you want

Returns:

  • (String)

    the option value



279
280
281
# File 'app/models/spree/variant.rb', line 279

def option_value(opt_name)
  option_values.detect { |option| option.option_type.name == opt_name }.try(:presentation)
end

#options=(options = []) ⇒ Object

Assign given options hash to option values.

Parameters:

  • options (Array<Hash{name: String, value: String}>) (defaults to: [])

    array of hashes with a name and value.



235
236
237
238
239
# File 'app/models/spree/variant.rb', line 235

def options=(options = [])
  options.each do |option|
    set_option_value(option[:name], option[:value])
  end
end

#options_textString

Creates a sentence out of the variant’s (sorted) option values.

Returns:

  • (String)

    a sentence-ified string of option values.



197
198
199
200
201
202
203
204
205
206
207
# File 'app/models/spree/variant.rb', line 197

def options_text
  values = option_values.includes(:option_type).sort_by do |option_value|
    option_value.option_type.position
  end

  values.to_a.map! do |ov|
    "#{ov.option_type.presentation}: #{ov.presentation}"
  end

  values.to_sentence({ words_connector: ", ", two_words_connector: ", " })
end

#price_difference_from_master(pricing_options = Spree::Config.default_pricing_options) ⇒ Object

Returns the difference in price from the master variant



292
293
294
295
296
297
# File 'app/models/spree/variant.rb', line 292

def price_difference_from_master(pricing_options = Spree::Config.default_pricing_options)
  master_price = product.master.price_for_options(pricing_options)
  variant_price = price_for_options(pricing_options)
  return unless master_price && variant_price
  Spree::Money.new(variant_price.amount - master_price.amount, currency: pricing_options.currency)
end

#price_same_as_master?(pricing_options = Spree::Config.default_pricing_options) ⇒ Boolean

Returns:

  • (Boolean)


299
300
301
302
# File 'app/models/spree/variant.rb', line 299

def price_same_as_master?(pricing_options = Spree::Config.default_pricing_options)
  diff = price_difference_from_master(pricing_options)
  diff && diff.zero?
end

#price_selectorSpree::Variant::PriceSelector

Returns an instance of the globally configured variant price selector class for this variant. It’s cached so we don’t create too many objects.

Returns:



287
288
289
# File 'app/models/spree/variant.rb', line 287

def price_selector
  @price_selector ||= Spree::Config.variant_price_selector_class.new(self)
end

#set_option_value(opt_name, opt_value) ⇒ Object

Sets an option type and value for the given name and value.

Parameters:

  • opt_name (String)

    the name of the option

  • opt_value (String)

    the value to set to the option



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'app/models/spree/variant.rb', line 245

def set_option_value(opt_name, opt_value)
  # no option values on master
  return if is_master

  option_type = Spree::OptionType.where(name: opt_name).first_or_initialize do |option|
    option.presentation = opt_name
    option.save!
  end

  current_value = option_values.detect { |option| option.option_type.name == opt_name }

  if current_value
    return if current_value.name == opt_value
    option_values.delete(current_value)
  else
    # then we have to check to make sure that the product has the option type
    unless product.option_types.include? option_type
      product.option_types << option_type
    end
  end

  option_value = Spree::OptionValue.where(option_type_id: option_type.id, name: opt_value).first_or_initialize do |option|
    option.presentation = opt_value
    option.save!
  end

  option_values << option_value
  save
end

#shipping_categorySpree::ShippingCategory

This returns the product’s shipping category if the shipping category ID on the variant is nil. It looks like an association, but really is an override.

Returns:



154
155
156
# File 'app/models/spree/variant.rb', line 154

def shipping_category
  super || product_shipping_category
end

#shipping_category_idInteger

This returns the product’s shipping category if if the shipping category ID on the variant is nil.

Returns:

  • (Integer)

    the variant’s shipping category id



162
163
164
# File 'app/models/spree/variant.rb', line 162

def shipping_category_id
  super || product_shipping_category_id
end

#should_track_inventory?Boolean

Shortcut method to determine if inventory tracking is enabled for this variant. This considers both variant tracking flag and site-wide inventory tracking settings.

Returns:

  • (Boolean)

    true if inventory tracking is enabled



349
350
351
# File 'app/models/spree/variant.rb', line 349

def should_track_inventory?
  track_inventory? && Spree::Config.track_inventory_levels
end

#sku_and_options_textString

Generates a string of the SKU and a list of all the option values.

Returns:

  • (String)


314
315
316
# File 'app/models/spree/variant.rb', line 314

def sku_and_options_text
  "#{sku} #{options_text}".strip
end

#tax_categorySpree::TaxCategory

This returns the product’s tax category if the tax category ID on the variant is nil. It looks like an association, but really is an override.

Returns:



145
146
147
# File 'app/models/spree/variant.rb', line 145

def tax_category
  super || product_tax_category
end

#total_on_hand(stock_location = nil) ⇒ Fixnum

Fetches the on-hand quantity of the variant.

Parameters:

  • stock_location (Spree::StockLocation) (defaults to: nil)

    Optionally restrict stock quantity check to a specific stock location. If unspecified it will check inventory in all available StockLocations.

Returns:

  • (Fixnum)

    the number currently on-hand



340
341
342
# File 'app/models/spree/variant.rb', line 340

def total_on_hand(stock_location = nil)
  Spree::Stock::Quantifier.new(self, stock_location).total_on_hand
end

#variant_propertiesArray<Spree::VariantPropertyRuleValue>

Determines the variant’s property values by verifying which of the product’s variant property rules apply to itself.

Returns:



357
358
359
360
361
# File 'app/models/spree/variant.rb', line 357

def variant_properties
  product.variant_property_rules.flat_map do |rule|
    rule.values if rule.applies_to_variant?(self)
  end.compact
end

#weight=(weight) ⇒ Bignum

Sets the weight for the variant.

Parameters:

  • weight (Any)

    the weight to set

Returns:

  • (Bignum)


178
179
180
# File 'app/models/spree/variant.rb', line 178

def weight=(weight)
  self[:weight] = Spree::LocalizedNumber.parse(weight) if weight.present?
end