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 :enum 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 :enum 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, FloorPlanRetriever, HeatingElementLine, HeatingElementPlan, ImageLibraryPlPcLadder, ImageLibraryRetriever, ImageRetriever, InventoryCommitter, KitAttributeGenerator, KitComposer, KitConsolidator, MarketingCycleCountScheduler, PopulateItemPopularity, PrepareNewKitComponent, ProductSpecificationsCloner, PublicationRetriever, ShippingBoxCalculator, SpecFixer, SpecificationsMasher, SpecificationsRetriever, SuggestedItemTool, SupportQrCode, SynchronizeRefurbSpecs, UpcBarcode, UpcMaker, VideoRetriever

Constant Summary collapse

TRANSLATABLE_ATTRIBUTES =

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 =

Translation namespace.

'ItemAttributes'
SKU_REGEXP =

Sku regexp.

/\A\w[\w\-.]+\w\z/
AMAZON_ASIN_REGEXP =

Amazon asin regexp.

/\b[A-Z0-9]{10}\b/
DISPLAY_TREATMENT_HIDE_QUANTITIES =

Display treatment hide quantities.

0
SINGLE_QUANTITY_SKUS =

Put sku in here that needs their own line always

[].freeze
DOUBLE_POLE_VOLTAGES =

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 =

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 =

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::Publication

Models::Publication::CACHE_RELEVANT_ATTRIBUTES, Models::Publication::MARKETPLACE_DOWNLOAD_TOKEN_FLOOR, Models::Publication::MARKETPLACE_DOWNLOAD_TOKEN_LIFETIME, Models::Publication::PUBLIC_LOCALES, Models::Publication::REVISION_CHAIN_LIMIT

Constants included from Models::Embeddable

Models::Embeddable::MAX_CONTENT_LENGTH

Constants included from Models::ItemSpecificationHelper

Models::ItemSpecificationHelper::PER_ITEM_SPEC_TOKENS

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Models::Schedulable

Models::Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Attributes included from Models::Publication

#available_in_canada, #available_in_usa, #publication_previous_page_paths, #serve_in_locale

Attributes included from Models::Translatable

#do_not_compact_translation_container

Attributes 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

Attributes included from Models::ItemLtreeSync

#skip_ltree_sync

Belongs to collapse

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

#incoming_related_publication_links, #item_embeddings, #related_publication_links, #related_publications, #translated_publication_links, #translated_publications

Methods included from Models::Embeddable

#content_embeddings

Methods included from Models::Taggable

#tag_records, #taggings

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_family_items, #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=, #amazon_title_differentiation, #amazon_title_differentiation=, #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::EdgeCachePurgeable

#enqueue_edge_cache_purge

Methods included from Models::CrossLinkable

#case_evidence, #content_links_count, #linked_articles, #linked_content, #linked_faqs, #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, cover_ratio_mismatch, #create_primary_image_from_pdf, #default_publication_logistics, #durable_download_token, #embeddable_chunked?, embeddable_content_types, #embeddable_locales, #embedding_content_changed?, #fast_country_discontinue, #file_name_for_download, #find_durable_download_token, #friendly_locale_names, #generate_publication_name, #has_literature_changed?, hybrid_search_publications, #latest_revision, #legacy_sku_revision_chain, #linked_revision_chain, #locale_for_embedding, #needs_vision_analysis?, #perform_country_discontinue, #product_category_must_be_publication, #publication_available_locales, #publication_base_name_clean_of_language, #publication_cache_relevant_change?, #publication_download_url, #publication_edge_cache_urls, #publication_landing_page_paths, #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, #publish_publication_updated_event, #purge_publication_cache, #retrieve_publications_search_text, #revision_chain, #secondary_product_category_must_not_be_publication, #should_queue_embedding?, #translation_for_language, with_publication_attached

Methods included from Models::HybridSearchable

ranked_ids, rrf_ranked_relation

Methods included from Models::Embeddable

#content_for_embedding, embeddable_content_types, #embeddable_locales, #embedding_content_hash, #embedding_current?, #embedding_eligible?, 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, with_shape_lock

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=, #rendered_specifications, #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::Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#amazon_asinvoid (readonly)

This method returns an undefined value.

Validates amazon asin.



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

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:



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

validates :name_en, :tax_class, presence: true

#refresh_specsObject

These are shortcuts for publications



220
221
222
# File 'app/models/item.rb', line 220

def refresh_specs
  @refresh_specs
end

#skip_item_calcObject

Returns the value of attribute skip_item_calc.



780
781
782
# File 'app/models/item.rb', line 780

def skip_item_calc
  @skip_item_calc
end

#skuvoid (readonly)

This method returns an undefined value.

Validates sku.



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

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' }

#steel_content_value_ratioObject (readonly)

Section 232 declaration inputs (see Declaration232) — a ratio outside 0..1
or a negative weight would produce negative rows on a customs declaration.

Validations:

  • Numericality ({ greater_than_or_equal_to: 0, less_than_or_equal_to: 1 })


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

validates :steel_content_value_ratio, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 1 }

#steel_content_weight_kgvoid (readonly)

This method returns an undefined value.

Validates steel content weight kg.



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

validates :steel_content_weight_kg, numericality: { greater_than_or_equal_to: 0 }

#tax_classObject (readonly)

Here we check specifically for the english name to be present

Validations:



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

validates :name_en, :tax_class, presence: true

#update_search_textObject

Returns the value of attribute update_search_text.



780
781
782
# File 'app/models/item.rb', line 780

def update_search_text
  @update_search_text
end

#update_upcObject

Returns the value of attribute update_upc.



780
781
782
# File 'app/models/item.rb', line 780

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:



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

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:



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

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:



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

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_localesArray<String>

Note:

all_tags method provided by Models::Taggable concern

Returns sorted distinct publication locales.

Returns:

  • (Array<String>)

    sorted distinct publication locales



809
810
811
# File 'app/models/item.rb', line 809

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

Async update all items product specifications.

Parameters:

  • public_only (Object) (defaults to: true)
  • active_only (Object) (defaults to: true)
  • limit (Object) (defaults to: nil)
  • not_updated_since (Object) (defaults to: nil)

Returns:

  • (Object)


2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
# File 'app/models/item.rb', line 2909

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(Item[:rendered_product_specifications_at].eq(nil).or(Item[:rendered_product_specifications_at].lt(not_updated_since))) if not_updated_since

  items = items.limit(limit) if limit
  items.distinct.ids.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:



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

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:



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

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:



675
676
677
678
679
# File 'app/models/item.rb', line 675

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:



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

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:



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

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:



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

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

.condition_select_optionsObject

Condition select options.

Returns:

  • (Object)


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

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:



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

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:



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

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:



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

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:



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

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:



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

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

.easystatsActiveRecord::Relation<Item>

A relation of Items that are easystats. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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

.get_infrared_heating_panel_tstat_options_for_web_and_apply_overrides(connection_method, tstat_options_override = [], available_in_catalog_id: nil) ⇒ Array<Hash>

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

Parameters:

  • connection_method (Object)
  • tstat_options_override (Object) (defaults to: [])
  • available_in_catalog_id (Object) (defaults to: nil)

Returns:

  • (Array<Hash>)


3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
# File 'app/models/item.rb', line 3279

def self.get_infrared_heating_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.infrared_heating_panel_controls_hardwired
             else
               base_item_scope.infrared_heating_panel_controls_plug_in
             end
  res = controls.map do |item|
    tstat_options_override.find 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_quantity_from_items_and_sqft(catalog_id, items, sqft, add_one_extra_of_last_item = false) ⇒ Object

Get quantity from items and sqft.

Parameters:

  • catalog_id (Object)
  • items (Object)
  • sqft (Object)
  • add_one_extra_of_last_item (Object) (defaults to: false)

Returns:

  • (Object)


