Class: Spree::Product

Inherits:
Base
  • Object
show all
Extended by:
FriendlyId
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

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.



110
111
112
# File 'app/models/spree/product.rb', line 110

def option_values_hash
  @option_values_hash
end

#prototype_idObject

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



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

def prototype_id
  @prototype_id
end

Class Method Details

.active(currency = nil) ⇒ Object



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

def self.active(currency = nil)
  not_discontinued.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



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

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).uniq
end

.distinct_by_product_ids(sort_order = nil) ⇒ Object



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'app/models/spree/product/scopes.rb', line 213

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



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

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



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

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



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

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

Returns:

  • (Boolean)


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

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

#can_supply?Boolean

determine if any variant (including master) can be supplied

Returns:

  • (Boolean)


175
176
177
# File 'app/models/spree/product.rb', line 175

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” -> […]



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

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 deleted? rather than checking the attribute directly. this allows extensions to override deleted? if they want to provide their own definition.

Returns:

  • (Boolean)


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

def deleted?
  !!deleted_at
end

#discontinue!Object



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

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

#discontinued?Boolean

Returns:

  • (Boolean)


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

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



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

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

#empty_option_values?Boolean

Returns:

  • (Boolean)


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

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



136
137
138
139
140
141
142
143
# File 'app/models/spree/product.rb', line 136

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

#has_variants?Boolean

the master variant is not a member of the variants array

Returns:

  • (Boolean)


121
122
123
# File 'app/models/spree/product.rb', line 121

def has_variants?
  variants.any?
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.



235
236
237
# File 'app/models/spree/product.rb', line 235

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

#property(property_name) ⇒ Object



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

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

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



214
215
216
217
218
219
220
221
222
# File 'app/models/spree/product.rb', line 214

def set_property(property_name, property_value, property_presentation = property_name)
  ActiveRecord::Base.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



125
126
127
# File 'app/models/spree/product.rb', line 125

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

#total_on_handObject



224
225
226
227
228
229
230
# File 'app/models/spree/product.rb', line 224

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



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

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