Class: CatalogItem

Inherits:
ApplicationRecord show all
Includes:
Memery, Models::Auditable, Models::CatalogItemAmazonHelper, Models::CatalogItemWalmartHelper, Models::CatalogItemWayfairHelper, Models::SaleDiscountable, Models::Translatable, OrderQuery, PgSearch::Model
Defined in:
app/models/catalog_item.rb

Overview

== Schema Information

Table name: catalog_items
Database name: primary

id :integer not null, primary key
alternate_warehouse_stock_reporting_max :integer
amazon_desired_product_type :string
amazon_fba_labeling :enum
amazon_fba_sku :string
amazon_fnsku :string
amazon_info_datetime :datetime
amazon_item_suppression_pull_datetime :datetime
amazon_listing_reported_product_type :string
amazon_reported_product_type :string
amount :decimal(8, 2)
amz_last_buy_box_winner_change :datetime
amz_min_seller_price_override :decimal(8, 2)
backup_tppn :string
business_discount_override :integer
clearance :boolean default(FALSE), not null
disable_amz_repricing :boolean default(FALSE), not null
discontinued_date :date
discontinued_part_number :string
google_feed :boolean default(FALSE), not null
google_feed_title :string
is_amz_buy_box_winner :boolean default(FALSE), not null
is_amz_featured_merchant :boolean default(FALSE), not null
item_suppressed_status :string
item_suppressed_status_message :string
last_inventory_advice_sent_json :jsonb
last_price_advice_sent_json :jsonb
legacy_fulfillment :integer default(0), not null
legacy_sale_price_effective_date :date
legacy_sale_price_expiration_date :date
margin_required :decimal(, )
max_discount :integer default(100)
min_stock_to_report :integer
new_price :decimal(8, 2)
new_price_effective_date :date
new_sale_price :decimal(8, 2)
old_amount :decimal(10, 2)
pack_at_kit_level :boolean default(FALSE), not null
parent_sku :string
pending_discontinue_date :date
position :integer default(100)
price_editable :boolean default(FALSE), not null
price_updated_at :datetime
quantity_1_lower_bound :integer
quantity_1_price_discount :decimal(6, 2)
quantity_1_price_type :enum
quantity_2_lower_bound :integer
quantity_2_price_discount :decimal(6, 2)
quantity_2_price_type :enum
quantity_3_lower_bound :integer
quantity_3_price_discount :decimal(6, 2)
quantity_3_price_type :enum
quantity_4_lower_bound :integer
quantity_4_price_discount :decimal(6, 2)
quantity_4_price_type :enum
quantity_5_lower_bound :integer
quantity_5_price_discount :decimal(6, 2)
quantity_5_price_type :enum
quantity_discount_price_type :enum
reserve_stock :integer
retail_price :decimal(8, 2)
retailer_information :jsonb
retailer_price_updated_at :datetime
retailer_requested_cost :decimal(10, 2)
sale_price :decimal(8, 2)
skip_url_checks :boolean default(FALSE), not null
state :string(30) default("active"), not null
third_party_description :text
third_party_name :string(255)
third_party_part_number :string(255)
third_party_product_type :string
third_party_promo_part_number :string
third_party_short_description :text
third_party_sku :string
translations :jsonb
url :string
url_last_checked :datetime
url_valid :boolean default(FALSE), not null
created_at :datetime
updated_at :datetime
amazon_variation_id :bigint
catalog_id :integer
coupon_id :integer
new_coupon_id :integer
store_item_id :integer not null

Indexes

index_catalog_items_on_amazon_fnsku (amazon_fnsku) UNIQUE
index_catalog_items_on_amazon_variation_id (amazon_variation_id)
index_catalog_items_on_catalog_id_and_store_item_id (catalog_id,store_item_id) UNIQUE
index_catalog_items_on_catalog_id_and_third_party_sku (catalog_id,third_party_sku) UNIQUE
index_catalog_items_on_catalog_id_and_tpn (catalog_id,third_party_promo_part_number) UNIQUE
index_catalog_items_on_coupon_id (coupon_id)
index_catalog_items_on_new_coupon_id (new_coupon_id)
index_catalog_items_on_parent_sku (parent_sku)
index_catalog_items_on_state_and_store_item_id_and_catalog_id (state,store_item_id,catalog_id)
index_catalog_items_on_state_and_url_valid (state,url_valid)
index_catalog_items_on_store_item_id_and_state (store_item_id,state)
index_catalog_items_on_third_party_part_number_and_catalog_id (third_party_part_number,catalog_id) UNIQUE WHERE (third_party_part_number IS NOT NULL)
index_catalog_items_on_third_party_sku_and_catalog_id (third_party_sku,catalog_id) UNIQUE WHERE (third_party_sku IS NOT NULL)

Foreign Keys

catalog_items_catalog_id_fkey (catalog_id => catalogs.id)
catalog_items_store_item_id_fk (store_item_id => store_items.id) ON DELETE => cascade
fk_rails_... (amazon_variation_id => amazon_variations.id)
fk_rails_... (coupon_id => coupons.id)
fk_rails_... (new_coupon_id => coupons.id) ON DELETE => nullify

Defined Under Namespace

Classes: CouponAndSalePriceValidator, LegacyPullAmazonCatalogItemListingsData, LegacySetAmazonCatalogItemPricesFromListingsData, Onboarder, PriceSync, PriceUpdatedHandler, QuantityBoundsValidator, Remapper, ScheduledPriceChanger

Constant Summary collapse

HIDDEN_STATES =
%w[active_hidden discontinued pending_client_review invalid_catalog_item pending_onboarding].freeze
EDI_FEED_STATUSES =
%w[active require_vendor_update pending_onboarding pending_vendor_update pending_discontinue].freeze
EDI_DELTA_FEED_CATEGORIES =

Omitting an item from a feed automatically will trigger a removal, therefore pending_discontinue should be excluded
from selection

%w[inventory_advice price_advice]
ORCHESTRATOR_STATES =
%w[active require_vendor_update pending_vendor_update pending_discontinue discontinued].freeze

Constants included from Models::CatalogItemAmazonHelper

Models::CatalogItemAmazonHelper::AMAZON_DEFAULT_BUSINESS_DISCOUNT, Models::CatalogItemAmazonHelper::AMAZON_MIN_PROFIT_MARGIN

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Instance Attribute Summary collapse

Attributes included from Models::CatalogItemAmazonHelper

#amazon_fnsku

Attributes included from Models::Translatable

#do_not_compact_translation_container

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has one collapse

Has many collapse

Has and belongs to many collapse

Delegated Instance Attributes collapse

Methods included from Models::CatalogItemAmazonHelper

#amazon_description, #amazon_feature_1, #amazon_feature_2, #amazon_feature_3, #amazon_feature_4, #amazon_feature_5, #amazon_feature_6, #amazon_feature_7, #amazon_feature_8, #amazon_feature_9, #amazon_generic_keyword, #amazon_target_keywords, #amazon_title, #amazon_variation, #title_for_amazon

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::CatalogItemWayfairHelper

#wayfair_catalog_item?, #wayfair_primary_product_url, #wayfair_product_url, #wayfair_pull_taxonomy_schema, #wayfair_sync_age, #wayfair_synced?, #wayfair_taxonomy_available?, #wayfair_taxonomy_category_id_in_effect, wayfairs

Methods included from Models::CatalogItemWalmartHelper

#walmart_catalog_item?, #walmart_retire_item, walmarts

Methods included from Models::CatalogItemAmazonHelper

#amalytix_tags, #amazon_asin, #amazon_business_price, #amazon_business_price_factor, #amazon_business_price_fba, #amazon_business_price_with_tax, #amazon_business_price_with_tax_fba, #amazon_catalog_item?, #amazon_current_images, #amazon_delete_information, #amazon_effective_business_price_discount, #amazon_effective_desired_product_type, #amazon_item_cost, #amazon_json_generator, #amazon_label_requirements, #amazon_list_price, #amazon_locales, #amazon_lowest_quantity_discount_price, #amazon_lowest_quantity_discounted_price, #amazon_maximum_seller_allowed_price_with_tax, #amazon_maximum_seller_allowed_price_with_tax_fba, #amazon_minimum_profit_margin, #amazon_minimum_seller_allowed_price_with_tax, #amazon_minimum_seller_allowed_price_with_tax_fba, #amazon_patch_information, #amazon_price_with_tax, #amazon_price_with_tax_fba, #amazon_product_data, #amazon_product_type_in_effect, #amazon_product_type_incoherent?, #amazon_product_url, #amazon_pull_buy_box_status, #amazon_pull_catalog_information, #amazon_pull_listing_information, #amazon_pull_listing_schema, #amazon_put_information, #amazon_quantity_discount_price, #amazon_sale_price_with_tax, #amazon_sale_price_with_tax_fba, #amazon_schema, #amazon_seller_item?, #amazon_send_patch_listing_information, #amazon_send_put_listing_information, #amazon_vendor_code, #amazon_vendor_item?, amazons, amazons_sellers, amazons_sellers_with_asins, amazons_with_asins, #amz_available_attributes, #api_ready_state?, #broadcast_amazon_dashboard_update, by_asins, #extract_amazon_procurement_cost_price, #extract_retailer_information, #fba_discount, #get_amz_all_patches, #get_amz_attribute, #get_amz_attributes, #has_amazon_fba?, #max_discount_allowed, #max_discount_allowed_dollars, #max_discount_allowed_percentage, #profit_margin_amazon_minimum_below_target, #profit_marging_amazon_maximum_seller_allowed_price, #profit_marging_amazon_minimum_seller_allowed_price, #ready_to_print_amazon_fba_labels?, #retailer_information_first_locale, with_asin

Methods included from Models::SaleDiscountable

#effective_price, #money_effective_price, #money_price, #money_sale_price

Methods included from Models::Translatable

#compact_translation_container, skip_compaction_for

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#add_kit_components_to_catalogObject

Returns the value of attribute add_kit_components_to_catalog.



153
154
155
# File 'app/models/catalog_item.rb', line 153

def add_kit_components_to_catalog
  @add_kit_components_to_catalog
end

#alternate_warehouse_stock_reporting_maxObject (readonly)



254
# File 'app/models/catalog_item.rb', line 254

validates :alternate_warehouse_stock_reporting_max, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true

#amountObject (readonly)



248
# File 'app/models/catalog_item.rb', line 248

validates :amount, presence: true, numericality: { greater_than_or_equal_to: 0.0 }

#item_idObject



653
654
655
# File 'app/models/catalog_item.rb', line 653

def item_id
  store_item&.item_id
end

#max_discountObject (readonly)



255
# File 'app/models/catalog_item.rb', line 255

