Class: Item

Overview

The Item model represents products in the inventory system.
Items can be physical goods, services, kits, or publications.
Includes pricing, specifications, product categorization, and e-commerce functionality.
== Schema Information

Table name: items(Product catalog table containing SKU and product metadata.)
Database name: primary

id :integer not null, primary key
all_pl_paths_ids :ltree default([]), is an Array
all_pl_paths_slugs :ltree is an Array
alternative_volts :string default([]), is an Array
always_available_online :boolean
amazon_asin :string(10)
amazon_optimized_listing :boolean default(FALSE), not null
base_weight :decimal(8, 4)
box2_shipping_height :decimal(6, 2)
box2_shipping_length :decimal(6, 2)
box2_shipping_weight :decimal(8, 4)
box2_shipping_width :decimal(6, 2)
box3_shipping_height :decimal(6, 2)
box3_shipping_length :decimal(6, 2)
box3_shipping_weight :decimal(8, 4)
box3_shipping_width :decimal(6, 2)
condition :string(20) default("new")
content_url :string
coo :string(255)
cycle_count_grouping :enum default("warehouse")
detailed_description_html :text
disable_heuristic_accessories :boolean default(FALSE), not null
display_treatment :integer
do_not_replenish :boolean default(FALSE), not null
dropship :boolean
feature_1 :string
feature_2 :string
feature_3 :string
feature_4 :string
feature_5 :string
featured_position :integer default(100), not null
first_sale_date :date
gross_weight :decimal(8, 4)
grouping :string
harmonization_code :string(255)
ideal_cable_spacing :float
ignore_product_line_on_item_view :boolean default(FALSE), not null
is_cable_system :boolean
is_discontinued :boolean default(FALSE), not null
is_kit :boolean default(FALSE), not null
item_category :string(255)
last_sale_date :date
legacy_accessories_specs :jsonb
legacy_image_path :string(255)
legacy_qty_out_of_stock :integer default(2), not null
legacy_ship_line_type :string(255) default("S")
legacy_specifications :string(255)
moq :integer
name :string(255)
option_name :string
oversize :boolean default(FALSE), not null
pc_path_ids :ltree
pc_path_slugs :ltree
pdf_image_descriptions :text
pdf_images_analyzed_at :datetime
popularity :integer default(0)
popularity_offset :integer default(0), not null
primary_pl_path_ids :ltree
primary_pl_path_slugs :ltree
product_packaging_flag_audited :boolean default(FALSE), not null
product_weight_flag_audited :boolean default(FALSE), not null
public_short_name :string(255)
publication_base_name :string
publication_locales :string is an Array
qty_warn_on_stock :integer default(5), not null
redirection_path :string
rendered_product_specifications :jsonb
rendered_product_specifications_at :datetime
requested_counter :integer default(0), not null
require_reservation :boolean
restricted_for_sales :boolean default(FALSE), not null
review_product_packaging_flag :boolean default(FALSE), not null
review_product_weight_flag :boolean default(FALSE), not null
search_keywords :text
search_text :text
search_text_tsv :tsvector
secondary_model_number :string
seo_description :text
seo_keywords :string
seo_title :string
shipping_class :integer default("parcel_service"), not null
shipping_height :decimal(6, 2)
shipping_length :decimal(6, 2)
shipping_weight :decimal(8, 4)
shipping_width :decimal(6, 2)
short_description :string(120)
skip_serial_number_reservation_screen :boolean
sku(WarmlyYours SKU code. Use LIKE filters for product family searches.) :string not null
sku_aliases :string default([]), is an Array
slug :string
support_priority :integer default(1000)
tax_class :string(3) not null
terms_and_conditions :text
terms_of_service :text
translations :jsonb
unique_kit_dimensions :boolean default(FALSE), not null
unlimited_inventory :boolean default(FALSE), not null
unspsc_code :string(8)
uom :string default("each"), not null
upc :string(255)
upc_on_box :boolean default(FALSE), not null
visible_for_support :boolean default(TRUE), not null
created_at :datetime
updated_at :datetime
brand_id :integer
creator_id :integer
exclusive_item_group_id :integer
literature_id :integer
new_item_id :integer
packaging_discrepancy_packing_id :integer
preferred_supplier_id :integer
primary_image_id :integer
primary_product_line_id(Primary product line foreign key for grouping and categorization.) :integer
product_category_id :integer
product_tax_code_id :integer
replacement_for_id :integer
secondary_product_category_id :integer
successor_item_id :integer
supplier_item_id :integer
updater_id :integer
upload_id :integer

Indexes

idx_items_all_pl_paths_ids (all_pl_paths_ids) USING gin
idx_items_pc_path_ids (pc_path_ids) USING gist
idx_items_primary_pl_path_ids (primary_pl_path_ids) USING gist
idx_pc_id_discontinued_kit (product_category_id,is_discontinued,is_kit)
idx_pc_id_is_disc_created (product_category_id,is_discontinued,created_at)
idx_pc_id_literature_id (product_category_id,literature_id)
idx_ppl_id_product_cat_id (primary_product_line_id,product_category_id)
idx_product_cat_id_created_at (product_category_id,created_at)
idx_product_category_id_id (product_category_id,id)
idx_upload_id (upload_id)
index_items_name_like (name)
index_items_on_all_pl_paths_slugs (all_pl_paths_slugs) USING gist
index_items_on_amazon_asin (amazon_asin) UNIQUE
index_items_on_brand_id (brand_id)
index_items_on_condition (condition)
index_items_on_exclusive_item_group_id (exclusive_item_group_id)
index_items_on_is_discontinued (is_discontinued)
index_items_on_is_kit (is_kit)
index_items_on_literature_id (literature_id)
index_items_on_new_item_id (new_item_id)
index_items_on_pc_path_slugs (pc_path_slugs) USING gist
index_items_on_primary_pl_path_slugs (primary_pl_path_slugs) USING gist
index_items_on_product_tax_code_id (product_tax_code_id)
index_items_on_publication_locales (publication_locales) USING gin
index_items_on_search_text_tsv (search_text_tsv) USING gin
index_items_on_secondary_model_number (secondary_model_number) WHERE (secondary_model_number IS NOT NULL)
index_items_on_secondary_product_category_id (secondary_product_category_id)
index_items_on_sku (sku) UNIQUE
index_items_on_sku_aliases (sku_aliases) USING gin
index_items_on_successor_item_id (successor_item_id)
index_items_on_upc_and_condition (upc,condition) UNIQUE
index_items_sku_like (sku) USING gin
items_product_category_id_condition_primary_product_line_id_idx (product_category_id,condition,primary_product_line_id)
items_rendered_product_specifications_idx (rendered_product_specifications) USING gin
items_supplier_item_id_idx (supplier_item_id)

Foreign Keys

fk_rails_... (brand_id => brands.id)
fk_rails_... (new_item_id => items.id)
fk_rails_... (packaging_discrepancy_packing_id => packings.id) ON DELETE => nullify
fk_rails_... (primary_product_line_id => product_lines.id)
fk_rails_... (product_tax_code_id => product_tax_codes.id)
fk_rails_... (secondary_product_category_id => product_categories.id)
fk_rails_... (successor_item_id => items.id)
items_product_category_id_fk (product_category_id => product_categories.id)
items_supplier_item_id_fk (supplier_item_id => supplier_items.id) ON DELETE => cascade

Defined Under Namespace

Classes: AmazonFnskuBarcode, ArchiveItem, ArticleRetriever, Auditor, AvailabilityDateChecker, Cloner, ConvertElectricalSpecs, CopyRelatedItemsFromSiblings, CustomMatCreator, CycleCountPrioritizer, CycleCountScheduler, DataMatrixBarcode, FloorPlanRetriever, HeatingElementLine, HeatingElementPlan, ImageLibraryPlPcLadder, ImageLibraryRetriever, ImageRetriever, InventoryCommitter, KitAttributeGenerator, KitComposer, KitConsolidator, MapSpecToTemplate, MarketingCycleCountScheduler, PopulateItemPopularity, PrepareNewKitComponent, ProductSpecificationsCloner, PublicationRetriever, ShippingBoxCalculator, SpecFixer, SpecificationsMasher, SpecificationsRetriever, SuggestedItemTool, SupportQrCode, SynchronizeRefurbSpecs, UpcBarcode, UpcMaker, VideoRetriever

Constant Summary collapse

TRANSLATABLE_ATTRIBUTES =
%i[detailed_description_html feature_1 feature_2 feature_3 feature_4 feature_5 name option_name public_short_name
search_keywords seo_description seo_keywords seo_title short_description terms_and_conditions terms_of_service].freeze
TRANSLATION_NAMESPACE =
'ItemAttributes'
SKU_REGEXP =
/\A\w[\w\-.]+\w\z/
AMAZON_ASIN_REGEXP =
/\b[A-Z0-9]{10}\b/
DISPLAY_TREATMENT_HIDE_QUANTITIES =
0
SINGLE_QUANTITY_SKUS =

Put sku in here that needs their own line always

[].freeze
DOUBLE_POLE_VOLTAGES =
[208.0, 240.0].freeze
FORECAST_EXCLUDED_SKU_PREFIXES =

SKU prefixes excluded from demand forecasting (custom/discontinued/special items)

%w[cth-dis- cths- cthx- ffx- tmcs tms-sp].freeze
SMARTPRESET_SERVICE_SKU =
%w[SMARTPRESETFORM-USA-A SMARTPRESETFORM-CAN-A SMART-PRESET-FORM-B SMART-PRESET-OJ-FORM-A SMART-PRESET-OJ-FORM-B].freeze
CI_EXIST_CHECK_SQL =
'select 1 from catalog_items ci inner join store_items si on ci.store_item_id = si.id where si.item_id = items.id and si.is_discontinued = false'

Constants included from Models::Embeddable

Models::Embeddable::DEFAULT_MODEL, Models::Embeddable::MAX_CONTENT_LENGTH

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Instance Attribute Summary collapse

Attributes included from Models::Publication

#available_in_canada, #available_in_usa, #serve_in_locale

Attributes included from Models::Translatable

#do_not_compact_translation_container

Attributes included from Models::ItemLtreeSync

#skip_ltree_sync

Belongs to collapse

Methods included from Models::ItemAmazonHelper

#amazon_variation

Methods included from Models::Auditable

#creator, #updater

Has one collapse

Has many collapse

Methods included from Models::ItemAmazonHelper

#amazon_marketplaces, #amazon_transparency_codes

Methods included from Models::CrossLinkable

#inbound_content_links, #outbound_content_links

Methods included from Models::Publication

#item_embeddings

Methods included from Models::Embeddable

#content_embeddings

Methods included from Models::Taggable

#tag_records, #taggings

Methods included from Models::Kittable

#kit_components, #kit_components_for_specs, #kit_parents, #kit_source_item_relations, #kit_target_item_relations, #kit_target_item_relations_for_specs

Has and belongs to many collapse

Delegated Instance Attributes collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::ItemAmazonHelper

#all_amazon_image_profiles, #amazon_description, #amazon_description=, #amazon_feature_1, #amazon_feature_1=, #amazon_feature_2, #amazon_feature_2=, #amazon_feature_3, #amazon_feature_3=, #amazon_feature_4, #amazon_feature_4=, #amazon_feature_5, #amazon_feature_5=, #amazon_feature_6, #amazon_feature_6=, #amazon_feature_7, #amazon_feature_7=, #amazon_feature_8, #amazon_feature_8=, #amazon_feature_9, #amazon_feature_9=, #amazon_features, #amazon_generic_keyword, #amazon_generic_keyword=, #amazon_product_types, #amazon_target_keywords, #amazon_title, #amazon_title=, #create_or_set_amazon_spec_value, #description_for_amazon, #is_amazon_item?, #reset_amazon_fields, #sync_amazon_asins, #title_for_amazon, with_asin

Methods included from Models::CrossLinkable

#content_links_count, #linked_content, #linked_posts, #linked_publications, #linked_showcases, #linked_videos

Methods included from Models::Publication

ai_search_publications, ai_search_warning?, #alias_sku, #analyze_pdf_images!, #available_service_locales, #available_to_all_locales?, #check_redirection_path, clear_ai_search_warning!, #compose_name_with_languages, #content_for_embedding, #cover_image_url, #create_primary_image_from_pdf, #default_publication_logistics, embeddable_content_types, #embeddable_locales, #embedding_content_changed?, #fast_country_discontinue, #file_name_for_download, #friendly_locale_names, generate_openai_query_embedding, #generate_publication_name, #has_literature_changed?, hybrid_search_publications, #locale_for_embedding, #needs_vision_analysis?, #perform_country_discontinue, #product_category_must_be_publication, #publication_available_locales, #publication_base_name_clean_of_language, #publication_pdf_changed?, #publication_sku_check, #publication_url, #publication_visible_to_public?, publications, publications_for_online_portal, publications_for_public, publications_for_public_in_store, publications_for_sales_portal, publications_for_support_portal, #publish_pdf_changed_event, #retrieve_publications_search_text, #secondary_product_category_must_not_be_publication, #set_search_text, #should_queue_embedding?, with_publication_attached

Methods included from Models::HybridSearchable

rrf_ranked_relation

Methods included from Models::Embeddable

#content_for_embedding, embeddable_content_types, #embeddable_locales, #embedding_content_hash, embedding_partition_class, #embedding_stale?, #embedding_type_name, #embedding_vector, #find_content_embedding, #find_similar, #generate_all_embeddings!, #generate_chunked_embeddings!, #generate_embedding!, #has_embedding?, #locale_for_embedding, #needs_chunking?, regenerate_all_embeddings, semantic_search

Methods included from Models::Taggable

#add_tag, all_tags, #has_tag?, normalize_tag_names, not_tagged_with, #remove_tag, #tag_list, #tag_list=, #taggable_type_for_tagging, tagged_with, #tags, #tags=, tags_cloud, tags_exclude, tags_include, with_all_tags, with_any_tags, without_all_tags, without_any_tags

Methods included from Models::Translatable

#compact_translation_container, skip_compaction_for

Methods included from Models::ItemPackageContentHelper

#build_package_content_entries, #condense_group_to_char_limit, #group_entries_for_max_unique_items, #package_content_html, #package_content_short_name, #package_contents_array, #package_contents_friendly

Methods included from Models::ItemSpecificationHelper

#amps, #amps=, #amps_static?, #area, #aspect, #btu_per_hour, #calculate_amps, #calculate_coverage_for_membrane, #calculate_geometric_sqft, #calculate_heated_area_for_panels, #calculate_heated_area_sqft_for_panels, #calculate_heated_area_sqm_for_panels, #calculate_kilowatts, #calculate_number_of_fixing_strips_at_3_in, #calculate_number_of_fixing_strips_at_4_in, #calculate_number_of_fixing_strips_at_5_in, #calculate_number_of_fixing_strips_at_cable_spacing, #calculate_ohms, #calculate_ohms_per_ft, #calculate_size_2d, #calculate_size_2d_ft, #calculate_size_2d_in, #calculate_size_2d_metric_cm, #calculate_size_2d_metric_mm, #calculate_size_3d, #calculate_size_3d_ft, #calculate_size_3d_in, #calculate_size_3d_metric_cm, #calculate_size_3d_metric_mm, #calculate_sqft, #calculate_thermal_power_per_hour, #calculate_volts, #calculate_watts, #calculate_watts_per_sqft, #can_use_amps_for_calculation?, #can_use_ohms_for_calculation?, #can_use_voltage_for_calculation?, #can_use_watts_for_calculation?, #cold_lead_length, #cold_lead_length=, #control_capacity, #coverage_at_3_5_in, #coverage_at_3_75_in, #coverage_at_3_in, #coverage_at_4_5_in, #coverage_at_4_in, #coverage_at_5_in, #coverage_formatted, #create_or_set_spec_value, #finish, #has_spec?, #is_dual_voltage?, #length, #length=, #length_formatted, #linear_ft, #maximum_current, #num_poles, #ohms, #ohms=, #ohms_law_calculator, #ohms_static?, #plan_grouping, #plan_grouping=, #set_spec_value, #spec, #spec_image, #spec_output, #spec_refresh_in_progress, #spec_unit, #spec_value, #specs_for_identifier_label, #sqft, #sqft=, #token_specs_values_for_liquid, #voltage, #voltage=, #voltage_formatted, #voltage_static?, #volts, #volts=, #vop, #watts, #watts=, #watts_formatted, #watts_per_linear_feet, #watts_per_sqft, #watts_per_sqft_at_3_5_in_spacing, #watts_per_sqft_at_3_75_in_spacing, #watts_per_sqft_at_3_in_spacing, #watts_per_sqft_at_4_5_in_spacing, #watts_per_sqft_at_4_in_spacing, #watts_per_sqft_at_5_in_spacing, #watts_static?, #width, #width=, #width_by_length_text, #width_by_length_text_2, #width_by_length_text_3, #width_formatted

Methods included from Models::Kittable

#box_contents, #consolidate_linked_kits, #get_kit_item_ids, #get_kit_items, #get_multi_box_item_contents_item_id_hash_array, #get_self_and_kit_item_ids, #get_self_and_kit_items, #is_part_of_a_kit?, #is_part_of_an_active_kit?, #kit_contents, kits, #shipping_dimensions_changed?, #store_item_for

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods included from Models::ItemLtreeSync

#build_ltree_paths_from_associations, bulk_sync_ltree_paths!, sync_items_for_product_categories!, sync_items_for_product_lines!, #sync_ltree_paths!

Methods included from Models::ItemScopable

by_product_category_id, by_product_category_id_direct, by_product_category_path, by_product_category_path_exact, by_product_category_url, by_product_category_url_exact, by_product_line_id, by_product_line_path, by_product_line_path_full, by_product_line_url, by_product_line_url_full, not_by_product_category_id, not_by_product_line_id

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#amazon_asinObject (readonly)



312
313
314
# File 'app/models/item.rb', line 312

validates :amazon_asin, uniqueness: true,
format: { with: AMAZON_ASIN_REGEXP, message: 'must be 10 alphanumeric characters starting with a letter' },
allow_blank: true

#name_enObject (readonly)

Here we check specifically for the english name to be present

Validations:



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

validates :name_en, :tax_class, presence: true

#refresh_specsObject

These are shortcuts for publications



215
216
217
# File 'app/models/item.rb', line 215

def refresh_specs
  @refresh_specs
end

#skip_item_calcObject

Returns the value of attribute skip_item_calc.



632
633
634
# File 'app/models/item.rb', line 632

def skip_item_calc
  @skip_item_calc
end

#skuObject (readonly)

rubocop:disable Rails/I18nLocaleTexts

Validations:

  • Presence
  • Uniqueness
  • Format ({ with: SKU_REGEXP, message: 'must only contain A-Z 0-9 - _ . and must end with a number or letter' })


309
310
311
# File 'app/models/item.rb', line 309

validates :sku, presence: true,
uniqueness: true,
format: { with: SKU_REGEXP, message: 'must only contain A-Z 0-9 - _ . and must end with a number or letter' }

#tax_classObject (readonly)

Here we check specifically for the english name to be present

Validations:



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

validates :name_en, :tax_class, presence: true

#update_search_textObject

Returns the value of attribute update_search_text.



632
633
634
# File 'app/models/item.rb', line 632

def update_search_text
  @update_search_text
