Class: Gemgento::Product

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
app/models/gemgento/product.rb

Overview

Author:

  • Gemgento LLC

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args) ⇒ Object

Attempts to return attribute_value before error.



153
154
155
156
157
158
159
# File 'app/models/gemgento/product.rb', line 153

def method_missing(method, *args)
  begin
    return self.attribute_value(method)
  rescue
    super
  end
end

Instance Attribute Details

#attribute_valuesObject

Returns the value of attribute attribute_values.



69
70
71
# File 'app/models/gemgento/product.rb', line 69

def attribute_values
  @attribute_values
end

#configurable_attribute_orderingObject

Returns the value of attribute configurable_attribute_ordering.



69
70
71
# File 'app/models/gemgento/product.rb', line 69

def configurable_attribute_ordering
  @configurable_attribute_ordering
end

#sync_neededObject

Returns the value of attribute sync_needed.



69
70
71
# File 'app/models/gemgento/product.rb', line 69

def sync_needed
  @sync_needed
end

Class Method Details

.filter(filters, store = nil) ⇒ ActiveRecord::Result

Filter products based on attribute values.

filter example:
  {attribute: Gemgento::ProductAttribute.find_by(code: 'size'), value: 'large'})
  or
  {attribute: Gemgento::ProductAttribute.find_by(code: 'size'), value: %w[large small]})
  or
  {attribute: [Gemgento::ProductAttribute.find_by(code: 'size'), Gemgento::ProductAttribute.find_by(code: 'dimension')], value: 'large'})
  or
  {attribute: [Gemgento::ProductAttribute.find_by(code: 'size'), Gemgento::ProductAttribute.find_by(code: 'dimension')], value: %w[large small]})
  or
  [{attribute: Gemgento::ProductAttribute.find_by(code: 'size'), value: 'large'}), {attribute: Gemgento::ProductAttribute.find_by(code: 'color'), value: 'red'})]

Filters can also take an optional operand, the default operand is '=' or 'IN' for an array

Parameters:

Returns:

  • (ActiveRecord::Result)


225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'app/models/gemgento/product.rb', line 225

def self.filter(filters, store = nil)
  store = Store.current if store.nil?

  filters = [filters] unless filters.is_a? Array
  products = self

  filters.each_with_index do |filter, index|
    filter[:attribute] = [filter[:attribute]] unless filter[:attribute].is_a? Array
    operand = filter[:value].is_a?(Array) ? 'IN' : ( filter.has_key?(:operand) ? filter[:operand] : '=' )
    value_placeholder = filter[:value].is_a?(Array) ? '(?)' : '?'

    unless filter[:attribute][0].frontend_input == 'select'
      products = products.joins(ActiveRecord::Base.escape_sql(
                                    "INNER JOIN gemgento_product_attribute_values AS value#{index} ON value#{index}.product_id = gemgento_products.id AND value#{index}.value #{operand} #{value_placeholder} AND value#{index}.store_id = ?
                INNER JOIN gemgento_product_attributes AS attribute#{index} ON attribute#{index}.id = value#{index}.product_attribute_id AND attribute#{index}.id IN (?)",
                                    filter[:value],
                                    store.id,
                                    filter[:attribute].map { |a| a.id }
                                )).distinct.readonly(false)
    else
      products = products.joins(ActiveRecord::Base.escape_sql(
                                    "INNER JOIN gemgento_product_attribute_values AS value#{index} ON value#{index}.product_id = gemgento_products.id
                INNER JOIN gemgento_product_attributes AS attribute#{index} ON attribute#{index}.id = value#{index}.product_attribute_id
                  AND attribute#{index}.id IN (?)
                INNER JOIN gemgento_product_attribute_options AS option#{index} ON option#{index}.product_attribute_id = attribute#{index}.id
                  AND value#{index}.value = option#{index}.value
                  AND option#{index}.label #{operand} #{value_placeholder}",
                                    filter[:attribute].map { |a| a.id },
                                    filter[:value]
                                )).distinct.readonly(false) # does not compare against values
    end
  end

  return products