validates :max_discount, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }, allow_nil: true

#new_priceObject (readonly)



246
# File 'app/models/catalog_item.rb', line 246

validates :new_price, presence: { if: :new_price_effective_date, message: 'Must be specified alongside new price effective date' }

#new_price_effective_dateObject (readonly)



247
# File 'app/models/catalog_item.rb', line 247

validates :new_price_effective_date, presence: { if: :new_price, message: 'Must be specified alongside new price' }

#new_sale_price_with_vatObject

Returns the value of attribute new_sale_price_with_vat.



153
154
155
# File 'app/models/catalog_item.rb', line 153

def new_sale_price_with_vat
  @new_sale_price_with_vat
end

#price_with_vatObject

Returns the value of attribute price_with_vat.



153
154
155
# File 'app/models/catalog_item.rb', line 153

def price_with_vat
  @price_with_vat
end

#reserve_stockObject (readonly)



253
# File 'app/models/catalog_item.rb', line 253

validates :reserve_stock, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, allow_nil: true

#sale_price_with_vatObject

Returns the value of attribute sale_price_with_vat.



153
154
155
# File 'app/models/catalog_item.rb', line 153

def sale_price_with_vat
  @sale_price_with_vat
end

#skip_check_kit_componentsObject

Returns the value of attribute skip_check_kit_components.



153
154
155
# File 'app/models/catalog_item.rb', line 153

def skip_check_kit_components
  @skip_check_kit_components
end

#skip_create_kit_componentsObject

Returns the value of attribute skip_create_kit_components.



153
154
155
# File 'app/models/catalog_item.rb', line 153

def skip_create_kit_components
  @skip_create_kit_components
end

#store_idsObject



649
650
651
# File 'app/models/catalog_item.rb', line 649

def store_ids
  store_items.map(&:store_id)
end

#third_party_name_enObject (readonly)



244
# File 'app/models/catalog_item.rb', line 244

validates :third_party_name_en, presence: { if: :third_party_name_en_required? }

#third_party_name_frObject (readonly)



245
# File 'app/models/catalog_item.rb', line 245

validates :third_party_name_fr, presence: { if: :third_party_name_fr_required? }

#third_party_part_numberObject (readonly)

validates :store_item_id, uniqueness: { scope: :catalog_id }

Validations:

  • Presence ({ if: :third_party_part_number_required? })


242
# File 'app/models/catalog_item.rb', line 242

validates :third_party_part_number, presence: { if: :third_party_part_number_required? }

#third_party_skuObject (readonly)



243
# File 'app/models/catalog_item.rb', line 243

validates :third_party_sku, presence: { if: :third_party_sku_required? }

Class Method Details

.accessoriesActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are accessories. Active Record Scope

Returns:

See Also:



329
# File 'app/models/catalog_item.rb', line 329

scope :accessories, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_ACCESSORIES)) }

.activeActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are active. Active Record Scope

Returns:

See Also:



302
# File 'app/models/catalog_item.rb', line 302

scope :active, -> { where(state: %w[active active_hidden]) }

.available_for_edi_feedsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are available for edi feeds. Active Record Scope

Returns:

See Also:



351
# File 'app/models/catalog_item.rb', line 351

scope :available_for_edi_feeds, -> { available_for_edi_feeds_by_states(EDI_FEED_STATUSES) }

.available_for_edi_feeds_by_statesActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are available for edi feeds by states. Active Record Scope

Returns:

See Also:



355
356
357
358
# File 'app/models/catalog_item.rb', line 355

scope :available_for_edi_feeds_by_states, ->(states) {
  states_to_use = states.presence || EDI_FEED_STATUSES
  where(state: states_to_use)
}

.by_skusActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are by skus. Active Record Scope

Returns:

See Also:



308
# File 'app/models/catalog_item.rb', line 308

scope :by_skus, ->(*skus) { with_item.where(items: { sku: [skus].flatten.uniq.compact }) }

.by_upcsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are by upcs. Active Record Scope

Returns:

See Also:



309
# File 'app/models/catalog_item.rb', line 309

scope :by_upcs, ->(*upcs) { with_item.where(items: { upc: [upcs].flatten.uniq.compact }) }

.calculate_discounted_price(price_before_discount, discount, max_discount = 1.0) ⇒ Object



484
485
486
487
488
# File 'app/models/catalog_item.rb', line 484

def self.calculate_discounted_price(price_before_discount, discount, max_discount = 1.0)
  price_before_discount ||= 0.0
  discount_factor = 1.0 - [max_discount, discount].min
  (price_before_discount * discount_factor).round(2)
end

.catalog_items_for_selectObject



474
475
476
# File 'app/models/catalog_item.rb', line 474

def self.catalog_items_for_select
  CatalogItem.includes([:catalog, { store_item: :item }]).order('catalogs.name,items.sku').map { |ci| ["#{ci.sku} - #{ci.name.first(30)} - #{ci.catalog.name}", ci.id] }
end

.complimentary_ofActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are complimentary of. Active Record Scope

Returns:

See Also:



301
# File 'app/models/catalog_item.rb', line 301

scope :complimentary_of, ->(pl) { with_product_lines.where('product_lines.id' => pl.full_complimentary_product_line_ids) }

.controlsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are controls. Active Record Scope

Returns:

See Also:



323
# File 'app/models/catalog_item.rb', line 323

scope :controls, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_CONTROLS)) }

.coupons_for_future_promo_selectObject



497
498
499
500
501
502
# File 'app/models/catalog_item.rb', line 497

def self.coupons_for_future_promo_select
  coupons = []
  coupons += Coupon.active_and_future.tier1.where(calculation_type_goods: 'MX')
  coupons += Coupon.active_and_future.tier3
  coupons.uniq.map { |c| [c.to_s, c.id] }.sort
end

.coupons_for_promo_selectObject



490
491
492
493
494
495
# File 'app/models/catalog_item.rb', line 490

def self.coupons_for_promo_select
  coupons = []
  coupons += Coupon.active.tier1.where(calculation_type_goods: 'MX')
  coupons += Coupon.active.tier3
  coupons.uniq.map { |c| [c.code, c.id] }.sort
end

.discontinuedActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are discontinued. Active Record Scope

Returns:

See Also:



305
# File 'app/models/catalog_item.rb', line 305

scope :discontinued, -> { where(state: %w[discontinued]) }

.edi_delta_feeds_for_partner_inventory_adviceActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are edi delta feeds for partner inventory advice. Active Record Scope

Returns:

See Also:



362
363
364
365
366
367
368
369
370
371
372
# File 'app/models/catalog_item.rb', line 362

scope :edi_delta_feeds_for_partner_inventory_advice, ->(partner) {
  # Here we want to a delta query based on store_items.last_inventory_updated_at
  where(<<-SQL.squish, partner: partner)
  store_items.last_inventory_updated_at IS NULL
  OR (catalog_items.last_inventory_advice_sent_json ->> :partner) IS NULL
  OR (
    store_items.last_inventory_updated_at::timestamptz >=
      (catalog_items.last_inventory_advice_sent_json ->> :partner)::timestamptz
  )
  SQL
}

.edi_delta_feeds_for_partner_price_adviceActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are edi delta feeds for partner price advice. Active Record Scope

Returns:

See Also:



373
374
375
376
377
378
379
380
381
382
383
# File 'app/models/catalog_item.rb', line 373

scope :edi_delta_feeds_for_partner_price_advice, ->(partner) {
  # Here we want to a delta query based on catalog_items.price_updated_at
  where(<<-SQL.squish, partner: partner)
    catalog_items.price_updated_at IS NULL
    OR (catalog_items.last_price_advice_sent_json ->> :partner) IS NULL
    OR (
      catalog_items.price_updated_at::timestamptz >=
        (catalog_items.last_price_advice_sent_json ->> :partner)::timestamptz
    )
  SQL
}

.for_edi_feedsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are for edi feeds. Active Record Scope

Returns:

See Also:



352
# File 'app/models/catalog_item.rb', line 352

scope :for_edi_feeds, -> { available_for_edi_feeds.with_item.ordered_by_sku }

.for_edi_feeds_by_statesActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are for edi feeds by states. Active Record Scope

Returns:

See Also:



359
360
361
# File 'app/models/catalog_item.rb', line 359

scope :for_edi_feeds_by_states, ->(states) {
  available_for_edi_feeds_by_states(states).with_item.ordered_by_sku
}

.for_google_feedActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are for google feed. Active Record Scope

Returns:

See Also:



339
# File 'app/models/catalog_item.rb', line 339

scope :for_google_feed, -> { for_online_catalog }

.for_online_catalogActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are for online catalog. Active Record Scope

Returns:

See Also:



335
336
337
338
# File 'app/models/catalog_item.rb', line 335

scope :for_online_catalog, -> {
  public_catalog_items
    .merge(Item.goods.non_publications)
}

.for_online_catalog_or_non_web_accessible_with_successor_itemActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are for online catalog or non web accessible with successor item. Active Record Scope

Returns:

See Also:



343
344
345
346
347
348
349
# File 'app/models/catalog_item.rb', line 343

scope :for_online_catalog_or_non_web_accessible_with_successor_item, -> {
  ids = for_online_catalog.pluck(:id)
  successor_item_ids = non_web_accessible.has_successor_item.pluck(Arel.sql('distinct items.successor_item_id'))
  successor_item_skus = Item.active.where(id: successor_item_ids).pluck(:sku)
  successor_item_cis = by_skus(successor_item_skus).where(state: 'active').pluck(:id)
  where('catalog_items.id IN (?)', (ids + successor_item_cis).uniq)
}

.for_orchestrator_keysActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are for orchestrator keys. Active Record Scope

Returns:

See Also:



353
# File 'app/models/catalog_item.rb', line 353

scope :for_orchestrator_keys, ->(orchestrator_keys) { where(state: ORCHESTRATOR_STATES).with_item.ordered_by_sku.joins(:catalog).merge(Catalog.where(orchestrator_key: orchestrator_keys)) }

.for_product_category_pathActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are for product category path. Active Record Scope

Returns:

See Also:



318
319
320
# File 'app/models/catalog_item.rb', line 318

scope :for_product_category_path, ->(slug_path) {
  slug_path.blank? ? none : with_item.where(Item[:pc_path_slugs].ltree_descendant(slug_path))
}

.for_product_line_pathActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are for product line path. Active Record Scope

Returns:

See Also:



315
316
317
# File 'app/models/catalog_item.rb', line 315

scope :for_product_line_path, ->(slug_path) {
  slug_path.blank? ? none : with_item.where(Item[:primary_pl_path_slugs].ltree_descendant(slug_path))
}