end

#update_upcObject

Returns the value of attribute update_upc.



632
633
634
# File 'app/models/item.rb', line 632

def update_upc
  @update_upc
end

Class Method Details

.accessoriesActiveRecord::Relation<Item>

A relation of Items that are accessories. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



409
# File 'app/models/item.rb', line 409

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

.activeActiveRecord::Relation<Item>

A relation of Items that are active. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



365
# File 'app/models/item.rb', line 365

scope :active,                  -> { where(is_discontinued: false) }

.active_in_catalog_idsActiveRecord::Relation<Item>

A relation of Items that are active in catalog ids. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

scope :active_in_catalog_ids, ->(*catalog_ids) {
  catalog_ids = [catalog_ids].flatten.filter_map(&:presence).uniq
  active
    .where("exists(#{CI_EXIST_CHECK_SQL} and ci.catalog_id IN (?))", catalog_ids.flatten)
}

.all_localesObject

NOTE: all_tags method provided by Models::Taggable concern



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

def self.all_locales
  pluck(Arel.sql('distinct unnest(publication_locales) as locale')).compact.sort
end

.async_update_all_items_product_specifications(public_only: true, active_only: true, limit: nil, not_updated_since: nil) ⇒ Object



2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
# File 'app/models/item.rb', line 2033

def self.async_update_all_items_product_specifications(public_only: true, active_only: true, limit: nil, not_updated_since: nil)
  # Select items based on the conditions and if they are due for a refresh
  items = Item.non_publications
  items = items.available_to_public if public_only # implies active
  items = items.active unless public_only && !active_only

  # Filter items that haven't been refreshed in the last week
  items = items.where('rendered_product_specifications_at IS NULL OR rendered_product_specifications_at < ?', not_updated_since) if not_updated_since

  items = items.limit(limit) if limit
  items.distinct.pluck(:id).each { |iid| ItemAttributeWorker.perform_async(iid) }
end

.available_to_publicActiveRecord::Relation<Item>

A relation of Items that are available to public. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



367
# File 'app/models/item.rb', line 367

scope :available_to_public,     -> { active.public_and_active_in_catalog_id(1, 2) }

.available_to_public_for_supportActiveRecord::Relation<Item>

A relation of Items that are available to public for support. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



369
# File 'app/models/item.rb', line 369

scope :available_to_public_for_support, -> { condition_new.where(visible_for_support: true).where(primary_product_line_id: ProductLine.for_support_portal.select(:id)) }

.by_primary_product_line_ids_with_descendantsActiveRecord::Relation<Item>

A relation of Items that are by primary product line ids with descendants. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



527
528
529
530
531
# File 'app/models/item.rb', line 527

scope :by_primary_product_line_ids_with_descendants, ->(*pl_ids) {
  ids = [pl_ids].flatten.compact.map(&:to_i)
  paths = ProductLine.where(id: ids).pluck(:ltree_path_ids).compact
  paths.empty? ? none : where(Item[:primary_pl_path_ids].ltree_descendant(paths))
}

.canonical_searchActiveRecord::Relation<Item>

A relation of Items that are canonical search. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



439
440
441
442
# File 'app/models/item.rb', line 439

scope :canonical_search,        ->(sku) {
  escaped = Regexp.escape(sku.to_s)
  where('items.sku ~ ?', "^#{escaped}-[A-Za-z]{1,2}$").order(sku: :desc)
}

.category_sortedActiveRecord::Relation<Item>

A relation of Items that are category sorted. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



382
# File 'app/models/item.rb', line 382

scope :category_sorted,         -> { joins(:product_category).order(ProductCategory[:priority], ProductCategory[:name], Item[:sku]) }

.cold_leadsActiveRecord::Relation<Item>

A relation of Items that are cold leads. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



416
# File 'app/models/item.rb', line 416

scope :cold_leads,              -> { where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_SPARE_PARTS_COLD_LEADS)) }

.condition_select_optionsObject



909
910
911
# File 'app/models/item.rb', line 909

def self.condition_select_options
  Item.conditions.keys.map { |c| [c.humanize.titleize, c] }
end

.controlsActiveRecord::Relation<Item>

A relation of Items that are controls. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



387
# File 'app/models/item.rb', line 387

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

.countertop_heatersActiveRecord::Relation<Item>

A relation of Items that are countertop heaters. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



488
# File 'app/models/item.rb', line 488

scope :countertop_heaters,      -> { by_product_category_path(LtreePaths::PC_HEATING_ELEMENTS_COUNTERTOP) }

.custom_matsActiveRecord::Relation<Item>

A relation of Items that are custom mats. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



487
# File 'app/models/item.rb', line 487

scope :custom_mats,             -> { by_product_category_path(LtreePaths::PC_HEATING_ELEMENTS_CUSTOM_MATS) }

.discontinuedActiveRecord::Relation<Item>

A relation of Items that are discontinued. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



366
# File 'app/models/item.rb', line 366

scope :discontinued,            -> { where(is_discontinued: true) }

.dropshipsActiveRecord::Relation<Item>

A relation of Items that are dropships. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



463
# File 'app/models/item.rb', line 463

scope :dropships,               -> { where(dropship: true) }

.easystatsActiveRecord::Relation<Item>

A relation of Items that are easystats. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



401
# File 'app/models/item.rb', line 401

scope :easystats,               -> { controls.where(Item[:primary_pl_path_slugs].ltree_descendant(LtreePaths::PL_FLOOR_HEATING_CONTROL_EASYSTAT)) }

.electrical_plan_controlsActiveRecord::Relation<Item>

A relation of Items that are electrical plan controls. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

scope :electrical_plan_controls, -> {
  controls
    .where(Item[:primary_pl_path_slugs].ltree_descendant(LtreePaths::PL_ALL_CONTROLS))
    .where.not(sku: %w[RLY-12PL SCE-ELEC])
}

.excludes_forecast_skusActiveRecord::Relation<Item>

A relation of Items that are excludes forecast skus. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



434
# File 'app/models/item.rb', line 434

scope :excludes_forecast_skus, -> { where.not('items.sku SIMILAR TO ?', "(#{FORECAST_EXCLUDED_SKU_PREFIXES.join('|')})%") }

.floor_heating_controlsActiveRecord::Relation<Item>

A relation of Items that are floor heating controls. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

scope :floor_heating_controls, -> { controls.where(Item[:primary_pl_path_slugs].ltree_descendant(LtreePaths::PL_FLOOR_HEATING_CONTROL)) }

.floor_heating_elementsActiveRecord::Relation<Item>

A relation of Items that are floor heating elements. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



407
# File 'app/models/item.rb', line 407

scope :floor_heating_elements,  -> { heating_elements.where(Item[:primary_pl_path_slugs].ltree_descendant(LtreePaths::PL_FLOOR_HEATING)) }

.for_support_portalActiveRecord::Relation<Item>

A relation of Items that are for support portal. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



459
# File 'app/models/item.rb', line 459

scope :for_support_portal,      -> { where(product_category_id: ProductCategory.for_support_portal.select(:id)) }

.forecastableActiveRecord::Relation<Item>

A relation of Items that are forecastable. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



436
# File 'app/models/item.rb', line 436

scope :forecastable, -> { active.non_kits.non_publications.non_shipping.excludes_forecast_skus }

.get_quantity_from_items_and_sqft(catalog_id, items, sqft, add_one_extra_of_last_item = false) ⇒ Object



1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
# File 'app/models/item.rb', line 1465

def self.get_quantity_from_items_and_sqft(catalog_id, items, sqft, add_one_extra_of_last_item = false)
  res_arr = []
  cost_arr = []
  2.times do |pass|
    area = sqft
    res = {}
    cost = 0.0
    last_i = items.length - 1
    items.each_with_index do |item, i|
      add_one_extra = if add_one_extra_of_last_item && (i == last_i) && pass.zero? # do this only on first pass
                        1
                      else
                        0
                      end
      qty = add_one_extra
      if item.sqft.to_f > 0.0
        qty += if (i == last_i) || ((pass == 1) && (items[i + 1].sqft < sqft)) # do this on last item or on second pass if the next item isn't big enough
                 (area / item.sqft).ceil.to_i # want more than enough to cover if last item
               else
                 (area / item.sqft).floor.to_i # want what will fit the area with leftover for smaller items
               end
      end
      next unless qty.positive?

      res[item.sku] = qty
      area -= (item.sqft.to_f * qty)
      # we need a pricing weight to choose which is the best result
      ci = item.catalog_items.detect { |ci2| ci2.catalog_id == catalog_id } || item.catalog_items.first
      cost += (qty * ci.amount.to_f)
    end
    res_arr << res
    cost_arr << cost
  end
  res_arr[cost_arr.index(cost_arr.min)]
end

.get_radiant_panel_tstat_options_for_web_and_apply_overrides(connection_method, tstat_options_override = [], available_in_catalog_id: nil) ⇒ Object

Here we want a static web page (e.g. app/views/pages/radiant-heat-panels) to use the same source of SKUs as Item does but allow it to override some of the stuff, including the order:



2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
# File 'app/models/item.rb', line 2365

def self.get_radiant_panel_tstat_options_for_web_and_apply_overrides(connection_method, tstat_options_override = [], available_in_catalog_id: nil)
  available_in_catalog_id = Catalog.locale_to_catalog_id(I18n.locale) if available_in_catalog_id.nil?
  num_overrides = tstat_options_override.size # find how many overrides we have for sorting
  base_item_scope = Item.public_and_active_in_catalog_id(available_in_catalog_id)
  controls = if connection_method == :hardwired
               base_item_scope.radiant_panel_controls_hardwired
             else
               base_item_scope.radiant_panel_controls_plug_in
             end
  res = controls.map do |item|
    tstat_options_override.detect do |h|
      h[:sku] == item.sku
    end || { sku: item.sku, name: item.name, image: item.primary_image.image_url(width: 300, height: 300) }
  end
  res.sort_by { |h| tstat_options_override.index(h) || num_overrides }
end

.get_towel_warmer_tstat_options_for_web_and_apply_overrides(connection_method, tstat_options_override = [], available_in_catalog_id: nil) ⇒ Object

Here we want a static web page (e.g. app/views/pages/towel-warmer/controls) to use the same source of SKUs as Item does but allow it to override some of the stuff, including the order:



2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
# File 'app/models/item.rb', line 2346

def self.get_towel_warmer_tstat_options_for_web_and_apply_overrides(connection_method, tstat_options_override = [], available_in_catalog_id: nil)
  available_in_catalog_id = Catalog.locale_to_catalog_id(I18n.locale) if available_in_catalog_id.nil?
  num_overrides = tstat_options_override.size # find how many overrides we have for sorting
  base_item_scope = Item.public_and_active_in_catalog_id(available_in_catalog_id)
  controls = if connection_method == :hardwired
               base_item_scope.towel_warmer_controls_hardwired
             else
               base_item_scope.towel_warmer_controls_plug_in
             end
  res = controls.map do |item|
    # here we
    tstat_options_override.detect do |h|
      h[:sku] == item.sku
    end || { sku: item.sku, name: item.name, image: item.primary_image.image_url(width: 300, height: 300) }
  end
  res.sort_by { |h| tstat_options_override.index(h) || num_overrides }
end

.goodsActiveRecord::Relation<Item>

A relation of Items that are goods. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



413
# File 'app/models/item.rb', line 413

scope :goods,                   -> { where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_GOODS)) }

.goods_and_services_for_publicActiveRecord::Relation<Item>

A relation of Items that are goods and services for public. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



437
# File 'app/models/item.rb', line 437

scope :goods_and_services_for_public, -> { available_to_public.active.non_publications }

.goods_visible_for_supportActiveRecord::Relation<Item>

A relation of Items that are goods visible for support. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



457
# File 'app/models/item.rb', line 457

scope :goods_visible_for_support, -> { non_publications.condition_new.goods.where(visible_for_support: true) }

.has_floor_sensorActiveRecord::Relation<Item>

A relation of Items that are has floor sensor. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



477
# File 'app/models/item.rb', line 477

scope :has_floor_sensor, -> { with_product_specification(:has_floor_sensor, 'y') }

.heating_elementsActiveRecord::Relation<Item>

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

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

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

.identifiers_searchActiveRecord::Relation<Item>

A relation of Items that are identifiers search. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



543
544
545
546
547
548
549
550
551
552
553
554
555
# File 'app/models/item.rb', line 543

scope :identifiers_search, ->(identifier) {
  where(%{
   items.sku ILIKE :identifier_wild
     OR items.upc = :identifier
     OR items.upc = RIGHT(:identifier,12)
     OR items.secondary_model_number ILIKE :identifier_wild
     OR items.amazon_asin = :identifier
     OR items.sku_aliases @> ARRAY[:identifier]::varchar[]
     OR supplier_items.supplier_sku ILIKE :identifier_wild
 },
        identifier_wild: "#{identifier}%",
        identifier:).left_outer_joins(:supplier_item).order(sanitize_sql_for_order([Arel.sql('items.is_discontinued = true,items.sku <-> ?'), identifier]))
}

.import_translation_keysObject



1869
1870
1871
# File 'app/models/item.rb', line 1869

def self.import_translation_keys
  Item.where.not(translations: {}).find_each(&:import_translation_keys)
end

.in_storeActiveRecord::Relation<Item>

A relation of Items that are in store. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



418
419
420
# File 'app/models/item.rb', line 418

scope :in_store, ->(*store_ids) {
  where('exists(select id from store_items where store_items.item_id = items.id and store_items.store_id IN (?) and store_items.is_discontinued = false)', [store_ids].flatten)
}

.indexableActiveRecord::Relation<Item>

A relation of Items that are indexable. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



506
# File 'app/models/item.rb', line 506

scope :indexable,               -> { not_tagged_with('Noindex') }

.install_kitsActiveRecord::Relation<Item>

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

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



485
# File 'app/models/item.rb', line 485

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

.insulationsActiveRecord::Relation<Item>

A relation of Items that are insulations. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



411
# File 'app/models/item.rb', line 411

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

.integration_kitsActiveRecord::Relation<Item>

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

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



404
# File 'app/models/item.rb', line 404

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

.item_identifier_label_sizes_select_optionsObject



921
922
923
# File 'app/models/item.rb', line 921

def self.item_identifier_label_sizes_select_options
  [['2.3125" x 4" (S-8505)', 'regular'], ['4" x 6" (S-17044)', 'large']]
end

.last_custom_mat_itemObject



925
926
927
# File 'app/models/item.rb', line 925

def self.last_custom_mat_item
  Item.by_product_line_path(LtreePaths::PL_FLOOR_HEATING_TEMPZONE_CUSTOM_MAT).merge(heating_elements).order('items.id DESC').first
end

.membranesActiveRecord::Relation<Item>

A relation of Items that are membranes. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



486
# File 'app/models/item.rb', line 486

scope :membranes,               -> { where(sku: ItemConstants::MEMBRANE_SKUS) }

.mirror_defoggersActiveRecord::Relation<Item>

A relation of Items that are mirror defoggers. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

scope :mirror_defoggers,        -> { where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_MIRROR_DEFOGGERS)) }

.mirrorsActiveRecord::Relation<Item>

A relation of Items that are mirrors. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



386
# File 'app/models/item.rb', line 386

scope :mirrors,                 -> { where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_MIRRORS)) }

.missing_shipping_dimensionsActiveRecord::Relation<Item>

A relation of Items that are missing shipping dimensions. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

scope :missing_shipping_dimensions, -> {
  arel = Item.arel_table
  goods.where(
    arel[:shipping_length].lteq(0).or(arel[:shipping_length].eq(nil))
      .or(arel[:shipping_width].lteq(0)).or(arel[:shipping_width].eq(nil))
      .or(arel[:shipping_height].lteq(0)).or(arel[:shipping_height].eq(nil))
      .or(arel[:shipping_weight].lteq(0)).or(arel[:shipping_weight].eq(nil))
  )
}

.needs_packaging_reviewedActiveRecord::Relation<Item>

A relation of Items that are needs packaging reviewed. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



541
# File 'app/models/item.rb', line 541

scope :needs_packaging_reviewed, -> { where(review_product_packaging_flag: true) }

.needs_weight_reviewedActiveRecord::Relation<Item>

A relation of Items that are needs weight reviewed. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



540
# File 'app/models/item.rb', line 540

scope :needs_weight_reviewed, -> { where(review_product_weight_flag: true) }

.non_dropshipsActiveRecord::Relation<Item>

A relation of Items that are non dropships. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



464
# File 'app/models/item.rb', line 464

scope :non_dropships,           -> { where('items.dropship = false or items.dropship is null') }

.non_kitsActiveRecord::Relation<Item>

A relation of Items that are non kits. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



429
# File 'app/models/item.rb', line 429

scope :non_kits,         -> { where(is_kit: false) }

.non_publicationsActiveRecord::Relation<Item>

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

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



428
# File 'app/models/item.rb', line 428

scope :non_publications, -> { where.not(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_PUBLICATIONS)) }

.non_shippingActiveRecord::Relation<Item>

A relation of Items that are non shipping. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



430
# File 'app/models/item.rb', line 430

scope :non_shipping,     -> { where.not(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_SHIPPING_AND_HANDLING)) }

.not_a_target_itemActiveRecord::Relation<Item>

A relation of Items that are not a target item. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



465
# File 'app/models/item.rb', line 465

scope :not_a_target_item,       -> { where_not_exists(:source_item_relations) }

.oj_programmable_tstatsActiveRecord::Relation<Item>

A relation of Items that are oj programmable tstats. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



399
# File 'app/models/item.rb', line 399

scope :oj_programmable_tstats,  -> { oj_tstats.where(Item[:name].matches('%Programmable%')).where.not(Item[:name].matches('%Non Programmable%')) }

.oj_tstatsActiveRecord::Relation<Item>

A relation of Items that are oj tstats. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



398
# File 'app/models/item.rb', line 398

scope :oj_tstats,               -> { thermostats.where(Item[:sku].matches('%4999%')) }

.order_by_specActiveRecord::Relation<Item>

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

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
# File 'app/models/item.rb', line 489

scope :order_by_spec,           ->(spec, datatype = 'varchar', direction = 'asc') {
  if %w[decimal integer numeric].include?(datatype.downcase)
    # For numeric types, extract numbers from text and handle NULL values
    order_clause = "CASE
      WHEN (items.rendered_product_specifications->'#{spec}'->>'raw') ~ '^[0-9]+\\.?[0-9]*$'
        THEN (items.rendered_product_specifications->'#{spec}'->>'raw')::#{datatype}
      WHEN (items.rendered_product_specifications->'#{spec}'->>'raw') ~ '^[0-9]+\\.?[0-9]*\\s+[a-zA-Z]+$'
        THEN (regexp_replace(items.rendered_product_specifications->'#{spec}'->>'raw', '[^0-9\\.]', '', 'g'))::#{datatype}
      ELSE NULL
     END #{direction}"
    order(Arel.sql(order_clause))
  else
    # For non-numeric types, use the original logic
    order(Arel.sql("(items.rendered_product_specifications->'#{spec}'->>'raw')::#{datatype} #{direction}"))
  end
}

.orderable_onlineActiveRecord::Relation<Item>

A relation of Items that are orderable online. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



438
# File 'app/models/item.rb', line 438