2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
# File 'app/models/item.rb', line 2087

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.find { |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_towel_warmer_tstat_options_for_web_and_apply_overrides(connection_method, tstat_options_override = [], available_in_catalog_id: nil) ⇒ Array<Hash>

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:

Parameters:

  • connection_method (Object)
  • tstat_options_override (Object) (defaults to: [])
  • available_in_catalog_id (Object) (defaults to: nil)

Returns:

  • (Array<Hash>)


3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
# File 'app/models/item.rb', line 3256

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



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

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:



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

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:



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

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:



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

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:



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

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:



691
692
693
694
695
696
697
698
699
700
701
702
703
# File 'app/models/item.rb', line 691

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

Import translation keys.

Returns:

  • (Object)


2699
2700
2701
# File 'app/models/item.rb', line 2699

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:



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

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:



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

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

.infrared_heating_panel_controls_dual_connectActiveRecord::Relation<Item>

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

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

scope :infrared_heating_panel_controls_dual_connect, -> { where(sku: ItemConstants::INFRARED_HEATING_PANELS_PLUG_IN_CONTROL_SKUS + ItemConstants::INFRARED_HEATING_PANELS_HARDWIRE_CONTROL_SKUS) }

.infrared_heating_panel_controls_hardwiredActiveRecord::Relation<Item>

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

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

scope :infrared_heating_panel_controls_hardwired, -> { where(sku: ItemConstants::INFRARED_HEATING_PANELS_HARDWIRE_CONTROL_SKUS) }

.infrared_heating_panel_controls_plug_inActiveRecord::Relation<Item>

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

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

scope :infrared_heating_panel_controls_plug_in, -> { where(sku: ItemConstants::INFRARED_HEATING_PANELS_PLUG_IN_CONTROL_SKUS) }

.infrared_heating_panelsActiveRecord::Relation<Item>

A relation of Items that are infrared heating panels. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

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

.infrared_heating_panels_hardwiredActiveRecord::Relation<Item>

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

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

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

.install_kitsActiveRecord::Relation<Item>

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

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

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:



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

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:



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

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

.item_identifier_label_sizes_select_optionsObject

Item identifier label sizes select options.

Returns:

  • (Object)


1213
1214
1215
# File 'app/models/item.rb', line 1213

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

Last custom mat item.

Returns:

  • (Object)


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

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:



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

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:



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

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:



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

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:



519
520
521
522
523
524
525
526
527
# File 'app/models/item.rb', line 519

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

scope :not_a_target_item,       -> { where.missing(: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:



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

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:



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

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:



636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
# File 'app/models/item.rb', line 636

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
}

.order_towel_warmer_controls_hardwiredActiveRecord::Relation<Item>

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

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

scope :order_towel_warmer_controls_hardwired, -> { in_order_of(:sku, ItemConstants::TOWEL_WARMER_HARDWIRE_CONTROL_SKUS) }

.orderable_onlineActiveRecord::Relation<Item>

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

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

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:



592
593
594
# File 'app/models/item.rb', line 592

scope :out_of_stock,            -> {
  joins(:store_items).where(StoreItem[:qty_available].lteq(Item[: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:



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

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:



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

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:



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

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:



681
682
683
684
685
686
# File 'app/models/item.rb', line 681

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:



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

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:



664
665
666
667
668
# File 'app/models/item.rb', line 664

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:



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

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

.ransackable_scopes(_auth_object = nil) ⇒ Object

Ransackable scopes.

Parameters:

  • _auth_object (Object) (defaults to: nil)

Returns:

  • (Object)


862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
# File 'app/models/item.rb', line 862

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
     cover_ratio_mismatch]
end

.relay_panelsActiveRecord::Relation<Item>

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

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

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:



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

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

.rendered_product_specs_keysObject

Rendered product specs keys.

Returns:

  • (Object)


2963
2964
2965
# File 'app/models/item.rb', line 2963

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

Rendered product specs options for select.

Parameters:

  • spec_key (Object)

Returns:

  • (Object)


2971
2972
2973
2974
2975
# File 'app/models/item.rb', line 2971

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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

.sku_is_membrane?(sku) ⇒ Object

Whether sku is membrane.

Parameters:

  • sku (Object)

Returns:

  • (Object)


1979
1980
1981
# File 'app/models/item.rb', line 1979

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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

.tax_classes_for_selectObject

Tax classes for select.

Returns:

  • (Object)


1206
1207
1208
# File 'app/models/item.rb', line 1206

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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

.uom_select_optionsObject

Uom select options.

Returns:

  • (Object)


1199
1200
1201
# File 'app/models/item.rb', line 1199

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:



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

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:



756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
# File 'app/models/item.rb', line 756

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:



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

scope :with_features,            -> { where.not(feature_1: nil).where.not(feature_2: nil).where.not(feature_3: nil).where.not(feature_4: nil).where.not(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:



614
615
616
617
618
619
620
621
622
623
# File 'app/models/item.rb', line 614

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:



567
568
569
# File 'app/models/item.rb', line 567

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:



570
571
572
# File 'app/models/item.rb', line 570

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_imagesActiveRecord::Relation<Image>

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

Returns:

  • (ActiveRecord::Relation<Image>)


3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
# File 'app/models/item.rb', line 3299

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(:or)
  query.where(combined_conditions)
end

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

All my publications.

Parameters:

  • categories (Object) (defaults to: nil)
  • locale (Object) (defaults to: nil)
  • publication_category_paths (Object) (defaults to: nil)

Returns:

  • (Object)


2508
2509
2510
# File 'app/models/item.rb', line 2508

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

All site maps.

Returns:

  • (Object)


2793
2794
2795
# File 'app/models/item.rb', line 2793

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

#all_uploadsObject

All uploads.

Returns:

  • (Object)


3387
3388
3389
# File 'app/models/item.rb', line 3387

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

#any_box_changed?Object

Whether any box changed.

Returns:

  • (Object)


3380
3381
3382
# File 'app/models/item.rb', line 3380

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

#articlesActiveRecord::Associations::CollectionProxy

The associated articles.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_and_belongs_to_many :articles

#assortment_instructionsActiveRecord::Associations::CollectionProxy

The associated assortment instructions.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_and_belongs_to_many :assortment_instructions, inverse_of: :items

#async_update_rendered_product_specificationsObject

Async update rendered product specifications.

Returns:

  • (Object)


2786
2787
2788
# File 'app/models/item.rb', line 2786

def async_update_rendered_product_specifications
  ItemAttributeWorker.perform_async(id)
end

#authenticated_url(dimensions = nil) ⇒ Object

Authenticated url.

Parameters:

  • dimensions (Object) (defaults to: nil)

Returns:

  • (Object)


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

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

Auto translate.

Parameters:

  • reset_previous_values (Object) (defaults to: false)
  • locales (Object) (defaults to: nil)

Returns:

  • (Object)


2609
2610
2611
2612
2613
2614
2615
2616
2617
# File 'app/models/item.rb', line 2609

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

Auto translate attributes.

Parameters:

  • reset_previous_values (Object) (defaults to: false)
  • locales (Object) (defaults to: nil)

Returns:

  • (Object)


2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
# File 'app/models/item.rb', line 2624

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

Auto translate product specifications.

Parameters:

  • reset_previous_values (Object) (defaults to: false)
  • locales (Object) (defaults to: nil)

Returns:

  • (Object)


2658
2659
2660
2661
2662
2663
2664
# File 'app/models/item.rb', line 2658

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_localesArray<String>

Returns all locales from the catalog this item is part of

Returns:

  • (Array<String>)


2842
2843
2844
2845
# File 'app/models/item.rb', line 2842

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

Available token options from product specifications.

Returns:

  • (Object)


3020
3021
3022
# File 'app/models/item.rb', line 3020

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

Base weight converted.

Parameters:

  • unit (Object) (defaults to: 'lbs')
  • precision (Object) (defaults to: nil)

Returns:

  • (Object)


2284
2285
2286
# File 'app/models/item.rb', line 2284

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

#box1_changed?Object

Whether box1 changed.

Returns:

  • (Object)


3345
3346
3347
# File 'app/models/item.rb', line 3345

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

#box1_defined?Object

Whether box1 defined.

Returns:

  • (Object)


3338
3339
3340
# File 'app/models/item.rb', line 3338

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

#box2_changed?Object

Whether box2 changed.

Returns:

  • (Object)


3359
3360
3361
# File 'app/models/item.rb', line 3359

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

#box2_defined?Object

Whether box2 defined.

Returns:

  • (Object)


3352
3353
3354
# File 'app/models/item.rb', line 3352

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

Box2 shipping dimensions to package.

Returns:

  • (Object)


2346
2347
2348
2349
2350
2351
# File 'app/models/item.rb', line 2346

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

Box2 shipping height converted.

Parameters:

  • unit (Object) (defaults to: 'in')
  • precision (Object) (defaults to: 0)

Returns:

  • (Object)


2226
2227
2228
2229
2230
# File 'app/models/item.rb', line 2226

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

Box2 shipping length converted.

Parameters:

  • unit (Object) (defaults to: 'in')
  • precision (Object) (defaults to: 0)

Returns:

  • (Object)


2204
2205
2206
2207
2208
# File 'app/models/item.rb', line 2204

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

Box2 shipping weight converted.

Parameters:

  • unit (Object) (defaults to: 'lbs')
  • precision (Object) (defaults to: nil)

Returns:

  • (Object)


2320
2321
2322
# File 'app/models/item.rb', line 2320

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

Box2 shipping width converted.

Parameters:

  • unit (Object) (defaults to: 'in')
  • precision (Object) (defaults to: 0)

Returns:

  • (Object)


2215
2216
2217
2218
2219
# File 'app/models/item.rb', line 2215

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?Object

Whether box3 changed.

Returns:

  • (Object)


3373
3374
3375
# File 'app/models/item.rb', line 3373

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

#box3_defined?Object

Whether box3 defined.

Returns:

  • (Object)


3366
3367
3368
# File 'app/models/item.rb', line 3366

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

Box3 shipping dimensions to package.

Returns:

  • (Object)


2356
2357
2358
2359
2360
2361
# File 'app/models/item.rb', line 2356

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

Box3 shipping height converted.

Parameters:

  • unit (Object) (defaults to: 'in')
  • precision (Object) (defaults to: 0)

Returns:

  • (Object)


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

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

Box3 shipping length converted.

Parameters:

  • unit (Object) (defaults to: 'in')
  • precision (Object) (defaults to: 0)

Returns:

  • (Object)


2237
2238
2239
2240
2241
# File 'app/models/item.rb', line 2237

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

Box3 shipping weight converted.

Parameters:

  • unit (Object) (defaults to: 'lbs')
  • precision (Object) (defaults to: nil)

Returns:

  • (Object)


2329
2330
2331
# File 'app/models/item.rb', line 2329

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

Box3 shipping width converted.

Parameters:

  • unit (Object) (defaults to: 'in')
  • precision (Object) (defaults to: 0)

Returns:

  • (Object)


2248
2249
2250
2251
2252
# File 'app/models/item.rb', line 2248

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

Boxes.

Returns:

  • (Object)


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

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

Boxes shipping dimensions and weights converted.

Parameters:

  • units (Object) (defaults to: { length: 'in', weight: 'lbs' })
  • precision (Object) (defaults to: 2)

Returns:

  • (Object)


1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
# File 'app/models/item.rb', line 1423

def boxes_shipping_dimensions_and_weights_converted(units: { length: 'in', weight: 'lbs' }, precision: 2)
  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: 2) ⇒ Object

Boxes shipping dimensions and weights display.

Parameters:

  • units (Object) (defaults to: { length: 'in', weight: 'lbs' })
  • precision (Object) (defaults to: 2)

Returns:

  • (Object)


1439
1440
1441
1442
1443
1444
1445
1446
# File 'app/models/item.rb', line 1439

def boxes_shipping_dimensions_and_weights_display(units: { length: 'in', weight: 'lbs' }, precision: 2)
  boxes_shipping_dimensions_and_weights_converted(units:, precision:).map do |b|
    # %g drops trailing zeros so 37.0 → "37", 0.27 → "0.27", 0.5 → "0.5".
    # Previous precision: 0 default rounded sub-1″ dims to 0, producing
    # nonsense like "37 in x 25 in x 0 in" for items with fractional height.
    "#{format('%g', b[0])} #{units[:length]} x #{format('%g', b[1])} #{units[:length]} x #{format('%g', b[2])} #{units[:length]} x #{format('%g', b[3])} #{units[:weight]}"
  end
end

#boxes_to_packagesObject

Boxes to packages.

Returns:

  • (Object)


1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
# File 'app/models/item.rb', line 1329

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_type_from_packdim_int(0))
    package.add_content(id, 1)
    packages << package
  end
  packages
end

#brandBrand?

The associated brand.

Returns:



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

belongs_to :brand, optional: true

#calculate_ideal_cable_spacingObject

Calculate ideal cable spacing.

Returns:

  • (Object)


2006
2007
2008
2009
2010
2011
2012
2013
2014
# File 'app/models/item.rb', line 2006

def calculate_ideal_cable_spacing
  return @preloaded_ideal_cable_spacing if instance_variable_defined?(:@preloaded_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?Object

Whether calculate is cable system.

Returns:

  • (Object)


1994
1995
1996
1997
1998
1999
2000
2001
# File 'app/models/item.rb', line 1994

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

Calculate item fields.

Returns:

  • (Object)


1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
# File 'app/models/item.rb', line 1298

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?Object

Whether can be packaged.

Returns:

  • (Object)


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

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

#canonical_pathString?

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

Returns:

  • (String, nil)


924
925
926
927
928
929
930
931
# File 'app/models/item.rb', line 924

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

Canonical sku.

Returns:

  • (Object)


2429
2430
2431
2432
2433
2434
2435
2436
# File 'app/models/item.rb', line 2429

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) ⇒ String?

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

Parameters:

  • locale (Object) (defaults to: I18n.locale)

Returns:

  • (String, nil)


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

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

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

#catalog_item_site_mapsActiveRecord::Associations::CollectionProxy

The associated catalog item site maps.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

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

#catalog_itemsActiveRecord::Associations::CollectionProxy

The associated catalog items.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

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

#catalogsActiveRecord::Associations::CollectionProxy

The associated catalogs.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

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)


2389
2390
2391
2392
2393
2394
2395
# File 'app/models/item.rb', line 2389

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::Associations::CollectionProxy

The associated compatible items.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

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

#container_typeObject

Container type.

Returns:

  • (Object)


3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
# File 'app/models/item.rb', line 3240

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

Content locales to render.

Returns:

  • (Object)


2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
# File 'app/models/item.rb', line 2850

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?Object

Whether controllable.

Returns:

  • (Object)


979
980
981
982
983
# File 'app/models/item.rb', line 979

def controllable?
  is_heating_element? ||
    is_towel_warmer? ||
    is_infrared_heating_panel?
end

#country_of_origin_iso3Object

Country of origin iso3.

Returns:

  • (Object)


905
906
907
908
909
# File 'app/models/item.rb', line 905

def country_of_origin_iso3
  return unless coo

  ISO3166::Country[coo]&.alpha3
end

#country_of_origin_nameObject

Country of origin name.

Returns:

  • (Object)


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

def country_of_origin_name
  return unless coo

  ISO3166::Country[coo]&.iso_short_name
end

#create_template_specificationsObject

Create template specifications.

Returns:

  • (Object)


2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
# File 'app/models/item.rb', line 2980

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

Current supplier cost.

Returns:

  • (Object)


970
971
972
973
974
# File 'app/models/item.rb', line 970

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

Deep dup.

Returns:

  • (Object)


227
228
229
230
231
232
233
234
235
236
237
238
# File 'app/models/item.rb', line 227

def deep_dup
  klone = deep_clone(
    include: :item_product_lines,
    except: %i[upc upload_id last_sale_date first_sale_date popularity amazon_asin literature_id]
  ) do |_original, copy|
    copy.popularity = nil if copy.is_a?(Item)
  end
  # A publication owns its literature 1:1 — the dup gets its own stored copy
  # of the PDF, never an alias of the original's Literature row.
  klone.literature = literature.dup_with_attachment if literature.present?
  klone
end

#default_short_descriptionObject

Default short description.

Returns:

  • (Object)


2544
2545
2546
2547
2548
# File 'app/models/item.rb', line 2544

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

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

#detailed_descriptionString

Creates a text description from the html rendered description

Returns:

  • (String)


2537
2538
2539
# File 'app/models/item.rb', line 2537

def detailed_description
  html_to_text(rendered_detailed_description_html)
end

#digital_assetsActiveRecord::Associations::CollectionProxy

The associated digital assets.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_and_belongs_to_many :digital_assets

#direct_packingsActiveRecord::Associations::CollectionProxy

The associated direct packings.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

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

#direct_product_specificationsActiveRecord::Associations::CollectionProxy

The associated direct product specifications.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

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

#discontinue_self_and_dependentsObject

Discontinue self and dependents.

Returns:

  • (Object)


1270
1271
1272
1273
1274
1275
1276
1277
1278
# File 'app/models/item.rb', line 1270

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)
    end
    update!(is_discontinued: true, cycle_count_grouping: 'not_set')
  end
end

#discontinue_store_and_catalog_itemsObject

Discontinue store and catalog items.

Returns:

  • (Object)


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

def discontinue_store_and_catalog_items
  return if is_publication?

  discontinue_self_and_dependents if is_discontinued?
  true
end

#ean13Object

Ean13.

Returns:

  • (Object)


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

def ean13
  return if upc.blank?

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

#edge_cache_urlsArray<String>

Absolute Cloudflare URLs for this item's pages AND their lazy section
fragments (documents/reviews/…), which Cloudflare caches as separate edge
entries from the page URL (see SiteMap#section_cache_urls). uniq: all_site_maps
merges two associations that can expand to overlapping section URLs.

Returns:

  • (Array<String>)


2815
2816
2817
# File 'app/models/item.rb', line 2815

def edge_cache_urls
  all_site_maps.flat_map { |sm| [sm.url, *sm.section_cache_urls] }.uniq
end

#edi_communication_logsActiveRecord::Associations::CollectionProxy

The associated edi communication logs.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :edi_communication_logs, through: :edi_documents

#edi_documentsActiveRecord::Associations::CollectionProxy

The associated edi documents.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

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:



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

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:



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

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

Effective option name.

Returns:

  • (Object)


3152
3153
3154
# File 'app/models/item.rb', line 3152

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

#effective_seo_description(char_limit: nil) ⇒ Object

Effective seo description.

Parameters:

  • char_limit (Object) (defaults to: nil)

Returns:

  • (Object)


1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
# File 'app/models/item.rb', line 1118

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

Effective seo keywords.

Returns:

  • (Object)


2584
2585
2586
2587
2588
2589
2590
2591
# File 'app/models/item.rb', line 2584

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

Effective seo title.

Parameters:

  • skip_sku (Object) (defaults to: false)

Returns:

  • (Object)


1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
# File 'app/models/item.rb', line 1136

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?

The associated exclusive item group.

Returns:



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

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

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

Express weight converted.

Parameters:

  • weight_in_lbs (Object)
  • unit (Object) (defaults to: 'lbs')
  • precision (Object) (defaults to: nil)

Returns:

  • (Object)


2271
2272
2273
2274
2275
2276
2277
# File 'app/models/item.rb', line 2271

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

#featuresArray<String>

Features are a sub category of specs with feature priority set

Returns:

  • (Array<String>)


2738
2739
2740
2741
2742
2743
2744
# File 'app/models/item.rb', line 2738

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

Fix name.

Returns:

  • (Object)


2553
2554
2555
2556
2557
2558
# File 'app/models/item.rb', line 2553

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

#fixture_keyObject

Fixture key.

Returns:

  • (Object)


2019
2020
2021
# File 'app/models/item.rb', line 2019

def fixture_key
  sku.to_s
end

#freight_classObject

Freight class.

Returns:

  • (Object)


1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
# File 'app/models/item.rb', line 1396

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

Generate refurbished sku.

Returns:

  • (Object)


2470
2471
2472
# File 'app/models/item.rb', line 2470

def generate_refurbished_sku
  "#{sku}-BTK"
end

#get_quantity_from_sqft(input_sqft, add_one_extra = false) ⇒ Object

Get quantity from sqft.

Parameters:

  • input_sqft (Object)
  • add_one_extra (Object) (defaults to: false)

Returns:

  • (Object)


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

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

#google_feed_image_profilesActiveRecord::Associations::CollectionProxy

The associated google feed image profiles.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :google_feed_image_profiles,
-> { website_image_profiles_excluding_card.image_order },
class_name: 'ImageProfile'

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

Gross weight converted.

Parameters:

  • unit (Object) (defaults to: 'lbs')
  • precision (Object) (defaults to: nil)

Returns:

  • (Object)


2293
2294
2295
# File 'app/models/item.rb', line 2293

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

#gtin13Object

Gtin13.

Returns:

  • (Object)


2126
2127
2128
# File 'app/models/item.rb', line 2126

def gtin13
  "0#{upc}"
end

#gtin_typeObject

Gtin type.

Returns:

  • (Object)


947
948
949
950
951
# File 'app/models/item.rb', line 947

def gtin_type
  return :missing if upc.blank?

  Item::UpcBarcode.identify_type(upc)
end

#gtin_type_friendlyObject

Gtin type friendly.

Returns:

  • (Object)


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

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) ⇒ String?

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

Parameters:

  • with_dot (Object) (defaults to: true)

Returns:

  • (String, nil)


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

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?Object

Whether has ean13.

Returns:

  • (Object)


1088
1089
1090
# File 'app/models/item.rb', line 1088

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

#has_features?Object

Whether has features.

Returns:

  • (Object)


2732
2733
2734
# File 'app/models/item.rb', line 2732

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

#has_heating_system?Object

Whether has heating system.

Returns:

  • (Object)


2047
2048
2049
# File 'app/models/item.rb', line 2047

def has_heating_system?
  primary_product_line.get_first_heating_system_type.present?
end

#has_stock?Object

Whether has stock.

Returns:

  • (Object)


1236
1237
1238
# File 'app/models/item.rb', line 1236

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

#has_stock_in_store?(st_id) ⇒ Object

Whether has stock in store.

Parameters:

  • st_id (Object)

Returns:

  • (Object)


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

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

#image_profilesActiveRecord::Associations::CollectionProxy

The associated image profiles.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :image_profiles, -> { image_order }

#image_url(dimensions, options = {}) ⇒ String?

Returns the primary image URL at the given dimensions.

Parameters:

  • dimensions (String)

    dimensions as 'WxH'

  • options (Hash) (defaults to: {})

    ImageKit transform options, forwarded to #image_url2

Options Hash (options):

  • thumbnail (Boolean)

    request a thumbnail

  • named (String, Symbol)

    named transform profile

  • cache (String)

    stable version token for the cache key

Returns:

  • (String, nil)

    the image URL



1493
1494
1495
1496
# File 'app/models/item.rb', line 1493

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

#image_url2(options = {}) ⇒ String?

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

Parameters:

Options Hash (options):

  • size (String)

    dimensions as 'WxH', authoritative over :width/:height

  • width (Integer)

    width in pixels

  • height (Integer)

    height in pixels

  • thumbnail (Boolean)

    request a thumbnail

  • named (String, Symbol)

    named transform profile

  • cache (String)

    stable version token for the cache key

Returns:

  • (String, nil)

    the image URL



1477
1478
1479
1480
1481
1482
1483
# File 'app/models/item.rb', line 1477

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::Associations::CollectionProxy

The associated images.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :images, through: :image_profiles

#import_translation_keysArray<TranslationKeyResource>

This method will import existing translations or link to existing ones

Returns:



2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
# File 'app/models/item.rb', line 2668

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.find_or_create_by_key!(text, TRANSLATION_NAMESPACE)
      # 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

Increment request counter.

Returns:

  • (Object)


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

def increment_request_counter
  return unless persisted?

  Item.update_counters id, requested_counter: 1
end

#inherited_featuresArray<String>

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

Returns:

  • (Array<String>)


2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
# File 'app/models/item.rb', line 2752

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:



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

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?Object

Whether is available to public.

Returns:

  • (Object)


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

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

#is_cable_accessory?Object

Whether is cable accessory.

Returns:

  • (Object)


1947
1948
1949
1950
# File 'app/models/item.rb', line 1947

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

#is_cable_fit_guide?Object

Whether is cable fit guide.

Returns:

  • (Object)


1929
1930
1931
1932
1933
# File 'app/models/item.rb', line 1929

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?Object

Whether is cable heating element.

Returns:

  • (Object)


1816
1817
1818
# File 'app/models/item.rb', line 1816

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

#is_cable_tape?Object

Whether is cable tape.

Returns:

  • (Object)


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

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?Object

Whether is cerazorb.

Returns:

  • (Object)


1851
1852
1853
# File 'app/models/item.rb', line 1851

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

#is_circuit_check?Object

Whether is circuit check.

Returns:

  • (Object)


1837
1838
1839
# File 'app/models/item.rb', line 1837

def is_circuit_check?
  sku == ItemConstants::CIRCUIT_CHECK_SKU
end

#is_cold_lead?Object

Whether is cold lead.

Returns:

  • (Object)


1530
1531
1532
# File 'app/models/item.rb', line 1530

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:



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

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?Object

Whether is cork.

Returns:

  • (Object)


1844
1845
1846
# File 'app/models/item.rb', line 1844

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:



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

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_publicationBoolean

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

Returns:

  • (Boolean)


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

def is_discontinued_or_publication
  is_discontinued? || is_publication?
end

#is_economy_snow_melt_control?Object

Whether is economy snow melt control.

Returns:

  • (Object)


1879
1880
1881
# File 'app/models/item.rb', line 1879

def is_economy_snow_melt_control?
  sku == ItemConstants::ECONOMY_SNOW_MELT_CONTROL_SKU
end

#is_floor_heating_product?Object

Whether is floor heating product.

Returns:

  • (Object)


1523
1524
1525
# File 'app/models/item.rb', line 1523

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

#is_goods?Object

Whether is goods.

Returns:

  • (Object)


1737
1738
1739
# File 'app/models/item.rb', line 1737

def is_goods?
  tax_class == 'g'
end

#is_heating_element?Object

Whether is heating element.

Returns:

  • (Object)


1809
1810
1811
# File 'app/models/item.rb', line 1809

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

#is_infrared_heating_panel?Object

Whether is infrared heating panel.

Returns:

  • (Object)


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

def is_infrared_heating_panel?
  pc_descendant_of_path?(LtreePaths::PC_INFRARED_HEATING_PANELS)
end

#is_infrared_heating_panel_dual_connect?Object

Whether is infrared heating panel dual connect.

Returns:

  • (Object)


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

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

#is_infrared_heating_panel_hardwired?Object

Whether is infrared heating panel hardwired.

Returns:

  • (Object)


1609
1610
1611
# File 'app/models/item.rb', line 1609

def is_infrared_heating_panel_hardwired?
  pc_descendant_of_path?(LtreePaths::PC_INFRARED_HEATING_PANELS_HARDWIRED)
end

#is_infrared_heating_panel_hardwired_control?Object

Whether is infrared heating panel hardwired control.

Returns:

  • (Object)


1630
1631
1632
# File 'app/models/item.rb', line 1630

def is_infrared_heating_panel_hardwired_control?
  sku.in?(ItemConstants::INFRARED_HEATING_PANELS_HARDWIRE_CONTROL_SKUS)
end

#is_infrared_heating_panel_plug_in?Object

Whether is infrared heating panel plug in.

Returns:

  • (Object)


1602
1603
1604
# File 'app/models/item.rb', line 1602

def is_infrared_heating_panel_plug_in?
  pc_descendant_of_path?(LtreePaths::PC_INFRARED_HEATING_PANELS_PLUG_IN)
end

#is_infrared_heating_panel_plug_in_control?Object

Whether is infrared heating panel plug in control.

Returns:

  • (Object)


1623
1624
1625
# File 'app/models/item.rb', line 1623

def is_infrared_heating_panel_plug_in_control?
  sku.in?(ItemConstants::INFRARED_HEATING_PANELS_PLUG_IN_CONTROL_SKUS)
end

#is_install_kit?Object

Whether is install kit.

Returns:

  • (Object)


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

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

#is_junction_box?Object

Whether is junction box.

Returns:

  • (Object)


1886
1887
1888
# File 'app/models/item.rb', line 1886

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

#is_led_mirror?Object

Whether is led mirror.

Returns:

  • (Object)


1588
1589
1590
# File 'app/models/item.rb', line 1588

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

#is_legacy_relay?Object

Whether is legacy relay.

Returns:

  • (Object)


1544
1545
1546
# File 'app/models/item.rb', line 1544

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

#is_made_to_order_led_mirror?Object

Whether is made to order led mirror.

Returns:

  • (Object)


1969
1970
1971
1972
1973
# File 'app/models/item.rb', line 1969

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?Object

Whether is membrane.

Returns:

  • (Object)


1955
1956
1957
# File 'app/models/item.rb', line 1955

def is_membrane?
  Item.sku_is_membrane?(sku)
end

#is_mirror?Object

Whether is mirror.

Returns:

  • (Object)


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

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

#is_mirror_defogger?Object

Whether is mirror defogger.

Returns:

  • (Object)


1637
1638
1639
# File 'app/models/item.rb', line 1637

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

#is_oj_programmable_tstat?Object

Whether is oj programmable tstat.

Returns:

  • (Object)


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

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?Object

Whether is oj programmable wifi tstat.

Returns:

  • (Object)


1779
1780
1781
# File 'app/models/item.rb', line 1779

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

#is_pipe_freeze_protection?Object

Whether is pipe freeze protection.

Returns:

  • (Object)


1684
1685
1686
# File 'app/models/item.rb', line 1684

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

#is_power?Object

Whether is power.

Returns:

  • (Object)


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

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

#is_power_module?Object

Whether is power module.

Returns:

  • (Object)


1537
1538
1539
# File 'app/models/item.rb', line 1537

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:



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

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_radiator?Object

Whether is radiator.

Returns:

  • (Object)


988
989
990
# File 'app/models/item.rb', line 988

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

#is_refurbished?Object

Whether is refurbished.

Returns:

  • (Object)


2477
2478
2479
# File 'app/models/item.rb', line 2477

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:



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

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?Object

Whether is reviewable.

Returns:

  • (Object)


2026
2027
2028
# File 'app/models/item.rb', line 2026

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

#is_roughin_kit?Object

Whether is roughin kit.

Returns:

  • (Object)


1962
1963
1964
# File 'app/models/item.rb', line 1962

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

#is_self_regulating_cable?Object

Whether is self regulating cable.

Returns:

  • (Object)


1572
1573
1574
1575
1576
# File 'app/models/item.rb', line 1572

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?Object

Whether is service.

Returns:

  • (Object)


1751
1752
1753
# File 'app/models/item.rb', line 1751

def is_service?
  tax_class == 'svc'
end

#is_sh_cable?Object

Whether is sh cable.

Returns:

  • (Object)


1922
1923
1924
# File 'app/models/item.rb', line 1922

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

#is_shipping?Object

Whether is shipping.

Returns:

  • (Object)


1758
1759
1760
# File 'app/models/item.rb', line 1758

def is_shipping?
  tax_class == 'shp'
end

#is_slab_heat_mat?Object

Whether is slab heat mat.

Returns:

  • (Object)


1823
1824
1825
# File 'app/models/item.rb', line 1823

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

#is_sm_cable?Object

Whether is sm cable.

Returns:

  • (Object)


1915
1916
1917
# File 'app/models/item.rb', line 1915

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

#is_smartstat?Object

Whether is smartstat.

Returns:

  • (Object)


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

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

#is_snow_melt_plaque?Object

Whether is snow melt plaque.

Returns:

  • (Object)


1872
1873
1874
# File 'app/models/item.rb', line 1872

def is_snow_melt_plaque?
  sku == ItemConstants::SNOW_MELT_PLAQUE_SKU
end

#is_snow_melting_control?Object

Whether is snow melting control.

Returns:

  • (Object)


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

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

#is_snow_melting_mat?Object

Whether is snow melting mat.

Returns:

  • (Object)


1830
1831
1832
# File 'app/models/item.rb', line 1830

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

#is_snow_melting_product?Object

Whether is snow melting product.

Returns:

  • (Object)


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

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

#is_spare_parts?Object

Whether is spare parts.

Returns:

  • (Object)


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

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

#is_thermalsheet?Object

Whether is thermalsheet.

Returns:

  • (Object)


1858
1859
1860
# File 'app/models/item.rb', line 1858

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

#is_thermostat?Object

Whether is thermostat.

Returns:

  • (Object)


1723
1724
1725
# File 'app/models/item.rb', line 1723

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

#is_towel_warmer?Object

Whether is towel warmer.

Returns:

  • (Object)


1670
1671
1672
# File 'app/models/item.rb', line 1670

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

#is_towel_warmer_dual_connect?Object

Whether is towel warmer dual connect.

Returns:

  • (Object)


1691
1692
1693
1694
1695
1696
1697
# File 'app/models/item.rb', line 1691

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?Object

Whether is towel warmer hardwired.

Returns:

  • (Object)


1677
1678
1679
# File 'app/models/item.rb', line 1677

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

#is_towel_warmer_hardwired_control?Object

Whether is towel warmer hardwired control.

Returns:

  • (Object)


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

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

#is_towel_warmer_plug_in?Object

Whether is towel warmer plug in.

Returns:

  • (Object)


1702
1703
1704
# File 'app/models/item.rb', line 1702

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

#is_towel_warmer_plug_in_control?Object

Whether is towel warmer plug in control.

Returns:

  • (Object)


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

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

#is_tz_cable?Object

Whether is tz cable.

Returns:

  • (Object)


1893
1894
1895
1896
# File 'app/models/item.rb', line 1893

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?Object

Whether is tz ruler cable.

Returns:

  • (Object)


1908
1909
1910
# File 'app/models/item.rb', line 1908

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

#is_tz_thin_cable?Object

Whether is tz thin cable.

Returns:

  • (Object)


1901
1902
1903
# File 'app/models/item.rb', line 1901

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

#is_underlayment?Object

Whether is underlayment.

Returns:

  • (Object)


1865
1866
1867
# File 'app/models/item.rb', line 1865

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:



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

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

Item available locales.

Returns:

  • (Object)


1050
1051
1052
# File 'app/models/item.rb', line 1050

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

#item_group_nameString?

The stable feed group identifier — the canonical family's slug (the
backfill preserved the legacy composite string, so itemGroupIds did not
change when grouping moved off path derivation).

Returns:

  • (String, nil)


3106
3107
3108
# File 'app/models/item.rb', line 3106

def item_group_name
  variant_group&.slug
end

#item_grouping_info(include_self: false, axis_filters: {}) ⇒ Data?

Sibling variants of this item — the canonical VariantGroup membership
(explicit, curated), filtered to what the website may sell.

Parameters:

  • include_self (Boolean) (defaults to: false)
  • axis_filters (Hash{String,Symbol=>Object}) (defaults to: {})

    spec token => value

Returns:

  • (Data, nil)

    pc_url/pl_url (paths), item_group_id/item_group_name
    (the group slug), variants (Item relation); nil when ungrouped or the
    category opts out of siblings



3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
# File 'app/models/item.rb', line 3117

def item_grouping_info(include_self: false, axis_filters: {})
  return if product_category.skip_siblings
  return unless (group = variant_group)

  variants = group.effective_items.active.condition_new
                  .joins(:product_category).where.not(ProductCategory[:custom_product].eq(true))
                  .order(:sku)
  axis_filters.each do |token, value|
    variants = variants.with_product_specification(token, value)
  end
  variants = variants.where.not(id: id.to_i) unless include_self

  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: product_category_path, pl_url: sales_product_line_path,
                   item_group_id: group.slug, item_group_name: group.slug, variants:)
end

#item_product_linesActiveRecord::Associations::CollectionProxy

The associated item product lines.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

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

#item_relationsActiveRecord::Associations::CollectionProxy

The associated item relations.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

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

#item_stateObject

Item state.

Returns:

  • (Object)


1016
1017
1018
1019
1020
1021
1022
1023
1024
# File 'app/models/item.rb', line 1016

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

#line_itemsActiveRecord::Associations::CollectionProxy

The associated line items.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :line_items

#literatureLiterature?

The associated literature.

Returns:



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

belongs_to :literature, optional: true

#loaded_rendered_product_specificationsObject

Loaded rendered product specifications.

Returns:

  • (Object)


2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
# File 'app/models/item.rb', line 2925

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

Make upc code.

Returns:

  • (Object)


2133
2134
2135
# File 'app/models/item.rb', line 2133

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

#missing_catalogsObject

Missing catalogs.

Returns:

  • (Object)


3324
3325
3326
# File 'app/models/item.rb', line 3324

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) ⇒ Item?