end

.order_by_attribute(attribute, direction = 'ASC', is_numeric = false, store = nil) ⇒ ActiveRecord::Result

Order ActiveRecord result by attribute values.

Parameters:

Returns:

  • (ActiveRecord::Result)


268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'app/models/gemgento/product.rb', line 268

def self.order_by_attribute(attribute, direction = 'ASC', is_numeric = false, store = nil)
  store = Store.current if store.nil?
  raise 'Direction must be equivalent to ASC or DESC' if direction != 'ASC' and direction != 'DESC'

  products = self

  unless attribute.frontend_input == 'select'
    products = products.joins(
        ActiveRecord::Base.escape_sql(
            'INNER JOIN gemgento_product_attribute_values ON gemgento_product_attribute_values.product_id = gemgento_products.id AND gemgento_product_attribute_values.product_attribute_id = ? AND gemgento_product_attribute_values.store_id = ? ' +
                'INNER JOIN gemgento_product_attributes ON gemgento_product_attributes.id = gemgento_product_attribute_values.product_attribute_id ',
            attribute.id,
            store.id
        ))

    if is_numeric
      products = products.reorder("CAST(gemgento_product_attribute_values.value AS SIGNED) #{direction}")
    else
      products = products.reorder("gemgento_product_attribute_values.value #{direction}")
    end
  else
    products = products.joins(
        ActiveRecord::Base.escape_sql(
            'INNER JOIN gemgento_product_attribute_values ON gemgento_product_attribute_values.product_id = gemgento_products.id AND gemgento_product_attribute_values.product_attribute_id = ? ' +
                'INNER JOIN gemgento_product_attributes ON gemgento_product_attributes.id = gemgento_product_attribute_values.product_attribute_id ' +
                'INNER JOIN gemgento_product_attribute_options ON gemgento_product_attribute_options.product_attribute_id = gemgento_product_attributes.id AND gemgento_product_attribute_options.value = gemgento_product_attribute_values.value ' +
                'AND gemgento_product_attribute_options.store_id = ?',
            attribute.id,
            store.id
        ))

    if is_numeric
      products = products.reorder("CAST(gemgento_product_attribute_options.order AS SIGNED) #{direction}")
    else
      products = products.reorder("gemgento_product_attribute_options.order #{direction}")
    end
  end

  products = products.readonly(false)

  return products
end

Instance Method Details

#after_touchObject



523
524
525
# File 'app/models/gemgento/product.rb', line 523

def after_touch
  # do nothing
end

#attribute_value(code, store = nil) ⇒ String, ...

Get an attribute value.

Parameters:

  • code (String)

    attribute code

  • store (Gemgento::Store) (defaults to: nil)

Returns:

  • (String, Boolean, nil)


116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'app/models/gemgento/product.rb', line 116

def attribute_value(code, store = nil)
  store = Gemgento::Store.current if store.nil?
  product_attribute_value = self.attribute_values.select { |value| !value.product_attribute.nil? && value.product_attribute.code == code.to_s && value.store_id == store.id }.first

  ## if the attribute is not currently associated with the product, check if it exists
  if product_attribute_value.nil?
    product_attribute = Gemgento::ProductAttribute.find_by(code: code)

    if product_attribute.nil? # throw an error if the code is not recognized
      raise "Unknown product attribute code - #{code}"
    end
  else
    product_attribute = product_attribute_value.product_attribute
  end

  value = product_attribute_value.nil? ? product_attribute.default_value : product_attribute_value.value
  return nil if value.nil?

  if product_attribute.frontend_input == 'boolean' || product_attribute.code == 'is_recurring'
    if value == 'Yes' || value == '1' || value == '1.0'
      value = true
    else
      value = false
    end
  elsif product_attribute.frontend_input == 'select'
    option = product_attribute.product_attribute_options.find_by(value: value, store: store)
    value = option.nil? ? nil : option.label
  end

  return value