scope :orderable_online,        -> { non_publications.available_to_public.active.packageable }

.out_of_stockActiveRecord::Relation<Item>

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

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

scope :out_of_stock,            -> {
  joins(:store_items).where('store_items.qty_available <= items.legacy_qty_out_of_stock ').where("store_items.location = 'AVAILABLE'")
}

.packageableActiveRecord::Relation<Item>

A relation of Items that are packageable. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



370
# File 'app/models/item.rb', line 370

scope :packageable,             -> { where(Item[:shipping_weight].gt(0)).where(Item[:base_weight].gt(0)).where(Item[:shipping_length].gt(0)).where(Item[:shipping_width].gt(0)).where(Item[:shipping_height].gt(0)) }

.power_modulesActiveRecord::Relation<Item>

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

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



403
# File 'app/models/item.rb', line 403

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

.powersActiveRecord::Relation<Item>

A relation of Items that are powers. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



405
# File 'app/models/item.rb', line 405

scope :powers,                  -> { where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_POWER)) }

.primary_product_line_slug_ltree_contActiveRecord::Relation<Item>

A relation of Items that are primary product line slug ltree cont. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



533
534
535
536
537
538
# File 'app/models/item.rb', line 533

scope :primary_product_line_slug_ltree_cont, ->(term) {
  sanitized = term.to_s.strip.tr(' ', '_').gsub(/[^a-zA-Z0-9_.]/, '')
  return none if sanitized.blank?

  where(Item[:primary_pl_path_slugs].ltree_descendant(sanitized))
}

.public_and_active_in_any_catalogsActiveRecord::Relation<Item>

A relation of Items that are public and active in any catalogs. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

scope :public_and_active_in_any_catalogs, -> {
  active
    .where("exists(#{CI_EXIST_CHECK_SQL} and ci.state NOT IN (?))", CatalogItem::HIDDEN_STATES)
}

.public_and_active_in_catalog_idActiveRecord::Relation<Item>

A relation of Items that are public and active in catalog id. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



516
517
518
519
520
# File 'app/models/item.rb', line 516

scope :public_and_active_in_catalog_id, ->(*catalog_ids) {
  catalog_ids = [catalog_ids].flatten.filter_map(&:presence).uniq
  active
    .where("exists(#{CI_EXIST_CHECK_SQL} and ci.state NOT IN (?) and ci.catalog_id IN (?))", CatalogItem::HIDDEN_STATES, catalog_ids.flatten)
}

.publications_searchActiveRecord::Relation<Item>

A relation of Items that are publications search. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



565
# File 'app/models/item.rb', line 565

scope :publications_search, ->(term) { canonical_search(term) }

.radiant_panel_controls_dual_connectActiveRecord::Relation<Item>

A relation of Items that are radiant panel controls dual connect. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



454
# File 'app/models/item.rb', line 454

scope :radiant_panel_controls_dual_connect, -> { where(sku: ItemConstants::RADIANT_PANEL_PLUG_IN_CONTROL_SKUS + ItemConstants::RADIANT_PANEL_HARDWIRE_CONTROL_SKUS) }

.radiant_panel_controls_hardwiredActiveRecord::Relation<Item>

A relation of Items that are radiant panel controls hardwired. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



453
# File 'app/models/item.rb', line 453

scope :radiant_panel_controls_hardwired, -> { where(sku: ItemConstants::RADIANT_PANEL_HARDWIRE_CONTROL_SKUS) }

.radiant_panel_controls_plug_inActiveRecord::Relation<Item>

A relation of Items that are radiant panel controls plug in. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



451
# File 'app/models/item.rb', line 451

scope :radiant_panel_controls_plug_in, -> { where(sku: ItemConstants::RADIANT_PANEL_PLUG_IN_CONTROL_SKUS) }

.radiant_panelsActiveRecord::Relation<Item>

A relation of Items that are radiant panels. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



450
# File 'app/models/item.rb', line 450

scope :radiant_panels,          -> { where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_RADIANT_PANELS)) }

.radiant_panels_hardwiredActiveRecord::Relation<Item>

A relation of Items that are radiant panels hardwired. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



452
# File 'app/models/item.rb', line 452

scope :radiant_panels_hardwired, -> { where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_RADIANT_PANELS_HARDWIRED)) }

.ransackable_scopes(_auth_object = nil) ⇒ Object



673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
# File 'app/models/item.rb', line 673

def self.ransackable_scopes(_auth_object = nil)
  %i[keywords_search by_product_line_id pdf_search
     by_product_category_id
     by_primary_product_line_ids_with_descendants
     active_in_catalog_ids
     public_and_active_in_catalog_id
     sku_and_aliases_search
     tags_include
     ai_search_publications
     hybrid_search_publications
     not_by_product_category_id
     not_by_product_line_id
     tags_exclude
     primary_product_line_slug_ltree_cont]
end

.relay_panelsActiveRecord::Relation<Item>

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

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



402
# File 'app/models/item.rb', line 402

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

.remote_servicesActiveRecord::Relation<Item>

A relation of Items that are remote services. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



415
# File 'app/models/item.rb', line 415

scope :remote_services,         -> { services.where(Item[:sku].matches('%REMOTE%')) }

.rendered_product_specs_keysObject



2075
2076
2077
# File 'app/models/item.rb', line 2075

def self.rendered_product_specs_keys
  pluck('distinct jsonb_object_keys(rendered_product_specifications)').sort.map(&:to_sym)
end

.rendered_product_specs_options_for_select(spec_key) ⇒ Object



2079
2080
2081
2082
2083
# File 'app/models/item.rb', line 2079

def self.rendered_product_specs_options_for_select(spec_key)
  Item.pluck("distinct rendered_product_specifications->'#{spec_key}'->>'output',rendered_product_specifications->'#{spec_key}'->>'raw'").reject do |v|
    v[0].nil?
  end.sort
end

.reservableActiveRecord::Relation<Item>

A relation of Items that are reservable. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



483
# File 'app/models/item.rb', line 483

scope :reservable,              -> { where(require_reservation: true) }

.reviewableActiveRecord::Relation<Item>

A relation of Items that are reviewable. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



466
# File 'app/models/item.rb', line 466

scope :reviewable,              -> { joins(:product_category, :product_lines).merge(ProductLine.reviewable).merge(ProductCategory.reviewable) }

.rough_in_kitsActiveRecord::Relation<Item>

A relation of Items that are rough in kits. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



484
# File 'app/models/item.rb', line 484

scope :rough_in_kits,           -> { by_product_line_path(LtreePaths::PL_FLOOR_HEATING_ROUGH_IN_KITS).merge(accessories) }

.sensorsActiveRecord::Relation<Item>

A relation of Items that are sensors. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



406
# File 'app/models/item.rb', line 406

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

.servicesActiveRecord::Relation<Item>

A relation of Items that are services. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



414
# File 'app/models/item.rb', line 414

scope :services,                -> { where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_SERVICES)) }

.shippingActiveRecord::Relation<Item>

A relation of Items that are shipping. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



462
# File 'app/models/item.rb', line 462

scope :shipping,                -> { where(tax_class: 'shp') }

.sku_aliases_searchActiveRecord::Relation<Item>

A relation of Items that are sku aliases search. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



443
# File 'app/models/item.rb', line 443

scope :sku_aliases_search,      ->(sku) { where('items.sku_aliases @> ARRAY[:sku]::varchar[]', sku:) }

.sku_and_aliases_searchActiveRecord::Relation<Item>

A relation of Items that are sku and aliases search. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



444
# File 'app/models/item.rb', line 444

scope :sku_and_aliases_search,  ->(sku) { where('items.sku ILIKE :sku OR items.sku_aliases @> ARRAY[:sku]::varchar[]', sku:) }

.sku_is_membrane?(sku) ⇒ Boolean

Returns:

  • (Boolean)


1401
1402
1403
# File 'app/models/item.rb', line 1401

def self.sku_is_membrane?(sku)
  ItemConstants::MEMBRANE_SKUS.include?(sku)
end

.sku_searchActiveRecord::Relation<Item>

A relation of Items that are sku search. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



557
# File 'app/models/item.rb', line 557

scope :sku_search, ->(sku) { canonical_search(sku) }

.smart_servicesActiveRecord::Relation<Item>

A relation of Items that are smart services. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



508
# File 'app/models/item.rb', line 508

scope :smart_services,           -> { where(Item[:primary_pl_path_slugs].ltree_descendant(LtreePaths::PL_SERVICES)) }

.smartpresetsActiveRecord::Relation<Item>

A relation of Items that are smartpresets. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



448
# File 'app/models/item.rb', line 448

scope :smartpresets,            -> { where(sku: SMARTPRESET_SERVICE_SKU) }

.smartstatsActiveRecord::Relation<Item>

A relation of Items that are smartstats. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



400
# File 'app/models/item.rb', line 400

scope :smartstats,              -> { controls.where(Item[:primary_pl_path_slugs].ltree_descendant(LtreePaths::PL_FLOOR_HEATING_CONTROL_SMARTSTAT)) }

.snow_melting_elementsActiveRecord::Relation<Item>

A relation of Items that are snow melting elements. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



408
# File 'app/models/item.rb', line 408

scope :snow_melting_elements,   -> { heating_elements.where(Item[:primary_pl_path_slugs].ltree_descendant(LtreePaths::PL_SNOW_MELTING)) }

.spare_partsActiveRecord::Relation<Item>

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

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



417
# File 'app/models/item.rb', line 417

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

.tax_classes_for_selectObject



917
918
919
# File 'app/models/item.rb', line 917

def self.tax_classes_for_select
  TAX_CLASSES.invert
end

.tempzonesActiveRecord::Relation<Item>

A relation of Items that are tempzones. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



449
# File 'app/models/item.rb', line 449

scope :tempzones,               -> { where(Item[:primary_pl_path_slugs].ltree_descendant(LtreePaths::PL_FLOOR_HEATING_TEMPZONE)).merge(heating_elements) }

.thermostatsActiveRecord::Relation<Item>

A relation of Items that are thermostats. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

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

.toolsActiveRecord::Relation<Item>

A relation of Items that are tools. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



412
# File 'app/models/item.rb', line 412

scope :tools,                   -> { where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_TOOLS)) }

.towel_warmer_controls_dual_connectActiveRecord::Relation<Item>

A relation of Items that are towel warmer controls dual connect. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

scope :towel_warmer_controls_dual_connect, -> { where(sku: ItemConstants::TOWEL_WARMER_PLUG_IN_CONTROL_SKUS + ItemConstants::TOWEL_WARMER_HARDWIRE_CONTROL_SKUS) }

.towel_warmer_controls_hardwiredActiveRecord::Relation<Item>

A relation of Items that are towel warmer controls hardwired. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



393
# File 'app/models/item.rb', line 393

scope :towel_warmer_controls_hardwired, -> { where(sku: ItemConstants::TOWEL_WARMER_HARDWIRE_CONTROL_SKUS) }

.towel_warmer_controls_plug_inActiveRecord::Relation<Item>

A relation of Items that are towel warmer controls plug in. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



392
# File 'app/models/item.rb', line 392

scope :towel_warmer_controls_plug_in, -> { where(sku: ItemConstants::TOWEL_WARMER_PLUG_IN_CONTROL_SKUS) }

.towel_warmersActiveRecord::Relation<Item>

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

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

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

.towel_warmers_hardwiredActiveRecord::Relation<Item>

A relation of Items that are towel warmers hardwired. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



391
# File 'app/models/item.rb', line 391

scope :towel_warmers_hardwired, -> { where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_TOWEL_WARMERS_HARDWIRED)) }

.towel_warmers_plug_inActiveRecord::Relation<Item>

A relation of Items that are towel warmers plug in. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

scope :towel_warmers_plug_in,   -> { where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_TOWEL_WARMERS_PLUG_IN)) }

.underlaymentsActiveRecord::Relation<Item>

A relation of Items that are underlayments. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



461
# File 'app/models/item.rb', line 461

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

.uom_select_optionsObject



913
914
915
# File 'app/models/item.rb', line 913

def self.uom_select_options
  %w[each feet]
end

.upgradesActiveRecord::Relation<Item>

A relation of Items that are upgrades. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



410
# File 'app/models/item.rb', line 410

scope :upgrades,                -> { where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_UPGRADES)) }

.with_all_catalog_items_discontinuedActiveRecord::Relation<Item>

A relation of Items that are with all catalog items discontinued. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
# File 'app/models/item.rb', line 608

scope :with_all_catalog_items_discontinued, -> {
  where(%{
    (
      SELECT COUNT(*)
      FROM catalog_items ci1
      INNER JOIN store_items si1 ON ci1.store_item_id = si1.id
      WHERE si1.item_id = items.id
    ) > 0
     AND
    (
      SELECT COUNT(*)
      FROM catalog_items ci1
      INNER JOIN store_items si1 ON ci1.store_item_id = si1.id
      WHERE si1.item_id = items.id
    ) =
    (
      SELECT COUNT(*)
      FROM catalog_items ci2
      INNER JOIN store_items si2 ON ci2.store_item_id = si2.id
      WHERE si2.item_id = items.id AND ci2.state = 'discontinued'
    )
  })
}

.with_featuresActiveRecord::Relation<Item>

A relation of Items that are with features. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



507
# File 'app/models/item.rb', line 507

scope :with_features,            -> { where.not(feature_1: nil, feature_2: nil, feature_3: nil, feature_4: nil, feature_5: nil) }

.with_product_specificationActiveRecord::Relation<Item>

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

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



467
468
469
470
471
472
473
474
475
476
# File 'app/models/item.rb', line 467

scope :with_product_specification, ->(token, value, grouping = nil, include_absence_of = false) {
  match_hsh = { raw: TypeCoercer.coerce(value) }
  grouping = grouping.to_s.titleize if grouping.is_a?(Symbol)
  match_hsh[:grouping] = grouping if grouping.present?
  test_condition_criteria = +'items.rendered_product_specifications @> :condition_1'
  test_condition_criteria << ' OR NOT (rendered_product_specifications ? :condition_2)' if include_absence_of
  condition_1 = { token.to_s => match_hsh }.to_json
  condition_2 = token.to_s
  where(test_condition_criteria, condition_1:, condition_2:)
}

.with_stockActiveRecord::Relation<Item>

A relation of Items that are with stock. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



421
422
423
# File 'app/models/item.rb', line 421

scope :with_stock, -> {
  where("exists(select id from store_items where store_items.item_id = items.id and qty_on_hand > 0 and store_items.location = 'AVAILABLE')")
}

.with_stock_forActiveRecord::Relation<Item>

A relation of Items that are with stock for. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



424
425
426
# File 'app/models/item.rb', line 424

scope :with_stock_for, ->(store_id) {
  where("exists(select id from store_items where store_items.store_id = ? and store_items.item_id = items.id and qty_on_hand > 0 and store_items.location = 'AVAILABLE')", store_id)
}

Instance Method Details

#all_imagesObject

Find all images linked to this item directly or by combination of product line or category ancestry
This is a simpler method than image retriever



2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
# File 'app/models/item.rb', line 2384

def all_images
  # Start simple find those that have direct links
  query = Image.left_outer_joins(:items)
               .left_outer_joins(:product_lines)
               .left_outer_joins(:product_categories)
  # Conditions as follow
  conditions = []
  plids = product_line_ancestry_ids
  pcids = product_category_ancestry_ids
  # Condition 1 - Image tagged specifically for this item
  conditions << Item[:id].eq(id)
  # Condition 2 are images tagged by any ancestry product line but no specific product category
  conditions << ProductLine[:id].in(plids).and(ProductCategory[:id].eq(nil)) if plids.present?
  # Condition 3 are images tagged by any ancestry product category but no specific product line
  conditions << ProductLine[:id].eq(nil).and(ProductCategory[:id].in(pcids)) if pcids.present?
  # Condition 4 are images tagged by both product line and category ancestries
  conditions << ProductLine[:id].in(plids).and(ProductCategory[:id].in(pcids)) if plids.present? && pcids.present?
  # Create an or condition
  combined_conditions = conditions.reduce { |acc, condition| acc.or(condition) }
  query.where(combined_conditions)
end

#all_my_publications(categories: nil, locale: nil, publication_category_paths: nil) ⇒ Object



1725
1726
1727
# File 'app/models/item.rb', line 1725

def all_my_publications(categories: nil, locale: nil, publication_category_paths: nil)
  Item::PublicationRetriever.new.process(self, categories:, locale:, publication_category_paths:).all_publications
end

#all_site_mapsObject



1940
1941
1942
# File 'app/models/item.rb', line 1940

def all_site_maps
  [] + site_maps.to_a + catalog_item_site_maps.to_a
end

#all_uploadsObject



2442
2443
2444
# File 'app/models/item.rb', line 2442

def all_uploads
  uploads.order(Upload[:created_at].desc)
end

#any_box_changed?Boolean

Returns:

  • (Boolean)


2438
2439
2440
# File 'app/models/item.rb', line 2438

def any_box_changed?
  box1_changed? || box2_changed? || box3_changed?
end

#articlesActiveRecord::Relation<Article>

Returns:

  • (ActiveRecord::Relation<Article>)

See Also:



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

has_and_belongs_to_many :articles

#assortment_instructionsActiveRecord::Relation<AssortmentInstruction>

Returns:

See Also:



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

has_and_belongs_to_many :assortment_instructions, inverse_of: :items

#async_update_rendered_product_specificationsObject



1936
1937
1938
# File 'app/models/item.rb', line 1936

def async_update_rendered_product_specifications
  ItemAttributeWorker.perform_async(id)
end

#authenticated_url(dimensions = nil) ⇒ Object



854
855
856
# File 'app/models/item.rb', line 854

def authenticated_url(dimensions = nil)
  (dimensions.nil? ? literature.attachment.url : literature.attachment.thumb(dimensions, format: 'png', frame: '0').url) if literature
end

#auto_translate(reset_previous_values: false, locales: nil) ⇒ Object



1793
1794
1795
1796
1797
1798
1799
1800
1801
# File 'app/models/item.rb', line 1793

def auto_translate(reset_previous_values: false, locales: nil)
  locales ||= content_locales_to_render
  Rails.logger.info "Product Specifications for Item #{id}:#{sku} will be rendered in #{locales.join(', ')}"
  # auto_translate_attributes(reset_previous_values: reset_previous_values, locales: locales)
  auto_translate_product_specifications(reset_previous_values:, locales:)
  reload
  update_rendered_product_specifications(locales:)
  :success
end

#auto_translate_attributes(reset_previous_values: false, locales: nil) ⇒ Object



1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
# File 'app/models/item.rb', line 1803

def auto_translate_attributes(reset_previous_values: false, locales: nil)
  # Auto translate attributes from the translatable list
  locales ||= content_locales_to_render
  res = {}
  TRANSLATABLE_ATTRIBUTES.each do |attr_name|
    next unless (english_val = send(:"#{attr_name}_en").presence)

    # We loop through the locales
    res[attr_name] = {}
    locales.each do |locale|
      # If our locale already exists and we specify we do not want to reset value, we can skip
      next if !reset_previous_values && Mobility.with_locale(locale) { send(attr_name.to_sym) }.present?

      # Translate
      localized_val = TranslationKey.translate(english_val, locale, namespace: TRANSLATION_NAMESPACE, auto_create: false, auto_translate: true, resource: self, resource_attribute: attr_name)
      next if localized_val.blank?

      # Store it in the locale
      Mobility.with_locale(locale) { send(:"#{attr_name}=", localized_val, locale:) }
      res[attr_name][locale] = localized_val
    end
  end
  if save && res
    :success
  else
    :error
  end