The newest revision of this publication — walked through the real
successor_item chain (see Publication#revision_chain); the legacy
canonical-SKU prefix match only runs for chains the backfill hasn't
linked yet. +public_only+ filters to revisions visible to the public.

Parameters:

  • public_only (Object) (defaults to: true)

Returns:



2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
# File 'app/models/item.rb', line 2403

def most_recent_revision(public_only = true)
  unless is_publication?
    items = Item.publications.where(*revision_sku_conditions)
    items = items.available_to_public if public_only
    return items.order(sku: :desc).first
  end

  chain = revision_chain
  if public_only
    visible_ids = Item.publications.available_to_public.where(id: chain.map(&:id)).pluck(:id).to_set
    chain = chain.select { |pub| visible_ids.include?(pub.id) }
  end
  chain.last
end

#new_itemItem?

The associated new item.

Returns:



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

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

#new_revision_skuString

Next revision SKU ("TZ-MANUAL-A" → "TZ-MANUAL-B"). The letter bumps off
the chain END (the newest known revision), not a bare SKU prefix scan —
the old scan could bump off an unrelated longer SKU sharing the prefix.

Returns:

  • (String)


2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
# File 'app/models/item.rb', line 2442

def new_revision_sku
  latest_sku = (is_publication? ? latest_revision.sku : canonical_sku)
  sku_match = latest_sku.to_s.match(/(.*)-([[:alpha:]])$/)
  if sku_match && (sku_match.length == 3)
    base     = sku_match[1]
    rev_code = sku_match[2]
    "#{base}-#{rev_code.succ}"
  elsif canonical_sku.present?
    "#{canonical_sku}-A"
  else
    raise "Publication #{id} #{sku} could not be parsed for revision (-a, -b, etc.)"
  end
end

#non_refurbished_skuObject

Non refurbished sku.

Returns:

  • (Object)


2484
2485
2486
# File 'app/models/item.rb', line 2484

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

#order_countObject

Order count.

Returns:

  • (Object)


2515
2516
2517
# File 'app/models/item.rb', line 2515

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

#orderable_online?Object

Whether orderable online.

Returns:

  • (Object)


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

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

Whether orderable online in locale.

Parameters:

  • locale (Object) (defaults to: I18n.locale)

Returns:

  • (Object)


1039
1040
1041
1042
1043
1044
1045
# File 'app/models/item.rb', line 1039

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::Associations::CollectionProxy

The associated orderable view product catalogs.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


333
334
335
336
# File 'app/models/item.rb', line 333

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

#packaging_discrepancy_packingPacking?

The associated packaging discrepancy packing.

Returns:



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

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

#packingsActiveRecord::Associations::CollectionProxy

The associated packings.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_and_belongs_to_many :packings, inverse_of: :items

#past_model?Object

Whether past model.

Returns:

  • (Object)


1463
1464
1465
# File 'app/models/item.rb', line 1463

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)