.for_where_to_buy_listActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are for where to buy list. Active Record Scope

Returns:

See Also:



398
399
400
# File 'app/models/catalog_item.rb', line 398

scope :for_where_to_buy_list, ->(parent_catalog_id) {
  joins(:catalog).where(state: 'active').where.not(catalog_id: [1, 2, 74], url: nil).where(Catalog[:parent_catalog_id].eq(parent_catalog_id)).order(:position, Catalog[:name]).pluck(Arel.sql('COALESCE(catalogs.public_name, catalogs.name), catalog_items.url'))
}

.has_successor_itemActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are has successor item. Active Record Scope

Returns:

See Also:



342
# File 'app/models/catalog_item.rb', line 342

scope :has_successor_item, -> { with_item.where.not(items: { successor_item_id: nil }) }

.heating_elementsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are heating elements. Active Record Scope

Returns:

See Also:



322
# File 'app/models/catalog_item.rb', line 322

scope :heating_elements, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_HEATING_ELEMENTS)) }

.hidden_from_catalogActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are hidden from catalog. Active Record Scope

Returns:

See Also:



384
# File 'app/models/catalog_item.rb', line 384

scope :hidden_from_catalog, -> { where(state: HIDDEN_STATES) }

.in_stockActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are in stock. Active Record Scope

Returns:

See Also:



388
# File 'app/models/catalog_item.rb', line 388

scope :in_stock, -> { with_item.where('store_items.qty_available - COALESCE(catalog_items.reserve_stock, 0) > 0 OR items.always_available_online IS TRUE') }

.in_stock_with_alternate_warehouse_store_itemsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are in stock with alternate warehouse store items. Active Record Scope

Returns:

See Also:



389
390
391
392
393
394
# File 'app/models/catalog_item.rb', line 389

scope :in_stock_with_alternate_warehouse_store_items, -> {
  with_item.where("(store_items.qty_available +
                      (select round(coalesce(sum(si.qty_available),0),0)
                      from store_items si inner join stores s on si.store_id = s.id and s.owner = 'warmlyyours'
                      where si.store_id <> store_items.store_id and si.item_id = store_items.item_id and si.location = 'AVAILABLE')) - COALESCE(catalog_items.reserve_stock, 0) > 0 OR items.always_available_online is true")
}

.inactiveActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are inactive. Active Record Scope

Returns:

See Also:



303
# File 'app/models/catalog_item.rb', line 303

scope :inactive, -> { where.not(state: %w[active active_hidden]) }

.install_kitsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are install kits. Active Record Scope

Returns:

See Also:



332
# File 'app/models/catalog_item.rb', line 332

scope :install_kits, -> { for_product_line_path(LtreePaths::PL_FLOOR_HEATING_TEMPZONE_INSTALLATION_KITS).merge(accessories) }

.insulationsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are insulations. Active Record Scope

Returns:

See Also:



330
# File 'app/models/catalog_item.rb', line 330

scope :insulations, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_ACCESSORIES_INSULATIONS)) }

.integration_kitsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are integration kits. Active Record Scope

Returns:

See Also:



328
# File 'app/models/catalog_item.rb', line 328

scope :integration_kits, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_POWER)).where(Item[:primary_pl_path_slugs].ltree_descendant(LtreePaths::PL_FLOOR_HEATING_CONTROL_INTEGRATION)) }

.last_custom_mat_catalog_item(catalog_id, volts) ⇒ Object



1046
1047
1048
1049
1050
1051
1052
1053
# File 'app/models/catalog_item.rb', line 1046

def self.last_custom_mat_catalog_item(catalog_id, volts)
  for_product_category_path(LtreePaths::PC_HEATING_ELEMENTS)
    .for_product_line_path(LtreePaths::PL_FLOOR_HEATING_TEMPZONE_CUSTOM_MAT)
    .where(catalog_id:)
    .where.not(items: { supplier_item_id: nil })
    .where('items.rendered_product_specifications @> ?', { voltage: { raw: volts } }.to_json)
    .order('items.created_at DESC').first
end

.main_catalogsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are main catalogs. Active Record Scope

Returns:

See Also:



350
# File 'app/models/catalog_item.rb', line 350

scope :main_catalogs, -> { where(catalog_id: Catalog.main_catalog_ids) }

.non_publicationsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are non publications. Active Record Scope

Returns:

See Also:



296
# File 'app/models/catalog_item.rb', line 296

scope :non_publications, -> { with_item.merge(Item.goods.non_publications) }

.non_web_accessibleActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are non web accessible. Active Record Scope

Returns:

See Also:



340
# File 'app/models/catalog_item.rb', line 340

scope :non_web_accessible, -> { main_catalogs.with_item.merge(Item.goods.non_publications).where('(items.is_discontinued IS TRUE) OR (store_items.is_discontinued IS TRUE) OR (catalog_items.state IN (?))', HIDDEN_STATES) }

.not_discontinuedActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are not discontinued. Active Record Scope

Returns:

See Also:



304
# File 'app/models/catalog_item.rb', line 304

scope :not_discontinued, -> { where.not(state: %w[discontinued]) }

.not_hidden_from_catalogActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are not hidden from catalog. Active Record Scope

Returns:

See Also:



385
# File 'app/models/catalog_item.rb', line 385

scope :not_hidden_from_catalog, -> { where.not(state: HIDDEN_STATES) }

.not_pending_onboardingActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are not pending onboarding. Active Record Scope

Returns:

See Also:



306
# File 'app/models/catalog_item.rb', line 306

scope :not_pending_onboarding, -> { where.not(state: %w[pending_onboarding]) }

.order_by_specActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are order by spec. Active Record Scope

Returns:

See Also:



397
# File 'app/models/catalog_item.rb', line 397

scope :order_by_spec, ->(spec, datatype = 'varchar', direction = 'asc') { joins(store_item: :item).merge(Item.order_by_spec(spec, datatype, direction)) }

.ordered_by_skuActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are ordered by sku. Active Record Scope

Returns:

See Also:



298
# File 'app/models/catalog_item.rb', line 298

scope :ordered_by_sku, -> { with_item.order(Item[:sku]) }

.out_of_stockActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are out of stock. Active Record Scope

Returns:

See Also:



395
# File 'app/models/catalog_item.rb', line 395

scope :out_of_stock, -> { with_item.where('store_items.qty_available - COALESCE(catalog_items.reserve_stock, 0) <= 0 AND items.always_available_online IS NOT TRUE') }

.pending_onboardingActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are pending onboarding. Active Record Scope

Returns:

See Also:



307
# File 'app/models/catalog_item.rb', line 307

scope :pending_onboarding, -> { where(state: %w[pending_onboarding]) }

.power_modulesActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are power modules. Active Record Scope

Returns:

See Also:



327
# File 'app/models/catalog_item.rb', line 327

scope :power_modules, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_POWER_MODULES)) }

.public_catalog_itemsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are public catalog items. Active Record Scope

Returns:

See Also:



310
# File 'app/models/catalog_item.rb', line 310

scope :public_catalog_items, -> { with_item.merge(StoreItem.active).active.not_hidden_from_catalog }

.relay_panelsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are relay panels. Active Record Scope

Returns:

See Also:



325
# File 'app/models/catalog_item.rb', line 325

scope :relay_panels, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_POWER_RELAY_PANELS)) }

.sensorsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are sensors. Active Record Scope

Returns:

See Also:



326
# File 'app/models/catalog_item.rb', line 326

scope :sensors, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_SENSORS)) }

.spare_partsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are spare parts. Active Record Scope

Returns:

See Also:



333
# File 'app/models/catalog_item.rb', line 333

scope :spare_parts, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_SPARE_PARTS)) }

.state_options_for_select(show_all_states = false) ⇒ Object



478
479
480
481
482
# File 'app/models/catalog_item.rb', line 478

def self.state_options_for_select(show_all_states = false)
  state_syms = CatalogItem.state_machines[:state].states.map { |s| s.name.to_sym }
  state_syms -= [:discontinued] unless show_all_states
  state_syms.map { |s| [s.to_s.humanize, s.to_s] }.sort # discontinued is only for Heatwave bg job
end

.thermostatsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are thermostats. Active Record Scope

Returns:

See Also:



324
# File 'app/models/catalog_item.rb', line 324

scope :thermostats, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_CONTROLS_THERMOSTATS)) }

.towel_warmersActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are towel warmers. Active Record Scope

Returns:

See Also:



331
# File 'app/models/catalog_item.rb', line 331

scope :towel_warmers, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_TOWEL_WARMERS)) }

.web_accessibleActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are web accessible. Active Record Scope

Returns:

See Also:



341
# File 'app/models/catalog_item.rb', line 341

scope :web_accessible, -> { main_catalogs.with_item.merge(Item.goods.non_publications).where('(items.is_discontinued IS FALSE) AND (store_items.is_discontinued IS FALSE) AND NOT(catalog_items.state IN (?))', HIDDEN_STATES) }

.with_itemActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are with item. Active Record Scope

Returns:

See Also:



294
# File 'app/models/catalog_item.rb', line 294

scope :with_item, -> { joins(store_item: :item) }

.with_item_and_classificationsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are with item and classifications. Active Record Scope

Returns:

See Also:



295
# File 'app/models/catalog_item.rb', line 295

scope :with_item_and_classifications, -> { joins(store_item: { item: %i[primary_product_line product_category] }).includes(store_item: { item: %i[primary_product_line product_category] }) }

.with_item_condition_newActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are with item condition new. Active Record Scope

Returns:

See Also:



297
# File 'app/models/catalog_item.rb', line 297

scope :with_item_condition_new, -> { with_item.merge(Item.active.condition_new) }

.with_product_categoryActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are with product category. Active Record Scope

Returns:

See Also:



300
# File 'app/models/catalog_item.rb', line 300

scope :with_product_category, -> { includes(store_item: { item: :product_category }).references(store_item: { item: :product_category }) }

.with_product_linesActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are with product lines. Active Record Scope

Returns:

See Also:



299
# File 'app/models/catalog_item.rb', line 299

scope :with_product_lines, -> { includes(store_item: { item: :product_lines }).references(store_item: { item: :product_lines }) }

.with_product_specificationActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are with product specification. Active Record Scope

Returns:

See Also:



396
# File 'app/models/catalog_item.rb', line 396

scope :with_product_specification, ->(token, value, grouping = nil) { with_item.merge(Item.with_product_specification(token, value, grouping)) }

.with_third_party_numberActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are with third party number. Active Record Scope

Returns:

See Also:



334
# File 'app/models/catalog_item.rb', line 334

scope :with_third_party_number, -> { where.not(third_party_part_number: [nil, '']) }

Instance Method Details