end

#auto_translate_product_specifications(reset_previous_values: false, locales: nil) ⇒ Object



1832
1833
1834
1835
1836
1837
1838
# File 'app/models/item.rb', line 1832

def auto_translate_product_specifications(reset_previous_values: false, locales: nil)
  locales ||= content_locales_to_render
  product_specifications.each do |ps|
    ps.auto_translate(reset_previous_values:, locales:)
  end
  :success
end

#available_content_localesObject

Returns all locales from the catalog this item is part of



1973
1974
1975
1976
# File 'app/models/item.rb', line 1973

def available_content_locales
  # Join to catalog and find out in which locale we need to generate content, english is implied
  Catalog.eager_load(:catalog_items).merge(catalog_items.not_discontinued).pluck(:locales).flatten.compact.uniq.sort
end

#available_token_options_from_product_specificationsObject



2119
2120
2121
# File 'app/models/item.rb', line 2119

def available_token_options_from_product_specifications
  (rendered_product_specifications || {}).each_with_object({}.with_indifferent_access) { |(rpk, rpv), hsh| hsh[rpk] = rpv[:output] }
end

#base_weight_converted(unit: 'lbs', precision: nil) ⇒ Object



1595
1596
1597
# File 'app/models/item.rb', line 1595

def base_weight_converted(unit: 'lbs', precision: nil)
  express_weight_converted base_weight, unit:, precision:
end

#box1_changed?Boolean

Returns:

  • (Boolean)


2418
2419
2420
# File 'app/models/item.rb', line 2418

def box1_changed?
  %i[shipping_weight shipping_length shipping_width shipping_weight].any? { |fn| send(:"#{fn}_previously_changed?") || send(:"will_save_change_to_#{fn}?") }
end

#box1_defined?Boolean

Returns:

  • (Boolean)


2414
2415
2416
# File 'app/models/item.rb', line 2414

def box1_defined?
  %i[shipping_weight shipping_length shipping_width shipping_weight].all? { |fn| send(fn)&.positive? }
end

#box2_changed?Boolean

Returns:

  • (Boolean)


2426
2427
2428
# File 'app/models/item.rb', line 2426

def box2_changed?
  %i[box2_shipping_weight box2_shipping_length box2_shipping_width box2_shipping_weight].any? { |fn| send(:"#{fn}_previously_changed?") || send(:"will_save_change_to_#{fn}?") }
end

#box2_defined?Boolean

Returns:

  • (Boolean)


2422
2423
2424
# File 'app/models/item.rb', line 2422

def box2_defined?
  %i[box2_shipping_weight box2_shipping_length box2_shipping_width box2_shipping_height].all? { |fn| send(fn)&.positive? }
end

#box2_shipping_dimensions_to_packageObject



1626
1627
1628
1629
1630
1631
# File 'app/models/item.rb', line 1626

def box2_shipping_dimensions_to_package
  return unless box2_defined?

  Shipping::Container.new(length: box2_shipping_length, width: box2_shipping_width, height: box2_shipping_height, weight: box2_shipping_weight,
                          container_type:)
end

#box2_shipping_height_converted(unit: 'in', precision: 0) ⇒ Object



1563
1564
1565
1566
1567
# File 'app/models/item.rb', line 1563

def box2_shipping_height_converted(unit: 'in', precision: 0)
  return unless box2_shipping_height

  RubyUnits::Unit.new("#{box2_shipping_height} in").convert_to(unit.to_s).scalar.to_f.round(precision)
end

#box2_shipping_length_converted(unit: 'in', precision: 0) ⇒ Object



1551
1552
1553
1554
1555
# File 'app/models/item.rb', line 1551

def box2_shipping_length_converted(unit: 'in', precision: 0)
  return unless box2_shipping_length

  RubyUnits::Unit.new("#{box2_shipping_length} in").convert_to(unit.to_s).scalar.to_f.round(precision)
end

#box2_shipping_weight_converted(unit: 'lbs', precision: nil) ⇒ Object



1611
1612
1613
# File 'app/models/item.rb', line 1611

def box2_shipping_weight_converted(unit: 'lbs', precision: nil)
  express_weight_converted box2_shipping_weight, unit:, precision:
end

#box2_shipping_width_converted(unit: 'in', precision: 0) ⇒ Object



1557
1558
1559
1560
1561
# File 'app/models/item.rb', line 1557

def box2_shipping_width_converted(unit: 'in', precision: 0)
  return unless box2_shipping_width

  RubyUnits::Unit.new("#{box2_shipping_width} in").convert_to(unit.to_s).scalar.to_f.round(precision)
end

#box3_changed?Boolean

Returns:

  • (Boolean)


2434
2435
2436
# File 'app/models/item.rb', line 2434

def box3_changed?
  %i[box3_shipping_weight box3_shipping_length box3_shipping_width box3_shipping_weight].any? { |fn| send(:"#{fn}_previously_changed?") || send(:"will_save_change_to_#{fn}?") }
end

#box3_defined?Boolean

Returns:

  • (Boolean)


2430
2431
2432
# File 'app/models/item.rb', line 2430

def box3_defined?
  %i[box3_shipping_weight box3_shipping_length box3_shipping_width box3_shipping_height].all? { |fn| send(fn)&.positive? }
end

#box3_shipping_dimensions_to_packageObject



1633
1634
1635
1636
1637
1638
# File 'app/models/item.rb', line 1633

def box3_shipping_dimensions_to_package
  return unless box3_defined?

  Shipping::Container.new(length: box3_shipping_length, width: box3_shipping_width, height: box3_shipping_height, weight: box3_shipping_weight,
                          container_type:)
end

#box3_shipping_height_converted(unit: 'in', precision: 0) ⇒ Object



1581
1582
1583
1584
1585
# File 'app/models/item.rb', line 1581

def box3_shipping_height_converted(unit: 'in', precision: 0)
  return unless box3_shipping_height

  RubyUnits::Unit.new("#{box3_shipping_height} in").convert_to(unit.to_s).scalar.to_f.round(precision)
end

#box3_shipping_length_converted(unit: 'in', precision: 0) ⇒ Object



1569
1570
1571
1572
1573
# File 'app/models/item.rb', line 1569

def box3_shipping_length_converted(unit: 'in', precision: 0)
  return unless box3_shipping_length

  RubyUnits::Unit.new("#{box3_shipping_length} in").convert_to(unit.to_s).scalar.to_f.round(precision)
end

#box3_shipping_weight_converted(unit: 'lbs', precision: nil) ⇒ Object



1615
1616
1617
# File 'app/models/item.rb', line 1615

def box3_shipping_weight_converted(unit: 'lbs', precision: nil)
  express_weight_converted box3_shipping_weight, unit:, precision:
end

#box3_shipping_width_converted(unit: 'in', precision: 0) ⇒ Object



1575
1576
1577
1578
1579
# File 'app/models/item.rb', line 1575

def box3_shipping_width_converted(unit: 'in', precision: 0)
  return unless box3_shipping_width

  RubyUnits::Unit.new("#{box3_shipping_width} in").convert_to(unit.to_s).scalar.to_f.round(precision)
end

#boxesObject



993
994
995
996
997
998
# File 'app/models/item.rb', line 993

def boxes
  b = [[shipping_length, shipping_width, shipping_height, shipping_weight]]
  b << [box2_shipping_length, box2_shipping_width, box2_shipping_height, box2_shipping_weight] if box2_defined?
  b << [box3_shipping_length, box3_shipping_width, box3_shipping_height, box3_shipping_weight] if box3_defined?
  b
end

#boxes_shipping_dimensions_and_weights_converted(units: { length: 'in', weight: 'lbs' }, precision: 0) ⇒ Object



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

def boxes_shipping_dimensions_and_weights_converted(units: { length: 'in', weight: 'lbs' }, precision: 0)
  boxes.map do |b|
    [
      RubyUnits::Unit.new("#{b[0]} in").convert_to(units[:length].to_s).scalar.to_f.round(precision),
      RubyUnits::Unit.new("#{b[1]} in").convert_to(units[:length].to_s).scalar.to_f.round(precision),
      RubyUnits::Unit.new("#{b[2]} in").convert_to(units[:length].to_s).scalar.to_f.round(precision),
      RubyUnits::Unit.new("#{b[3]} lb").convert_to(units[:weight].to_s).scalar.to_f.round(precision)
    ]
  end
end

#boxes_shipping_dimensions_and_weights_display(units: { length: 'in', weight: 'lbs' }, precision: 0) ⇒ Object



1084
1085
1086
# File 'app/models/item.rb', line 1084

def boxes_shipping_dimensions_and_weights_display(units: { length: 'in', weight: 'lbs' }, precision: 0)
  boxes_shipping_dimensions_and_weights_converted(units:, precision:).map { |b| "#{b[0]} #{units[:length]} x #{b[1]} #{units[:length]} x #{b[2]} #{units[:length]} x #{b[3]} #{units[:weight]}" }
end

#boxes_to_packagesObject



1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
# File 'app/models/item.rb', line 1000

def boxes_to_packages
  packages = []
  boxes.each do |box|
    le, wi, he, we, = box + [0]
    package = Shipping::Container.new(length: le, width: wi, height: he, weight: we, container_type: Shipment.container_types.invert[0])
    package.add_content(id, 1)
    packages << package
  end
  packages
end

#brandBrand

Returns:

See Also:



256
# File 'app/models/item.rb', line 256

belongs_to :brand, optional: true

#calculate_ideal_cable_spacingObject



1419
1420
1421
1422
1423
1424
1425
# File 'app/models/item.rb', line 1419

def calculate_ideal_cable_spacing
  res = nil
  if primary_product_line && primary_product_line.get_first_heating_system_type.present?
    res = HeatingElementProductLineOption.get_field_array_from_options(:ideal_cable_spacing, product_line_id: primary_product_line.get_first_heating_system_type.id).first
  end
  res
end

#calculate_is_cable_system?Boolean

Returns:

  • (Boolean)


1410
1411
1412
1413
1414
1415
1416
1417
# File 'app/models/item.rb', line 1410

def calculate_is_cable_system?
  res = nil
  if primary_product_line && primary_product_line.get_first_heating_system_type.present? && HeatingElementProductLineOption.get_field_array_from_options(:require_cable_spacing,
                                                                                                                                                         product_line_id: primary_product_line.get_first_heating_system_type.id).first
    res = true
  end
  res
end

#calculate_item_fieldsObject



975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
# File 'app/models/item.rb', line 975

def calculate_item_fields
  # always set largest shipping dimension to shipping_length, then shipping_width, then shipping_height
  sorted_dims = [shipping_length, shipping_width, shipping_height].sort_by(&:to_f)
  self.shipping_length = sorted_dims[2]
  self.shipping_width = sorted_dims[1]
  self.shipping_height = sorted_dims[0]
  box2_sorted_dims = [box2_shipping_length, box2_shipping_width, box2_shipping_height].sort_by(&:to_f)
  self.box2_shipping_length = box2_sorted_dims[2]
  self.box2_shipping_width = box2_sorted_dims[1]
  self.box2_shipping_height = box2_sorted_dims[0]
  box3_sorted_dims = [box3_shipping_length, box3_shipping_width, box3_shipping_height].sort_by(&:to_f)
  self.box3_shipping_length = box3_sorted_dims[2]
  self.box3_shipping_width = box3_sorted_dims[1]
  self.box3_shipping_height = box3_sorted_dims[0]
  self.ideal_cable_spacing = (calculate_ideal_cable_spacing if (self.is_cable_system = calculate_is_cable_system?))
  true
end

#can_be_packaged?Boolean

Returns:

  • (Boolean)


966
967
968
# File 'app/models/item.rb', line 966

def can_be_packaged?
  Item.packageable.where(id:).present?
end

#canonical_pathObject

Hierarchical canonical path via product line ancestry + SKU.
e.g. "towel-warmer/barcelona/TW-BC-08BS-FS2"



720
721
722
723
724
725
726
727
# File 'app/models/item.rb', line 720

def canonical_path
  return nil unless primary_product_line

  pl_path = primary_product_line.canonical_path
  return nil if pl_path.blank?

  "#{pl_path}/#{sku}"
end

#canonical_skuObject



1680
1681
1682
1683
1684
1685
1686
1687
# File 'app/models/item.rb', line 1680

def canonical_sku
  sku_match = sku.match(/(.*)-([[:alpha:]])$/)
  if sku_match && (sku_match.length == 3)
    sku_match[1]
  else
    sku
  end
end

#canonical_url(locale: I18n.locale) ⇒ Object

Canonical URL with locale prefix.
e.g. "/en-US/towel-warmer/barcelona/TW-BC-08BS-FS2"



731
732
733
734
735
736
# File 'app/models/item.rb', line 731

def canonical_url(locale: I18n.locale)
  path = canonical_path
  return nil unless path

  "/#{locale}/#{path}"
end

#catalog_item_site_mapsActiveRecord::Relation<SiteMap>

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



285
# File 'app/models/item.rb', line 285

has_many :catalog_item_site_maps, class_name: 'SiteMap', source: :site_maps, through: :catalog_items

#catalog_itemsActiveRecord::Relation<CatalogItem>

Returns:

See Also:



263
# File 'app/models/item.rb', line 263

has_many :catalog_items, through: :store_items, inverse_of: :item

#catalogsActiveRecord::Relation<Catalog>

Returns:

  • (ActiveRecord::Relation<Catalog>)

See Also:



264
# File 'app/models/item.rb', line 264

has_many :catalogs, through: :catalog_items

#check_oversize?(carrier: nil) ⇒ Boolean

Check if item is oversize, optionally for a specific carrier
When no carrier specified, checks all carriers (item is oversize if ANY carrier flags it)

Parameters:

  • carrier (String, nil) (defaults to: nil)

    Optional carrier name (e.g., 'UPS', 'FedEx', 'Purolator', 'Canpar')

Returns:

  • (Boolean)


1666
1667
1668
1669
1670
1671
1672
# File 'app/models/item.rb', line 1666

def check_oversize?(carrier: nil)
  r = run_shipping_audit(carrier:)
  r.any?(&:oversize_conditions?)
rescue StandardError
  logger.error "Oversize check failed on item id #{id}, returning false"
  false
end

#compatible_itemsActiveRecord::Relation<CompatibleItem>

Returns:

  • (ActiveRecord::Relation<CompatibleItem>)

See Also:



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

has_many :compatible_items, -> { merge(ItemRelation.replacement_parts) }, through: :source_item_relations, source: :source_item

#container_typeObject



2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
# File 'app/models/item.rb', line 2334

def container_type
  # let's guess, if ships_via_crate? crate, if ships LTL, pallet, otherwise carton
  if ships_via_crate?
    Shipment.container_types.keys[2] # 'crate'
  elsif shipping_class == 'freight_service'
    Shipment.container_types.keys[1] # 'pallet'
  else
    Shipment.container_types.keys.first # 'carton'
  end
end

#content_locales_to_renderObject



1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
# File 'app/models/item.rb', line 1978

def content_locales_to_render
  if is_publication?
    Mobility.available_locales
  else
    locales = (Mobility.available_locales & ([I18n.default_locale] + available_content_locales)).uniq
    # Because en-US and en-CA are at this point the same, we don't bother rendering these, if this changes, this is
    # where you remove the following code.  We'll put french in the same category why not.  Don't solve this problem
    # until we actually have a product sold in france and canada
    locales.delete(:'en-US')
    locales.delete(:'en-CA')
    locales.delete(:'fr-CA') if locales.include?(:fr)
    locales
  end
end

#controllable?Boolean

Returns:

  • (Boolean)


761
762
763
764
765
# File 'app/models/item.rb', line 761

def controllable?
  is_heating_element? ||
    is_towel_warmer? ||
    is_radiant_panel?
end

#country_of_origin_iso3Object



705
706
707
708
709
# File 'app/models/item.rb', line 705

def country_of_origin_iso3
  return unless coo

  ISO3166::Country[coo]&.alpha3
end

#country_of_origin_nameObject



699
700
701
702
703
# File 'app/models/item.rb', line 699

def country_of_origin_name
  return unless coo

  ISO3166::Country[coo]&.iso_short_name
end

#create_template_specificationsObject



2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
# File 'app/models/item.rb', line 2085

def create_template_specifications
  return OpenStruct.new(created: [], errors: ['Missing Product Line or Product Category']) unless primary_product_line_id || product_category_id

  res = Item::SpecificationsRetriever.new.process(self, template: true)
  res_classic = Item::SpecificationsRetriever.new.process(self, template: false)

  ps_created = []
  ps_errors = []
  res.specifications.each do |template_spec|
    # Check if there is already a spec for this, only template if blank
    next if spec_value(template_spec.token).present? || res_classic.specifications.any? { |s| s.token == template_spec.token }

    s = template_spec.dup
    s.product_category_id = nil
    s.product_line_id = nil
    s.template = false
    s.item_ids = [id].compact
    s.template_product_specification_id = template_spec.id
    if s.save
      ps_created << s
    else
      ps_errors << "Template Spec id #{template_spec.id} : #{template_spec} could not be created for item #{id}|#{sku}.  Errors: #{s.errors_to_s}"
    end
  end
  OpenStruct.new(created: ps_created, errors: ps_errors)
end

#current_supplier_costObject



755
756
757
758
759
# File 'app/models/item.rb', line 755

def current_supplier_cost
  return 'not found' if supplier_items.empty? || (current_price = supplier_items.order(created_at: :desc).first&.current_price).nil?

  "#{current_price.unit_cost} #{current_price.currency}"
end

#deep_dupObject



219
220
221
222
223
224
225
226
# File 'app/models/item.rb', line 219

def deep_dup
  deep_clone(
    include: :item_product_lines,
    except: %i[upc upload_id last_sale_date first_sale_date popularity amazon_asin]
  ) do |_original, copy|
    copy.popularity = nil if copy.is_a?(Item)
  end
end

#default_short_descriptionObject



1748
1749
1750
1751
1752
# File 'app/models/item.rb', line 1748

def default_short_description
  return unless (dd = detailed_description.to_s.squish.presence) && dd.present?

  dd.truncate(120, separator: ' ')
end

#detailed_descriptionObject

Creates a text description from the html rendered description



1744
1745
1746
# File 'app/models/item.rb', line 1744

def detailed_description
  html_to_text(rendered_detailed_description_html)
end

#digital_assetsActiveRecord::Relation<DigitalAsset>

Returns:

See Also:



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

has_and_belongs_to_many :digital_assets

#direct_packingsActiveRecord::Relation<Packing>

Returns:

  • (ActiveRecord::Relation<Packing>)

See Also:



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

has_many :direct_packings, class_name: 'Packing', inverse_of: :item, dependent: :destroy

#direct_product_specificationsActiveRecord::Relation<ProductSpecification>

Returns:

See Also:



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

has_and_belongs_to_many :direct_product_specifications, class_name: 'ProductSpecification', dependent: :destroy, inverse_of: :items

#discontinue_self_and_dependentsObject



956
957
958
959
960
961
962
963
964
# File 'app/models/item.rb', line 956

