Class: CatalogItem

Inherits:
ApplicationRecord show all
Includes:
Memery, Models::Auditable, Models::CatalogItemAmazonHelper, Models::CatalogItemWalmartHelper, Models::CatalogItemWayfairHelper, Models::SaleDiscountable, Models::Translatable, 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
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_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_... (coupon_id => coupons.id)
fk_rails_... (new_coupon_id => coupons.id) ON DELETE => nullify

Defined Under Namespace

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

Constant Summary collapse

HIDDEN_STATES =

Recognised hidden states.

%w[active_hidden discontinued pending_client_review invalid_catalog_item pending_onboarding].freeze
EDI_FEED_STATUSES =

Recognised 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].freeze
ORCHESTRATOR_STATES =

Recognised orchestrator states.

%w[active require_vendor_update pending_vendor_update pending_discontinue discontinued].freeze

Constants included from Models::CatalogItemWalmartHelper

Models::CatalogItemWalmartHelper::WALMART_ALREADY_RETIRED_ERROR_CODE

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

Constants included from Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

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 Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

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.



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

def add_kit_components_to_catalog
  @add_kit_components_to_catalog
end

#alternate_warehouse_stock_reporting_maxObject (readonly)



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

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

#amountObject (readonly)



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

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

#item_idObject



676
677
678
# File 'app/models/catalog_item.rb', line 676

def item_id
  store_item&.item_id
end

#max_discountObject (readonly)



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

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

#new_priceObject (readonly)



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

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

#new_price_effective_dateObject (readonly)



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

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

#reserve_stockObject (readonly)



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

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

#skip_check_kit_componentsObject

Returns the value of attribute skip_check_kit_components.



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

def skip_check_kit_components
  @skip_check_kit_components
end

#skip_create_kit_componentsObject

Returns the value of attribute skip_create_kit_components.



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

def skip_create_kit_components
  @skip_create_kit_components
end

#store_idsObject



672
673
674
# File 'app/models/catalog_item.rb', line 672

def store_ids
  store_items.map(&:store_id)
end

#third_party_name_enObject (readonly)



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

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

#third_party_name_frObject (readonly)



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

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? })


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

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

#third_party_skuObject (readonly)



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

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

#third_party_sku_variant_idObject (readonly)

Marketplace variant selector (e.g. Wayfair piid): a given variant must map to
at most one of our catalog items per catalog. allow_nil so the many single /
non-variant items (no selector) coexist — a blank selector is normalized to
nil (see the normalizes block below) so a form-submitted "" doesn't trip
this against another blank item. The partial DB index is the backstop.

Validations:



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

validates :third_party_sku_variant_id, uniqueness: { scope: :catalog_id }, allow_nil: true

Class Method Details

.accessoriesActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are accessories. Active Record Scope

Returns:

See Also:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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



486
487
488
489
490
# File 'app/models/catalog_item.rb', line 486

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



476
477
478
# File 'app/models/catalog_item.rb', line 476

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:



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

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:



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

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

.coupons_for_future_promo_selectObject



499
500
501
502
503
504
# File 'app/models/catalog_item.rb', line 499

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



492
493
494
495
496
497
# File 'app/models/catalog_item.rb', line 492

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:



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

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:



355
356
357
358
359
360
361
362
363
364
365
# File 'app/models/catalog_item.rb', line 355

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:



366
367
368
369
370
371
372
373
374
375
376
# File 'app/models/catalog_item.rb', line 366

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:



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

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:



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

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:



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

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:



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

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:



338
339
340
341
342
343
344
# File 'app/models/catalog_item.rb', line 338