#all_amazon_image_profilesObject

Alias for
to: :item#all_amazon_image_profiles

Returns:

  • (Object)
           to: :item#all_amazon_image_profiles
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#alternate_warehouse_stock_fractionObject

Returns the fraction (as decimal) of alternate warehouse stock to report
Uses catalog-level setting, defaults to 25% (0.25)
This is for EXTERNAL reporting only (feeds, website, EDI)



863
864
865
# File 'app/models/catalog_item.rb', line 863

def alternate_warehouse_stock_fraction
  (catalog&.alternate_warehouse_stock_fraction || 25) / 100.0
end

#alternate_warehouse_store_itemsObject



980
981
982
# File 'app/models/catalog_item.rb', line 980

def alternate_warehouse_store_items
  StoreItem.where(item_id: store_item.item_id).where.not(store_id: store_item.store_id).available.joins(:store).merge(Store.warmlyyours_warehouses)
end

#amazon_browse_nodesActiveRecord::Relation<AmazonBrowseNode>

Returns:

See Also:



180
# File 'app/models/catalog_item.rb', line 180

has_and_belongs_to_many :amazon_browse_nodes

#amazon_catalog_item_flagsActiveRecord::Relation<AmazonCatalogItemFlag>

Returns:

See Also:



178
# File 'app/models/catalog_item.rb', line 178

has_many    :amazon_catalog_item_flags, dependent: :destroy

#amazon_marketplaceAmazonMarketplace



172
# File 'app/models/catalog_item.rb', line 172

has_one     :amazon_marketplace, through: :catalog

#amazon_variant_catalog_itemsObject



1127
1128
1129
1130
1131
# File 'app/models/catalog_item.rb', line 1127

def amazon_variant_catalog_items
  return CatalogItem.none unless item.amazon_variation && item.amazon_variation.items.present?

  catalog_items_in_same_catalog.merge(item.amazon_variation.items).includes(:item)
end

#amount_currency_symbolObject



745
746
747
# File 'app/models/catalog_item.rb', line 745

def amount_currency_symbol
  catalog.currency_symbol
end

#available_in_edi_feed?Boolean

Returns:

  • (Boolean)


657
658
659
# File 'app/models/catalog_item.rb', line 657

def available_in_edi_feed?
  EDI_FEED_STATUSES.include?(state)
end

#available_storesObject



645
646
647
# File 'app/models/catalog_item.rb', line 645

def available_stores
  Store.where(company_id: catalog.company_id).order(:name)
end

#bom_priceObject



737
738
739
# File 'app/models/catalog_item.rb', line 737

def bom_price
  retail_price || amount
end

#calculate_minimum_price_for_margin(target_margin) ⇒ Object

Provide a target margin below a 100

Raises:

  • (ArgumentError)


546
547
548
549
550
551
552
553
# File 'app/models/catalog_item.rb', line 546

def calculate_minimum_price_for_margin(target_margin)
  return unless unit_cogs && unit_cogs.positive? && target_margin # skip non-positive unit_cogs

  raise ArgumentError, 'Target margin must be less than 100%' if target_margin >= 100

  minimum_price = unit_cogs / (1 - (target_margin / 100.0))
  minimum_price.round(2) # Round to 2 decimal places for currency formatting
end

#calculate_profit_margin(price) ⇒ Object



538
539
540
541
542
543
# File 'app/models/catalog_item.rb', line 538

def calculate_profit_margin(price)
  return unless unit_cogs && price

  profit = (price - unit_cogs)
  ((profit / price) * 100).round(2)
end

#calculate_sale_price(percentage_off, include_vat: false, from_msrp: false) ⇒ Object

Calculates the sale price based on a percentage off (0-100), specify also if price with VAT should be used as the starting amount



1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
# File 'app/models/catalog_item.rb', line 1163

def calculate_sale_price(percentage_off, include_vat: false, from_msrp: false)
  return nil unless percentage_off.present? && percentage_off.positive?
  raise 'sale price percentage off must be between 0 and 100' unless (1..100).include?(percentage_off)

  starting_amount = if from_msrp
                      if include_vat
                        msrp_with_vat
                      else
                        msrp
                      end
                    elsif include_vat
                      price_with_vat
                    else
                      amount
                    end
  if percentage_off.nil? && starting_amount.nil?
    msg = "calculate_sale_price: Catalog Item ID #{id} cannot have sale price calculated, percentage off: #{percentage_off}, starting_amount: #{starting_amount}"
    ErrorReporting.warning(msg)
    logger.warn msg
    return nil
  end
  calculated_sale_price = (starting_amount * (1.0 - (percentage_off.to_f / 100))).round(2)
  calculated_sale_price /= (1.0 + tax_rate) if include_vat && tax_rate
  calculated_sale_price.round(2)
end

#calculated_updated_price_from_parentObject



1095
1096
1097
1098
1099
1100
1101
1102
1103
# File 'app/models/catalog_item.rb', line 1095

def calculated_updated_price_from_parent
  return unless (parent_amount = parent_catalog_item&.amount)

  # Determine discount to use
  discount = parent_catalog_discount || 0.0
  return unless discount.between?(0.0, 1.0)

  (parent_amount * (1.0 - discount)).round(2)
end

#catalogCatalog

Returns:

See Also:



166
# File 'app/models/catalog_item.rb', line 166

belongs_to  :catalog, touch: true, inverse_of: :catalog_items

#catalog_discount_humanObject



807
808
809
810
811
# File 'app/models/catalog_item.rb', line 807

def catalog_discount_human
  return unless catalog.parent_catalog_discount

  "#{(catalog.parent_catalog_discount * 100).round(2)} %"
end

#catalog_items_in_same_catalog(starting_scope = nil) ⇒ Object



1117
1118
1119
1120
1121
1122
1123
1124
1125
# File 'app/models/catalog_item.rb', line 1117

def catalog_items_in_same_catalog(starting_scope = nil)
  starting_scope ||= CatalogItem.active
  CatalogItem.includes(:catalog)
             .where(catalog_id:)
             .with_item
             .with_product_lines
             .with_product_category
             .order(ProductCategory[:priority], CatalogItem[:amount])
end

#check_for_price_updateObject (protected)



1292
1293
1294
1295
1296
1297
# File 'app/models/catalog_item.rb', line 1292

def check_for_price_update
  return unless amount_changed?

  self.price_updated_at = Time.current
  self.old_amount = amount_was
end

#check_kit_components_availableObject (protected)



1282
1283
1284
1285
1286
# File 'app/models/catalog_item.rb', line 1282

def check_kit_components_available
  return unless missing_kit_components?

  errors.add(:base, 'All kit components must be present in the target catalog')
end

#check_ok_to_destroyObject (protected)



1288
1289
1290
# File 'app/models/catalog_item.rb', line 1288

def check_ok_to_destroy
  errors.add(:base, 'Cannot destroy because there are dependent line_items, please discontinue instead') unless ok_to_destroy?
end

#check_store_item_is_availableObject (protected)



1278
1279
1280
# File 'app/models/catalog_item.rb', line 1278

def check_store_item_is_available
  errors.add(:store_item_id, 'Store Item must be part of an available location') if store_item && !store_item.available_location?
end

#check_upcObject (protected)



1318
1319
1320
1321
1322
1323
1324
# File 'app/models/catalog_item.rb', line 1318

def check_upc
  # An active public item needs a upc
  return unless upc_required? && item.upc.blank?

  item.update_upc = true
  item.save
end

#complete_stateObject



733
734
735
# File 'app/models/catalog_item.rb', line 733

def complete_state
  "#{human_state_name} - #{HIDDEN_STATES.include?(state) ? 'not public' : 'public'}"
end

#content_locales_to_renderObject



1059
1060
1061
# File 'app/models/catalog_item.rb', line 1059

def content_locales_to_render
  (Mobility.available_locales & ([I18n.default_locale] + (catalog.locales || []))).uniq.sort
end

#couponCoupon

Returns:

See Also:



167
# File 'app/models/catalog_item.rb', line 167

belongs_to  :coupon, optional: true

#create_component_catalog_itemsObject



555
556
557
558
559
560
561
562
# File 'app/models/catalog_item.rb', line 555

def create_component_catalog_items
  store_id = store_item.store_id
  location = store_item.location
  item.kit_components.each do |component_item|
    create_single_component_in_catalog_item(store_id, location, component_item)
  end
  true
end

#create_single_component_in_catalog_item(store_id, location, component_item) ⇒ Object



564
565
566
567
568
569
570
571
572
573
574
# File 'app/models/catalog_item.rb', line 564

def create_single_component_in_catalog_item(store_id, location, component_item)
  si = StoreItem.where(item_id: component_item.id, store_id:, location:).first
  si = StoreItem.create!(item_id: component_item.id, store_id:, location:, qty_on_hand: 0, qty_committed: 0, handling_charge: 0, unit_cogs: 0) if si.nil?
  existing = CatalogItem.where(catalog_id:, store_item_id: si.id).first
  return existing if existing.present? # Catalog Item already exists

  parent_catalog_item = parent_catalog.nil? ? nil : CatalogItem.where(catalog: parent_catalog, store_item: si).first
  price = parent_catalog_item.nil? ? nil : parent_catalog_item.amount
  price *= (1 - parent_catalog_discount) if price && parent_catalog_discount
  CatalogItem.create!(catalog_id:, store_item_id: si.id, amount: price || 0, max_discount: 100, state: 'active_hidden')
end

#currencyObject

Alias for Catalog#currency

Returns:

  • (Object)

    Catalog#currency

See Also:



185
186
187
188
189
# File 'app/models/catalog_item.rb', line 185

delegate :parent_catalog,
:currency,
:currency_symbol,
:tax_rate,
to: :catalog

#currency_symbolObject

Alias for Catalog#currency_symbol

Returns:

  • (Object)

    Catalog#currency_symbol

See Also:



185
186
187
188
189
# File 'app/models/catalog_item.rb', line 185

delegate :parent_catalog,
:currency,
:currency_symbol,
:tax_rate,
to: :catalog

#deep_dupObject



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'app/models/catalog_item.rb', line 135

def deep_dup
  deep_clone(except: %i[third_party_part_number
                        third_party_promo_part_number
                        third_party_sku
                        new_price
                        new_price_effective_date
                        coupon_id
                        sale_price
                        new_coupon_id
                        new_sale_price
                        price_updated_at
                        parent_sku
                        clearance]) do |_original, copy|
    copy.clearance = nil
  end
end

#dependent_catalog_items(filtered_by_catalog_id = nil) ⇒ Object

Find all similar catalog items in the child catalogs, optionally filter only
to one specific catalog



838
839
840
841
842
# File 'app/models/catalog_item.rb', line 838