end

#categories(store = nil) ⇒ Object

Categories related to the product.

Parameters:

Returns:



509
510
511
512
# File 'app/models/gemgento/product.rb', line 509

def categories(store = nil)
  return super if store.nil?
  Gemgento::Category.where(id: self.product_categories.where(store: store).pluck(:category_id))
end

#configurable?Boolean

Check if the product is configurable.

Returns:

  • (Boolean)


494
495
496
# File 'app/models/gemgento/product.rb', line 494

def configurable?
  magento_type == 'configurable'
end

#configurable_attribute_order(store = nil, active_only = true) ⇒ Object

Return the ordering of configurable attribute values.

Parameters:

  • store (Store, nil) (defaults to: nil)
  • active_only (Boolean) (defaults to: true)
  • (Hash(Hash(Array(Integer))))


344
345
346
# File 'app/models/gemgento/product.rb', line 344

def configurable_attribute_order(store = nil, active_only = true)
  self.configurable_attribute_ordering ||= self.get_configurable_attribute_ordering(store, active_only)
end

#current_category(category_id = nil, store = nil) ⇒ Gemgento::Category

Determine the current category of a product based on the active navigation categories related to the product. A preferred category id can be specified, if this category is not found in the products navigation categories, then the lowest level navigation category is returned.

Parameters:

  • category_id (Integer) (defaults to: nil)

    id of a preferred category to return

  • store (Gemgento::Store) (defaults to: nil)

Returns:



483
484
485
486
487
488
489
# File 'app/models/gemgento/product.rb', line 483

def current_category(category_id = nil, store = nil)
  @current_category ||= begin
    self.categories(store || Gemgento::Store.current).active.navigation.find(category_id)
  rescue ActiveRecord::RecordNotFound
    self.categories(store || Gemgento::Store.current).active.navigation.bottom_level.first!
  end
end

#get_configurable_attribute_ordering(store, active_only) ⇒ Hash(Hash(Array(Integer)))

Calculate the ordering of configurable attribute values

Parameters:

  • store (Store, nil)
  • active_only (Boolean)

Returns:

  • (Hash(Hash(Array(Integer))))


353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'app/models/gemgento/product.rb', line 353

def get_configurable_attribute_ordering(store, active_only)
  store = Store.current if store.nil?
  order = {}

  if self.magento_type != 'configurable' && !self.configurable_products.empty?
    configurable_product = self.configurable_products.first
  else
    configurable_product = self
  end

  if active_only
    simple_products = configurable_product.simple_products.active.eager
  else
    simple_products = self.simple_products.eager
  end

  configurable_attributes = self.product_attribute_set.product_attributes.
      where(is_configurable: true, frontend_input: 'select', scope: 'global')

  configurable_attributes.each do |attribute|
    order[attribute.code] = []

    simple_products = simple_products.sort_by do |simple_product|
      if o = simple_product.product_attribute_options.find_by(product_attribute: attribute, store: store)
        o.order
      else
        0
      end
    end

    mapping = {}
    simple_products.each do |simple_product|
      value = simple_product.attribute_value(attribute.code, store)
      mapping[value] = [] if mapping[value].nil?
      mapping[value] << simple_product.id unless mapping[value].include? simple_product.id
    end

    order[attribute.code] = []
    mapping.each do |k, value|
      order[attribute.code] << { value: k, simple_product_ids: value }
    end
  end

  return order
end

#in_stock?(quantity = 1, store = nil) ⇒ Boolean

Determine if product has a specific inventory level.

Parameters:

  • quantity (Integer, BigDecimal, Float) (defaults to: 1)
  • store (Gemgento::Store) (defaults to: nil)

Returns:

  • (Boolean)


166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'app/models/gemgento/product.rb', line 166