Examples:

item.pc_descendant_of_path?(LtreePaths::PC_TOWEL_WARMERS)

Parameters:

  • path_slug (Object)

Returns:

  • (Boolean)


1659
1660
1661
1662
1663
1664
1665
# File 'app/models/item.rb', line 1659

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)

Examples:

item.pl_descendant_of_path?(LtreePaths::PL_FLOOR_HEATING)

Parameters:

  • path_slug (Object)

Returns:

  • (Boolean)


1646
1647
1648
1649
1650
1651
1652
# File 'app/models/item.rb', line 1646

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)


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

def plan_sensitive?
  is_heating_element?
end

#preferred_supplierSupplier?

The associated preferred supplier.

Returns:



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

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

#primary_imageImage?

The associated primary image.

Returns:



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

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

#primary_image_url(options = {}) ⇒ String?

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

Parameters:

Options Hash (options):

  • size (String)

    dimensions as 'WxH', authoritative over :width/:height

  • thumbnail (Boolean)

    request a thumbnail

  • named (String, Symbol)

    named transform profile

Returns:

  • (String, nil)

    the image URL



1514
1515
1516
1517
1518
# File 'app/models/item.rb', line 1514

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?

The associated primary product line.

Returns:



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

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

#primary_product_line_slug_ltreeObject