scope :for_online_catalog_or_non_web_accessible_with_successor_item, -> {
  ids = for_online_catalog.ids
  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').ids
  where(catalog_items: { id: (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:



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

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:



313
314
315
# File 'app/models/catalog_item.rb', line 313

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:



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

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:



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

scope :for_where_to_buy_list, ->(parent_catalog_id) {
  joins(:catalog).where(state: 'active').where.not(catalog_id: [1, 2, 74]).where.not(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:



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

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:



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

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:



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

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:



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

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:



382
383
384
385
386
387
# File 'app/models/catalog_item.rb', line 382

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:



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

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:



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

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:



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

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:



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

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



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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

.primary_catalogs_firstActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are primary catalogs first. Active Record Scope

Returns:

See Also:



279
280
281
282
# File 'app/models/catalog_item.rb', line 279

scope :primary_catalogs_first, -> {
  in_order_of(:catalog_id, CatalogConstants::ALL_MAIN_CATALOG_IDS, filter: false)
    .order(Arel.sql('catalogs.name ASC'))
}

.primary_catalogs_first_then_by_state_and_nameActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are primary catalogs first then by state and name. Active Record Scope

Returns:

See Also:



283
284
285
286
287
# File 'app/models/catalog_item.rb', line 283

scope :primary_catalogs_first_then_by_state_and_name, -> {
  in_order_of(:catalog_id, CatalogConstants::ALL_MAIN_CATALOG_IDS, filter: false)
    .in_order_of(:state, %w[active pending_client_review pending_onboarding require_vendor_update pending_vendor_update pending_discontinue active_hidden discontinued invalid_catalog_item], filter: false)
    .order(Arel.sql('catalogs.name ASC'))
}

.public_catalog_itemsActiveRecord::Relation<CatalogItem>

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

Returns:

See Also:



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

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:



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

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:



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

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:



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

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



480
481
482
483
484
# File 'app/models/catalog_item.rb', line 480

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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)



903
904
905
# File 'app/models/catalog_item.rb', line 903

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

#alternate_warehouse_store_itemsObject



1020
1021
1022
# File 'app/models/catalog_item.rb', line 1020

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:



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

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



1167
1168
1169
1170
1171
# File 'app/models/catalog_item.rb', line 1167

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



768
769
770
# File 'app/models/catalog_item.rb', line 768

def amount_currency_symbol
  catalog.currency_symbol
end

#available_in_edi_feed?Boolean

Returns:

  • (Boolean)


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

def available_in_edi_feed?
  EDI_FEED_STATUSES.include?(state)
end

#available_storesObject



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

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

#bom_priceObject



760
761
762
# File 'app/models/catalog_item.rb', line 760

def bom_price
  retail_price || amount
end

#calculate_minimum_price_for_margin(target_margin) ⇒ Object

Provide a target margin below a 100

Raises:

  • (ArgumentError)


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

def calculate_minimum_price_for_margin(target_margin)
  return unless 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



540
541
542
543
544
545
# File 'app/models/catalog_item.rb', line 540

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



1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
# File 'app/models/catalog_item.rb', line 1203

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



1135
1136
1137
1138
1139
1140
1141
1142
1143
# File 'app/models/catalog_item.rb', line 1135

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



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

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



1157
1158
1159
1160
1161
1162
1163
1164
1165
# File 'app/models/catalog_item.rb', line 1157

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)



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

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)



1322
1323
1324
1325
1326
# File 'app/models/catalog_item.rb', line 1322

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)



1328
1329
1330
# File 'app/models/catalog_item.rb', line 1328

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)



1318
1319
1320
# File 'app/models/catalog_item.rb', line 1318

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)



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

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



756
757
758
# File 'app/models/catalog_item.rb', line 756

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

#content_locales_to_renderObject



1099
1100
1101
# File 'app/models/catalog_item.rb', line 1099

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



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

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



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

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&.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:



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

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

#currency_symbolObject

Alias for Catalog#currency_symbol

Returns:

  • (Object)

    Catalog#currency_symbol

See Also:



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

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

#deep_dupObject



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

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|
    # clearance is NOT NULL with a false default; reset the clone to false
    # rather than nil, which would violate the constraint on save.
    copy.clearance = false
  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



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

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



1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
# File 'app/models/catalog_item.rb', line 1073

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)


506
507
508
509
510
511
# File 'app/models/catalog_item.rb', line 506

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



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

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

#edi_documentsActiveRecord::Relation<EdiDocument>

Returns:

See Also:



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

has_many    :edi_documents, dependent: :destroy

#edi_offer_new_sale_priceObject



662
663
664
665
666
# File 'app/models/catalog_item.rb', line 662

def edi_offer_new_sale_price
  return new_sale_price_with_vat if tax_rate.present?

  new_sale_price.to_f.positive? ? new_sale_price : nil
end

#edi_offer_priceObject

EDI offer prices. European Mirakl partners carry a catalog tax_rate and
list VAT-inclusive, so the *_with_vat helpers apply. Tax-exclusive
markets (e.g. Best Buy Canada — CAD, no VAT) have a nil tax_rate, which
makes those helpers return nil; for those, fall back to the plain pre-tax
price/sale so the offer feed isn't sent priceless.



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

def edi_offer_price
  tax_rate.present? ? price_with_vat : amount
end

#edi_offer_sale_priceObject



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