def dependent_catalog_items(filtered_by_catalog_id = nil)
  catalog.children.select { |c| filtered_by_catalog_id.nil? || c.id == filtered_by_catalog_id }.map do |child_cat|
    child_cat.catalog_items.where(store_item_id:)
  end.flatten.uniq
end

#discounted_price(customer = nil, target = nil) ⇒ Object



1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
# File 'app/models/catalog_item.rb', line 1033

def discounted_price(customer = nil, target = nil)
  discount = (begin
    target.pricing_program_discount_factor
  rescue StandardError
    nil
  end) || (begin
    customer.pricing_program_discount_factor
  rescue StandardError
    nil
  end)
  CatalogItem.calculate_discounted_price(amount, discount, 1.0)
end

#diverging_current_price_vs_proposed_price?Boolean

Returns:

  • (Boolean)


504
505
506
507
508
509
# File 'app/models/catalog_item.rb', line 504

def diverging_current_price_vs_proposed_price?
  return false unless parent_catalog_discount
  return false unless (cup = calculated_updated_price_from_parent)

  amount != cup
end

#edi_communication_logsActiveRecord::Relation<EdiCommunicationLog>

Returns:

See Also:



177
# File 'app/models/catalog_item.rb', line 177

has_many    :edi_communication_logs, through: :edi_documents

#edi_customer_ids_for_catalogObject

Returns the list of EDI customer IDs for this catalog, cached for reuse



674
675
676
# File 'app/models/catalog_item.rb', line 674

def edi_customer_ids_for_catalog
  @edi_customer_ids_for_catalog ||= catalog.customers.where(id: Edi::BaseOrchestrator.customer_ids_edi_enabled).pluck(:id)
end

#edi_documentsActiveRecord::Relation<EdiDocument>

Returns:

See Also:



176
# File 'app/models/catalog_item.rb', line 176

has_many    :edi_documents, dependent: :destroy

#edi_orchestrators_for_catalogObject

Returns orchestrators for EDI customers in this catalog, cached for reuse
Uses the class-level orchestrator cache for fast lookups



680
681
682
683
684
# File 'app/models/catalog_item.rb', line 680

def edi_orchestrators_for_catalog
  @edi_orchestrators_for_catalog ||= edi_customer_ids_for_catalog.filter_map do |customer_id|
    Edi::BaseOrchestrator.orchestrator_for_customer_id(customer_id)
  end.uniq(&:partner)
end

#edi_partner_skuObject



582
583
584
# File 'app/models/catalog_item.rb', line 582

def edi_partner_sku
  third_party_part_number.presence || third_party_sku.presence || sku
end

#effective_seo_description(char_limit: nil) ⇒ Object



727
728
729
730
731
# File 'app/models/catalog_item.rb', line 727

def effective_seo_description(char_limit: nil)
  d = seo_description.presence || item.effective_seo_description(char_limit:) || ''
  d << " (#{item.sku})" unless Regexp.new(item.sku).match(d)
  d
end

#european?Boolean

Returns:

  • (Boolean)


856
857
858
# File 'app/models/catalog_item.rb', line 856

def european?
  catalog.country&.eu_country? || false
end

#exported_catalog_itemExportedCatalogItem



170
# File 'app/models/catalog_item.rb', line 170

has_one     :exported_catalog_item, dependent: :delete

#facet_tokensObject

Alias for
to: :item#facet_tokens

Returns:

  • (Object)
           to: :item#facet_tokens
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#get_successor_online_catalog_itemObject



1241
1242
1243
1244
# File 'app/models/catalog_item.rb', line 1241

def get_successor_online_catalog_item
  res = catalog.catalog_items.for_online_catalog.by_skus(item.successor_item.sku).first if !item_is_web_accessible? && item.successor_item.present?
  res
end

#google_feedsActiveRecord::Relation<GoogleFeed>

Returns:

See Also:



175
# File 'app/models/catalog_item.rb', line 175

has_many    :google_feeds, dependent: :destroy

#in_google_feed?Boolean

Returns:

  • (Boolean)


1265
1266
1267
# File 'app/models/catalog_item.rb', line 1265

def in_google_feed?
  self.class.for_google_feed.exists?(id: id)
end

#in_hide_from_feed_state?Boolean

Returns:

  • (Boolean)


1137
1138
1139
# File 'app/models/catalog_item.rb', line 1137

def in_hide_from_feed_state?
  HIDDEN_STATES.include?(state)
end

#in_main_catalog?Boolean (protected)

Returns:

  • (Boolean)


1326
1327
1328
# File 'app/models/catalog_item.rb', line 1326

def in_main_catalog?
  Catalog.main_catalog_ids.include?(catalog_id)
end

#inventory_message_enabled?Boolean

Returns:

  • (Boolean)


661
662
663
# File 'app/models/catalog_item.rb', line 661

def inventory_message_enabled?
  inventory_message_processors.present?
end

#inventory_message_processorsObject

Retrieves all inventory message processors for edi customers carrying this item
This can be used to push inventory at once to all channels for one item



688
689
690
691
692
# File 'app/models/catalog_item.rb', line 688

def inventory_message_processors
  edi_orchestrators_for_catalog.filter_map do |o|
    o.inventory_message_processor if o.inventory_message_enabled? && o.respond_to?(:inventory_message_processor)
  end
end

#is_available_to_publicObject

Alias for
to: :item#is_available_to_public

Returns:

  • (Object)
           to: :item#is_available_to_public
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_cable_accessory?Object

Alias for
to: :item#is_cable_accessory?

Returns:

  • (Object)
           to: :item#is_cable_accessory?
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_cable_fit_guide?Object

Alias for
to: :item#is_cable_fit_guide?

Returns:

  • (Object)
           to: :item#is_cable_fit_guide?
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_circuit_check?Object

Alias for
to: :item#is_circuit_check?

Returns:

  • (Object)
           to: :item#is_circuit_check?
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_control?Object

Alias for
to: :item#is_control?

Returns:

  • (Object)
           to: :item#is_control?
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_discontinued?Boolean Also known as: is_discontinued

Returns:

  • (Boolean)


1246
1247
1248
# File 'app/models/catalog_item.rb', line 1246

def is_discontinued?
  discontinued? || pending_discontinue?
end

#is_heating_element?Object

Alias for
to: :item#is_heating_element?

Returns:

  • (Object)
           to: :item#is_heating_element?
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_membrane?Object

Alias for
to: :item#is_membrane?

Returns:

  • (Object)
           to: :item#is_membrane?
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_publication?Object

Alias for
to: :item#is_publication?

Returns:

  • (Object)
           to: :item#is_publication?
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_roughin_kit?Object

Alias for
to: :item#is_roughin_kit?

Returns:

  • (Object)
           to: :item#is_roughin_kit?
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_snow_melt_plaque?Object

Alias for
to: :item#is_snow_melt_plaque?

Returns:

  • (Object)
           to: :item#is_snow_melt_plaque?
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_underlayment?Object

Alias for
to: :item#is_underlayment?

Returns:

  • (Object)
           to: :item#is_underlayment?
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#itemItem

Returns:

See Also:



169
# File 'app/models/catalog_item.rb', line 169

has_one     :item, through: :store_item, inverse_of: :catalog_items

#item_is_web_accessible?Boolean

Returns:

  • (Boolean)


1235
1236
1237
1238
1239
# File 'app/models/catalog_item.rb', line 1235

def item_is_web_accessible?
  !in_hide_from_feed_state? &&
    !item&.is_discontinued &&
    !store_item&.is_discontinued
end

#line_itemsActiveRecord::Relation<LineItem>

Returns:

See Also:



173
# File 'app/models/catalog_item.rb', line 173

has_many    :line_items, dependent: :restrict_with_exception

#listing_message_enabled?Boolean

Returns:

  • (Boolean)


669
670
671
# File 'app/models/catalog_item.rb', line 669

def listing_message_enabled?
  listing_message_processors.present?
end

#listing_message_processorsObject

Retrieves all listing message processors for edi customers carrying this item
This can be used to patch listing data at once to all channels for one item



706
707
708
709
710
# File 'app/models/catalog_item.rb', line 706

def listing_message_processors
  edi_orchestrators_for_catalog.filter_map do |o|
    o.listing_message_processor if o.respond_to?(:listing_message_processor) && o.listing_message_enabled?
  end
end

#map_differenceObject

Difference between retail price and MAP (negative = violation)



830
831
832
833
834
# File 'app/models/catalog_item.rb', line 830

def map_difference
  return unless retail_price.present? && map_price.present?

  (retail_price - map_price).round(2)
end

#map_priceObject

MAP (Minimum Advertised Price) based on catalog's map_percentage setting
Only meaningful for vendor catalogs where retailer controls pricing



815
816
817
818
819
# File 'app/models/catalog_item.rb', line 815

def map_price
  return unless msrp.present?

  (msrp * catalog.map_percentage).round(2)
end

#map_to_stores(store_ids) ⇒ Object

This method creates a link between a catalog item and additional stores
It is useful for amazon FBA where we keep track of the stock in amazon's warehouses
Simply pass an array an store id to map the catalog item to and it will do the rest
Note that this method never removes the primary store item for a catalog item



594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
# File 'app/models/catalog_item.rb', line 594

def map_to_stores(store_ids)
  return unless store_ids.present?

  store_ids.uniq.each do |sid|
    s = Store.find(sid)
    # If store id already exist, nothing to do
    next if store_items.detect { |si| si.store_id == s.id }

    # if not quick build
    if catalog.company_id != s.company_id
      errors.add(:base, "Store #{s.id} #{s.short_name} does not belong to same company as catalog item #{id}")
    elsif si = StoreItem.where(store_id: s.id, item_id: item.id, location: 'AVAILABLE').first # See if we already have this si
      # Then link it
      store_items << si
    else # Create a new one (includes implicit linking)
      store_items.create(store_id: s.id, item_id: item.id, qty_on_hand: 0, qty_committed: 0, unit_cogs: amount, location: 'AVAILABLE')
    end
  end
  # Any stores to remove?
  store_items.available.each do |si|
    next if store_item.store_id == si.store_id # The primary store is never removable
    next if si.store_id.in?(store_ids)

    # This store is not in the selected list, try to delete it
    errors.add(:base, "Store Item #{si.id} cannot be deleted due to the presence of stock") unless si.ok_to_destroy? && si.destroy
  end
  store_items
end

#map_violation?Boolean

Check if the retailer's current price is below MAP

Returns:

  • (Boolean)


822
823
824
825
826
827
# File 'app/models/catalog_item.rb', line 822

def map_violation?
  return false unless catalog.retailer_type_vendor?
  return false unless retail_price.present? && map_price.present?

  retail_price < map_price