def discontinue_self_and_dependents
  self.class.transaction do
    store_items.each do |si|
      si.catalog_items.each(&:discontinue) # This event updates the state and hides the catalog from feed
      si.update_attribute(:is_discontinued, true) # rubocop:disable Rails/SkipsModelValidations
    end
    update!(is_discontinued: true, cycle_count_grouping: 'not_set')
  end
end

#discontinue_store_and_catalog_itemsObject



949
950
951
952
953
954
# File 'app/models/item.rb', line 949

def discontinue_store_and_catalog_items
  return if is_publication?

  discontinue_self_and_dependents if is_discontinued?
  true
end

#ean13Object



843
844
845
846
847
848
849
850
851
852
# File 'app/models/item.rb', line 843

def ean13
  return if upc.blank?

  case upc.size
  when 12
    "0#{upc}"
  when 13
    upc
  end
end

#edi_communication_logsActiveRecord::Relation<EdiCommunicationLog>

Returns:

See Also:



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

has_many :edi_communication_logs, through: :edi_documents

#edi_documentsActiveRecord::Relation<EdiDocument>

Returns:

See Also:



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

has_many :edi_documents, dependent: :destroy

#effective_nmfc_classObject

Alias for Product_category#effective_nmfc_class

Returns:

  • (Object)

    Product_category#effective_nmfc_class

See Also:



640
641
642
643
644
645
646
647
648
649
# File 'app/models/item.rb', line 640

delegate :is_control?,
:is_upgrade?,
:is_accessory?,
:is_relay_panel?,
:is_custom_mat?,
:effective_nmfc_code,
:effective_nmfc_class,
:is_publication?,
to: :product_category,
allow_nil: true

#effective_nmfc_codeObject

Alias for Product_category#effective_nmfc_code

Returns:

  • (Object)

    Product_category#effective_nmfc_code

See Also:



640
641
642
643
644
645
646
647
648
649
# File 'app/models/item.rb', line 640

delegate :is_control?,
:is_upgrade?,
:is_accessory?,
:is_relay_panel?,
:is_custom_mat?,
:effective_nmfc_code,
:effective_nmfc_class,
:is_publication?,
to: :product_category,
allow_nil: true

#effective_option_nameObject



2275
2276
2277
# File 'app/models/item.rb', line 2275

def effective_option_name
  option_name.presence || public_short_name.presence || name
end

#effective_seo_description(char_limit: nil) ⇒ Object



858
859
860
861
862
863
864
865
866
867
868
869
870
# File 'app/models/item.rb', line 858

def effective_seo_description(char_limit: nil)
  char_limit ||= SEO_META_DESCRIPTION_MAX_LENGTH
  d = seo_description.presence || short_description.presence || public_description_text || name
  return unless d

  # We replace by spaces first in case line feed is used as a sentence separator.
  d.tr!("\n", ' ')
  d.tr!("\r", ' ')
  d.tr!("\t", ' ')
  d.squeeze!(' ')
  d = d.truncate(char_limit, separator: /\s/) if char_limit
  d.squish
end

#effective_seo_keywordsObject



1775
1776
1777
1778
1779
1780
1781
1782
# File 'app/models/item.rb', line 1775

def effective_seo_keywords
  return seo_keywords.presence if seo_keywords.present?

  pl_keywords = primary_product_line&.effective_seo_keywords
  return pl_keywords.join(',') if pl_keywords.is_a?(Array)

  pl_keywords.presence
end

#effective_seo_title(skip_sku: false) ⇒ Object



872
873
874
875
876
877
878
879
880
881
882
883
# File 'app/models/item.rb', line 872

def effective_seo_title(skip_sku: false)
  s = (seo_title.presence || public_short_name.presence || public_name.presence).to_s
  if !skip_sku && (s.size + sku.size + 3) < SEO_TITLE_MAX_LENGTH && s.exclude?(sku)
    # if we have room add the sku
    s << " (#{sku})"
  end
  if s.size > SEO_TITLE_MAX_LENGTH
    # truncate
    s.truncate(SEO_TITLE_MAX_LENGTH, separator: /\s/, omission: '')
  end
  s
end

#exclusive_item_groupExclusiveItemGroup



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

belongs_to :exclusive_item_group, inverse_of: :items, optional: true

#express_weight_converted(weight_in_lbs, unit: 'lbs', precision: nil) ⇒ Object



1587
1588
1589
1590
1591
1592
1593
# File 'app/models/item.rb', line 1587

def express_weight_converted(weight_in_lbs, unit: 'lbs', precision: nil)
  return unless weight_in_lbs

  precision ||= (unit == 'g' ? 0 : 2)
  d = RubyUnits::Unit.new("#{weight_in_lbs} lbs")
  d.convert_to(unit.to_s).scalar.to_f.round(precision)
end

#facetObject



2256
2257
2258
2259
2260
2261
2262
2263
2264
# File 'app/models/item.rb', line 2256

def facet
  return unless (pl = primary_product_line&.get_first_show_in_sales_portal_ancestor(exclude_main_line: false))
  return unless (pc = product_category)

  potential_facets = pl.facets.select { |f| f.product_category_ids.to_a.include?(pc.id) }
  return if potential_facets.blank?

  potential_facets.first
end

#facet_sort_keysObject



2271
2272
2273
# File 'app/models/item.rb', line 2271

def facet_sort_keys
  facet&.sort_keys
end

#facet_tokensObject



2267
2268
2269
# File 'app/models/item.rb', line 2267

def facet_tokens
  facet&.grouping_spec_tokens&.reject(&:blank?)
end

#featuresObject

Features are a sub category of specs with feature priority set



1896
1897
1898
1899
1900
1901
1902
# File 'app/models/item.rb', line 1896

def features
  if has_features?
    [feature_1, feature_2, feature_3, feature_4, feature_5].filter_map(&:presence)
  else
    inherited_features # Look for inherited features
  end
end

#fix_nameObject



1754
1755
1756
1757
1758
1759
# File 'app/models/item.rb', line 1754

def fix_name
  # ' is interpreted as ′
  self.name = name.tr("'", '′')
  # " is interpreted as ″
  self.name = name.tr('"', '″')
end

#fixture_keyObject



1427
1428
1429
# File 'app/models/item.rb', line 1427

def fixture_key
  sku.to_s
end

#freight_classObject



1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
# File 'app/models/item.rb', line 1054

def freight_class
  freight_class = nil
  # important: item.base_weight is the better quality number and means Net Weight so the correct number to use when calculating item weight because tare weight (of the box, pallet, crate etc) is added after
  if base_weight.present? && shipping_volume_in_cubic_feet.present?
    pcf = base_weight / shipping_volume_in_cubic_feet
    Shipping::UpsFreight::FREIGHT_CLASS_BY_PCF.each do |fc, pcf_limits|
      if pcf >= pcf_limits[:lower] && pcf < pcf_limits[:upper]
        freight_class = fc.to_i
        break
      end
    end
  end
  freight_class
end

#generate_refurbished_skuObject



1705
1706
1707
# File 'app/models/item.rb', line 1705

def generate_refurbished_sku
  "#{sku}-BTK"
end

#get_quantity_from_sqft(input_sqft, add_one_extra = false) ⇒ Object



1459
1460
1461
1462
1463
# File 'app/models/item.rb', line 1459

def get_quantity_from_sqft(input_sqft, add_one_extra = false)
  qty = add_one_extra ? 1 : 0
  qty += (input_sqft / sqft).ceil.to_i if sqft.to_f > 0.0
  qty
end

#gross_weight_converted(unit: 'lbs', precision: nil) ⇒ Object



1599
1600
1601
# File 'app/models/item.rb', line 1599

def gross_weight_converted(unit: 'lbs', precision: nil)
  express_weight_converted gross_weight, unit:, precision:
end

#gtin13Object



1501
1502
1503
# File 'app/models/item.rb', line 1501

def gtin13
  "0#{upc}"
end

#gtin_typeObject



738
739
740
741
742
# File 'app/models/item.rb', line 738

def gtin_type
  return :missing if upc.blank?

  Item::UpcBarcode.identify_type(upc)
end

#gtin_type_friendlyObject



744
745
746
747
748
749
750
751
752
753
# File 'app/models/item.rb', line 744

def gtin_type_friendly
  case gtin_type
  when :gtin12
    'GTIN-12/UPC'
  when :gtin13
    'GTIN-13/EAN'
  else
    'Unknown/Invalid'
  end
end

#harmonization_code_hs6(with_dot = true) ⇒ Object

Returns the first 6 digits of the harmonization code (HS6)



1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
# File 'app/models/item.rb', line 1510

def harmonization_code_hs6(with_dot = true)
  return nil if harmonization_code.blank?

  digits = harmonization_code.delete('.').first(6)
  if with_dot
    "#{digits[0..3]}.#{digits[4..5]}"
  else
    digits
  end
end

#has_ean13?Boolean

Returns:

  • (Boolean)


839
840
841
# File 'app/models/item.rb', line 839

def has_ean13?
  upc && upc.size == 13
end

#has_features?Boolean

Returns:

  • (Boolean)


1891
1892
1893
# File 'app/models/item.rb', line 1891

def has_features?
  [feature_1, feature_2, feature_3, feature_4, feature_5].any?(&:present?)
end

#has_heating_system?Boolean

Returns:

  • (Boolean)


1443
1444
1445
# File 'app/models/item.rb', line 1443

def has_heating_system?
  primary_product_line.get_first_heating_system_type.present?
end

#has_stock?Boolean

Returns:

  • (Boolean)


935
936
937
# File 'app/models/item.rb', line 935

def has_stock?
  self.class.with_stock.exists?(id:)
end

#has_stock_in_store?(st_id) ⇒ Boolean

Returns:

  • (Boolean)


939
940
941
# File 'app/models/item.rb', line 939

def has_stock_in_store?(st_id)
  self.class.with_stock_for(st_id).exists?(id:)
end

#image_profilesActiveRecord::Relation<ImageProfile>

Returns:

See Also:



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

has_many :image_profiles, -> { image_order }

#image_url(dimensions, options = {}) ⇒ Object



1110
1111
1112
1113
# File 'app/models/item.rb', line 1110

def image_url(dimensions, options = {})
  opts = { size: dimensions }.merge(options)
  image_url2(opts)
end

#image_url2(options = {}) ⇒ Object

Use this new form that passes the options straight to the url generator



1102
1103
1104
1105
1106
1107
1108
# File 'app/models/item.rb', line 1102

def image_url2(options = {})
  if primary_image
    primary_image.image_url(options)
  elsif new_item&.primary_image
    new_item.primary_image.image_url(options)
  end
end

#imagesActiveRecord::Relation<Image>

Returns:

  • (ActiveRecord::Relation<Image>)

See Also:



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

has_many :images, through: :image_profiles

#import_translation_keysObject

This method will import existing translations or link to existing ones



1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
# File 'app/models/item.rb', line 1841

def import_translation_keys
  tkrs = []
  translations.each do |(locale_string, existing_translations)|
    existing_translations.each do |attr, translation|
      next unless attr.to_sym.in?(TRANSLATABLE_ATTRIBUTES)
      # The presence of resources implies it was imported
      next if translation_key_resources.joins(translation_key: :translations).where(resource_attribute: attr).joins(translation_key: :translations).where(TranslationText[:locale].eq(locale_string)).exists?
      next if (text = TranslationKey.newline_normalize(send(:"#{attr}_en"))).blank?
      next unless TranslationKey.translatable?(text)

      # Does this translation key exists?
      tk = TranslationKey.where(key: text, namespace: TRANSLATION_NAMESPACE).first_or_create!
      # Do we need to import this translation
      tk.translations.where(locale: locale_string.first(2)).first_or_create!(text: translation)
      # And link our new resource
      tkrs << tk.translation_key_resources.where(resource: self, resource_attribute: attr).first_or_create!
    end
  end

  # Import the product specifications
  spec_ids = Item::SpecificationsMasher.new.process(item: self).specifications.values.pluck(:product_specification_id).uniq.compact
  specifications_to_import = ProductSpecification.where(id: spec_ids)

  tkrs += specifications_to_import.map(&:import_translation_keys).flatten.compact

  tkrs
end

#increment_request_counterObject



771
772
773
774
775
# File 'app/models/item.rb', line 771

def increment_request_counter
  return unless persisted?

  Item.update_counters id, requested_counter: 1 # rubocop:disable Rails/SkipsModelValidations
end

#inherited_featuresObject

Seek inherited features from the primary product line
To inherit features, a product line's product category must be empty or matching
e.g Tempzone Floor Heating has Heating Elements as a product category
then items belonging to Tempzone Floor Heating must also be heating elements to
inherit its features



1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
# File 'app/models/item.rb', line 1909

def inherited_features
  features_result = []
  if (ppl = primary_product_line) && (pc_url = product_category_url)
    # Only take product lines which have features defined directly
    ppl.self_and_ancestors.select(&:has_features?).each do |pl|
      # If the product line is 'loose' we can use its features
      # The product category of the item must match
      if pl.product_categories.empty? || pl.product_categories.any? { |pc| pc_url.starts_with?(pc.url) }
        features_result = pl.features
        break
      end
    end
  end
  # If we get here there's nothing to inherit
  features_result.filter_map(&:presence)
end

#is_accessory?Object

Alias for Product_category#is_accessory?

Returns:

  • (Object)

    Product_category#is_accessory?

See Also:



640
641
642
643
644
645
646
647
648
649
# File 'app/models/item.rb', line 640

delegate :is_control?,
:is_upgrade?,
:is_accessory?,
:is_relay_panel?,
:is_custom_mat?,
:effective_nmfc_code,
:effective_nmfc_class,
:is_publication?,
to: :product_category,
allow_nil: true

#is_available_to_public?Boolean

Returns:

  • (Boolean)


798
799
800
801
# File 'app/models/item.rb', line 798

def is_available_to_public?
  logger.debug "Checking if #{id} / #{sku} is available to public"
  orderable_view_product_catalogs.present?
end

#is_cable_accessory?Boolean

Returns:

  • (Boolean)


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

def is_cable_accessory?
  product_category&.is_accessory? &&
    pl_descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_TEMPZONE_THIN_CABLE)
end

#is_cable_fit_guide?Boolean

Returns:

  • (Boolean)


1370
1371
1372
1373
1374
# File 'app/models/item.rb', line 1370

def is_cable_fit_guide?
  product_category&.is_accessory? &&
    pl_descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_TEMPZONE_THIN_CABLE) &&
    name.to_s.downcase.include?('cable fixing plastic strip')
end

#is_cable_heating_element?Boolean

Returns:

  • (Boolean)


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

def is_cable_heating_element?
  pc_descendant_of_path?(LtreePaths::PC_HEATING_ELEMENTS_HEATING_CABLES)
end

#is_cable_tape?Boolean

Returns:

  • (Boolean)


1376
1377
1378
1379
1380
# File 'app/models/item.rb', line 1376

def is_cable_tape?
  product_category&.is_accessory? &&
    pl_descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_TEMPZONE_THIN_CABLE) &&
    name.to_s.downcase.include?('tape')
end

#is_cerazorb?Boolean

Returns:

  • (Boolean)


1325
1326
1327
# File 'app/models/item.rb', line 1325

def is_cerazorb?
  primary_product_line&.is_cerazorb? && product_category&.is_insulation?
end

#is_circuit_check?Boolean

Returns:

  • (Boolean)


1317
1318
1319
# File 'app/models/item.rb', line 1317

def is_circuit_check?
  sku == ItemConstants::CIRCUIT_CHECK_SKU
end

#is_cold_lead?Boolean

Returns:

  • (Boolean)


1131
1132
1133
# File 'app/models/item.rb', line 1131

def is_cold_lead?
  pc_descendant_of_path?(LtreePaths::PC_SPARE_PARTS_COLD_LEADS)
end

#is_control?Object

Alias for Product_category#is_control?

Returns:

  • (Object)

    Product_category#is_control?

See Also:



640
641
642
643
644
645
646
647
648
649
# File 'app/models/item.rb', line 640

delegate :is_control?,
:is_upgrade?,
:is_accessory?,
:is_relay_panel?,
:is_custom_mat?,
:effective_nmfc_code,
:effective_nmfc_class,
:is_publication?,
to: :product_category,
allow_nil: true

#is_cork?Boolean

Returns:

  • (Boolean)


1321
1322
1323
# File 'app/models/item.rb', line 1321

def is_cork?
  primary_product_line&.is_cork? && product_category&.is_insulation?
end

#is_custom_mat?Object

Alias for Product_category#is_custom_mat?

Returns:

  • (Object)

    Product_category#is_custom_mat?

See Also:



640
641
642
643
644
645
646
647
648
649
# File 'app/models/item.rb', line 640

delegate :is_control?,
:is_upgrade?,
:is_accessory?,
:is_relay_panel?,
:is_custom_mat?,
:effective_nmfc_code,
:effective_nmfc_class,
:is_publication?,
to: :product_category,
allow_nil: true

#is_discontinued_or_publicationObject

Helper for validation: skips weight checks for discontinued items and publications



695
696
697
# File 'app/models/item.rb', line 695

def is_discontinued_or_publication
  is_discontinued? || is_publication?
end

#is_economy_snow_melt_control?Boolean

Returns:

  • (Boolean)


1341
1342
1343
# File 'app/models/item.rb', line 1341

def is_economy_snow_melt_control?
  sku == ItemConstants::ECONOMY_SNOW_MELT_CONTROL_SKU
end

#is_floor_heating_product?Boolean

Returns:

  • (Boolean)


1127
1128
1129
# File 'app/models/item.rb', line 1127

def is_floor_heating_product?
  is_heating_element? && pl_descendant_of_path?(LtreePaths::PL_FLOOR_HEATING)
end

#is_goods?Boolean

Returns:

  • (Boolean)


1259
1260
1261
# File 'app/models/item.rb', line 1259

def is_goods?
  tax_class == 'g'
end

#is_heating_element?Boolean

Returns:

  • (Boolean)


1301
1302
1303
# File 'app/models/item.rb', line 1301

def is_heating_element?
  pc_descendant_of_path?(LtreePaths::PC_HEATING_ELEMENTS)
end

#is_install_kit?Boolean

Returns:

  • (Boolean)


1143
1144
1145
# File 'app/models/item.rb', line 1143

def is_install_kit?
  sku&.start_with?(ItemConstants::INSTALL_KIT_SKU_PREFIX)
end

#is_junction_box?Boolean

Returns:

  • (Boolean)


1345
1346
1347
# File 'app/models/item.rb', line 1345

def is_junction_box?
  ItemConstants::JUNCTION_BOX_SKUS.include?(sku)
end

#is_led_mirror?Boolean

Returns:

  • (Boolean)


1165
1166
1167
# File 'app/models/item.rb', line 1165

def is_led_mirror?
  is_mirror? && pl_descendant_of_path?(LtreePaths::PL_LED_MIRROR)
end

#is_legacy_relay?Boolean

Returns:

  • (Boolean)


1139
1140
1141
# File 'app/models/item.rb', line 1139

def is_legacy_relay?
  sku&.start_with?(ItemConstants::LEGACY_RELAY_SKU_PREFIX)
end

#is_made_to_order_led_mirror?Boolean

Returns:

  • (Boolean)