Primary product line slug ltree.

Returns:

  • (Object)


1793
1794
1795
# File 'app/models/item.rb', line 1793

def primary_product_line_slug_ltree
  primary_product_line&.slug_ltree&.to_s
end

#primary_variant_groupsActiveRecord::Relation<VariantGroup>

Groups that point at this item as their curated lead child — cleared (not
blocked) when the item goes away, so the items FK can't abort the destroy.

Returns:

See Also:



326
327
# File 'app/models/item.rb', line 326

has_many :primary_variant_groups, class_name: 'VariantGroup', foreign_key: :primary_item_id,
dependent: :nullify, inverse_of: :primary_item

#product_categoryProductCategory?

The associated product category.

Returns:



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

belongs_to :product_category

#product_category_ancestry_idsObject

Product category ancestry ids.

Returns:

  • (Object)


2498
2499
2500
# File 'app/models/item.rb', line 2498

def product_category_ancestry_ids
  ProductCategory.self_and_ancestors_ids(product_category_id)
end

#product_category_nameObject

Product category name.

Returns:

  • (Object)


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

def product_category_name
  ProductCategoryConstants.name_for(product_category_id)
end

#product_category_pathString

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

Returns:

  • (String)


3058
3059
3060
3061
3062
3063
3064
3065
3066
# File 'app/models/item.rb', line 3058