def edi_offer_sale_price
  return sale_price_with_vat if tax_rate.present?

  sale_price.to_f.positive? ? sale_price : nil
end

#edi_orchestrators_for_catalogObject

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



703
704
705
706
707
# File 'app/models/catalog_item.rb', line 703

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



584
585
586
# File 'app/models/catalog_item.rb', line 584

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

#effective_seo_description(char_limit: nil) ⇒ Object



750
751
752
753
754
# File 'app/models/catalog_item.rb', line 750

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)


896
897
898
# File 'app/models/catalog_item.rb', line 896

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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



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

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)


1305
1306
1307
# File 'app/models/catalog_item.rb', line 1305

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

#in_hide_from_feed_state?Boolean

Returns:

  • (Boolean)


1177
1178
1179
# File 'app/models/catalog_item.rb', line 1177

def in_hide_from_feed_state?
  HIDDEN_STATES.include?(state)
end

#in_main_catalog?Boolean (protected)

Returns:

  • (Boolean)


1366
1367
1368
# File 'app/models/catalog_item.rb', line 1366

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

#inventory_message_enabled?Boolean

Returns:

  • (Boolean)


684
685
686
# File 'app/models/catalog_item.rb', line 684

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



711
712
713
714
715
# File 'app/models/catalog_item.rb', line 711

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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)


1286
1287
1288
# File 'app/models/catalog_item.rb', line 1286

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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)


1275
1276
1277
1278
1279
# File 'app/models/catalog_item.rb', line 1275

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_issuesActiveRecord::Relation<ListingIssue>

Returns:

See Also:



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

has_many    :listing_issues, dependent: :destroy

#listing_message_enabled?Boolean

Returns:

  • (Boolean)


692
693
694
# File 'app/models/catalog_item.rb', line 692

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



729
730
731
732
733
# File 'app/models/catalog_item.rb', line 729

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)



853
854
855
856
857
# File 'app/models/catalog_item.rb', line 853

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



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

def map_price
  return if msrp.blank?

  (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



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
622
623
# File 'app/models/catalog_item.rb', line 596

def map_to_stores(store_ids)
  return if store_ids.blank?

  store_ids.uniq.each do |sid|
    s = Store.find(sid)
    # If store id already exist, nothing to do
    next if store_items.find { |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)


845
846
847
848
849
850
# File 'app/models/catalog_item.rb', line 845

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)


1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
# File 'app/models/catalog_item.rb', line 1253

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



814
815
816
# File 'app/models/catalog_item.rb', line 814

def msrp
  root_catalog_item.try(:amount)
end

#msrp_with_vatObject



818
819
820
821
822
# File 'app/models/catalog_item.rb', line 818

def msrp_with_vat
  return if tax_rate.blank?

  root_catalog_item.try(:price_with_vat)
end

#name(locale: nil) ⇒ Object



1104
1105
1106
1107
1108
1109
1110
1111
# File 'app/models/catalog_item.rb', line 1104

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'

#new_sale_price_with_vatObject



639
640
641
642
643
644
645
# File 'app/models/catalog_item.rb', line 639

def new_sale_price_with_vat
  return if tax_rate.blank?

  res = ((tax_rate + 1) * (new_sale_price || 0.0)).round(2)
  res = nil if res == 0.0
  res
end

#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



969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
# File 'app/models/catalog_item.rb', line 969

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.min_by { |h| h[:next_available_date] }
  return nil if next_available_entry_to_use.blank?

  next_available_entry_to_use
end

#next_available_by_warehouse(use_alternate_warehouse: false) ⇒ Object



1042
1043
1044
1045
1046
# File 'app/models/catalog_item.rb', line 1042

def next_available_by_warehouse(use_alternate_warehouse: false)
  store_items_by_warehouse.to_h do |si|
    [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



1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
# File 'app/models/catalog_item.rb', line 1049

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.to_h do |si|
    [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)



993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
# File 'app/models/catalog_item.rb', line 993

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.min_by { |h| h[:next_available_date] }
  return nil if next_available_entry_to_use.blank?

  next_available_entry_to_use
end

#notify_of_price_updateObject (protected)



1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
# File 'app/models/catalog_item.rb', line 1339

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)


867
868
869
# File 'app/models/catalog_item.rb', line 867

def ok_to_destroy?
  !line_items.exists?
end

#on_order(use_alternate_warehouse: false) ⇒ Object



1036
1037
1038
1039
1040
# File 'app/models/catalog_item.rb', line 1036