end

#missing_kit_components?Boolean

Returns:

  • (Boolean)


1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
# File 'app/models/catalog_item.rb', line 1213

def missing_kit_components?
  return false unless item&.is_kit?

  kit_component_item_skus = item.kit_components.pluck(:sku)
  kit_components_count = kit_component_item_skus.size
  catalog_items = CatalogItem.joins(store_item: :item).where(catalog_id:, items: { sku: kit_component_item_skus }, store_items: { location: store_item.location })
  catalog_item_skus = catalog_items.pluck(:sku)
  catalog_items_count = catalog_item_skus.size
  if kit_components_count == catalog_items_count
    false
  else
    logger.error "Missing kit components in catalog #{catalog_id} for #{item.sku}: #{kit_component_item_skus - catalog_item_skus}"
    true
  end
end

#msrpObject



791
792
793
# File 'app/models/catalog_item.rb', line 791

def msrp
  root_catalog_item.try(:amount)
end

#msrp_with_vatObject



795
796
797
798
799
# File 'app/models/catalog_item.rb', line 795

def msrp_with_vat
  return unless tax_rate.present?

  root_catalog_item.try(:price_with_vat)
end

#name(locale: nil) ⇒ Object



1064
1065
1066
1067
1068
1069
1070
1071
# File 'app/models/catalog_item.rb', line 1064

def name(locale: nil)
  I18n.with_locale(locale || I18n.locale) do
    n = third_party_name.presence
    n ||= item.amazon_title if amazon_catalog_item?
    n ||= item&.public_name
    n
  end
end

#new_couponCoupon

Returns:

See Also:



168
# File 'app/models/catalog_item.rb', line 168

belongs_to  :new_coupon, optional: true, class_name: 'Coupon'

#next_available(use_store_item: nil, use_alternate_warehouse: false) ⇒ Object

Returns next available for internal CRM use (100% real stock)
discontinued and pending discontinue item will report nil for next available



929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
# File 'app/models/catalog_item.rb', line 929

def next_available(use_store_item: nil, use_alternate_warehouse: false)
  return nil if discontinued? || pending_discontinue?

  use_store_item ||= store_item
  next_available_hsh_arr = [use_store_item.next_available]
  if use_alternate_warehouse
    # What's next available in the other warehouse? Include 100% (real stock) with 1 week delay
    alternate_warehouse_store_items.each do |alternate_si|
      alt_next_available_hsh = alternate_si.next_available
      if alt_next_available_hsh.present?
        alt_next_available_hsh = OpenStruct.new(next_available_qty: alt_next_available_hsh[:next_available_qty],
                                                next_available_date: (alt_next_available_hsh[:next_available_date] + 1.week)).freeze
      end
      next_available_hsh_arr << alt_next_available_hsh
    end
  end
  # here we discard all the nil entries and sort by next available date, returning the first entry, i.e. the next available
  next_available_entry_to_use = next_available_hsh_arr.compact.sort_by { |h| h[:next_available_date] }.first
  return nil unless next_available_entry_to_use.present?

  next_available_entry_to_use
end

#next_available_by_warehouse(use_alternate_warehouse: false) ⇒ Object



1002
1003
1004
1005
1006
# File 'app/models/catalog_item.rb', line 1002

def next_available_by_warehouse(use_alternate_warehouse: false)
  store_items_by_warehouse.each_with_object({}) do |si, hsh|
    hsh[si.store.name] = next_available(use_store_item: si, use_alternate_warehouse:)
  end
end

#next_available_by_warehouse_with_depth_limit(use_alternate_warehouse: false, max_depth: 10, current_depth: 0) ⇒ Object

Version with depth limit to prevent infinite recursion



1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
# File 'app/models/catalog_item.rb', line 1009

def next_available_by_warehouse_with_depth_limit(use_alternate_warehouse: false, max_depth: 10, current_depth: 0)
  # Prevent infinite recursion
  if current_depth >= max_depth
    Rails.logger.warn "Maximum depth limit (#{max_depth}) reached for catalog item #{id} (#{item&.sku}). Possible circular reference in kit structure."
    return {}
  end

  store_items_by_warehouse.each_with_object({}) do |si, hsh|
    hsh[si.store.name] = next_available_with_depth_limit(use_store_item: si, use_alternate_warehouse:, max_depth:, current_depth: current_depth + 1)
  end
end

#next_available_with_depth_limit(use_store_item: nil, use_alternate_warehouse: false, max_depth: 10, current_depth: 0) ⇒ Object

Version with depth limit to prevent infinite recursion (100% real stock for internal use)



953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
# File 'app/models/catalog_item.rb', line 953

def next_available_with_depth_limit(use_store_item: nil, use_alternate_warehouse: false, max_depth: 10, current_depth: 0)
  # Prevent infinite recursion
  if current_depth >= max_depth
    Rails.logger.warn "Maximum depth limit (#{max_depth}) reached for catalog item #{id} (#{item&.sku}). Possible circular reference in kit structure."
    return nil
  end

  use_store_item ||= store_item
  next_available_hsh_arr = [use_store_item.next_available]
  if use_alternate_warehouse
    # What's next available in the other warehouse? Include 100% (real stock) with 1 week delay
    alternate_warehouse_store_items.each do |alternate_si|
      alt_next_available_hsh = alternate_si.next_available_with_depth_limit(max_depth: max_depth, current_depth: current_depth + 1)
      if alt_next_available_hsh.present?
        alt_next_available_hsh = OpenStruct.new(next_available_qty: alt_next_available_hsh[:next_available_qty],
                                                next_available_date: (alt_next_available_hsh[:next_available_date] + 1.week)).freeze
      end
      next_available_hsh_arr << alt_next_available_hsh
    end
  end
  # here we discard all the nil entries and sort by next available date, returning the first entry, i.e. the next available
  next_available_entry_to_use = next_available_hsh_arr.compact.sort_by { |h| h[:next_available_date] }.first
  return nil unless next_available_entry_to_use.present?

  next_available_entry_to_use
end

#notify_of_price_updateObject (protected)



1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
# File 'app/models/catalog_item.rb', line 1299

def notify_of_price_update
  Rails.configuration.event_store.publish(
    Events::PriceUpdated.new(data: {
      catalog_item_id: id,
      price_was: old_amount,
      price_now: amount
    }),
    stream_name: "CatalogItem-#{id}"
  )
end

#ok_to_destroy?Boolean

Returns:

  • (Boolean)


844
845
846
# File 'app/models/catalog_item.rb', line 844

def ok_to_destroy?
  !line_items.exists?
end

#on_order(use_alternate_warehouse: false) ⇒ Object



996
997
998
999
1000
# File 'app/models/catalog_item.rb', line 996

def on_order(use_alternate_warehouse: false)
  store_items_by_warehouse.each_with_object({}) do |si, hsh|
    hsh[si.store.name] = on_order_for_store_item(use_store_item: si, use_alternate_warehouse:)
  end
end

#on_order_for_store_item(use_store_item: nil, use_alternate_warehouse: false) ⇒ Object

Returns on order quantities for internal CRM use (100% real stock)
discontinued and pending discontinue item will report 0 for on order



911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
# File 'app/models/catalog_item.rb', line 911

def on_order_for_store_item(use_store_item: nil, use_alternate_warehouse: false)
  return 0 if discontinued? || pending_discontinue?

  use_store_item ||= store_item
  on_order_arr = use_store_item.on_order
  if use_alternate_warehouse
    # What's on order for the other warehouses? Include 100% (real stock) with 1 week delay
    alternate_warehouse_store_items.each do |alternate_si|
      alt_on_order_arr = alternate_si.on_order
      alt_on_order_arr.each { |h| h[:promised_delivery_date] = (h[:promised_delivery_date] + 1.week) }
      on_order_arr.concat(alt_on_order_arr)
    end
  end
  on_order_arr
end

#out_of_stock(use_threshhold = nil) ⇒ Object



848
849
850
851
852
853
854
# File 'app/models/catalog_item.rb', line 848

def out_of_stock(use_threshhold = nil)
  # Use catalog_item.reserve_stock instead of legacy item threshold
  stock_reserved = use_threshhold || reserve_stock.to_i
  return true if store_item && (store_item.qty_available - stock_reserved) <= 0

  false
end

#oversize?Object

Alias for
to: :item#oversize?

Returns:

  • (Object)
           to: :item#oversize?
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#parent_catalogObject

Alias for Catalog#parent_catalog

Returns:

  • (Object)

    Catalog#parent_catalog

See Also:



185
186
187
188
189
# File 'app/models/catalog_item.rb', line 185

delegate :parent_catalog,
:currency,
:currency_symbol,
:tax_rate,
to: :catalog

#parent_catalog_currency_symbolObject



764
765
766
767
768
# File 'app/models/catalog_item.rb', line 764

def parent_catalog_currency_symbol
  return unless parent_catalog

  parent_catalog.currency_symbol
end

#parent_catalog_discountObject



511
512
513
514
515
516
# File 'app/models/catalog_item.rb', line 511

def parent_catalog_discount
  return 0.0 unless catalog.parent_catalog
  return catalog.parent_catalog_discount_refurb if item.condition_refurbished?

  catalog.parent_catalog_discount
end

#parent_catalog_itemObject

If a parent_catalog exists as defined in the catalog of this catalog item
then find the matching catalog item by store item id which should be similar



772
773
774
775
776
# File 'app/models/catalog_item.rb', line 772

def parent_catalog_item
  return unless parent_catalog

  parent_catalog.catalog_items.where(store_item_id:).first
end

#parent_catalog_item_amountObject



778
779
780
781
782
# File 'app/models/catalog_item.rb', line 778

def parent_catalog_item_amount
  return unless pci = parent_catalog_item

  pci.amount
end

#percentage_off_from_msrpObject



801
802
803
804
805
# File 'app/models/catalog_item.rb', line 801

def percentage_off_from_msrp
  return unless msrp && effective_price

  ((1.0 - (effective_price / msrp)) * 100).round(2)
end

#prefix: :new_sale_price_effective_dateObject

Alias for New_coupon#effective_date

Returns:

  • (Object)

    New_coupon#prefix: :new_sale_price_effective_date

See Also:



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

delegate :effective_date,
:expiration_date,
to: :new_coupon,
prefix: :new_sale_price,
allow_nil: true

#prefix: :new_sale_price_expiration_dateObject

Alias for New_coupon#expiration_date

Returns:

  • (Object)

    New_coupon#prefix: :new_sale_price_expiration_date

See Also:



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

delegate :effective_date,
:expiration_date,
to: :new_coupon,
prefix: :new_sale_price,
allow_nil: true