def product_category_path
  pc_path = pc_path_slugs.to_s
  # Normalize to parent category for grouping purposes
  return LtreePaths::PC_INFRARED_HEATING_PANELS if pc_path.starts_with?('goods.infrared_heating_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

Product category priority.

Returns:

  • (Object)


1159
1160
1161
# File 'app/models/item.rb', line 1159

def product_category_priority
  ProductCategoryConstants.priority_for(product_category_id)
end

#product_category_slugString

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

Returns:

  • (String)


3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
# File 'app/models/item.rb', line 3045

def product_category_slug
  use_pc_url = product_category_url
  use_pc_url = 'goods-infrared-heating-panels' if use_pc_url.starts_with?('goods-infrared-heating-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

Product category url.

Returns:

  • (Object)


1801
1802
1803
# File 'app/models/item.rb', line 1801

def product_category_url
  product_category&.url
end

#product_line_ancestry_idsObject

Product line ancestry ids.

Returns:

  • (Object)


2491
2492
2493
# File 'app/models/item.rb', line 2491

def product_line_ancestry_ids
  ProductLine.self_and_ancestors_ids(product_line_ids)
end

#product_line_for_heating_systemObject

Product line for heating system.

Returns:

  • (Object)


2054
2055
2056
2057
2058
2059
2060
# File 'app/models/item.rb', line 2054

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

Product line for public sales portal.

Returns:

  • (Object)


2040
2041
2042
# File 'app/models/item.rb', line 2040

def product_line_for_public_sales_portal
  primary_product_line&.get_first_public_for_sales
end

#product_line_for_reviewObject

Product line for review.

Returns:

  • (Object)


2033
2034
2035
# File 'app/models/item.rb', line 2033

def product_line_for_review
  primary_product_line&.get_first_reviewable
end

#product_line_ids_constellationArray<Integer>

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

Returns:

  • (Array<Integer>)


1004
1005
1006
1007
1008
1009
1010
1011
# File 'app/models/item.rb', line 1004

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::Associations::CollectionProxy

The associated product lines.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :product_lines, through: :item_product_lines

#product_specifications(filters = {}) ⇒ Array<ProductSpecification>

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

Parameters:

  • filters (Object) (defaults to: {})

Returns:



2597
2598
2599
2600
2601
# File 'app/models/item.rb', line 2597

def product_specifications(filters = {})
  filters[:propagation] ||= %w[unrestricted item]
  res = Item::SpecificationsRetriever.new.process(self, filters)
  res.specifications
end

#product_tax_codeProductTaxCode?

The associated product tax code.

Returns:



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

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

#prune_empty_specsObject

Prune empty specs.

Returns:

  • (Object)


3010
3011
3012
3013
3014
3015
# File 'app/models/item.rb', line 3010

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

Prune unused translations.

Returns:

  • (Object)


3394
3395
3396
3397
3398
3399
# File 'app/models/item.rb', line 3394

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_htmlString?

Returns:

  • (String, nil)


2569
2570
2571
2572
# File 'app/models/item.rb', line 2569

def public_description_html
  (rendered_detailed_description_html.presence || primary_product_line&.description_html.presence)&.html_safe
  # rubocop:enable Rails/OutputSafety
end

#public_description_textObject

Public description text.

Returns:

  • (Object)


2577
2578
2579
# File 'app/models/item.rb', line 2577

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

#public_file_nameObject

Public file name.

Returns:

  • (Object)


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

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

#public_nameObject

Public name.

Returns:

  • (Object)


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

def public_name
  public_short_name.presence || name
end

#public_specificationsArray<OpenStruct>

Only specifications intended for the public are retrieved

Returns:

  • (Array<OpenStruct>)


2718
2719
2720
# File 'app/models/item.rb', line 2718

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

#publication_relationsActiveRecord::Associations::CollectionProxy

The associated publication relations.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

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

#publish_shipping_dimensions_changed_event(options = {}) ⇒ void

This method returns an undefined value.

Publishes Events::ItemShippingDimensionsChanged when shipping-box dims
actually changed on this save. The subscribed async handler
(Shipping::ItemPackingRefreshHandler) runs ItemMd5Extractor in a
background job so the foreground save isn't blocked on Packing.upsert
plus the HABTM item_ids write.

Gate is any_box_changed? only — the broader "no packing exists yet /
existing packing is invalid" conditions from the old inline path are
re-checked inside ItemMd5Extractor on the handler side, so we don't
need a DB hit here just to decide whether to publish.

Replaces the former synchronous set_packaged_items_md5_hash callback.

Parameters:

  • options (Hash) (defaults to: {})

    publish options

Options Hash (options):

  • force (Boolean)

    publish even when no box dimensions changed



3172
3173
3174
3175
3176
3177
3178
3179
# File 'app/models/item.rb', line 3172

def publish_shipping_dimensions_changed_event(options = {})
  return unless any_box_changed? || options[:force]

  Rails.configuration.event_store.publish(
    Events::ItemShippingDimensionsChanged.new(data: { item_id: id }),
    stream_name: "Item-#{id}"
  )
end

#purchase_order_itemsActiveRecord::Associations::CollectionProxy

The associated purchase order items.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :purchase_order_items

#purge_edge_cache(include_product_line: false) ⇒ Object

Purge edge cache.

Parameters:

  • include_product_line (Object) (defaults to: false)

Returns:

  • (Object)


2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
# File 'app/models/item.rb', line 2823

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 = edge_cache_urls
  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?Object

Whether quantities indicator visible.

Returns:

  • (Object)


2065
2066
2067
# File 'app/models/item.rb', line 2065

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

#refurbished_versionItem?

The associated refurbished version.

Returns:



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

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

The associated related source items.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

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

The associated related target items.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

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

The associated related target items for specs.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :related_target_items_for_specs, through: :target_item_relations_for_specs, source: :target_item

#relativesActiveRecord::Relation<Item>

Find all direct siblings and relatives items descendending the product category and product line tree

Returns:

  • (ActiveRecord::Relation<Item>)


3026
3027
3028
3029
3030
3031
3032
3033
3034
# File 'app/models/item.rb', line 3026

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
  siblings = siblings.where(Item[:primary_pl_path_ids].ltree_descendant(primary_pl_path_ids)) if primary_pl_path_ids.present?
  siblings = siblings.where.not(id:) if id
  siblings.limit(100).order(:sku)
end

#render_product_specifications(tokens: nil) ⇒ Object

Render product specifications.

Parameters:

  • tokens (Object) (defaults to: nil)

Returns:

  • (Object)


2774
2775
2776
2777
2778
2779
2780
2781
# File 'app/models/item.rb', line 2774

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

Rendered detailed description html.

Returns:

  • (Object)


2563
2564
2565
# File 'app/models/item.rb', line 2563

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

Rendered product specifications grouped.

Parameters:

  • locale (Object) (defaults to: Mobility.locale)
  • filter_grouping (Object) (defaults to: nil)
  • sort_order (Object) (defaults to: [])

Returns:

  • (Object)


2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
# File 'app/models/item.rb', line 2943

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?

The associated replacement for.

Returns:



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

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

#requires_distributor_id_code?Object

Whether requires distributor id code.

Returns:

  • (Object)


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

def requires_distributor_id_code?
  sku&.start_with?('UWG4-4999', 'AWG4-4999') && !sku&.include?('-WY')
end

#retrieve_faqsObject

Retrieve faqs.

Returns:

  • (Object)


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

def retrieve_faqs
  Item::ArticleRetriever.new(article_types: %w[faq]).process(item: self).articles
end

#returned_rma_itemsActiveRecord::Associations::CollectionProxy

The associated returned rma items.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :returned_rma_items, class_name: 'Rma', foreign_key: :returned_item_id

#revised_publication_for_publicObject

Revised publication for public.

Returns:

  • (Object)


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

def revised_publication_for_public
  revision = most_recent_revision(:public)
  revision == self ? nil : revision
end

#revision_sku_conditionsArray(String, Hash)

Exact revision-chain predicate: the canonical SKU itself or CANONICAL-X
(single letter). Locale variants (…-SPANISH-A), lookalike same-prefix SKUs
(…1), and LIKE wildcards in the canonical (% / _) are all excluded.
Case-insensitive (~*) to match the previous ILIKE semantics.

Returns:

  • (Array(String, Hash))

    conditions for +where(*conditions)+



2462
2463
2464
2465
# File 'app/models/item.rb', line 2462

def revision_sku_conditions
  ["sku = :canonical OR sku ~* :pattern",
   { canonical: canonical_sku, pattern: "^#{Regexp.escape(canonical_sku)}-[A-Za-z]$" }]
end

#revisionsActiveRecord::Relation<Item>

All revisions of this publication, newest SKU first. A revision is the
canonical SKU plus a single-letter suffix (-A, -B) — locale variants like
-SPANISH-A / -FRENCH-A are NOT revisions (see #revision_sku_conditions).

Returns:

  • (ActiveRecord::Relation<Item>)


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

def revisions
  Item.publications.where(*revision_sku_conditions).order(sku: :desc)
end

#rma_reason_codes(include_advanced: true) ⇒ Object

Rma reason codes.

Parameters:

  • include_advanced (Object) (defaults to: true)

Returns:

  • (Object)


1181
1182
1183
1184
1185
1186
1187
# File 'app/models/item.rb', line 1181

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:



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

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:



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

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_nameString?

Returns a name suitable for edit feed with any special character that will
destroy a csv formatting

Returns:

  • (String, nil)


1072
1073
1074
1075
1076
1077
1078
# File 'app/models/item.rb', line 1072

def safe_name
  return if name.blank?

  name.gsub("'", 'ft')
      .gsub('"', 'in')
      .gsub(%r{[^0-9A-Za-z.-/]}, ' ').squish
end

#sales_product_line_pathString?

Returns ltree path for sales product line (preferred - reuses already-loaded ProductLine)

Returns:

  • (String, nil)


3079
3080
3081
3082
3083
# File 'app/models/item.rb', line 3079

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

Sales product line slug.

Returns:

  • (Object)


3071
3072
3073
3074
3075
# File 'app/models/item.rb', line 3071

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?

The associated secondary product category.

Returns:



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

belongs_to :secondary_product_category, class_name: 'ProductCategory', optional: true

#selection_nameObject

Selection name.

Returns:

  • (Object)


2531
2532
2533
# File 'app/models/item.rb', line 2531

def selection_name
  "#{sku} - #{name}#{' [Discontinued]' if is_discontinued?}"
end

#serial_numbersActiveRecord::Associations::CollectionProxy

The associated serial numbers.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :serial_numbers, through: :store_items

#set_grouping(force: false) ⇒ void

This method returns an undefined value.

Records a grouping string identifier, this speeds up variant grouping lookup for
google and reviews.io api calls.

Parameters:

  • force (Object) (defaults to: false)


3185
3186
3187
3188
3189
3190
3191
# File 'app/models/item.rb', line 3185

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

#shift_images(locale: 'en', prefix: nil) ⇒ Hash

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')

Returns:

  • (Hash)

    result with success, errors, changes_made keys



3429
3430
3431
# File 'app/models/item.rb', line 3429

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) ⇒ Array<Float>?

unit_system SI = Standard International, e.g Metric
ucs = United States Customary Unit

Parameters:

  • unit (Object) (defaults to: 'in')
  • precision (Object) (defaults to: 0)

Returns:

  • (Array<Float>, nil)


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

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

Shipping dimensions display.

Returns:

  • (Object)


1414
1415
1416
# File 'app/models/item.rb', line 1414

def shipping_dimensions_display
  "#{shipping_length}″ (L) x #{shipping_width}″ (W) x #{shipping_height}″ (H)"
end

#shipping_dimensions_to_packageObject

Shipping dimensions to package.

Returns:

  • (Object)


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

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

Shipping height converted.

Parameters:

  • unit (Object) (defaults to: 'in')
  • precision (Object) (defaults to: 0)

Returns:

  • (Object)


2193
2194
2195
2196
2197
# File 'app/models/item.rb', line 2193

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

Shipping length converted.

Parameters:

  • unit (Object) (defaults to: 'in')
  • precision (Object) (defaults to: 0)

Returns:

  • (Object)


2171
2172
2173
2174
2175
# File 'app/models/item.rb', line 2171

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::Associations::CollectionProxy

The associated shipping options.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :shipping_options

#shipping_volumeObject

Shipping volume.

Returns:

  • (Object)


1378
1379
1380
1381
1382
# File 'app/models/item.rb', line 1378

def shipping_volume
  boxes.sum { |b| b[0] * b[1] * b[2] }
rescue StandardError
  0
end

#shipping_volume_in_cubic_feetObject

Shipping volume in cubic feet.

Returns:

  • (Object)


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

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

Shipping weight converted.

Parameters:

  • unit (Object) (defaults to: 'lbs')
  • precision (Object) (defaults to: nil)

Returns:

  • (Object)


2311
2312
2313
# File 'app/models/item.rb', line 2311

def shipping_weight_converted(unit: 'lbs', precision: nil)
  express_weight_converted shipping_weight, unit:, precision:
end

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

Shipping width converted.

Parameters:

  • unit (Object) (defaults to: 'in')
  • precision (Object) (defaults to: 0)

Returns:

  • (Object)


2182
2183
2184
2185
2186
# File 'app/models/item.rb', line 2182

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?Object

Whether ships in single box.

Returns:

  • (Object)


3331
3332
3333
# File 'app/models/item.rb', line 3331

def ships_in_single_box?
  !(box2_defined? || box3_defined?)
end

#ships_via_crate?Object

Whether ships via crate.

Returns:

  • (Object)


3233
3234
3235
# File 'app/models/item.rb', line 3233

def ships_via_crate?
  is_made_to_order_led_mirror?
end

#ships_via_freight?Object

Whether ships via freight.

Returns:

  • (Object)


1986
1987
1988
1989
# File 'app/models/item.rb', line 1986

def ships_via_freight?
  # adding current pallet/freight shipping rules
  freight_service?
end

#siblingsObject

Siblings.

Returns:

  • (Object)


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

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_md5ActiveRecord::Relation<Packing>

Finds packings of this item but only for a single quantity

Returns:

  • (ActiveRecord::Relation<Packing>)


3222
3223
3224
3225
3226
3227
3228
# File 'app/models/item.rb', line 3222

def single_item_packings_from_md5
  if (md5 = Shipping::Md5HashItem.process(self).md5)
    packings.where(md5:)
  else
    Packing.none
  end
end

#site_mapsActiveRecord::Associations::CollectionProxy

The associated site maps.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

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

#sku_and_nameObject

Sku and name.

Returns:

  • (Object)


1451
1452
1453
# File 'app/models/item.rb', line 1451

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

#source_item_relationsActiveRecord::Associations::CollectionProxy

The associated source item relations.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :source_item_relations, class_name: 'ItemRelation', foreign_key: :target_item_id, dependent: :destroy

#specific_itemsActiveRecord::Associations::CollectionProxy

The associated specific items.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :specific_items, through: :item_relations, class_name: 'Item', source: :item

#specific_publicationsActiveRecord::Associations::CollectionProxy

The associated specific publications.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :specific_publications, through: :publication_relations, class_name: 'Item', source: :publication

#specificationsArray<OpenStruct>

Retrieves rendered specifications as an object

Returns:

  • (Array<OpenStruct>)


2705
2706
2707
# File 'app/models/item.rb', line 2705

def specifications
  rendered_product_specifications&.values&.map { |s| OpenStruct.new(s) } || []
end

#specifications_for_item_labelObject

Specifications for item label.

Returns:

  • (Object)


2725
2726
2727
# File 'app/models/item.rb', line 2725

def specifications_for_item_label
  specifications.select { |rps| rps.print_on_item_label && rps.output.present? }.sort_by(&:name)
end

#specifications_groupedObject

Specifications grouped.

Returns:

  • (Object)


2712
2713
2714
# File 'app/models/item.rb', line 2712

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)


1081
1082
1083
# File 'app/models/item.rb', line 1081

def star?
  (tags || []).intersect?(%w[Star star])
end

#store_itemsActiveRecord::Associations::CollectionProxy

The associated store items.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :store_items, inverse_of: :item, dependent: :destroy

#successor_itemItem?

The associated successor item.

Returns:



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

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:



803
804
805
# File 'app/models/item.rb', line 803

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:



803
804
805
# File 'app/models/item.rb', line 803

delegate :suggested_item_stock_threshold,
:suggested_item_applies_to,
to: :suggested_item_tool

#superceded_itemsActiveRecord::Associations::CollectionProxy

The associated superceded items.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :superceded_items, class_name: 'Item', foreign_key: :successor_item_id

#supplier_itemSupplierItem?

The associated supplier item.

Returns:



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

belongs_to :supplier_item, inverse_of: :items, optional: true

#supplier_itemsActiveRecord::Associations::CollectionProxy

The associated supplier items.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :supplier_items, inverse_of: :item, dependent: :destroy

#supplier_skusObject

Supplier skus.

Returns:

  • (Object)


1057
1058
1059
# File 'app/models/item.rb', line 1057

def supplier_skus
  ([secondary_model_number] + supplier_items.active.distinct.pluck(:supplier_sku)).filter_map(&:presence).uniq
end

#support_product_line_pathString?

Returns ltree path for support product line (preferred - reuses already-loaded ProductLine)

Returns:

  • (String, nil)


3096
3097
3098
3099
3100
# File 'app/models/item.rb', line 3096

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

Support product line slug.

Returns:

  • (Object)


3088
3089
3090
3091
3092
# File 'app/models/item.rb', line 3088

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

Tags display.

Returns:

  • (Object)


816
817
818
# File 'app/models/item.rb', line 816

def tags_display
  tags.join(', ')
end

#target_item_relationsActiveRecord::Associations::CollectionProxy

The associated target item relations.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :target_item_relations, class_name: 'ItemRelation', foreign_key: :source_item_id, dependent: :destroy

#target_item_relations_for_specsActiveRecord::Associations::CollectionProxy

The associated target item relations for specs.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

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

Third party skus.

Parameters:

  • limit (Object) (defaults to: 1)

Returns:

  • (Object)


1065
1066
1067
# File 'app/models/item.rb', line 1065

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

Thumbnail url.

Parameters:

  • size (Object) (defaults to: '30x30')

Returns:

  • (Object)


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

def thumbnail_url(size: '30x30')
  image_url2(size:, thumbnail: true)
end

#to_liquidObject

To liquid.

Returns:

  • (Object)


3420
3421
3422
# File 'app/models/item.rb', line 3420

def to_liquid
  Liquid::ItemDrop.new self
end

#to_packdimsObject

To packdims.

Returns:

  • (Object)


3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
# File 'app/models/item.rb', line 3196

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::PACKDIM_CONTAINER_INTS[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::PACKDIM_CONTAINER_INTS[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::PACKDIM_CONTAINER_INTS[container_type]]
  end
  packdims
end

#to_sObject

To s.

Returns:

  • (Object)


2522
2523
2524
2525
2526
# File 'app/models/item.rb', line 2522

def to_s
  s = "#{sku} - #{name}"
  s += ' (Discontinued)' if is_discontinued?
  s
end

#total_shipping_weightObject

Total shipping weight.

Returns:

  • (Object)


3404
3405
3406
# File 'app/models/item.rb', line 3404

def total_shipping_weight
  boxes.sum { |b| b[3].to_f }
end

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

Total shipping weight converted.

Parameters:

  • unit (Object) (defaults to: 'lbs')
  • precision (Object) (defaults to: nil)

Returns:

  • (Object)


2302
2303
2304
# File 'app/models/item.rb', line 2302

def total_shipping_weight_converted(unit: 'lbs', precision: nil)
  express_weight_converted total_shipping_weight, unit:, precision:
end

#total_shipping_weight_of_kit_componentsObject

Total shipping weight of kit components.

Returns:

  • (Object)


3411
3412
3413
3414
3415
# File 'app/models/item.rb', line 3411

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::Associations::CollectionProxy

The associated translation key resources.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :translation_key_resources, as: :resource, dependent: :destroy

#update_boxes_from_packages(packages) ⇒ Object

Update boxes from packages.

Parameters:

  • packages (Object)

Returns:

  • (Object)


1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
# File 'app/models/item.rb', line 1344

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

Update rendered product specifications.

Parameters:

  • user_id (Object) (defaults to: nil)
  • tokens (Object) (defaults to: nil)
  • locales (Object) (defaults to: nil)

Returns:

  • (Object)


2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
# File 'app/models/item.rb', line 2871

def update_rendered_product_specifications(user_id: nil, tokens: nil, locales: nil)
  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
    purge_edge_cache(include_product_line: false) # Async as well
  end

  res
end

#uploadUpload?

The associated upload.

Returns:



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

belongs_to :upload, optional: true

#uploadsActiveRecord::Associations::CollectionProxy

The associated uploads.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_and_belongs_to_many :uploads, dependent: :destroy

#url_pathsObject

Url paths.

Returns:

  • (Object)


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

def url_paths
  p = []
  p += primary_product_line.url_paths
  p << sku
  p.compact
end

#variant_axis_tokensArray<String>

The variant axes of this item's family (e.g. ["voltage", "size"]).

Returns:

  • (Array<String>)


3139
3140
3141
# File 'app/models/item.rb', line 3139

def variant_axis_tokens
  variant_group&.axis_tokens || []
end

#variant_groupVariantGroup?

The item's DEFAULT variant family (channel-agnostic). Reads the loaded
association when present so collection renders can preload
:variant_groups and stay N+1-free.

Returns:



824
825
826
827
828
829
830
# File 'app/models/item.rb', line 824

def variant_group
  if variant_groups.loaded?
    variant_groups.detect { |group| group.parent_id.nil? && group.catalog_id.nil? }
  else
    variant_groups.merge(VariantGroup.defaults).first
  end
end

#variant_group_for(catalog = nil) ⇒ VariantGroup?

The variant family governing this item on a catalog: an explicit override
membership wins, otherwise the default family resolves the catalog
(inheriting the default's members when the override has none).

Parameters:

  • catalog (Catalog, Integer, nil) (defaults to: nil)

    catalog or id; nil = the default family

Returns:



837
838
839
840
841
842
843
844
845
846
847
848
849
# File 'app/models/item.rb', line 837

def variant_group_for(catalog = nil)
  if catalog.present?
    catalog_id = catalog.respond_to?(:id) ? catalog.id : catalog.to_i
    explicit =
      if variant_groups.loaded?
        variant_groups.detect { |group| group.catalog_id == catalog_id && group.parent_id.present? }
      else
        variant_groups.where(catalog_id: catalog_id).where.not(parent_id: nil).first
      end
    return explicit if explicit
  end
  variant_group&.resolved_for(catalog)
end

#variant_group_membersActiveRecord::Relation<VariantGroupMember>

Variant-family memberships: at most one DEFAULT family plus one override
per channel (model-validated). See VariantGroup for the inheritance rules.

Returns:

See Also:



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

has_many :variant_group_members, dependent: :destroy

#variant_groupsActiveRecord::Associations::CollectionProxy

The associated variant groups.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :variant_groups, through: :variant_group_members

#variant_sort_keysArray<String>

The family's variant-picker sort order.

Returns:

  • (Array<String>)


3145
3146
3147
# File 'app/models/item.rb', line 3145

def variant_sort_keys
  variant_group&.effective_sort_keys || []
end

#variants(include_self: false) ⇒ ActiveRecord::Relation<Item>

A more strict version of relatives, we don't look at descendants, only exact matches

Parameters:

  • include_self (Object) (defaults to: false)

Returns:

  • (ActiveRecord::Relation<Item>)


3039
3040
3041
# File 'app/models/item.rb', line 3039

def variants(include_self: false)
  item_grouping_info(include_self:)&.variants || Item.none
end

#versions_for_audit_trail(_params = {}) ⇒ Object

Versions for audit trail.

Parameters:

  • _params (Object) (defaults to: {})

Returns:

  • (Object)


3437
3438
3439
3440
3441
3442
3443
3444
3445
# File 'app/models/item.rb', line 3437

def versions_for_audit_trail(_params = {})
  RecordVersion.where(<<~SQL.squish, id:, item_id_json: { item_id: id }.to_json)
    (item_type = 'Item' AND item_id = :id)
    OR (
      item_type = 'ImageProfile'
      AND reference_data @> :item_id_json
    )
  SQL
end

#view_product_catalogsActiveRecord::Associations::CollectionProxy

The associated view product catalogs.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


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

has_many :view_product_catalogs

#warm_cache(include_product_line: true) ⇒ void

This method returns an undefined value.

Finds the associated sitemap (url) end points of this item and its product line
Perform a cache warmup

Parameters:

  • include_product_line (Object) (defaults to: true)


2801
2802
2803
2804
2805
2806
2807
2808
# File 'app/models/item.rb', line 2801

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?Object

Whether will be restricted for sales.

Returns:

  • (Object)


882
883
884
885
# File 'app/models/item.rb', line 882

def will_be_restricted_for_sales?
  restricted_for_sales ||
    (product_category&.restricted_for_sales && primary_product_line&.restricted_for_sales)
end