def on_order(use_alternate_warehouse: false)
  store_items_by_warehouse.to_h do |si|
    [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



951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
# File 'app/models/catalog_item.rb', line 951

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



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

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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:



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

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

#parent_catalog_currency_symbolObject



787
788
789
790
791
# File 'app/models/catalog_item.rb', line 787

def parent_catalog_currency_symbol
  return unless parent_catalog

  parent_catalog.currency_symbol
end

#parent_catalog_discountObject



513
514
515
516
517
518
# File 'app/models/catalog_item.rb', line 513

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



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

def parent_catalog_item
  return unless parent_catalog

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

#parent_catalog_item_amountObject



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

def parent_catalog_item_amount
  return unless (pci = parent_catalog_item)

  pci.amount
end

#percentage_off_from_msrpObject



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

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:



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

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:



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

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:



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

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:



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

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

#price_message_enabled?Boolean

Returns:

  • (Boolean)


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

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



720
721
722
723
724
# File 'app/models/catalog_item.rb', line 720

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)


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

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)


1350
1351
1352
# File 'app/models/catalog_item.rb', line 1350

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

#price_with_vatObject



625
626
627
628
629
# File 'app/models/catalog_item.rb', line 625

def price_with_vat
  return if tax_rate.blank?

  ((tax_rate + 1) * (amount || 0.0)).round(2)
end

#primary_catalogObject



588
589
590
# File 'app/models/catalog_item.rb', line 588

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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)


1185
1186
1187
# File 'app/models/catalog_item.rb', line 1185

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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



1061
1062
1063
1064
1065
1066
1067
# File 'app/models/catalog_item.rb', line 1061

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

#profit_marginObject



520
521
522
# File 'app/models/catalog_item.rb', line 520

def profit_margin
  calculate_profit_margin amount
end

#profit_margin_new_priceObject



536
537
538
# File 'app/models/catalog_item.rb', line 536

def profit_margin_new_price
  calculate_profit_margin new_price
end

#profit_margin_new_sale_priceObject



532
533
534
# File 'app/models/catalog_item.rb', line 532

def profit_margin_new_sale_price
  calculate_profit_margin new_sale_price
end

#profit_margin_retailer_requested_costObject



524
525
526
# File 'app/models/catalog_item.rb', line 524

def profit_margin_retailer_requested_cost
  calculate_profit_margin retailer_requested_cost
end

#profit_margin_sale_priceObject



528
529
530
# File 'app/models/catalog_item.rb', line 528

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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)



1400
1401
1402
1403
1404
1405
1406
# File 'app/models/catalog_item.rb', line 1400

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



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

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

#push_price_messageObject



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

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:



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

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:



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

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.



909
910
911
912
913
914
915
916
917
918
# File 'app/models/catalog_item.rb', line 909

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)



1354
1355
1356
# File 'app/models/catalog_item.rb', line 1354

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

#refurbishedObject



1269
1270
1271
1272
1273
# File 'app/models/catalog_item.rb', line 1269

def refurbished
  return unless (irv = item.refurbished_version)

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

#reported_nameObject



1117
1118
1119
1120
# File 'app/models/catalog_item.rb', line 1117

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

#reported_name_for_googleObject



1122
1123
1124
# File 'app/models/catalog_item.rb', line 1122

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



923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
# File 'app/models/catalog_item.rb', line 923

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 }



1030
1031
1032
1033
1034
# File 'app/models/catalog_item.rb', line 1030

def reported_stocks(use_alternate_warehouse: false)
  store_items_by_warehouse.to_h do |si|
    [si.store.name, reported_stock(use_store_item: si, use_alternate_warehouse:)]
  end
end

#reported_vendor_sku(_orchestrator_partner = nil) ⇒ Object



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

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

#reset_edi_delta_feed_fieldsObject



1296
1297
1298
1299
1300
1301
1302
1303
# File 'app/models/catalog_item.rb', line 1296

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



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

def retail_price_currency_symbol
  catalog.currency_symbol
end

#retailer_probesActiveRecord::Relation<CatalogItemRetailerProbe>

Returns:

See Also:



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

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

#root_catalog_itemObject



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

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)


1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
# File 'app/models/catalog_item.rb', line 1189

def sale_price_in_effect?(exclude_effective_date: false, date_to_check: nil)
  return false if sale_price.blank?

  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



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

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



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

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



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

def sale_price_reset_if_no_coupon
  return if coupon.present?

  self.sale_price = nil
end

#sale_price_with_vatObject