def in_stock?(quantity = 1, store = nil)
  store = Store.current if store.nil?

  if self.magento_type == 'configurable'
    inventories = Inventory.where(product_id: self.simple_products.active.select(:id), store: store)

    if inventories.empty? # no inventories means inventory is not tracked
      return true
    else
      inventories.each do |inventory|
        return true if inventory.in_stock?(quantity)
      end

      return false
    end

  else
    if inventory = self.inventories.find_by(store: store)
      return inventory.in_stock?(quantity)
    else
      return true
    end
  end
end

#is_catalog_visible?Boolean

Determine if the product is catalog visible.

Returns:

  • (Boolean)


402
403
404
# File 'app/models/gemgento/product.rb', line 402

def is_catalog_visible?
  return [2, 4].include?(self.visibility)
end

#manage_cache_expires_atDateTime?

If the product has a cache_expires_at date set, make sure it hasn’t expired. If it has, set it again.

Returns:

  • (DateTime, nil)


445
446
447
# File 'app/models/gemgento/product.rb', line 445

def manage_cache_expires_at
  self.set_cache_expires_at if self.cache_expires_at && self.cache_expires_at < Time.now
end

#mark_deletedVoid

Mark a product deleted.

Returns:

  • (Void)


194
195
196
197
# File 'app/models/gemgento/product.rb', line 194

def mark_deleted
  self.deleted_at = Time.now
  self.shopify_adapter.destroy if self.shopify_adapter
end

#mark_deleted!Void

Mark a product deleted and save.

Returns:

  • (Void)


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

def mark_deleted!
  mark_deleted
  self.save
end

#on_sale?(user_group = nil, store = nil, quantity = 1.0) ⇒ Boolean

Determine if product is on sale.

Parameters:

  • user_group (Gemgento::UserGroup) (defaults to: nil)
  • store (Store) (defaults to: nil)
  • quantity (Float) (defaults to: 1.0)

Returns:

  • (Boolean)

    Boolean



327
328
329
# File 'app/models/gemgento/product.rb', line 327

def on_sale?(user_group = nil, store = nil, quantity = 1.0)
  return self.attribute_value('price', store).to_f != self.price(user_group, store, quantity)
end

#original_price(store = nil) ⇒ Float

Get the original, non sale, price for a product.

Parameters:

  • store (Store, nil) (defaults to: nil)

Returns:

  • (Float)


335
336
337
# File 'app/models/gemgento/product.rb', line 335

def original_price(store = nil)
  return self.attribute_value('price', store).to_f
end

#price(user_group = nil, store = nil, quantity = 1.0) ⇒ Object

Get the product price.

Parameters:

  • user_group (Gemgento::UserGroup) (defaults to: nil)
  • store (Store) (defaults to: nil)
  • quantity (Float) (defaults to: 1.0)

Returns:

  • Float



317
318
319
# File 'app/models/gemgento/product.rb', line 317

def price(user_group = nil, store = nil, quantity = 1.0)
  Gemgento::Price.new(self, user_group, store, quantity).calculate
end

#remove_from_active_quotesObject



514
515
516
517
518
519
520
521
# File 'app/models/gemgento/product.rb', line 514

def remove_from_active_quotes
  self.line_items
      .joins('INNER JOIN gemgento_quotes ON gemgento_line_items.itemizable_id = gemgento_quotes.id')
      .joins('LEFT JOIN gemgento_orders ON gemgento_quotes.id = gemgento_orders.quote_id')
      .where(gemgento_line_items: { itemizable_type: 'Gemgento::Quote' })
      .where(gemgento_orders: { id: nil }).where('gemgento_quotes.created_at >= ?', 30.days.ago)
      .destroy_all
end

#set_attribute_value(code, value, store = nil) ⇒ Boolean

Set an attribute value.

Parameters:

  • code (String)

    attribute code

  • value (String, Boolean, Integer, Float, BigDecimal)
  • store (Gemgento::Store) (defaults to: nil)

Returns:

  • (Boolean)


81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'app/models/gemgento/product.rb', line 81