1395
1396
1397
1398
1399
# File 'app/models/item.rb', line 1395

def is_made_to_order_led_mirror?
  # here we only care if it shipped by Lumidesign, because some smaller component will just be added to the LED crate
  # rather than take up it's own pallet
  primary_product_line&.is_made_to_order_led_mirror? # && product_category&.is_mirror?
end

#is_membrane?Boolean

Returns:

  • (Boolean)


1387
1388
1389
# File 'app/models/item.rb', line 1387

def is_membrane?
  Item.sku_is_membrane?(sku)
end

#is_mirror?Boolean

Returns:

  • (Boolean)


1161
1162
1163
# File 'app/models/item.rb', line 1161

def is_mirror?
  pc_descendant_of_path?(LtreePaths::PC_MIRRORS)
end

#is_mirror_defogger?Boolean

Returns:

  • (Boolean)


1193
1194
1195
# File 'app/models/item.rb', line 1193

def is_mirror_defogger?
  pc_descendant_of_path?(LtreePaths::PC_MIRROR_DEFOGGERS)
end

#is_oj_programmable_tstat?Boolean

Returns:

  • (Boolean)


1279
1280
1281
# File 'app/models/item.rb', line 1279

def is_oj_programmable_tstat?
  is_thermostat? && LtreePaths::PL_OJ_PROGRAMMABLE_TSTAT_LINES.any? { |path| pl_descendant_of_path?(path) }
end

#is_oj_programmable_wifi_tstat?Boolean

Returns:

  • (Boolean)


1283
1284
1285
# File 'app/models/item.rb', line 1283

def is_oj_programmable_wifi_tstat?
  ItemConstants::OJ_PROGRAMMABLE_WIFI_TSTAT_SKU_PREFIXES.any? { |prefix| sku&.start_with?(prefix) }
end

#is_pipe_freeze_protection?Boolean

Returns:

  • (Boolean)


1227
1228
1229
# File 'app/models/item.rb', line 1227

def is_pipe_freeze_protection?
  is_heating_element? && pl_descendant_of_path?(LtreePaths::PL_PIPE_FREEZE_PROTECTION)
end

#is_power?Boolean

Returns:

  • (Boolean)


1255
1256
1257
# File 'app/models/item.rb', line 1255

def is_power?
  pc_descendant_of_path?(LtreePaths::PC_POWER)
end

#is_power_module?Boolean

Returns:

  • (Boolean)


1135
1136
1137
# File 'app/models/item.rb', line 1135

def is_power_module?
  ItemConstants::POWER_MODULE_SKU_PREFIXES.any? { |prefix| sku&.include?(prefix) }
end

#is_publication?Object

Alias for Product_category#is_publication?

Returns:

  • (Object)

    Product_category#is_publication?

See Also:



640
641
642
643
644
645
646
647
648
649
# File 'app/models/item.rb', line 640

delegate :is_control?,
:is_upgrade?,
:is_accessory?,
:is_relay_panel?,
:is_custom_mat?,
:effective_nmfc_code,
:effective_nmfc_class,
:is_publication?,
to: :product_category,
allow_nil: true

#is_radiant_panel?Boolean

Returns:

  • (Boolean)


1169
1170
1171
# File 'app/models/item.rb', line 1169

def is_radiant_panel?
  pc_descendant_of_path?(LtreePaths::PC_RADIANT_PANELS)
end

#is_radiant_panel_dual_connect?Boolean

Returns:

  • (Boolean)


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

def is_radiant_panel_dual_connect?
  is_radiant_panel? && spec_value(:connection_method) == 'Plug-in or Hardwired'
end

#is_radiant_panel_hardwired?Boolean

Returns:

  • (Boolean)


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

def is_radiant_panel_hardwired?
  pc_descendant_of_path?(LtreePaths::PC_RADIANT_PANELS_HARDWIRED)
end

#is_radiant_panel_hardwired_control?Boolean

Returns:

  • (Boolean)


1189
1190
1191
# File 'app/models/item.rb', line 1189

def is_radiant_panel_hardwired_control?
  sku.in?(ItemConstants::RADIANT_PANEL_HARDWIRE_CONTROL_SKUS)
end

#is_radiant_panel_plug_in?Boolean

Returns:

  • (Boolean)


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

def is_radiant_panel_plug_in?
  pc_descendant_of_path?(LtreePaths::PC_RADIANT_PANELS_PLUG_IN)
end

#is_radiant_panel_plug_in_control?Boolean

Returns:

  • (Boolean)


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

def is_radiant_panel_plug_in_control?
  sku.in?(ItemConstants::RADIANT_PANEL_PLUG_IN_CONTROL_SKUS)
end

#is_radiator?Boolean

Returns:

  • (Boolean)


767
768
769
# File 'app/models/item.rb', line 767

def is_radiator?
  is_heating_element? || is_towel_warmer? || is_radiant_panel? || is_mirror_defogger?
end

#is_refurbished?Boolean

Returns:

  • (Boolean)


1709
1710
1711
# File 'app/models/item.rb', line 1709

def is_refurbished?
  condition_refurbished?
end

#is_relay_panel?Object

Alias for Product_category#is_relay_panel?

Returns:

  • (Object)

    Product_category#is_relay_panel?

See Also:



640
641
642
643
644
645
646
647
648
649
# File 'app/models/item.rb', line 640

delegate :is_control?,
:is_upgrade?,
:is_accessory?,
:is_relay_panel?,
:is_custom_mat?,
:effective_nmfc_code,
:effective_nmfc_class,
:is_publication?,
to: :product_category,
allow_nil: true

#is_reviewable?Boolean

Returns:

  • (Boolean)


1431
1432
1433
# File 'app/models/item.rb', line 1431

def is_reviewable?
  Item.reviewable.where(id:).present?
end

#is_roughin_kit?Boolean

Returns:

  • (Boolean)


1391
1392
1393
# File 'app/models/item.rb', line 1391

def is_roughin_kit?
  sku&.include?(ItemConstants::ROUGHIN_KIT_SKU_PATTERN)
end

#is_self_regulating_cable?Boolean

Returns:

  • (Boolean)


1155
1156
1157
1158
1159
# File 'app/models/item.rb', line 1155

def is_self_regulating_cable?
  pc_descendant_of_path?(LtreePaths::PC_HEATING_ELEMENTS_HEATING_CABLES) &&
    (pl_descendant_of_path?(LtreePaths::PL_PIPE_FREEZE_PROTECTION_SELF_REG) ||
     pl_descendant_of_path?(LtreePaths::PL_ROOF_GUTTER_DEICING_SELF_REG))
end

#is_service?Boolean

Returns:

  • (Boolean)


1267
1268
1269
# File 'app/models/item.rb', line 1267

def is_service?
  tax_class == 'svc'
end

#is_sh_cable?Boolean

Returns:

  • (Boolean)


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

def is_sh_cable?
  is_heating_element? && pl_descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_SLAB_HEAT_CABLE)
end

#is_shipping?Boolean

Returns:

  • (Boolean)


1271
1272
1273
# File 'app/models/item.rb', line 1271

def is_shipping?
  tax_class == 'shp'
end

#is_slab_heat_mat?Boolean

Returns:

  • (Boolean)


1309
1310
1311
# File 'app/models/item.rb', line 1309

def is_slab_heat_mat?
  is_heating_element? && pl_descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_SLAB_HEAT_MAT)
end

#is_sm_cable?Boolean

Returns:

  • (Boolean)


1362
1363
1364
# File 'app/models/item.rb', line 1362

def is_sm_cable?
  is_heating_element? && pl_descendant_of_path?(LtreePaths::PL_SNOW_MELTING_CABLE)
end

#is_smartstat?Boolean

Returns:

  • (Boolean)


1275
1276
1277
# File 'app/models/item.rb', line 1275

def is_smartstat?
  ItemConstants::IID_SMARTSTATS.include?(id)
end

#is_snow_melt_plaque?Boolean

Returns:

  • (Boolean)


1337
1338
1339
# File 'app/models/item.rb', line 1337

def is_snow_melt_plaque?
  sku == ItemConstants::SNOW_MELT_PLAQUE_SKU
end

#is_snow_melting_control?Boolean

Returns:

  • (Boolean)


1151
1152
1153
# File 'app/models/item.rb', line 1151

def is_snow_melting_control?
  pl_descendant_of_path?(LtreePaths::PL_SNOW_MELTING_CONTROL)
end

#is_snow_melting_mat?Boolean

Returns:

  • (Boolean)


1313
1314
1315
# File 'app/models/item.rb', line 1313

def is_snow_melting_mat?
  is_heating_element? && pl_descendant_of_path?(LtreePaths::PL_SNOW_MELTING_MAT)
end

#is_snow_melting_product?Boolean

Returns:

  • (Boolean)


1147
1148
1149
# File 'app/models/item.rb', line 1147

def is_snow_melting_product?
  pl_descendant_of_path?(LtreePaths::PL_SNOW_MELTING)
end

#is_spare_parts?Boolean

Returns:

  • (Boolean)


1263
1264
1265
# File 'app/models/item.rb', line 1263

def is_spare_parts?
  pc_descendant_of_path?(LtreePaths::PC_SPARE_PARTS)
end

#is_thermalsheet?Boolean

Returns:

  • (Boolean)


1329
1330
1331
# File 'app/models/item.rb', line 1329

def is_thermalsheet?
  primary_product_line&.is_thermalsheet? && product_category&.is_insulation?
end

#is_thermostat?Boolean

Returns:

  • (Boolean)


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

def is_thermostat?
  pc_descendant_of_path?(LtreePaths::PC_CONTROLS_THERMOSTATS)
end

#is_towel_warmer?Boolean

Returns:

  • (Boolean)


1219
1220
1221
# File 'app/models/item.rb', line 1219

def is_towel_warmer?
  pc_descendant_of_path?(LtreePaths::PC_TOWEL_WARMERS)
end

#is_towel_warmer_dual_connect?Boolean

Returns:

  • (Boolean)


1231
1232
1233
1234
1235
1236
1237
# File 'app/models/item.rb', line 1231

def is_towel_warmer_dual_connect?
  pc_descendant_of_path?(LtreePaths::PC_TOWEL_WARMERS_DUAL_CONNECTION) ||
    (
      pc_descendant_of_path?(LtreePaths::PC_TOWEL_WARMERS) &&
      spec_value(:connection_method) == 'Plug-in or Hardwired'
    )
end

#is_towel_warmer_hardwired?Boolean

Returns:

  • (Boolean)


1223
1224
1225
# File 'app/models/item.rb', line 1223

def is_towel_warmer_hardwired?
  pc_descendant_of_path?(LtreePaths::PC_TOWEL_WARMERS_HARDWIRED)
end

#is_towel_warmer_hardwired_control?Boolean

Returns:

  • (Boolean)


1247
1248
1249
# File 'app/models/item.rb', line 1247

def is_towel_warmer_hardwired_control?
  sku.in?(ItemConstants::TOWEL_WARMER_HARDWIRE_CONTROL_SKUS)
end

#is_towel_warmer_plug_in?Boolean

Returns:

  • (Boolean)


1239
1240
1241
# File 'app/models/item.rb', line 1239

def is_towel_warmer_plug_in?
  pc_descendant_of_path?(LtreePaths::PC_TOWEL_WARMERS_PLUG_IN)
end

#is_towel_warmer_plug_in_control?Boolean

Returns:

  • (Boolean)


1243
1244
1245
# File 'app/models/item.rb', line 1243

def is_towel_warmer_plug_in_control?
  sku.in?(ItemConstants::TOWEL_WARMER_PLUG_IN_CONTROL_SKUS)
end

#is_tz_cable?Boolean

Returns:

  • (Boolean)


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

def is_tz_cable?
  is_heating_element? && (pl_descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_TEMPZONE_CABLE) ||
                          pl_descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_TEMPZONE_RULER_CABLE))
end

#is_tz_ruler_cable?Boolean

Returns:

  • (Boolean)


1358
1359
1360
# File 'app/models/item.rb', line 1358

def is_tz_ruler_cable?
  is_heating_element? && pl_descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_TEMPZONE_RULER_CABLE)
end

#is_tz_thin_cable?Boolean

Returns:

  • (Boolean)


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

def is_tz_thin_cable?
  is_heating_element? && pl_descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_TEMPZONE_THIN_CABLE)
end

#is_underlayment?Boolean

Returns:

  • (Boolean)


1333
1334
1335
# File 'app/models/item.rb', line 1333

def is_underlayment?
  primary_product_line&.is_underlayment? && product_category&.is_insulation?
end

#is_upgrade?Object

Alias for Product_category#is_upgrade?

Returns:

  • (Object)

    Product_category#is_upgrade?

See Also:



640
641
642
643
644
645
646
647
648
649
# File 'app/models/item.rb', line 640

delegate :is_control?,
:is_upgrade?,
:is_accessory?,
:is_relay_panel?,
:is_custom_mat?,
:effective_nmfc_code,
:effective_nmfc_class,
:is_publication?,
to: :product_category,
allow_nil: true

#item_available_localesObject



812
813
814
# File 'app/models/item.rb', line 812

def item_available_locales
  LocaleUtility::SITE_LOCALES.select { |l| orderable_online_in_locale?(l) }
end

#item_group_name(context: :sales) ⇒ Object



2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
# File 'app/models/item.rb', line 2190

def item_group_name(context: :sales)
  pl_slug = case context
            when :sales
              sales_product_line_slug
            when :support
              support_product_line_slug
            else
              raise ArgumentError, "#{context} not supported, :sales or :support expected"
            end

  return if pl_slug.blank?
  return if (pc_slug = product_category_slug).blank?

  grouping_elements = [pl_slug.tr('.', '-').tr('_', '-').split('-'), pc_slug.split('-')].flatten
  grouping_elements.delete('goods')
  grouping_elements = grouping_elements.map(&:singularize)
  # Compress name a bit
  grouping_elements = grouping_elements.each_with_index.map { |el, i| i < 2 ? el.first(2) : el }
  grouping_elements = grouping_elements.compact.uniq
  gn = grouping_elements.join('-').parameterize
  if gn.size > 50
    "#{gn.first(15)}-#{Digest::MD5.hexdigest(gn)}"
  else
    gn
  end
end

#item_grouping_info(include_self: false, facet_filters: {}, context: nil) ⇒ Object



2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
# File 'app/models/item.rb', line 2217

def item_grouping_info(include_self: false, facet_filters: {}, context: nil)
  return if product_category.skip_siblings

  context ||= :sales
  # Use ltree paths for efficient queries (no DB lookup for path resolution)
  use_pl_path = case context
                when :sales
                  sales_product_line_path
                when :support
                  support_product_line_path
                else
                  raise ArgumentError, "#{context} not supported, :sales or :support expected"
                end

  use_pc_path = product_category_path

  if use_pl_path.present?
    # Use ltree path-based scopes (no additional DB lookups)
    variants = Item.active.condition_new.by_product_category_path(use_pc_path).by_product_line_path(use_pl_path).order(:sku)
    # eliminate customs
    variants = variants.joins(:product_category).where.not(ProductCategory[:custom_product].eq(true))
    group_name = item_group_name(context:)
    item_group_id = Digest::MD5.hexdigest(group_name)

    facet_filters.each do |facet, value|
      variants = variants.with_product_specification(facet, value)
    end
  else
    variants = Item.none
  end
  variants = variants.where.not(id: id.to_i) unless include_self
  # Keep legacy field names for backward compatibility but store paths
  result_class = Data.define(:pc_url, :pl_url, :item_group_id, :item_group_name, :variants) do
    def initialize(pc_url: nil, pl_url: nil, item_group_id: nil, item_group_name: nil, variants: nil) = super
  end
  result_class.new(pc_url: use_pc_path, pl_url: use_pl_path, item_group_id:, item_group_name:, variants:)
end

#item_product_linesActiveRecord::Relation<ItemProductLine>

Returns:

See Also:



271
# File 'app/models/item.rb', line 271

has_many :item_product_lines, -> { order(:position) }, inverse_of: :item, dependent: :destroy

#item_relationsActiveRecord::Relation<PublicationItem>

Returns:

See Also:



281
# File 'app/models/item.rb', line 281

has_many :item_relations, class_name: 'PublicationItem', foreign_key: :publication_id, inverse_of: :publication, dependent: :destroy

#item_stateObject



788
789
790
791
792
793
794
795
796
# File 'app/models/item.rb', line 788

def item_state
  if is_discontinued
    'Discontinued'
  elsif is_available_to_public?
    'Published'
  else
    'Private'
  end
end

#line_itemsActiveRecord::Relation<LineItem>

Returns:

See Also:



272
# File 'app/models/item.rb', line 272

has_many :line_items

#literatureLiterature

Returns:

See Also:



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

belongs_to :literature, optional: true

#loaded_rendered_product_specificationsObject



2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
# File 'app/models/item.rb', line 2046

def loaded_rendered_product_specifications
  rp_specs = rendered_product_specifications || {}
  # Collect all product specification ids
  ps_ids = rp_specs.values.pluck('product_specification_id').uniq.compact
  # Preload the product specifications
  product_specifications = ProductSpecification.includes(:items, :product_line, :product_category).where(id: ps_ids).index_by(&:id)
  rp_specs.each_value do |v|
    v['product_specification'] = product_specifications[v['product_specification_id']]
  end
  rp_specs
end

#make_upc_codeObject



1505
1506
1507
# File 'app/models/item.rb', line 1505

def make_upc_code
  self.upc = Item::UpcMaker.new.process if upc.blank?
end

#missing_catalogsObject



2406
2407
2408
# File 'app/models/item.rb', line 2406

def missing_catalogs
  Catalog.where.not(id: catalog_items.pluck(:catalog_id)).where(store_id: store_items.pluck(:store_id)).where.not(is_discontinued: true).includes(:store).order(Catalog[:id].in([1, 2]).desc).order(Catalog[:name])
end

#most_recent_revision(public_only = true) ⇒ Object



1674
1675
1676
1677
1678
# File 'app/models/item.rb', line 1674

def most_recent_revision(public_only = true)
  items = Item.publications.where('sku ILIKE ?', "#{canonical_sku}%").where('length(sku) <= ?', canonical_sku.size + 2)
  items = items.available_to_public if public_only
  items.order(sku: :desc).first
end

#new_itemItem

Returns:

See Also:



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

belongs_to :new_item, class_name: 'Item', optional: true

#new_revision_skuObject



1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
# File 'app/models/item.rb', line 1689

def new_revision_sku
  prev_revs = Item.where('sku ILIKE ?', "#{canonical_sku}%").order(sku: :desc)
  sku_match = prev_revs.first.sku.match(/(.*)-([[:alpha:]])$/)
  if sku_match && (sku_match.length == 3)
    # Get the revision code
    rev_code = sku_match[2]
    # Add one
    new_rev = rev_code.succ
    "#{canonical_sku}-#{new_rev}"
  elsif canonical_sku.present?
    "#{canonical_sku}-A"
  else
    raise "Last revision #{last_rev.id} #{last_rev.sku} could not be parsed for revision (-a, -b, etc.)"
  end
end

#non_refurbished_skuObject



1713
1714
1715
# File 'app/models/item.rb', line 1713

def non_refurbished_sku
  sku.delete_suffix('-BTK')
end

#order_countObject