631
632
633
634
635
636
637
# File 'app/models/catalog_item.rb', line 631

def sale_price_with_vat
  return if tax_rate.blank?

  res = ((tax_rate + 1) * (sale_price || 0.0)).round(2)
  res = nil if res == 0.0
  res
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



1247
1248
1249
1250
1251
# File 'app/models/catalog_item.rb', line 1247

def sale_price_with_vat_percentage_off
  return 0.0 if sale_price_with_vat.blank?

  ((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



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

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

#sellable_online_qty(use_store_item: nil) ⇒ Integer

Units actually offerable to shoppers for THIS catalog: the primary
warehouse's available stock minus the reserve buffer, never negative.
This is the reserve math behind #out_of_stock and therefore
#product_stock_status — the In / Out of Stock status shoppers see (which
additionally honors the item's always_available_online override).
Alternate-warehouse stock is deliberately NOT counted here — it only
contributes to #reported_stock for external feeds.

Parameters:

  • use_store_item (StoreItem, nil) (defaults to: nil)

    override the primary store item

Returns:

  • (Integer)

    sellable quantity, clamped at 0



889
890
891
892
893
894
# File 'app/models/catalog_item.rb', line 889

def sellable_online_qty(use_store_item: nil)
  use_store_item ||= store_item
  return 0 unless use_store_item

  [use_store_item.qty_available.to_i - reserve_stock.to_i, 0].max
end

#seo_descriptionObject

Alias for
to: :item#seo_description

Returns:

  • (Object)
           to: :item#seo_description
    

See Also:



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
223
# File 'app/models/catalog_item.rb', line 192

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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



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

def seo_title
  item.effective_seo_title
end

#set_or_clear_discontinued_dateObject (protected)



1390
1391
1392
1393
1394
1395
1396
1397
1398
# File 'app/models/catalog_item.rb', line 1390

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)


1181
1182
1183
# File 'app/models/catalog_item.rb', line 1181

def should_appear_in_feed?
  !in_hide_from_feed_state? && product_category_visible_in_feed?
end

#siblingsObject



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

def siblings
  CatalogItem.where(catalog_id:)
end

#single_sku?Boolean

Returns:

  • (Boolean)


1173
1174
1175
# File 'app/models/catalog_item.rb', line 1173

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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



1069
1070
1071
# File 'app/models/catalog_item.rb', line 1069

def sku_and_name
  "#{item.sku} - #{item.name}"
end

#specObject

Alias for
to: :item#spec

Returns:

  • (Object)
           to: :item#spec
    

See Also:



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
223
# File 'app/models/catalog_item.rb', line 192

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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)


1291
1292
1293
1294
# File 'app/models/catalog_item.rb', line 1291

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:



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

has_and_belongs_to_many :store_items, inverse_of: :catalog_items

#store_items_by_warehouseObject



1024
1025
1026
# File 'app/models/catalog_item.rb', line 1024

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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:



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

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

#third_party_name_en_required?Boolean (protected)

Returns:

  • (Boolean)


1382
1383
1384
# File 'app/models/catalog_item.rb', line 1382

def third_party_name_en_required?
  active? && catalog.third_party_name_en_required
end

#third_party_name_fr_required?Boolean (protected)

Returns:

  • (Boolean)


1386
1387
1388
# File 'app/models/catalog_item.rb', line 1386

def third_party_name_fr_required?
  active? && catalog.third_party_name_fr_required
end

#third_party_part_number_labelObject



1095
1096
1097
# File 'app/models/catalog_item.rb', line 1095

def third_party_part_number_label
  catalog.determine_retailer_part_number_label
end

#third_party_part_number_required?Boolean (protected)

Returns:

  • (Boolean)


1374
1375
1376
# File 'app/models/catalog_item.rb', line 1374

def third_party_part_number_required?
  active? && catalog.third_party_part_number_required
end

#third_party_sku_required?Boolean (protected)

Returns:

  • (Boolean)


1378
1379
1380
# File 'app/models/catalog_item.rb', line 1378

def third_party_sku_required?
  active? && catalog.third_party_sku_required
end

#to_sObject



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

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

#tweak_ebay_skuObject



578
579
580
581
582
# File 'app/models/catalog_item.rb', line 578

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:



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

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:



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
223
# File 'app/models/catalog_item.rb', line 192

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)


1370
1371
1372
# File 'app/models/catalog_item.rb', line 1370

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



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

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



1150
1151
1152
1153
1154
1155
# File 'app/models/catalog_item.rb', line 1150

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