#prefix: :sale_price_effective_dateObject

Alias for Coupon#effective_date

Returns:

  • (Object)

    Coupon#prefix: :sale_price_effective_date

See Also:



229
230
231
232
233
# File 'app/models/catalog_item.rb', line 229

delegate :effective_date,
:expiration_date,
to: :coupon,
prefix: :sale_price,
allow_nil: true

#prefix: :sale_price_expiration_dateObject

Alias for Coupon#expiration_date

Returns:

  • (Object)

    Coupon#prefix: :sale_price_expiration_date

See Also:



229
230
231
232
233
# File 'app/models/catalog_item.rb', line 229

delegate :effective_date,
:expiration_date,
to: :coupon,
prefix: :sale_price,
allow_nil: true

#price_message_enabled?Boolean

Returns:

  • (Boolean)


665
666
667
# File 'app/models/catalog_item.rb', line 665

def price_message_enabled?
  price_message_processors.present?
end

#price_message_processorsObject

Retrieves all price message processors for edi customers carrying this item
This can be used to push price at once to all channels for one item



697
698
699
700
701
# File 'app/models/catalog_item.rb', line 697

def price_message_processors
  edi_orchestrators_for_catalog.filter_map do |o|
    o.price_message_processor if o.price_message_enabled? && o.respond_to?(:price_message_processor)
  end
end

#price_out_of_date?Boolean

This method determines if the catalog item is out of date compared to its parent catalog item, return true/false or nil if not applicable.

Returns:

  • (Boolean)


754
755
756
757
758
759
760
761
762
# File 'app/models/catalog_item.rb', line 754

def price_out_of_date?
  return false unless (pci = parent_catalog_item) # Not applicable if we don't have a parent catalog item
  return false unless (parent_price_updated_at = pci.price_updated_at) # Not applicable unless the parent catalog item has a price update
  return false unless catalog.price_sync_timed? # Don't care if not a timed catalog
  return true unless price_updated_at # Nil value will need to assume an out of date price if parent has one

  price_age_in_days = parent_price_updated_at - price_updated_at # should be an integer
  price_age_in_days >= catalog.price_sync_delay # Price is out of date when the catalog price delay is met or exceeded
end

#price_updated?Boolean (protected)

Returns:

  • (Boolean)


1310
1311
1312
# File 'app/models/catalog_item.rb', line 1310

def price_updated?
  saved_changes.key?('amount')
end

#primary_catalogObject



586
587
588
# File 'app/models/catalog_item.rb', line 586

def primary_catalog
  catalog_id.in?(CatalogConstants::ALL_MAIN_CATALOG_IDS)
end

#primary_imageObject

Alias for
to: :item#primary_image

Returns:

  • (Object)
           to: :item#primary_image
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#primary_product_lineObject

Alias for
to: :item#primary_product_line

Returns:

  • (Object)
           to: :item#primary_product_line
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#primary_product_line_idObject

Alias for
to: :item#primary_product_line_id

Returns:

  • (Object)
           to: :item#primary_product_line_id
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#product_categoryObject

Alias for
to: :item#product_category

Returns:

  • (Object)
           to: :item#product_category
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#product_category_visible_in_feed?Boolean

Returns:

  • (Boolean)


1145
1146
1147
# File 'app/models/catalog_item.rb', line 1145

def product_category_visible_in_feed?
  product_category&.available_to_public
end

#product_linesObject

Alias for
to: :item#product_lines

Returns:

  • (Object)
           to: :item#product_lines
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#product_stock_statusObject



1021
1022
1023
1024
1025
1026
1027
# File 'app/models/catalog_item.rb', line 1021

def product_stock_status
  if item.always_available_online? || !out_of_stock
    'InStock'
  else
    'OutOfStock'
  end
end

#profit_marginObject



518
519
520
# File 'app/models/catalog_item.rb', line 518

def profit_margin
  calculate_profit_margin amount
end

#profit_margin_new_priceObject



534
535
536
# File 'app/models/catalog_item.rb', line 534

def profit_margin_new_price
  calculate_profit_margin new_price
end

#profit_margin_new_sale_priceObject



530
531
532
# File 'app/models/catalog_item.rb', line 530

def profit_margin_new_sale_price
  calculate_profit_margin new_sale_price
end

#profit_margin_retailer_requested_costObject



522
523
524
# File 'app/models/catalog_item.rb', line 522

def profit_margin_retailer_requested_cost
  calculate_profit_margin retailer_requested_cost
end

#profit_margin_sale_priceObject



526
527
528
# File 'app/models/catalog_item.rb', line 526

def profit_margin_sale_price
  calculate_profit_margin sale_price
end

#public_description_htmlObject

Alias for
to: :item#public_description_html

Returns:

  • (Object)
           to: :item#public_description_html
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#public_nameObject

Alias for
to: :item#public_name

Returns:

  • (Object)
           to: :item#public_name
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#purge_edge_cacheObject (protected)



1360
1361
1362
1363
1364
1365
1366
# File 'app/models/catalog_item.rb', line 1360

def purge_edge_cache
  return unless saved_changes?
  # only need to do this for main catalogs
  return unless Catalog.main_catalog_ids.include?(catalog_id)

  site_maps.each(&:purge_edge_cache)
end

#push_inventory_messageObject

Performs an integration push where applicable for stock to partner carrying this catalog item



714
715
716
# File 'app/models/catalog_item.rb', line 714

def push_inventory_message
  inventory_message_processors.map { |imp| imp.process(catalog_items: CatalogItem.where(id:)) }.flatten
end

#push_price_messageObject



718
719
720
721
# File 'app/models/catalog_item.rb', line 718

def push_price_message
  logger.warn "No price message processors found for catalog item #{id}" if price_message_processors.empty?
  price_message_processors.map { |imp| imp.process(catalog_items: CatalogItem.where(id:)) }.flatten
end

#qty_availableObject

Alias for Store_item#qty_available

Returns:

  • (Object)

    Store_item#qty_available

See Also:



224
225
226
227
# File 'app/models/catalog_item.rb', line 224

delegate :qty_available,
:qty_available_outside_order,
:unit_cogs,
to: :store_item

#qty_available_outside_orderObject

Alias for Store_item#qty_available_outside_order

Returns:

  • (Object)

    Store_item#qty_available_outside_order

See Also:



224
225
226
227
# File 'app/models/catalog_item.rb', line 224

delegate :qty_available,
:qty_available_outside_order,
:unit_cogs,
to: :store_item

#real_stock(use_store_item: nil, use_alternate_warehouse: false) ⇒ Object

Returns REAL stock available (100% of all warehouses) for internal CRM use
Use this for cycle counts, pick items, order management, quotes, etc.



869
870
871
872
873
874
875
876
877
878
# File 'app/models/catalog_item.rb', line 869

def real_stock(use_store_item: nil, use_alternate_warehouse: false)
  use_store_item ||= store_item
  qty_available = use_store_item.qty_available.to_i
  if use_alternate_warehouse
    alternate_warehouse_store_items.each do |alternate_si|
      qty_available += alternate_si.qty_available.to_i
    end
  end
  qty_available
end

#refresh_google_feedObject (protected)



1314
1315
1316
# File 'app/models/catalog_item.rb', line 1314

def refresh_google_feed
  Feed::Google::ListGenerator.new(catalog_item_ids: [id])
end

#refurbishedObject



1229
1230
1231
1232
1233
# File 'app/models/catalog_item.rb', line 1229

def refurbished
  return unless irv = item.refurbished_version

  irv.catalog_items.where(catalog_id:).not_hidden_from_catalog.first
end

#reported_nameObject



1077
1078
1079
1080
# File 'app/models/catalog_item.rb', line 1077

def reported_name
  # name.encode("ASCII", :invalid => :replace, :undef => :replace, :replace => "")
  name
end

#reported_name_for_googleObject



1082
1083
1084
# File 'app/models/catalog_item.rb', line 1082

def reported_name_for_google
  google_feed_title.presence || reported_name
end

#reported_stock(use_store_item: nil, use_alternate_warehouse: false, safety_stock: nil) ⇒ Object

Stock for EXTERNAL reporting (feeds, website, EDI)
Uses catalog's alternate_warehouse_stock_fraction and reserve_stock
discontinued and pending discontinue item will report 0 for stock



883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
# File 'app/models/catalog_item.rb', line 883

def reported_stock(use_store_item: nil, use_alternate_warehouse: false, safety_stock: nil)
  return 0 if discontinued? || pending_discontinue?

  safety_stock ||= reserve_stock.to_i
  use_store_item ||= store_item
  qty_available = use_store_item.qty_available.to_i
  if use_alternate_warehouse
    # What's in the other warehouse? We report a fraction (default 25%) of that stock
    fraction = alternate_warehouse_stock_fraction
    alternate_warehouse_store_items.each do |alternate_si|
      alternate_stock = (alternate_si.qty_available * fraction).ceil
      # If alternate_warehouse_stock_reporting_max is set, cap the alternate stock at that value
      alternate_stock = [alternate_stock, alternate_warehouse_stock_reporting_max].min if alternate_warehouse_stock_reporting_max.present?
      qty_available += alternate_stock
    end
  end
  # if we have a min reported stock (safety_stock) set we will subtract it first, 2 avail with min of 2 to report will report 0
  qty_available = [qty_available - safety_stock, 0].max
  # If our always available stock is set we will use that value if the qty available is below
  if use_store_item.permanent_qty_available&.zero?
    0
  else
    [qty_available, min_stock_to_report, use_store_item.permanent_qty_available].compact.max
  end
end

#reported_stocks(use_alternate_warehouse: false) ⇒ Object

Returns a hash of store ids and reported stock available in each
{ 'WarmlyYours-CA': 14, 'WarmlyYours-US': 20 }



990
991
992
993
994
# File 'app/models/catalog_item.rb', line 990

def reported_stocks(use_alternate_warehouse: false)
  store_items_by_warehouse.each_with_object({}) do |si, hsh|
    hsh[si.store.name] = reported_stock(use_store_item: si, use_alternate_warehouse:)
  end
end

#reported_vendor_sku(_orchestrator_partner = nil) ⇒ Object



1073
1074
1075
# File 'app/models/catalog_item.rb', line 1073

def reported_vendor_sku(_orchestrator_partner = nil)
  third_party_sku.presence || sku
end

#reset_edi_delta_feed_fieldsObject



1256
1257
1258
1259
1260
1261
1262
1263
# File 'app/models/catalog_item.rb', line 1256