def set_attribute_value(code, value, store = nil)
  store = Store.current if store.nil?

  product_attribute = ProductAttribute.find_by(code: code)

  if product_attribute.nil?
    return false
  else
    # enforce a single attribute value per attribute per store per product
    product_attribute_values = ProductAttributeValue.where(product_id: self.id, product_attribute_id: product_attribute.id, store: store)

    if product_attribute_values.size > 1
      ProductAttributeValue.where(product_id: self.id, product_attribute_id: product_attribute.id, store: store).where('id != ?', product_attribute_values.first.id).destroy_all
    end

    # set the attribute value
    product_attribute_value = ProductAttributeValue.where(product_id: self.id, product_attribute_id: product_attribute.id, store: store).first_or_initialize
    product_attribute_value.product = self
    product_attribute_value.product_attribute = product_attribute
    product_attribute_value.value = value
    product_attribute_value.store = store
    product_attribute_value.save!

    self.product_attribute_values << product_attribute_value unless self.product_attribute_values.include?(product_attribute_value)
    self.attribute_values = nil # reload cached attributes values

    return true
  end
end

#set_cache_expires_atVoid

Calculate the datetime that the product cache should expire.

Returns:

  • (Void)


452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
# File 'app/models/gemgento/product.rb', line 452

def set_cache_expires_at
  self.cache_expires_at = nil

  Store.all.each do |store|
    UserGroup.all.each do |user_group|
      if Price.new(self, user_group, store).has_special?
        date =  self.attribute_value('special_to_date', store)
      else
        date =  PriceRule.first_to_expire(self, user_group, store)
      end

      next if date.nil?
      self.cache_expires_at = date if self.cache_expires_at.nil? || date < self.cache_expires_at
    end
  end

  self.sync_needed = false
  self.save
end

#set_configurable_products_by_magento_ids(magento_ids) ⇒ void

This method returns an undefined value.

Set the associated configurable products, using an array of Magento product IDs.

Parameters:

  • magento_ids (Array(Integer))

    Magento IDs of the associated configurable products



428
429
430
431
432
433
434
435
436
437
438
439
440
# File 'app/models/gemgento/product.rb', line 428

def set_configurable_products_by_magento_ids(magento_ids)
  configurable_product_ids = []

  magento_ids.each do |magento_id|
    configurable_product = Product.active.find_by(magento_id: magento_id)
    next if configurable_product.nil?

    self.configurable_products << configurable_product unless self.configurable_products.include? configurable_product
    configurable_product_ids << configurable_product.id
  end

  self.configurable_products.delete(self.configurable_products.where('configurable_product_id NOT IN (?)', configurable_product_ids))
end

#set_simple_products_by_magento_ids(magento_ids) ⇒ void

This method returns an undefined value.

Set the associated simple products, using an array of Magento product IDs.

Parameters:

  • magento_ids (Array(Integer))

    Magento IDs of the associated simple products



410
411
412
413
414
415
416
417
418
419
420
421
422
# File 'app/models/gemgento/product.rb', line 410

def set_simple_products_by_magento_ids(magento_ids)
  simple_product_ids = []

  magento_ids.each do |magento_id|
    simple_product = Product.active.find_by(magento_id: magento_id)
    next if simple_product.nil?

    self.simple_products << simple_product unless self.simple_products.include? simple_product
    simple_product_ids << simple_product.id
  end

  self.simple_products.delete(self.simple_products.where('simple_product_id NOT IN (?)', simple_product_ids))
end

#simple?Boolean

Check if the product is simple.

Returns:

  • (Boolean)


501
502
503
# File 'app/models/gemgento/product.rb', line 501

def simple?
  magento_type == 'simple'
end

#sync_needed?Boolean

Returns:

  • (Boolean)


71
72
73
# File 'app/models/gemgento/product.rb', line 71

def sync_needed?
  self.sync_needed.to_bool
end

#to_paramObject



472
473
474
# File 'app/models/gemgento/product.rb', line 472

def to_param
  "#{self.id}-#{self.url_key}"
end