Class: Spree::Product

Inherits:
Base
  • Object
show all
Extended by:
FriendlyId
Defined in:
app/models/spree/product.rb,
app/models/spree/product/scopes.rb

Overview

Note:

this model uses paranoia. #destroy will only soft-destroy records and the default scope hides soft-destroyed records using WHERE deleted_at IS NULL.

Products represent an entity for sale in a store. Products can have variations, called variants. Product properties include description, permalink, availability, shipping category, etc. that do not change by variant.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

page

Methods included from Spree::Preferences::Preferable

#default_preferences, #defined_preferences, #get_preference, #has_preference!, #has_preference?, #preference_default, #preference_type, #set_preference

Instance Attribute Details

#option_values_hashObject

Returns the value of attribute option_values_hash.



83
84
85
# File 'app/models/spree/product.rb', line 83

def option_values_hash
  @option_values_hash
end

#prototype_idObject

Overrides the prototype_id setter in order to ensure it is cast to an integer.

Parameters:

  • value (#to_i)

    the intended new value



112
113
114
# File 'app/models/spree/product.rb', line 112

def prototype_id
  @prototype_id
end

Class Method Details

.active(currency = nil) ⇒ Object



195
196
197
# File 'app/models/spree/product/scopes.rb', line 195

def self.active(currency = nil)
  not_deleted.available(nil, currency)
end

.add_search_scope(name, &block) ⇒ Object



7
8
9
10
# File 'app/models/spree/product/scopes.rb', line 7

def self.add_search_scope(name, &block)
  self.singleton_class.send(:define_method, name.to_sym, &block)
  search_scopes << name.to_sym
end

.add_simple_scopes(scopes) ⇒ Object



21
22
23
24
25
26
27
28
# File 'app/models/spree/product/scopes.rb', line 21

def self.add_simple_scopes(scopes)
  scopes.each do |name|
    # We should not define price scopes here, as they require something slightly different
    next if name.to_s.include?("master_price")
    parts = name.to_s.match(/(.*)_by_(.*)/)
    self.scope(name.to_s, -> { order("#{Product.quoted_table_name}.#{parts[2]} #{parts[1] == 'ascend' ?  "ASC" : "DESC"}") })
  end
end

.available(available_on = nil, currency = nil) ⇒ Object

Can’t use add_search_scope for this as it needs a default argument



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

def self.available(available_on = nil, currency = nil)
  joins(:master => :prices).where("#{Product.quoted_table_name}.available_on <= ?", available_on || Time.now)
end

.distinct_by_product_ids(sort_order = nil) ⇒ Object



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'app/models/spree/product/scopes.rb', line 204

def self.distinct_by_product_ids(sort_order = nil)
  sort_column = sort_order.split(" ").first

  # Postgres will complain when using ordering by expressions not present in
  # SELECT DISTINCT. e.g.
  #
  #   PG::InvalidColumnReference: ERROR:  for SELECT DISTINCT, ORDER BY
  #   expressions must appear in select list. e.g.
  #
  #   SELECT  DISTINCT "spree_products".* FROM "spree_products" LEFT OUTER JOIN
  #   "spree_variants" ON "spree_variants"."product_id" = "spree_products"."id" AND "spree_variants"."is_master" = 't'
  #   AND "spree_variants"."deleted_at" IS NULL LEFT OUTER JOIN "spree_prices" ON
  #   "spree_prices"."variant_id" = "spree_variants"."id" AND "spree_prices"."currency" = 'USD'
  #   AND "spree_prices"."deleted_at" IS NULL WHERE "spree_products"."deleted_at" IS NULL AND ('t'='t')
  #   ORDER BY "spree_prices"."amount" ASC LIMIT 10 OFFSET 0
  #
  # Don't allow sort_column, a variable coming from params,
  # to be anything but a column in the database
  if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' && !column_names.include?(sort_column)
    all
  else
    distinct
  end
end

.like_any(fields, values) ⇒ ActiveRecord::Relation

Poor man’s full text search.

Filters products to those which have any of the strings in values in any of the fields in fields.

Parameters:

  • fields (Array{String,Symbol})

    columns of the products table to search for values

  • values (Array{String})

    strings to search through fields for

Returns:

  • (ActiveRecord::Relation)

    scope with WHERE clause for search applied



170
171
172
173
174
175
176
# File 'app/models/spree/product.rb', line 170

def self.like_any(fields, values)
  where fields.map { |field|
    values.map { |value|
      arel_table[field].matches("%#{value}%")
    }.inject(:or)
  }.inject(:or)
end

.property_conditions(property) ⇒ Object



30
31
32
33
34
35
36
37
# File 'app/models/spree/product/scopes.rb', line 30

def self.property_conditions(property)
  properties = Property.table_name
  conditions = case property
  when String   then { "#{properties}.name" => property }
  when Property then { "#{properties}.id" => property.id }
  else               { "#{properties}.id" => property.to_i }
  end
end

.simple_scopesObject



12
13
14
15
16
17
18
19
# File 'app/models/spree/product/scopes.rb', line 12

def self.simple_scopes
  [
    :ascend_by_updated_at,
    :descend_by_updated_at,
    :ascend_by_name,
    :descend_by_name
  ]
end

Instance Method Details

#available?Boolean

Determines if product is available. A product is available if it has not been deleted and the available_on date is in the past.

Returns:

  • (Boolean)

    true if this product is available



149
150
151
# File 'app/models/spree/product.rb', line 149

def available?
  !(available_on.nil? || available_on.future?) && !deleted?
end

#categorise_variants_from_option(opt_type) ⇒ Hash

Groups variants by the specified option type.

Parameters:

  • opt_type (String)

    the name of the option type to group by

Returns:

  • (Hash)

    option_type as keys, array of variants as values.



157
158
159
160
# File 'app/models/spree/product.rb', line 157

def categorise_variants_from_option(opt_type)
  return {} unless option_types.include?(opt_type)
  variants.active.group_by { |v| v.option_values.detect { |o| o.option_type == opt_type} }
end

#deleted?Boolean

Use for checking whether this product has been deleted. Provided for overriding the logic for determining if a product is deleted.

Returns:

  • (Boolean)

    true if this product is deleted



141
142
143
# File 'app/models/spree/product.rb', line 141

def deleted?
  !!deleted_at
end

#display_imageSpree::Image

Image that can be used for the product.

Will first search for images on the product, then those belonging to the variants. If all else fails, will return a new image object.

Returns:



248
249
250
# File 'app/models/spree/product.rb', line 248

def display_image
  images.first || variant_images.first || Spree::Image.new
end

#duplicateSpree::Product

Creates a new product with the same attributes, variants, etc.

Returns:



132
133
134
135
# File 'app/models/spree/product.rb', line 132

def duplicate
  duplicator = ProductDuplicator.new(self)
  duplicator.duplicate
end

#empty_option_values?Boolean

Returns true if there are no option values.

Returns:

  • (Boolean)

    true if there are no option values



187
188
189
190
191
# File 'app/models/spree/product.rb', line 187

def empty_option_values?
  options.empty? || options.any? do |opt|
    opt.option_type.option_values.empty?
  end
end

#ensure_option_types_exist_for_values_hashArray

Ensures option_types and product_option_types exist for keys in option_values_hash.

Returns:

  • (Array)

    the option_values



121
122
123
124
125
126
127
# File 'app/models/spree/product.rb', line 121

def ensure_option_types_exist_for_values_hash
  return if option_values_hash.nil?
  option_values_hash.keys.map(&:to_i).each do |id|
    self.option_type_ids << id unless option_type_ids.include?(id)
    product_option_types.create(option_type_id: id) unless product_option_types.pluck(:option_type_id).include?(id)
  end
end

#has_variants?Boolean

Returns true if there are any variants.

Returns:

  • (Boolean)

    true if there are any variants



93
94
95
# File 'app/models/spree/product.rb', line 93

def has_variants?
  variants.any?
end

#masterSpree::Variant

Override so if the master variant is deleted, we can still find it.

Returns:



239
240
241
# File 'app/models/spree/product.rb', line 239

def master
  super || variants_including_master.with_deleted.where(is_master: true).first
end

#possible_promotionsArray

Returns all advertised and not-rejected promotions.

Returns:

  • (Array)

    all advertised and not-rejected promotions



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

def possible_promotions
  promotion_ids = promotion_rules.map(&:promotion_id).uniq
  Spree::Promotion.advertised.where(id: promotion_ids).reject(&:expired?)
end

#property(property_name) ⇒ String

Returns the value of the given property. nil if property is undefined on this product.

Parameters:

  • property_name (String)

    the name of the property to find

Returns:

  • (String)

    the value of the given property. nil if property is undefined on this product



195
196
197
198
# File 'app/models/spree/product.rb', line 195

def property(property_name)
  return nil unless prop = properties.find_by(name: property_name)
  product_properties.find_by(property: prop).try(:value)
end

#set_property(property_name, property_value) ⇒ Object

Assigns the given value to the given property.

Parameters:

  • property_name (String)

    the name of the property

  • property_value (String)

    the property value



204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'app/models/spree/product.rb', line 204

def set_property(property_name, property_value)
  ActiveRecord::Base.transaction do
    # Works around spree_i18n #301
    property = if Property.exists?(name: property_name)
      Property.where(name: property_name).first
    else
      Property.create(name: property_name, presentation: property_name)
    end
    product_property = ProductProperty.where(product: self, property: property).first_or_initialize
    product_property.value = property_value
    product_property.save!
  end
end

#tax_categorySpree::TaxCategory

Returns tax category for this product, or the default tax category.

Returns:



98
99
100
101
102
103
104
# File 'app/models/spree/product.rb', line 98

def tax_category
  if self[:tax_category_id].nil?
    TaxCategory.where(is_default: true).first
  else
    TaxCategory.find(self[:tax_category_id])
  end
end

#total_on_handFixnum, Infinity

The number of on-hand stock items; Infinity if any variant does not track inventory.

Returns:

  • (Fixnum, Infinity)


228
229
230
231
232
233
234
# File 'app/models/spree/product.rb', line 228

def total_on_hand
  if any_variants_not_track_inventory?
    Float::INFINITY
  else
    stock_items.sum(:count_on_hand)
  end
end

#variants_and_option_values(current_currency = nil) ⇒ Array<Spree::Variant>

Returns all variants with at least one option value.

Parameters:

  • current_currency (String) (defaults to: nil)

    currency to filter variants by; defaults to Spree’s default

Returns:

  • (Array<Spree::Variant>)

    all variants with at least one option value



180
181
182
183
184
# File 'app/models/spree/product.rb', line 180

def variants_and_option_values(current_currency = nil)
  variants.includes(:option_values).active(current_currency).select do |variant|
    variant.option_values.any?
  end
end