1729
1730
1731
# File 'app/models/item.rb', line 1729

def order_count
  Order.contains_item_ids([id]).count
end

#orderable_online?Boolean

Returns:

  • (Boolean)


943
944
945
946
947
# File 'app/models/item.rb', line 943

def orderable_online?
  Item.orderable_online.exists?(id:) &&
    store_items.active.available.present? &&
    catalog_items.active.present?
end

#orderable_online_in_locale?(locale = I18n.locale) ⇒ Boolean

Returns:

  • (Boolean)


804
805
806
807
808
809
810
# File 'app/models/item.rb', line 804

def orderable_online_in_locale?(locale = I18n.locale)
  # leverage preloaded reloation if available
  scoped_records = orderable_view_product_catalogs if orderable_view_product_catalogs.loaded?
  # If not loaded, then retrieve only what we need
  scoped_records ||= orderable_view_product_catalogs.select(:catalog_id)
  scoped_records.any? { |vpc| vpc.catalog_id == Catalog.locale_to_catalog_id(locale) }
end

#orderable_view_product_catalogsActiveRecord::Relation<ViewProductCatalog>

Returns:

See Also:



267
268
269
270
# File 'app/models/item.rb', line 267

has_many :orderable_view_product_catalogs,
-> { visible_to_public },
foreign_key: :item_id,
class_name: 'ViewProductCatalog'

#packaging_discrepancy_packingPacking

Returns:

See Also:



258
# File 'app/models/item.rb', line 258

belongs_to :packaging_discrepancy_packing, class_name: 'Packing', optional: true

#packingsActiveRecord::Relation<Packing>

Returns:

  • (ActiveRecord::Relation<Packing>)

See Also:



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

has_and_belongs_to_many :packings, inverse_of: :items

#past_model?Boolean

Returns:

  • (Boolean)


1097
1098
1099
# File 'app/models/item.rb', line 1097

def past_model?
  (tags || []).include?('past-model') || is_discontinued? || primary_product_line&.past_model?
end

#pc_descendant_of_path?(path_slug) ⇒ Boolean

Check if item's product category is or descends from the given path
Uses pc_path_slugs stored directly on item (no DB query)
Example: item.pc_descendant_of_path?(LtreePaths::PC_TOWEL_WARMERS)

Returns:

  • (Boolean)


1211
1212
1213
1214
1215
1216
1217
# File 'app/models/item.rb', line 1211

def pc_descendant_of_path?(path_slug)
  return false if pc_path_slugs.blank? || path_slug.blank?

  slugs = pc_path_slugs.to_s
  path = path_slug.to_s
  slugs == path || slugs.start_with?("#{path}.")
end

#pl_descendant_of_path?(path_slug) ⇒ Boolean

Check if item's primary product line is or descends from the given path
Uses ltree path stored directly on item (no DB query)
Example: item.pl_descendant_of_path?(LtreePaths::PL_FLOOR_HEATING)

Returns:

  • (Boolean)


1200
1201
1202
1203
1204
1205
1206
# File 'app/models/item.rb', line 1200

def pl_descendant_of_path?(path_slug)
  return false if primary_pl_path_slugs.blank? || path_slug.blank?

  slugs = primary_pl_path_slugs.to_s
  path = path_slug.to_s
  slugs == path || slugs.start_with?("#{path}.")
end

#plan_sensitive?Boolean

Plan sensitive items are those tied up to the geometry of an installation plan

Returns:

  • (Boolean)


1093
1094
1095
# File 'app/models/item.rb', line 1093

def plan_sensitive?
  is_heating_element?
end

#preferred_supplierSupplier

Returns:

See Also:



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

belongs_to :preferred_supplier, class_name: 'Supplier', optional: true

#primary_imageImage

Returns:

See Also:



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

belongs_to :primary_image, class_name: 'Image', optional: true

#primary_image_url(options = {}) ⇒ Object

Returns the primary image URL for display (e.g., cart toast).
Uses WYS_MAIN profile first, falls back to primary_image association.



1121
1122
1123
1124
1125
# File 'app/models/item.rb', line 1121

def primary_image_url(options = {})
  wys_main = image_profiles.find { |p| p.image_type == 'WYS_MAIN' }
  image = wys_main&.image || primary_image
  image&.image_url(options)
end

#primary_product_lineProductLine



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

belongs_to :primary_product_line, class_name: 'ProductLine', inverse_of: :primary_items, optional: true

#primary_product_line_slug_ltreeObject



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

def primary_product_line_slug_ltree
  primary_product_line&.slug_ltree&.to_s
end

#product_categoryProductCategory



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

belongs_to :product_category

#product_category_ancestry_idsObject



1721
1722
1723
# File 'app/models/item.rb', line 1721

def product_category_ancestry_ids
  ProductCategory.self_and_ancestors_ids(product_category_id)
end

#product_category_nameObject



885
886
887
# File 'app/models/item.rb', line 885

def product_category_name
  ProductCategoryConstants.name_for(product_category_id)
end

#product_category_pathObject

Returns ltree path for product category grouping (preferred - no DB lookup needed)



2154
2155
2156
2157
2158
2159
2160
2161
2162
# File 'app/models/item.rb', line 2154

def product_category_path
  pc_path = pc_path_slugs.to_s
  # Normalize to parent category for grouping purposes
  return LtreePaths::PC_RADIANT_PANELS if pc_path.starts_with?('goods.radiant_panels')
  return LtreePaths::PC_TOWEL_WARMERS if pc_path.starts_with?('goods.towel_warmers') && !pc_path.starts_with?('goods.towel_warmers.dual_connection')
  return 'goods.towel_warmers.dual_connection' if pc_path.starts_with?('goods.towel_warmers.dual_connection')

  pc_path
end

#product_category_priorityObject



889
890
891
# File 'app/models/item.rb', line 889

def product_category_priority
  ProductCategoryConstants.priority_for(product_category_id)
end

#product_category_slugObject

Returns product category URL for grouping (legacy - use product_category_path for new code)



2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
# File 'app/models/item.rb', line 2142

def product_category_slug
  use_pc_url = product_category_url
  use_pc_url = 'goods-radiant-panels' if use_pc_url.starts_with?('goods-radiant-panels')
  if use_pc_url.starts_with?('goods-towel-warmers-dual-connection')
    use_pc_url = 'goods-towel-warmers-dual-connection'
  elsif use_pc_url.starts_with?('goods-towel-warmers')
    use_pc_url = 'goods-towel-warmers'
  end
  use_pc_url
end

#product_category_urlObject



1296
1297
1298
# File 'app/models/item.rb', line 1296

def product_category_url
  product_category&.url
end

#product_line_ancestry_idsObject



1717
1718
1719
# File 'app/models/item.rb', line 1717

def product_line_ancestry_ids
  ProductLine.self_and_ancestors_ids(product_line_ids)
end

#product_line_for_heating_systemObject



1447
1448
1449
1450
1451
1452
1453
# File 'app/models/item.rb', line 1447

def product_line_for_heating_system
  (begin
    primary_product_line.get_first_heating_system_type
  rescue StandardError
    nil
  end)
end

#product_line_for_public_sales_portalObject



1439
1440
1441
# File 'app/models/item.rb', line 1439

def product_line_for_public_sales_portal
  primary_product_line&.get_first_public_for_sales
end

#product_line_for_reviewObject



1435
1436
1437
# File 'app/models/item.rb', line 1435

def product_line_for_review
  primary_product_line&.get_first_reviewable
end

#product_line_ids_constellationObject

This pulls all product line id including inherited for this item, for the root system,
and complimentary_product_line_ids



779
780
781
782
783
784
785
786
# File 'app/models/item.rb', line 779

def product_line_ids_constellation
  pl_ids = []
  pl_ids += product_line_ids || [] # This will implicitly include the root system which is always an ancestor
  pl_ids += root_system_product_line&.complimentary_product_line_ids || [] # The complimentaries of the root system
  pl_ids += primary_product_line&.complimentary_product_line_ids || [] # The complimentaries of only this product line
  pl_ids = pl_ids.compact.uniq
  ProductLine.self_and_ancestors_ids(pl_ids).compact.uniq
end

#product_linesActiveRecord::Relation<ProductLine>

Returns:

See Also:



273
# File 'app/models/item.rb', line 273

has_many :product_lines, through: :item_product_lines

#product_specifications(filters = {}) ⇒ Object

Retrieves all product specifications available to a given item, this does not
retrieve rendered specs but the specs definition used to build them



1786
1787
1788
1789
1790
# File 'app/models/item.rb', line 1786

def product_specifications(filters = {})
  filters[:propagation] ||= %w[0 1] # Unrestricted or Item only
  res = Item::SpecificationsRetriever.new.process(self, filters)
  res.specifications
end

#product_tax_codeProductTaxCode



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

belongs_to :product_tax_code, inverse_of: :items, optional: true

#prune_empty_specsObject



2112
2113
2114
2115
2116
2117
# File 'app/models/item.rb', line 2112

def prune_empty_specs
  ds = direct_product_specifications.empty_non_template_specs
  ds_count = ds.size
  ds.destroy_all
  ds_count
end

#prune_unused_translationsObject



2446
2447
2448
2449
2450
2451
# File 'app/models/item.rb', line 2446

def prune_unused_translations
  valid_locales = content_locales_to_render
  translations.each do |(locale, _translation_attribute)|
    translations.delete(locale) unless locale.to_sym.in?(valid_locales)
  end
end

#public_description_htmlObject



1765
1766
1767
1768
1769
# File 'app/models/item.rb', line 1765

def public_description_html
  # rubocop:disable Rails/OutputSafety -- Content is trusted, generated from Liquid templates
  (rendered_detailed_description_html.presence || primary_product_line&.description_html.presence)&.html_safe
  # rubocop:enable Rails/OutputSafety
end

#public_description_textObject



1771
1772
1773
# File 'app/models/item.rb', line 1771

def public_description_text
  detailed_description.presence || primary_product_line&.description.presence
end

#public_file_nameObject



897
898
899
# File 'app/models/item.rb', line 897

def public_file_name
  "#{public_name}.#{Mime::Type.file_extension_of(literature.mime_type) || 'pdf'}"
end

#public_nameObject



893
894
895
# File 'app/models/item.rb', line 893

def public_name
  public_short_name.presence || name
end

#public_specificationsObject

Only specifications intended for the public are retrieved



1883
1884
1885
# File 'app/models/item.rb', line 1883

def public_specifications
  specifications.select { |s| s.output.present? && s.grouping != 'Features' && s.grouping != 'Highlights' && s.visibility == 'open_visibility' }
end

#publication_relationsActiveRecord::Relation<PublicationItem>

Returns:

See Also:



280
# File 'app/models/item.rb', line 280

has_many :publication_relations, class_name: 'PublicationItem', inverse_of: :item, dependent: :destroy

#purchase_order_itemsActiveRecord::Relation<PurchaseOrderItem>

Returns:

See Also:



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

has_many :purchase_order_items

#purge_edge_cache(include_product_line: false) ⇒ Object



1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
# File 'app/models/item.rb', line 1955

def purge_edge_cache(include_product_line: false)
  # This doesn't exist unless staging or production
  return :disabled unless Cache::EdgeCacheUtility.edge_cache_enabled?

  res = []
  urls = all_site_maps.map(&:url)
  if urls.present?
    Rails.logger.info "Purge Edge Cache called for item id #{id}"
    jid = EdgeCacheWorker.perform_async('urls' => urls)
    res += urls.map { |url| { url:, jid: } }
  end
  if include_product_line && (pl = primary_product_line)
    res += pl.purge_edge_cache
  end
  res
end

#quantities_indicator_visible?Boolean

Returns:

  • (Boolean)


1455
1456
1457
# File 'app/models/item.rb', line 1455

def quantities_indicator_visible?
  display_treatment.nil? || display_treatment != Item::DISPLAY_TREATMENT_HIDE_QUANTITIES
end

#refresh_google_feedObject



2025
2026
2027
2028
2029
2030
2031
# File 'app/models/item.rb', line 2025

def refresh_google_feed
  # Filtering will happen downstream for what is google feed or not
  return if (cids = catalog_items.for_google_feed.pluck(:id)).blank?

  GoogleFeedGeneratorWorker.perform_async('catalog_item_ids' => catalog_items.for_google_feed.pluck(:id))
  cids
end

#refurbished_versionItem

Returns:

See Also:



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

has_one :refurbished_version, class_name: 'Item', foreign_key: :new_item_id

Returns:

  • (ActiveRecord::Relation<RelatedSourceItem>)

See Also:



287
# File 'app/models/item.rb', line 287

has_many :related_source_items, through: :source_item_relations, source: :source_item

Returns:

  • (ActiveRecord::Relation<RelatedTargetItem>)

See Also:



288
# File 'app/models/item.rb', line 288

has_many :related_target_items, through: :target_item_relations, source: :target_item

Returns:

  • (ActiveRecord::Relation<RelatedTargetItemsForSpec>)

See Also:



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

has_many :related_target_items_for_specs, through: :target_item_relations_for_specs, source: :target_item

#relativesObject

Find all direct siblings and relatives items descendending the product category and product line tree



2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
# File 'app/models/item.rb', line 2124

def relatives
  return Item.none unless persisted? && (product_category_id || primary_product_line_id)

  siblings = Item.all
  siblings = siblings.by_product_category_id(product_category_id) if product_category
  if primary_pl_path_ids.present?
    siblings = siblings.where(Item[:primary_pl_path_ids].ltree_descendant(primary_pl_path_ids))
  end
  siblings = siblings.where.not(id:) if id
  siblings.limit(100).order(:sku)
end

#render_product_specifications(tokens: nil) ⇒ Object



1927
1928
1929
1930
1931
1932
1933
1934
# File 'app/models/item.rb', line 1927

def render_product_specifications(tokens: nil)
  res = Item::SpecificationsMasher.new.process(item: self, tokens:).specifications
  if tokens.present? # We will merge the results
    (rendered_product_specifications || {}).merge(res)
  else
    res
  end
end

#rendered_detailed_description_htmlObject



1761
1762
1763
# File 'app/models/item.rb', line 1761

def rendered_detailed_description_html
  @rendered_detailed_description_html ||= (detailed_description_html.present? ? detailed_description_html_template_instance.render(token_specs_values_for_liquid(include_legacy: true)) : nil)
end

#rendered_product_specifications_grouped(locale: Mobility.locale, filter_grouping: nil, sort_order: []) ⇒ Object



2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
# File 'app/models/item.rb', line 2058

def rendered_product_specifications_grouped(locale: Mobility.locale, filter_grouping: nil, sort_order: [])
  Mobility.with_locale(locale) do
    filtered = loaded_rendered_product_specifications
               .select { |_k, v| filter_grouping.nil? || v['grouping'] == filter_grouping }

    sorted = filtered.sort_by do |k, v|
      [
        v['grouping'] || nil, # Sort nil as last
        sort_order.index(k.to_sym) || Float::INFINITY, # Sort by sort_order if present
        v['name'] || nil # Sort nil as last
      ]
    end

    sorted.group_by { |_k, v| v['grouping'] || 'Other' }
  end
end

#replacement_forItem

Returns:

See Also:



249
# File 'app/models/item.rb', line 249

belongs_to :replacement_for, class_name: 'Item', optional: true

#requires_distributor_id_code?Boolean

Returns:

  • (Boolean)


1287
1288
1289
# File 'app/models/item.rb', line 1287

def requires_distributor_id_code?
  (sku&.start_with?('UWG4-4999') || sku&.start_with?('AWG4-4999')) && !sku&.include?('-WY')
end

#retrieve_faqsObject



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

def retrieve_faqs
  Item::ArticleRetriever.new(article_types: %w[faq]).process(item: self).articles
end

#returned_rma_itemsActiveRecord::Relation<Rma>

Returns:

  • (ActiveRecord::Relation<Rma>)

See Also:



274
# File 'app/models/item.rb', line 274

has_many :returned_rma_items, class_name: 'Rma', foreign_key: :returned_item_id

#revised_publication_for_publicObject



970
971
972
973
# File 'app/models/item.rb', line 970

def revised_publication_for_public
  revision = most_recent_revision(:public)
  revision == self ? nil : revision
end

#rma_reason_codes(include_advanced: true) ⇒ Object



901
902
903
904
905
906
907
# File 'app/models/item.rb', line 901

def rma_reason_codes(include_advanced: true)
  rrcs = RmaReasonCode.active
  rrcs = rrcs.for_product_category_ids(product_category_id).for_product_line_ids(primary_product_line_id) if product_category_id && primary_product_line_id
  # If we're not requesting advanced, then you only get the basic reason codes
  rrcs = rrcs.where(advanced_reason_code: false) unless include_advanced
  rrcs
end

#root_system_product_lineObject

Alias for Primary_product_line#root_system_product_line

Returns:

  • (Object)

    Primary_product_line#root_system_product_line

See Also:



651
652
653
# File 'app/models/item.rb', line 651

delegate :root_system_product_line,
to: :primary_product_line,
allow_nil: true

#run_shipping_audit(carrier: nil) ⇒ Array<Shipping::PackageAuditor>

Run shipping audit for all boxes, optionally for a specific carrier

Parameters:

  • carrier (String, nil) (defaults to: nil)

    Optional carrier name (e.g., 'UPS', 'FedEx', 'Purolator', 'Canpar')

Returns:



1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
# File 'app/models/item.rb', line 1643

def run_shipping_audit(carrier: nil)
  carriers = carrier.present? ? [carrier] : nil
  r = []
  r1 = Shipping::PackageAuditor.new
  r1.process(shipping_dimensions_to_package, carriers:)
  r2 = r3 = nil
  if box2_defined?
    r2 = Shipping::PackageAuditor.new
    r2.process(box2_shipping_dimensions_to_package, carriers:)
  end
  if box3_defined?
    r3 = Shipping::PackageAuditor.new
    r3.process(box3_shipping_dimensions_to_package, carriers:)
  end
  r << r1
  r << r2 if r2
  r << r3 if r3
  r
end

#safe_nameObject

Returns a name suitable for edit feed with any special character that will
destroy a csv formatting



826
827
828
829
830
831
832
# File 'app/models/item.rb', line 826

def safe_name
  return if name.blank?

  name.gsub("'", 'ft')
      .gsub('"', 'in')
      .gsub(%r{[^0-9A-Za-z.-/]}, ' ').squish
end

#sales_product_line_pathObject

Returns ltree path for sales product line (preferred - reuses already-loaded ProductLine)



2171
2172
2173
2174
2175
# File 'app/models/item.rb', line 2171

def sales_product_line_path
  pl_path = primary_product_line&.get_first_show_in_sales_portal_ancestor_path
  pl_path ||= product_lines.map(&:get_first_show_in_sales_portal_ancestor_path).first
  pl_path
end

#sales_product_line_slugObject



2164
2165
2166
2167
2168
# File 'app/models/item.rb', line 2164

def sales_product_line_slug
  slt = primary_product_line&.get_first_show_in_sales_portal_ancestor_slug_ltree
  slt ||= product_lines.map(&:get_first_show_in_sales_portal_ancestor_slug_ltree).first
  slt
end

#secondary_product_categoryProductCategory



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

belongs_to :secondary_product_category, class_name: 'ProductCategory', optional: true

#selection_nameObject



