Class: Spree::Product

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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

belongs_to_required_by_default, page, spree_base_scopes

Methods included from Spree::Preferences::Preferable

#clear_preferences, #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.



106
107
108
# File 'app/models/spree/product.rb', line 106

def option_values_hash
  @option_values_hash
end

#prototype_idObject

Adding properties and option types on creation based on a chosen prototype



165
166
167
# File 'app/models/spree/product.rb', line 165

def prototype_id
  @prototype_id
end

Class Method Details

.active(currency = nil) ⇒ Object



205
206
207
# File 'app/models/spree/product/scopes.rb', line 205

def self.active(currency = nil)
  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)
  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
29
# 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_(.*)/)
    scope(name.to_s, -> { order(Arel.sql("#{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



199
200
201
202
# File 'app/models/spree/product/scopes.rb', line 199

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

.like_any(fields, values) ⇒ Object



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

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

.not_discontinued(only_not_discontinued = true) ⇒ Object



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

def self.not_discontinued(only_not_discontinued = true)
  if only_not_discontinued != '0' && only_not_discontinued
    where("#{Product.quoted_table_name}.discontinue_on IS NULL or #{Product.quoted_table_name}.discontinue_on >= ?", Time.zone.now)
  else
    all
  end
end

.property_conditions(property) ⇒ Object



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

def self.property_conditions(property)
  properties = Property.table_name
  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

determine if product is available. deleted products and products with nil or future available_on date are not available

Returns:

  • (Boolean)


198
199
200
# File 'app/models/spree/product.rb', line 198

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

#backorderable?Boolean

Cant use short form block syntax due to github.com/Netflix/fast_jsonapi/issues/259

Returns:

  • (Boolean)


139
140
141
# File 'app/models/spree/product.rb', line 139

def backorderable?
  variants_including_master.any?(&:backorderable?)
end

#backordered?Boolean

determine if any variant (including master) is out of stock and backorderable

Returns:

  • (Boolean)


216
217
218
# File 'app/models/spree/product.rb', line 216

def backordered?
  variants_including_master.any?(&:backordered?)
end

#brandObject



281
282
283
# File 'app/models/spree/product.rb', line 281

def brand
  taxons.joins(:taxonomy).find_by(spree_taxonomies: { name: Spree.t(:taxonomy_brands_name) })
end

#can_supply?Boolean

determine if any variant (including master) can be supplied

Returns:

  • (Boolean)


211
212
213
# File 'app/models/spree/product.rb', line 211

def can_supply?
  variants_including_master.any?(&:can_supply?)
end

#categorise_variants_from_option(opt_type) ⇒ Object

split variants list into hash which shows mapping of opt value onto matching variants eg categorise_variants_from_option(color) => -> […], “blue” -> […]



222
223
224
225
226
# File 'app/models/spree/product.rb', line 222

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

#categoryObject



285
286
287
# File 'app/models/spree/product.rb', line 285

def category
  taxons.joins(:taxonomy).find_by(spree_taxonomies: { name: Spree.t(:taxonomy_categories_name) })
end

#default_variantObject



152
153
154
# File 'app/models/spree/product.rb', line 152

def default_variant
  has_variants? ? variants.first : master
end

#default_variant_idObject



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

def default_variant_id
  default_variant.id
end

#deleted?Boolean

use deleted? rather than checking the attribute directly. this allows extensions to override deleted? if they want to provide their own definition.

Returns:

  • (Boolean)


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

def deleted?
  !!deleted_at
end

#discontinue!Object



202
203
204
# File 'app/models/spree/product.rb', line 202

def discontinue!
  update_attribute(:discontinue_on, Time.current)
end

#discontinued?Boolean

Returns:

  • (Boolean)


206
207
208
# File 'app/models/spree/product.rb', line 206

def discontinued?
  !!discontinue_on && discontinue_on <= Time.current
end

#duplicateObject

for adding products which are closely related to existing ones define “duplicate_extra” for site-specific actions, eg for additional fields



183
184
185
186
# File 'app/models/spree/product.rb', line 183

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

#empty_option_values?Boolean

Returns:

  • (Boolean)


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

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

#ensure_option_types_exist_for_values_hashObject

Ensures option_types and product_option_types exist for keys in option_values_hash



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

def ensure_option_types_exist_for_values_hash
  return if option_values_hash.nil?

  required_option_type_ids = option_values_hash.keys.map(&:to_i)
  missing_option_type_ids = required_option_type_ids - option_type_ids
  missing_option_type_ids.each do |id|
    product_option_types.create(option_type_id: id)
  end
end

#find_or_build_masterObject



143
144
145
# File 'app/models/spree/product.rb', line 143

def find_or_build_master
  master || build_master
end

#has_variants?Boolean

the master variant is not a member of the variants array

Returns:

  • (Boolean)


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

def has_variants?
  variants.any?
end

#in_stock?Boolean

Cant use short form block syntax due to github.com/Netflix/fast_jsonapi/issues/259

Returns:

  • (Boolean)


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

def in_stock?
  variants_including_master.any?(&:in_stock?)
end

#masterObject

Master variant may be deleted (i.e. when the product is deleted) which would make AR’s default finder return nil. This is a stopgap for that little problem.



277
278
279
# File 'app/models/spree/product.rb', line 277

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

#property(property_name) ⇒ Object



252
253
254
# File 'app/models/spree/product.rb', line 252

def property(property_name)
  product_properties.joins(:property).find_by(spree_properties: { name: property_name }).try(:value)
end

#purchasable?Boolean

Cant use short form block syntax due to github.com/Netflix/fast_jsonapi/issues/259

Returns:

  • (Boolean)


129
130
131
# File 'app/models/spree/product.rb', line 129

def purchasable?
  variants_including_master.any?(&:purchasable?)
end

#set_property(property_name, property_value, property_presentation = property_name) ⇒ Object



256
257
258
259
260
261
262
263
264
# File 'app/models/spree/product.rb', line 256

def set_property(property_name, property_value, property_presentation = property_name)
  ApplicationRecord.transaction do
    # Works around spree_i18n #301
    property = Property.create_with(presentation: property_presentation).find_or_create_by(name: property_name)
    product_property = ProductProperty.where(product: self, property: property).first_or_initialize
    product_property.value = property_value
    product_property.save!
  end
end

#tax_categoryObject



160
161
162
# File 'app/models/spree/product.rb', line 160

def tax_category
  super || TaxCategory.find_by(is_default: true)
end

#total_on_handObject



266
267
268
269
270
271
272
# File 'app/models/spree/product.rb', line 266

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) ⇒ Object

Suitable for displaying only variants that has at least one option value. There may be scenarios where an option type is removed and along with it all option values. At that point all variants associated with only those values should not be displayed to frontend users. Otherwise it breaks the idea of having variants



240
241
242
243
244
# File 'app/models/spree/product.rb', line 240

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