def reset_edi_delta_feed_fields
  EDI_DELTA_FEED_CATEGORIES.each do |category|
    message_sent_json_column_name = :"last_#{category}_sent_json"
    if respond_to?(message_sent_json_column_name) && try(message_sent_json_column_name).present? # if this column exists in CatalogItem, then let's reset it, which ensures it will get sent in the next schedule EDI feed for that category of EDI message
      update_column(message_sent_json_column_name, {})
    end
  end
end

#retail_price_currency_symbolObject



749
750
751
# File 'app/models/catalog_item.rb', line 749

def retail_price_currency_symbol
  catalog.currency_symbol
end

#retailer_probesActiveRecord::Relation<CatalogItemRetailerProbe>

Returns:

See Also:



179
# File 'app/models/catalog_item.rb', line 179

has_many    :retailer_probes, class_name: 'CatalogItemRetailerProbe', dependent: :destroy

#root_catalog_itemObject



784
785
786
787
788
789
# File 'app/models/catalog_item.rb', line 784

def root_catalog_item
  root_catalog = catalog.root
  return self unless root_catalog != catalog

  root_catalog.catalog_items.find_by(store_item_id:)
end

#sale_price_in_effect?(exclude_effective_date: false, date_to_check: nil) ⇒ Boolean

Returns:

  • (Boolean)


1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
# File 'app/models/catalog_item.rb', line 1149

def sale_price_in_effect?(exclude_effective_date: false, date_to_check: nil)
  return false unless sale_price.present?

  unless exclude_effective_date
    date_to_check ||= Date.current
    # If today is before the sale start, don't send
    return false if sale_price_effective_date && date_to_check < sale_price_effective_date
    # If today is past the sale expiration, don't send
    return false if sale_price_expiration_date && date_to_check > sale_price_expiration_date
  end
  true
end

#sale_price_percentage_offObject

Returns the percentage off (0-100) of an item comparing sale price to catalog price



1195
1196
1197
1198
1199
# File 'app/models/catalog_item.rb', line 1195

def sale_price_percentage_off
  return 0.0 unless sale_price.present? && amount.present? && amount.positive?

  ((1.0 - (sale_price / amount)) * 100).round(2)
end

#sale_price_percentage_off=(percentage_off) ⇒ Object

Assign a percentage off (0-100) and the sale price will be calculated based on the catalog item price without VAT



1190
1191
1192
# File 'app/models/catalog_item.rb', line 1190

def sale_price_percentage_off=(percentage_off)
  calculate_sale_price(percentage_off)
end

#sale_price_reset_if_no_couponObject (protected)

You cannot have a sale price without a coupon, so before anything, we remove these



1272
1273
1274
1275
1276
# File 'app/models/catalog_item.rb', line 1272

def sale_price_reset_if_no_coupon
  return if coupon.present?

  self.sale_price = nil
end

#sale_price_with_vat_percentage_offObject

Returns the percentage off (0-100) of an item comparing sale price with VAT to catalog price with VAT



1207
1208
1209
1210
1211
# File 'app/models/catalog_item.rb', line 1207

def sale_price_with_vat_percentage_off
  return 0.0 unless sale_price_with_vat.present?

  ((1.0 - (sale_price_with_vat / price_with_vat)) * 100).round(2)
end

#sale_price_with_vat_percentage_off=(percentage_off) ⇒ Object

Assign a percentage off (0-100) and the sale price will be calculated based on the catalog item price with VAT included



1202
1203
1204
# File 'app/models/catalog_item.rb', line 1202

def sale_price_with_vat_percentage_off=(percentage_off)
  calculate_sale_price(percentage_off, include_vat: true)
end

#seo_descriptionObject

Alias for
to: :item#seo_description

Returns:

  • (Object)
           to: :item#seo_description
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#seo_keywordsObject

Alias for
to: :item#seo_keywords

Returns:

  • (Object)
           to: :item#seo_keywords
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#seo_titleObject



723
724
725
# File 'app/models/catalog_item.rb', line 723

def seo_title
  item.effective_seo_title
end

#set_or_clear_discontinued_dateObject (protected)



1350
1351
1352
1353
1354
1355
1356
1357
1358
# File 'app/models/catalog_item.rb', line 1350

def set_or_clear_discontinued_date
  return unless state_changed?

  if %w[pending_discontinue discontinued].include?(state) && %w[active active_hidden pending_onboarding pending_vendor_update invalid_catalog_item].include?(state_was)
    self.discontinued_date = Date.current
  elsif %w[pending_discontinue discontinued].include?(state_was) && %w[active active_hidden pending_onboarding pending_vendor_update invalid_catalog_item].include?(state)
    self.discontinued_date = nil
  end
end

#should_appear_in_feed?Boolean

Returns:

  • (Boolean)


1141
1142
1143
# File 'app/models/catalog_item.rb', line 1141

def should_appear_in_feed?
  !in_hide_from_feed_state? && product_category_visible_in_feed?
end

#siblingsObject



1106
1107
1108
# File 'app/models/catalog_item.rb', line 1106

def siblings
  CatalogItem.where(catalog_id:)
end

#single_sku?Boolean

Returns:

  • (Boolean)


1133
1134
1135
# File 'app/models/catalog_item.rb', line 1133

def single_sku?
  variants(include_self: true).size <= 1
end

#site_mapsActiveRecord::Relation<SiteMap>

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



174
# File 'app/models/catalog_item.rb', line 174

has_many    :site_maps, as: :resource, dependent: :destroy

#skuObject

Alias for
to: :item#sku

Returns:

  • (Object)
           to: :item#sku
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#sku_and_nameObject



1029
1030
1031
# File 'app/models/catalog_item.rb', line 1029

def sku_and_name
  item.sku + ' - ' + item.name
end

#specObject

Alias for
to: :item#spec

Returns:

  • (Object)
           to: :item#spec
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#spec_outputObject

Alias for
to: :item#spec_output

Returns:

  • (Object)
           to: :item#spec_output
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#specificationsObject

Alias for
to: :item#specifications

Returns:

  • (Object)
           to: :item#specifications
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#specifications_groupedObject

Alias for
to: :item#specifications_grouped

Returns:

  • (Object)
           to: :item#specifications_grouped
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#state_requires_edi_warning_on_order?Boolean

Returns:

  • (Boolean)


1251
1252
1253
1254
# File 'app/models/catalog_item.rb', line 1251

def state_requires_edi_warning_on_order?
  # here we warn when this item is in a state that should not be ordered via EDI, but allow order to go through
  %w[active require_vendor_update].include?(state) != true
end

#storeStore

Returns:

See Also:



171
# File 'app/models/catalog_item.rb', line 171

has_one     :store, through: :store_item

#store_itemStoreItem

Returns:

See Also:



165
# File 'app/models/catalog_item.rb', line 165

belongs_to  :store_item, inverse_of: :catalog_items

#store_itemsActiveRecord::Relation<StoreItem>

Returns:

See Also:



181
# File 'app/models/catalog_item.rb', line 181

has_and_belongs_to_many :store_items, inverse_of: :catalog_items

#store_items_by_warehouseObject



984
985
986
# File 'app/models/catalog_item.rb', line 984

def store_items_by_warehouse
  store_items.joins(:store).includes(:store).merge(Store.warmlyyours_warehouses)
end

#tax_classObject

Alias for
to: :item#tax_class

Returns:

  • (Object)
           to: :item#tax_class
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#tax_rateObject

Alias for Catalog#tax_rate

Returns:

  • (Object)

    Catalog#tax_rate

See Also:



185
186
187
188
189
# File 'app/models/catalog_item.rb', line 185

delegate :parent_catalog,
:currency,
:currency_symbol,
:tax_rate,
to: :catalog

#third_party_name_en_required?Boolean (protected)

Returns:

  • (Boolean)


1342
1343
1344
# File 'app/models/catalog_item.rb', line 1342

def third_party_name_en_required?
  active? && catalog.third_party_name_en_required
end

#third_party_name_fr_required?Boolean (protected)

Returns:

  • (Boolean)


1346
1347
1348
# File 'app/models/catalog_item.rb', line 1346

def third_party_name_fr_required?
  active? && catalog.third_party_name_fr_required
end

#third_party_part_number_labelObject



1055
1056
1057
# File 'app/models/catalog_item.rb', line 1055

def third_party_part_number_label
  catalog.determine_retailer_part_number_label
end

#third_party_part_number_required?Boolean (protected)

Returns:

  • (Boolean)


1334
1335
1336
# File 'app/models/catalog_item.rb', line 1334

def third_party_part_number_required?
  active? && catalog.third_party_part_number_required
end

#third_party_sku_required?Boolean (protected)

Returns:

  • (Boolean)


1338
1339
1340
# File 'app/models/catalog_item.rb', line 1338

def third_party_sku_required?
  active? && catalog.third_party_sku_required
end

#to_sObject



741
742
743
# File 'app/models/catalog_item.rb', line 741

def to_s
  "CatalogItem[#{id}]"
end

#tweak_ebay_skuObject



576
577
578
579
580
# File 'app/models/catalog_item.rb', line 576

def tweak_ebay_sku
  return unless catalog.orchestrator_key == 'ebay_us'

  update(third_party_sku: item.sku.tr('.', '*'))
end

#unit_cogsObject

Alias for Store_item#unit_cogs

Returns:

  • (Object)

    Store_item#unit_cogs

See Also:



224
225
226
227
# File 'app/models/catalog_item.rb', line 224

delegate :qty_available,
:qty_available_outside_order,
:unit_cogs,
to: :store_item

#upcObject

Alias for
to: :item#upc

Returns:

  • (Object)
           to: :item#upc
    

See Also:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/catalog_item.rb', line 191

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:facet_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#upc_required?Boolean (protected)

Returns:

  • (Boolean)


1330
1331
1332
# File 'app/models/catalog_item.rb', line 1330

def upc_required?
  available_in_edi_feed? && catalog.require_upc?
end

#update_price_from_parentObject

When the catalog item state moves from pending client update to active, the price is updated



1087
1088
1089
1090
1091
1092
1093
# File 'app/models/catalog_item.rb', line 1087

def update_price_from_parent
  return unless parent_catalog_item

  push_price_service = Catalog::PushCatalogItemPrice.new
  result = push_price_service.process(parent_catalog_item, target_catalog_id: catalog_id)
  result.all_price_pushed?
end

#variants(catalog_items_scope: nil, include_self: false, facet_filters: {}) ⇒ Object



1110
1111
1112
1113
1114
1115
# File 'app/models/catalog_item.rb', line 1110

def variants(catalog_items_scope: nil, include_self: false, facet_filters: {})
  item_variants = item.item_grouping_info(include_self:, facet_filters:)&.variants
  return CatalogItem.none if item_variants.blank?

  catalog_items_in_same_catalog(CatalogItem.for_online_catalog).merge(item_variants).includes(:item)
end