1739
1740
1741
# File 'app/models/item.rb', line 1739

def selection_name
  "#{sku} - #{name}#{' [Discontinued]' if is_discontinued?}"
end

#serial_numbersActiveRecord::Relation<SerialNumber>

Returns:

See Also:



286
# File 'app/models/item.rb', line 286

has_many :serial_numbers, through: :store_items

#set_grouping(force: false) ⇒ Object

Records a grouping string identifier, this speeds up variant grouping lookup for
google and reviews.io api calls.



2289
2290
2291
2292
2293
2294
2295
# File 'app/models/item.rb', line 2289

def set_grouping(force: false)
  return unless is_goods? || is_service?
  return if is_publication?
  return unless force || product_category_id_changed? || primary_product_line_id_changed?

  self.grouping = item_group_name
end

#set_packaged_items_md5_hash(options = {}) ⇒ Object



2279
2280
2281
2282
2283
2284
2285
# File 'app/models/item.rb', line 2279

def set_packaged_items_md5_hash(options = {})
  # Since we're doing this after save, we have to detect using previously_changed?
  return unless packings.from_item.empty? || packings.from_item.any?(&:invalid?) || any_box_changed? || options[:force]

  # Possible here to check and clear all packings referencing this item
  Shipping::ItemMd5Extractor.new.process(self)
end

#shift_images(locale: 'en', prefix: nil) ⇒ Object

Shifts images up to fill empty slots in the image type order for this item
This method will move images to fill gaps in the sequence, maintaining the proper order

Parameters:

  • locale (String) (defaults to: 'en')

    The locale for the images (default: 'en')

  • prefix (String, nil) (defaults to: nil)

    Optional prefix to limit shifting to a specific group (e.g., 'AMZ', 'WAL')



2471
2472
2473
# File 'app/models/item.rb', line 2471

def shift_images(locale: 'en', prefix: nil)
  Image::ImageShiftingService.shift_images_for_item(id, locale: locale, prefix: prefix)
end

#shipping_dimensions(unit: 'in', precision: 0) ⇒ Object

unit_system SI = Standard International, e.g Metric
ucs = United States Customary Unit



1523
1524
1525
1526
1527
1528
1529
1530
1531
# File 'app/models/item.rb', line 1523

def shipping_dimensions(unit: 'in', precision: 0)
  return unless shipping_length && shipping_width && shipping_height

  [
    shipping_length_converted(unit:, precision:),
    shipping_width_converted(unit:, precision:),
    shipping_height_converted(unit:, precision:)
  ]
end

#shipping_dimensions_displayObject



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

def shipping_dimensions_display
  "#{shipping_length}″ (L) x #{shipping_width}″ (W) x #{shipping_height}″ (H)"
end

#shipping_dimensions_to_packageObject



1619
1620
1621
1622
1623
1624
# File 'app/models/item.rb', line 1619

def shipping_dimensions_to_package
  return unless box1_defined?

  Shipping::Container.new(length: shipping_length, width: shipping_width, height: shipping_height, weight: shipping_weight,
                          container_type:)
end

#shipping_height_converted(unit: 'in', precision: 0) ⇒ Object



1545
1546
1547
1548
1549
# File 'app/models/item.rb', line 1545

def shipping_height_converted(unit: 'in', precision: 0)
  return unless shipping_height

  RubyUnits::Unit.new("#{shipping_height} in").convert_to(unit.to_s).scalar.to_f.round(precision)
end

#shipping_length_converted(unit: 'in', precision: 0) ⇒ Object



1533
1534
1535
1536
1537
# File 'app/models/item.rb', line 1533

def shipping_length_converted(unit: 'in', precision: 0)
  return unless shipping_length

  RubyUnits::Unit.new("#{shipping_length} in").convert_to(unit.to_s).scalar.to_f.round(precision)
end

#shipping_optionsActiveRecord::Relation<ShippingOption>

Returns:

See Also:



275
# File 'app/models/item.rb', line 275

has_many :shipping_options

#shipping_volumeObject



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

def shipping_volume
  boxes.sum { |b| b[0] * b[1] * b[2] }
rescue StandardError
  0
end

#shipping_volume_in_cubic_feetObject



1048
1049
1050
1051
1052
# File 'app/models/item.rb', line 1048

def shipping_volume_in_cubic_feet
  res = nil
  res = shipping_volume / 1728.0 if shipping_volume.present? && shipping_volume.positive?
  res
end

#shipping_weight_converted(unit: 'lbs', precision: nil) ⇒ Object



1607
1608
1609
# File 'app/models/item.rb', line 1607

def shipping_weight_converted(unit: 'lbs', precision: nil)
  express_weight_converted shipping_weight, unit:, precision:
end

#shipping_width_converted(unit: 'in', precision: 0) ⇒ Object



1539
1540
1541
1542
1543
# File 'app/models/item.rb', line 1539

def shipping_width_converted(unit: 'in', precision: 0)
  return unless shipping_width

  RubyUnits::Unit.new("#{shipping_width} in").convert_to(unit.to_s).scalar.to_f.round(precision)
end

#ships_in_single_box?Boolean

Returns:

  • (Boolean)


2410
2411
2412
# File 'app/models/item.rb', line 2410

def ships_in_single_box?
  !(box2_defined? || box3_defined?)
end

#ships_via_crate?Boolean

Returns:

  • (Boolean)


2330
2331
2332
# File 'app/models/item.rb', line 2330

def ships_via_crate?
  is_made_to_order_led_mirror?
end

#ships_via_freight?Boolean

Returns:

  • (Boolean)


1405
1406
1407
1408
# File 'app/models/item.rb', line 1405

def ships_via_freight?
  # adding current pallet/freight shipping rules
  freight_service?
end

#siblingsObject



929
930
931
932
933
# File 'app/models/item.rb', line 929

def siblings
  return Item.none unless primary_product_line_id

  Item.by_product_line_id(primary_product_line_id).where.not(id:)
end

#single_item_packings_from_md5Object

Finds packings of this item but only for a single quantity



2322
2323
2324
2325
2326
2327
2328
# File 'app/models/item.rb', line 2322

def single_item_packings_from_md5
  if (md5 = Shipping::Md5HashItem.process(self).md5)
    packings.where(md5:)
  else
    Packing.none
  end
end

#site_mapsActiveRecord::Relation<SiteMap>

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



284
# File 'app/models/item.rb', line 284

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

#sku_and_nameObject



1088
1089
1090
# File 'app/models/item.rb', line 1088

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

#source_item_relationsActiveRecord::Relation<ItemRelation>

Returns:

See Also:



276
# File 'app/models/item.rb', line 276

has_many :source_item_relations, class_name: 'ItemRelation', foreign_key: :target_item_id, dependent: :destroy

#specific_itemsActiveRecord::Relation<Item>

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



283
# File 'app/models/item.rb', line 283

has_many :specific_items, through: :item_relations, class_name: 'Item', source: :item

#specific_publicationsActiveRecord::Relation<Item>

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



282
# File 'app/models/item.rb', line 282

has_many :specific_publications, through: :publication_relations, class_name: 'Item', source: :publication

#specificationsObject

Retrieves rendered specifications as an object



1874
1875
1876
# File 'app/models/item.rb', line 1874

def specifications
  rendered_product_specifications&.values&.map { |s| OpenStruct.new(s) } || []
end

#specifications_for_item_labelObject



1887
1888
1889
# File 'app/models/item.rb', line 1887

def specifications_for_item_label
  specifications.select { |rps| rps.print_on_item_label && rps.output.present? }.sort_by(&:name)
end

#specifications_groupedObject



1878
1879
1880
# File 'app/models/item.rb', line 1878

def specifications_grouped
  specifications.sort_by { |s| [s.grouping, s.name] }.group_by(&:grouping)
end

#star?Boolean

Makes a star appear in the document library

Returns:

  • (Boolean)


835
836
837
# File 'app/models/item.rb', line 835

def star?
  (tags || []).intersect?(%w[Star star])
end

#store_itemsActiveRecord::Relation<StoreItem>

Returns:

See Also:



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

has_many :store_items, inverse_of: :item, dependent: :destroy

#successor_itemItem

Returns:

See Also:



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

belongs_to :successor_item, class_name: 'Item', optional: true

#suggested_item_applies_toObject

Alias for Suggested_item_tool#suggested_item_applies_to

Returns:

  • (Object)

    Suggested_item_tool#suggested_item_applies_to

See Also:



655
656
657
# File 'app/models/item.rb', line 655

delegate :suggested_item_stock_threshold,
:suggested_item_applies_to,
to: :suggested_item_tool

#suggested_item_stock_thresholdObject

Alias for Suggested_item_tool#suggested_item_stock_threshold

Returns:

  • (Object)

    Suggested_item_tool#suggested_item_stock_threshold

See Also:



655
656
657
# File 'app/models/item.rb', line 655

delegate :suggested_item_stock_threshold,
:suggested_item_applies_to,
to: :suggested_item_tool

#superceded_itemsActiveRecord::Relation<Item>

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



265
# File 'app/models/item.rb', line 265

has_many :superceded_items, class_name: 'Item', foreign_key: :successor_item_id

#supplier_itemSupplierItem



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

belongs_to :supplier_item, inverse_of: :items, optional: true

#supplier_itemsActiveRecord::Relation<SupplierItem>

Returns:

See Also:



277
# File 'app/models/item.rb', line 277

has_many :supplier_items, inverse_of: :item, dependent: :destroy

#supplier_skusObject



816
817
818
# File 'app/models/item.rb', line 816

def supplier_skus
  ([secondary_model_number] + supplier_items.active.distinct.pluck(:supplier_sku)).filter_map(&:presence).uniq
end

#support_product_line_pathObject

Returns ltree path for support product line (preferred - reuses already-loaded ProductLine)



2184
2185
2186
2187
2188
# File 'app/models/item.rb', line 2184

def support_product_line_path
  pl_path = primary_product_line&.get_first_show_in_support_portal_ancestor_path
  pl_path ||= product_lines.map(&:get_first_show_in_support_portal_ancestor_path).first
  pl_path
end

#support_product_line_slugObject



2177
2178
2179
2180
2181
# File 'app/models/item.rb', line 2177

def support_product_line_slug
  slt = primary_product_line&.get_first_show_in_support_portal_ancestor_slug_ltree
  slt ||= product_lines.map(&:get_first_show_in_support_portal_ancestor_slug_ltree).first
  slt
end

#tags_displayObject



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

def tags_display
  tags.join(', ')
end

#target_item_relationsActiveRecord::Relation<ItemRelation>

Returns:

See Also:



278
# File 'app/models/item.rb', line 278

has_many :target_item_relations, class_name: 'ItemRelation', foreign_key: :source_item_id, dependent: :destroy

#target_item_relations_for_specsActiveRecord::Relation<ItemRelation>

Returns:

See Also:



279
# File 'app/models/item.rb', line 279

has_many :target_item_relations_for_specs, -> { where(include_in_spec: true) }, class_name: 'ItemRelation', foreign_key: :source_item_id

#third_party_skus(limit: 1) ⇒ Object



820
821
822
# File 'app/models/item.rb', line 820

def third_party_skus(limit: 1)
  (catalog_items.where.not(third_party_sku: nil).where.not(CatalogItem[:third_party_sku].matches(sku)).pluck(:third_party_sku) - [sku]).filter_map(&:presence).uniq.sort.first(limit)
end

#thumbnail_url(size: '30x30') ⇒ Object



1115
1116
1117
# File 'app/models/item.rb', line 1115

def thumbnail_url(size: '30x30')
  image_url2(size:, thumbnail: true)
end

#to_liquidObject



2463
2464
2465
# File 'app/models/item.rb', line 2463

def to_liquid
  Liquid::ItemDrop.new self
end

#to_packdimsObject



2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
# File 'app/models/item.rb', line 2297

def to_packdims
  # Organize our packdims, ceiling with decimal precision of 1, also min of 0.1, and sort to make sure that lengthxwidthxheight are in descending order for convention
  sle, swi, she = [[shipping_length.ceil(1), 0.1].max, [shipping_width.ceil(1), 0.1].max, [shipping_height.ceil(1), 0.1].max].sort.reverse
  # Weight gets an extra decimal precision
  swg = [shipping_weight.ceil(2), 0.01].max
  # we store an array of decimal 7,2.  To prevent 0 which would cause a validation issue, the smallest packable weight will be 0.01 lbs (the weight of a standard envelope)
  packdims = [[sle, swi, she, swg, Shipment.container_types[container_type]]]
  if box2_defined?
    box2_sle, box2_swi, box2_she = [[box2_shipping_length.ceil(1), 0.1].max, [box2_shipping_width.ceil(1), 0.1].max, [box2_shipping_height.ceil(1), 0.1].max].sort.reverse
    # Weight gets an extra decimal precision
    box2_swg = [box2_shipping_weight.ceil(2), 0.01].max
    # we store an array of decimal 7,2.  To prevent 0 which would cause a validation issue, the smallest packable weight will be 0.01 lbs (the weight of a standard envelope)
    packdims << [box2_sle, box2_swi, box2_she, box2_swg, Shipment.container_types[container_type]]
  end
  if box3_defined?
    box3_sle, box3_swi, box3_she = [[box3_shipping_length.ceil(1), 0.1].max, [box3_shipping_width.ceil(1), 0.1].max, [box3_shipping_height.ceil(1), 0.1].max].sort.reverse
    # Weight gets an extra decimal precision
    box3_swg = [box3_shipping_weight.ceil(2), 0.01].max
    # we store an array of decimal 7,2.  To prevent 0 which would cause a validation issue, the smallest packable weight will be 0.01 lbs (the weight of a standard envelope)
    packdims << [box3_sle, box3_swi, box3_she, box3_swg, Shipment.container_types[container_type]]
  end
  packdims
end

#to_sObject



1733
1734
1735
1736
1737
# File 'app/models/item.rb', line 1733

def to_s
  s = "#{sku} - #{name}"
  s << ' (Discontinued)' if is_discontinued?
  s
end

#total_shipping_weightObject



2453
2454
2455
# File 'app/models/item.rb', line 2453

def total_shipping_weight
  boxes.sum { |b| b[3].to_f }
end

#total_shipping_weight_converted(unit: 'lbs', precision: nil) ⇒ Object



1603
1604
1605
# File 'app/models/item.rb', line 1603

def total_shipping_weight_converted(unit: 'lbs', precision: nil)
  express_weight_converted total_shipping_weight, unit:, precision:
end

#total_shipping_weight_of_kit_componentsObject



2457
2458
2459
2460
2461
# File 'app/models/item.rb', line 2457

def total_shipping_weight_of_kit_components
  return total_shipping_weight unless is_kit?

  kit_target_item_relations.includes(:target_item).sum { |tir| tir.quantity * tir.target_item.total_shipping_weight }
end

#translation_key_resourcesActiveRecord::Relation<TranslationKeyResource>

Returns:

See Also:



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

has_many :translation_key_resources, as: :resource, dependent: :destroy

#update_boxes_from_packages(packages) ⇒ Object



1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
# File 'app/models/item.rb', line 1011

def update_boxes_from_packages(packages)
  # use packages to populate and/or reset boxes
  self.shipping_length = packages[0].try(:length)
  self.shipping_width = packages[0].try(:width)
  self.shipping_height = packages[0].try(:height)
  self.shipping_weight = packages[0].try(:weight)
  if packages.size > 1
    self.box2_shipping_length = packages[1].try(:length)
    self.box2_shipping_width = packages[1].try(:width)
    self.box2_shipping_height = packages[1].try(:height)
    self.box2_shipping_weight = packages[1].try(:weight)
  else
    self.box2_shipping_length = nil
    self.box2_shipping_width = nil
    self.box2_shipping_height = nil
    self.box2_shipping_weight = nil
  end
  if packages.size > 2
    self.box3_shipping_length = packages[2].try(:length)
    self.box3_shipping_width = packages[2].try(:width)
    self.box3_shipping_height = packages[2].try(:height)
    self.box3_shipping_weight = packages[2].try(:weight)
  else
    self.box3_shipping_length = nil
    self.box3_shipping_width = nil
    self.box3_shipping_height = nil
    self.box3_shipping_weight = nil
  end
  save
end

#update_rendered_product_specifications(user_id: nil, tokens: nil, locales: nil, skip_google_feed: true) ⇒ Object



1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
# File 'app/models/item.rb', line 1993

def update_rendered_product_specifications(user_id: nil, tokens: nil, locales: nil, skip_google_feed: true)
  return :skipped if new_record? || is_publication?

  locales ||= content_locales_to_render

  self.refresh_specs = false # prevent looping
  self.skip_item_calc = true # not needed
  # Render for each available locale, always include english
  locales.uniq.each do |locale|
    # If this item is not offered in the locale no need to generate this
    Mobility.with_locale(locale) do
      self.rendered_product_specifications = render_product_specifications(tokens:)
    end
  end
  self.rendered_product_specifications_at = Time.current
  # This makes sure we don't store bad translations that are not supposed to be there
  # this can yield unsavory results when rendering
  prune_unused_translations

  # If an updater_id is specified, we override the stamp
  res = :fail
  CurrentScope.with_user_id(user_id) do
    res = :success if save
  end
  if res == :success
    refresh_google_feed unless skip_google_feed # Done Async
    purge_edge_cache(include_product_line: false) # Async as well
  end

  res
end

#uploadUpload

Returns:

See Also:



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

belongs_to :upload, optional: true

#uploadsActiveRecord::Relation<Upload>

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



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

has_and_belongs_to_many :uploads, dependent: :destroy

#url_pathsObject



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

def url_paths
  p = []
  p += primary_product_line.url_paths
  p << sku
  p.compact
end

#variants(include_self: false) ⇒ Object

A more strict version of relatives, we don't look at descendants, only exact matches



2137
2138
2139
# File 'app/models/item.rb', line 2137

def variants(include_self: false)
  item_grouping_info(include_self:)&.variants || Item.none
end

#versions_for_audit_trail(_params = {}) ⇒ Object



2475
2476
2477
2478
2479
2480
2481
2482
2483
# File 'app/models/item.rb', line 2475

def versions_for_audit_trail(_params = {})
  RecordVersion.where(<<~SQL.squish, id:)
    (item_type = 'Item' AND item_id = :id)
    OR (
      item_type = 'ImageProfile'
      AND reference_data @> '{"item_id": :id}'
    )
  SQL
end

#view_product_catalogsActiveRecord::Relation<ViewProductCatalog>

Returns:

See Also:



266
# File 'app/models/item.rb', line 266

has_many :view_product_catalogs

#warm_cache(include_product_line: true) ⇒ Object

Finds the associated sitemap (url) end points of this item and its product line
Perform a cache warmup



1946
1947
1948
1949
1950
1951
1952
1953
# File 'app/models/item.rb', line 1946

def warm_cache(include_product_line: true)
  return :disabled unless Cache::EdgeCacheUtility.edge_cache_enabled?

  all_site_maps.each(&:warm_cache)
  return unless include_product_line && (pl = primary_product_line)

  pl.warm_cache
end

#will_be_restricted_for_sales?Boolean

Returns:

  • (Boolean)


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

def will_be_restricted_for_sales?
  restricted_for_sales ||
    (product_category&.restricted_for_sales && primary_product_line&.restricted_for_sales)
end