Module: Models::ItemSpecificationHelper

Extended by:
ActiveSupport::Concern
Includes:
Memery
Included in:
Item
Defined in:
app/concerns/models/item_specification_helper.rb

Overview

ActiveSupport::Concern mixin: item specification helper.

Constant Summary collapse

PER_ITEM_SPEC_TOKENS =

Tokens whose values are coincidentally identical across unrelated items but should
never be collapsed into a single ProductSpecification row. Two towel warmers at 175W are
not "the same spec" the way two product names "Pipe Adapter" are — editing one must not
silently mutate the others.

The nightly consolidator is gone, and with it the only consumer of the merge opt-out —
which is why this helper no longer sets that flag. The column survives as
legacy_do_not_merge, read and written now only by the finished one-time electrical
migrations under lib/online_migrations/data_migrations/. The list stays because those
migrations key off it, and because it still records which tokens must be written
per-item rather than shared.

%i[watts voltage amps ohms].freeze

Instance Method Summary collapse

Instance Method Details

#ampsObject

Amps.

Returns:

  • (Object)


537
538
539
540
# File 'app/concerns/models/item_specification_helper.rb', line 537

def amps
  a = spec_value(:amps)
  a.is_a?(Numeric) ? a : nil
end

#amps=(val) ⇒ void

This method returns an undefined value.

Sets the amps.

Parameters:

  • val (Object)


545
546
547
# File 'app/concerns/models/item_specification_helper.rb', line 545

def amps=(val)
  create_or_set_spec_value(token: :amps, name: 'Amps', units: 'A', grouping: 'Electrical', text_blurb: val)
end

#amps_static?Boolean

Whether amps static.

Returns:

  • (Boolean)


736
737
738
# File 'app/concerns/models/item_specification_helper.rb', line 736

def amps_static?
  rendered_specifications.dig(:amps, :static).to_b
end

#area(output_unit: :sqft) ⇒ Object

Area.

Parameters:

  • output_unit (Symbol) (defaults to: :sqft)

Returns:

  • (Object)


313
314
315
316
317
318
319
320
321
322
323
# File 'app/concerns/models/item_specification_helper.rb', line 313

def area(output_unit: :sqft)
  v = spec_value(:coverage, output_unit:)
  v ||= spec_value(:sqft, output_unit: :output_unit)
  if v.nil? && length.present? && width.present?
    cv = (length(output_unit: :in) * width(output_unit: :in))

    cvunit = RubyUnits::Unit.new("#{cv} sqin")
    v = cvunit.convert_to(output_unit.to_s).scalar.to_f
  end
  v
end

#aspectObject

Aspect.

Returns:

  • (Object)


916
917
918
919
920
921
922
923
924
925
926
# File 'app/concerns/models/item_specification_helper.rb', line 916

def aspect
  return unless (height = spec_value(:height)).present? && (width == spec_value(:width)).present?

  if height.to_i == width.to_i
    'Vertical / Horizontal'
  elsif height.to_i > width.to_i
    'Vertical'
  else
    'Horizontal'
  end
end

#btu_per_hourObject

Btu per hour.

Returns:

  • (Object)


785
786
787
788
789
790
791
792
793
# File 'app/concerns/models/item_specification_helper.rb', line 785

def btu_per_hour
  return unless (w = watts.presence)
  return unless w.to_f.positive?

  # If we have a thermal efficiency we will use it to calculate or assume 100
  efficiency = spec_value(:thermal_output_efficiency)&.to_f || 100.0
  # Apply the BTU conversion
  (w.to_f * (efficiency / 100.0) * RoomConfiguration::BTU_PER_HOUR_PER_WATT).round
end

#calculate_amps(precision: 2) ⇒ Object

Calculate amps.

Parameters:

  • precision (Integer) (defaults to: 2)

Returns:

  • (Object)


577
578
579
# File 'app/concerns/models/item_specification_helper.rb', line 577

def calculate_amps(precision: 2)
  ohms_law_calculator.current&.round(precision)
end

#calculate_coverage_for_membrane(output_unit: :sqft) ⇒ Object

Calculate coverage for membrane.

Parameters:

  • output_unit (Symbol) (defaults to: :sqft)

Returns:

  • (Object)


296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'app/concerns/models/item_specification_helper.rb', line 296

def calculate_coverage_for_membrane(output_unit: :sqft)
  coverage = 0.0
  if is_membrane?
    coverage = spec_value(:coverage, output_unit:)
  elsif is_kit?
    kit_target_item_relations.includes(:target_item).find_each do |tir|
      next if tir.target_item.nil? || !tir.target_item.is_membrane?

      coverage += tir.target_item.spec_value(:coverage, output_unit:) * tir.quantity
    end
  end
  coverage&.positive? ? coverage.round(2) : nil
end

#calculate_geometric_sqftObject

Calculate geometric sqft.

Returns:

  • (Object)


828
829
830
# File 'app/concerns/models/item_specification_helper.rb', line 828

def calculate_geometric_sqft
  calculate_sqft(heated_area: false)
end

#calculate_heated_area_for_panels(output_unit: :sqft) ⇒ Object

Calculates the coverage area for an infrared heating panel based on the watts, output_unit can be :sqm for square meters.

Parameters:

  • output_unit (Symbol) (defaults to: :sqft)

Returns:

  • (Object)


270
271
272
273
274
275
276
277
278
279
# File 'app/concerns/models/item_specification_helper.rb', line 270

def calculate_heated_area_for_panels(output_unit: :sqft)
  return if (w = watts).blank?
  return unless is_infrared_heating_panel?

  coverage_panel_in_sq_ft = (w.to_f / 7).ceil
  return coverage_panel_in_sq_ft if output_unit == :sqft

  coverage_unitized = "#{coverage_panel_in_sq_ft} sqft"
  RubyUnits::Unit.new(coverage_unitized).convert_to(output_unit.to_s).scalar.to_f.round
end

#calculate_heated_area_sqft_for_panelsObject

Calculate heated area sqft for panels.

Returns:

  • (Object)


283
284
285
# File 'app/concerns/models/item_specification_helper.rb', line 283

def calculate_heated_area_sqft_for_panels
  calculate_heated_area_for_panels(output_unit: :sqft)
end

#calculate_heated_area_sqm_for_panelsObject

Calculate heated area sqm for panels.

Returns:

  • (Object)


289
290
291
# File 'app/concerns/models/item_specification_helper.rb', line 289

def calculate_heated_area_sqm_for_panels
  calculate_heated_area_for_panels(output_unit: :sqm)
end

#calculate_kilowatts(precision: 2) ⇒ Object

Calculate kilowatts.

Parameters:

  • precision (Integer) (defaults to: 2)

Returns:

  • (Object)


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

def calculate_kilowatts(precision: 2)
  return unless (w = watts || calculate_watts)

  (w.to_f / 1000).round(precision)
end

#calculate_number_of_fixing_strips_at_3_inObject

Calculate number of fixing strips at 3 in.

Returns:

  • (Object)


404
405
406
# File 'app/concerns/models/item_specification_helper.rb', line 404

def calculate_number_of_fixing_strips_at_3_in
  calculate_number_of_fixing_strips_at_cable_spacing(3)
end

#calculate_number_of_fixing_strips_at_4_inObject

Calculate number of fixing strips at 4 in.

Returns:

  • (Object)


410
411
412
# File 'app/concerns/models/item_specification_helper.rb', line 410

def calculate_number_of_fixing_strips_at_4_in
  calculate_number_of_fixing_strips_at_cable_spacing(4)
end

#calculate_number_of_fixing_strips_at_5_inObject

Calculate number of fixing strips at 5 in.

Returns:

  • (Object)


416
417
418
# File 'app/concerns/models/item_specification_helper.rb', line 416

def calculate_number_of_fixing_strips_at_5_in
  calculate_number_of_fixing_strips_at_cable_spacing(5)
end

#calculate_number_of_fixing_strips_at_cable_spacing(spacing_in_inches = 4) ⇒ Object

Calculate number of fixing strips at cable spacing.

Parameters:

  • spacing_in_inches (Integer) (defaults to: 4)

Returns:

  • (Object)


389
390
391
392
393
394
395
396
397
398
399
400
# File 'app/concerns/models/item_specification_helper.rb', line 389

def calculate_number_of_fixing_strips_at_cable_spacing(spacing_in_inches = 4)
  return unless (cable_length = linear_ft)

  coefficients = { 3 => 0.1875, 4 => 0.225, 5 => 0.3125 }
  raise "Invalid spacing #{spacing_in_inches}, must be #{coefficients.keys.join(', ')}" unless spacing_in_inches.in?(coefficients.keys)

  coefficient = coefficients[spacing_in_inches]
  qty_fixing_strips = ((cable_length * coefficient) + 6)
  qty_fixing_strips = (qty_fixing_strips / 10).ceil * 10
  # minimum 30
  [30, qty_fixing_strips].max
end

#calculate_ohms(precision: 2) ⇒ Object

Calculate ohms.

Parameters:

  • precision (Integer) (defaults to: 2)

Returns:

  • (Object)


694
695
696
# File 'app/concerns/models/item_specification_helper.rb', line 694

def calculate_ohms(precision: 2)
  ohms_law_calculator.resistance&.round(precision)
end

#calculate_ohms_per_ftObject

Calculate ohms per ft.

Returns:

  • (Object)


683
684
685
686
687
688
689
# File 'app/concerns/models/item_specification_helper.rb', line 683

def calculate_ohms_per_ft
  l = spec_value(:heating_cable_length, output_unit: 'ft') || spec_value(:length, output_unit: 'ft')
  o = ohms
  return unless o.is_a?(Numeric) && l.is_a?(Numeric) && o.positive? && l.positive?

  (o.to_f / l).round(3)
end

#calculate_size_2d(output_unit: nil, precision: nil) ⇒ Object

Calculate size 2d.

Parameters:

  • output_unit (Object, nil) (defaults to: nil)
  • precision (Object, nil) (defaults to: nil)

Returns:

  • (Object)


599
600
601
602
603
604
605
606
607
# File 'app/concerns/models/item_specification_helper.rb', line 599

def calculate_size_2d(output_unit: nil, precision: nil)
  output_unit ||= spec_unit(:width)
  precision ||= { mm: 0, in: 1, m: 2, ft: 2 }[output_unit&.to_sym]
  s = [
    spec_value(:width, output_unit:, precision:),
    spec_value(:length, output_unit:, precision:) || spec_value(:height, output_unit:, precision:)
  ].join(' x ')
  "#{s} #{output_unit}"
end

#calculate_size_2d_ftObject

Calculate size 2d ft.

Returns:

  • (Object)


656
657
658
# File 'app/concerns/models/item_specification_helper.rb', line 656

def calculate_size_2d_ft
  calculate_size_2d(output_unit: 'ft', precision: 2)
end

#calculate_size_2d_inObject

Calculate size 2d in.

Returns:

  • (Object)


650
651
652
# File 'app/concerns/models/item_specification_helper.rb', line 650

def calculate_size_2d_in
  calculate_size_2d(output_unit: 'in', precision: 0)
end

#calculate_size_2d_metric_cmObject

Calculate size 2d metric cm.

Returns:

  • (Object)


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

def calculate_size_2d_metric_cm
  calculate_size_2d(output_unit: 'cm', precision: 0)
end

#calculate_size_2d_metric_mmObject

Calculate size 2d metric mm.

Returns:

  • (Object)


662
663
664
# File 'app/concerns/models/item_specification_helper.rb', line 662

def calculate_size_2d_metric_mm
  calculate_size_2d(output_unit: 'mm', precision: 0)
end

#calculate_size_3d(output_unit: nil, precision: nil) ⇒ Object

Calculate size 3d.

Parameters:

  • output_unit (Object, nil) (defaults to: nil)
  • precision (Object, nil) (defaults to: nil)

Returns:

  • (Object)


613
614
615
616
617
618
619
620
621
622
# File 'app/concerns/models/item_specification_helper.rb', line 613

def calculate_size_3d(output_unit: nil, precision: nil)
  output_unit ||= spec_unit(:width)
  precision ||= { mm: 0, in: 1, m: 2, ft: 2 }[output_unit&.to_sym]
  s = [
    spec_value(:width, output_unit:, precision:),
    spec_value(:length, output_unit:, precision:) || spec_value(:height, output_unit:, precision:),
    spec_value(:depth, output_unit:, precision:)
  ].join(' x ')
  "#{s} #{output_unit}"
end

#calculate_size_3d_ftObject

Calculate size 3d ft.

Returns:

  • (Object)


644
645
646
# File 'app/concerns/models/item_specification_helper.rb', line 644

def calculate_size_3d_ft
  calculate_size_3d(output_unit: 'ft', precision: 2)
end

#calculate_size_3d_inObject

Calculate size 3d in.

Returns:

  • (Object)


638
639
640
# File 'app/concerns/models/item_specification_helper.rb', line 638

def calculate_size_3d_in
  calculate_size_3d(output_unit: 'in', precision: 0)
end

#calculate_size_3d_metric_cmObject

Calculate size 3d metric cm.

Returns:

  • (Object)


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

def calculate_size_3d_metric_cm
  calculate_size_3d(output_unit: 'cm', precision: 0)
end

#calculate_size_3d_metric_mmObject

Calculate size 3d metric mm.

Returns:

  • (Object)


626
627
628
# File 'app/concerns/models/item_specification_helper.rb', line 626

def calculate_size_3d_metric_mm
  calculate_size_3d(output_unit: 'mm', precision: 0)
end

#calculate_sqft(cable_spacing: nil, heated_area: true) ⇒ Object

Calculate sqft.

Parameters:

  • cable_spacing (Object, nil) (defaults to: nil)
  • heated_area (Boolean) (defaults to: true)

Returns:

  • (Object)


836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
# File 'app/concerns/models/item_specification_helper.rb', line 836

def calculate_sqft(cable_spacing: nil, heated_area: true)
  calculated_area = nil
  effective_width = BigDecimal(width, 2) if width
  effective_length = BigDecimal(length, 2) if length
  if effective_length&.positive?
    case product_category.surface_calculation_method
    when 'standard'
      if effective_width&.positive?
        if (is_slab_heat_mat? || is_snow_melting_mat?) && heated_area
          # Apply padding rule of 3" (1.5" per side bleed out for heat)
          effective_width += 3.0
        end
        calculated_area = (effective_length * effective_width / 144.0) if effective_width.positive?
      end
    when 'cable_spacing'
      cable_spacing = calculate_ideal_cable_spacing unless cable_spacing && (cable_spacing > 0.0)
      effective_width = (cable_spacing || effective_width).to_f
      calculated_area = (effective_length * effective_width / 144.0) if effective_width.positive?
    end
  end
  static_area = rendered_specifications.dig(:coverage, :raw) if rendered_specifications.dig(:coverage, :static).to_b
  effective_area = calculated_area || static_area
  effective_area = BigDecimal(effective_area, 2) if effective_area
  effective_area
end

#calculate_thermal_power_per_hourInteger?

This is like btu per hour but kW

Returns:

  • (Integer, nil)


797
798
799
800
801
802
803
804
805
# File 'app/concerns/models/item_specification_helper.rb', line 797

def calculate_thermal_power_per_hour
  return unless (w = watts.presence)
  return unless w.to_f.positive?

  # If we have a thermal efficiency we will use it to calculate or assume 100
  efficiency = spec_value(:thermal_output_efficiency)&.to_f || 100.0
  # Convert to kw/h
  (w.to_f * (efficiency / 100.0) / 1000).round(2)
end

#calculate_volts(precision: 0) ⇒ Object

The voltage V in volts (V) is equal to the current I in amps (A) times the resistance R in ohms (Ω):

Parameters:

  • precision (Integer) (defaults to: 0)

Returns:

  • (Object)


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

def calculate_volts(precision: 0)
  ohms_law_calculator.voltage&.round(precision)
end

#calculate_watts(precision: 0) ⇒ Object

The power P in watts (W) is equal to the voltage V in volts (V) times the current I in amps (A):

Parameters:

  • precision (Integer) (defaults to: 0)

Returns:

  • (Object)


591
592
593
# File 'app/concerns/models/item_specification_helper.rb', line 591

def calculate_watts(precision: 0)
  ohms_law_calculator.power&.round(precision)
end

#calculate_watts_per_sqft(precision: 2) ⇒ Object

Calculate watts per sqft.

Parameters:

  • precision (Integer) (defaults to: 2)

Returns:

  • (Object)


816
817
818
819
820
821
822
823
824
# File 'app/concerns/models/item_specification_helper.rb', line 816

def calculate_watts_per_sqft(precision: 2)
  tw = watts || calculate_watts
  sqft = spec_value(:coverage) || calculate_sqft
  return unless tw&.positive? && sqft&.positive?

  wps = (BigDecimal(tw.to_s) / sqft).round(precision)
  wps = wps.to_i if wps.frac.zero? # Makes 15.0 => 15
  wps
end

#can_use_amps_for_calculation?Boolean

Whether can use amps for calculation.

Returns:

  • (Boolean)


742
743
744
# File 'app/concerns/models/item_specification_helper.rb', line 742

def can_use_amps_for_calculation?
  amps_static? && amps && amps > 0
end

#can_use_ohms_for_calculation?Boolean

Whether can use ohms for calculation.

Returns:

  • (Boolean)


718
719
720
# File 'app/concerns/models/item_specification_helper.rb', line 718

def can_use_ohms_for_calculation?
  ohms_static? && ohms && ohms > 0
end

#can_use_voltage_for_calculation?Boolean

Whether can use voltage for calculation.

Returns:

  • (Boolean)


730
731
732
# File 'app/concerns/models/item_specification_helper.rb', line 730

def can_use_voltage_for_calculation?
  voltage_static? && voltage && voltage > 0
end

#can_use_watts_for_calculation?Boolean

Whether can use watts for calculation.

Returns:

  • (Boolean)


706
707
708
# File 'app/concerns/models/item_specification_helper.rb', line 706

def can_use_watts_for_calculation?
  watts_static? && watts && watts > 0
end

#cold_lead_lengthObject

Cold lead length.

Returns:

  • (Object)


256
257
258
# File 'app/concerns/models/item_specification_helper.rb', line 256

def cold_lead_length
  spec_value(:cold_lead_length)
end

#cold_lead_length=(val) ⇒ void

This method returns an undefined value.

Sets the cold lead length.

Parameters:

  • val (Object)


263
264
265
# File 'app/concerns/models/item_specification_helper.rb', line 263

def cold_lead_length=(val)
  create_or_set_spec_value(token: :cold_lead_length, name: 'Cold Lead Length', units: 'in', grouping: 'Electrical', text_blurb: val)
end

#control_capacityObject

Control capacity.

Returns:

  • (Object)


773
774
775
# File 'app/concerns/models/item_specification_helper.rb', line 773

def control_capacity
  (maximum_current || 0) * (num_poles || 1)
end

#coverage_at_3_5_inObject

Coverage at 3 5 in.

Returns:

  • (Object)


348
349
350
351
352
# File 'app/concerns/models/item_specification_helper.rb', line 348

def coverage_at_3_5_in
  return unless (l = linear_ft)

  (l.to_f * 0.291667).round(1)
end

#coverage_at_3_75_inObject

Coverage at 3 75 in.

Returns:

  • (Object)


356
357
358
359
360
# File 'app/concerns/models/item_specification_helper.rb', line 356

def coverage_at_3_75_in
  return unless (l = linear_ft)

  (l.to_f * 0.3125).round(1)
end

#coverage_at_3_inObject

Coverage at 3 in.

Returns:

  • (Object)


340
341
342
343
344
# File 'app/concerns/models/item_specification_helper.rb', line 340

def coverage_at_3_in
  return unless (l = linear_ft)

  (l.to_f * 0.25).round(1)
end

#coverage_at_4_5_inObject

Coverage at 4 5 in.

Returns:

  • (Object)


372
373
374
375
376
# File 'app/concerns/models/item_specification_helper.rb', line 372

def coverage_at_4_5_in
  return unless (l = linear_ft)

  (l.to_f * 0.375).round(1)
end

#coverage_at_4_inObject

Coverage at 4 in.

Returns:

  • (Object)


364
365
366
367
368
# File 'app/concerns/models/item_specification_helper.rb', line 364

def coverage_at_4_in
  return unless (l = linear_ft)

  (l.to_f * 0.333333).round(1)
end

#coverage_at_5_inObject

Coverage at 5 in.

Returns:

  • (Object)


380
381
382
383
384
# File 'app/concerns/models/item_specification_helper.rb', line 380

def coverage_at_5_in
  return unless (l = linear_ft)

  (l.to_f * 0.416667).round(1)
end

#coverage_formattedObject

Coverage formatted.

Returns:

  • (Object)


900
901
902
# File 'app/concerns/models/item_specification_helper.rb', line 900

def coverage_formatted
  spec_output(:coverage)
end

#create_or_set_spec_value(grouping:, text_blurb:, token: nil, name: nil, method: 'text', units: nil, visibility: 'open_visibility', locale: nil, skip_spec_refresh: false, link_to_product_line: false) ⇒ Object

Create or set spec value.

Parameters:

  • grouping (Object)
  • text_blurb (Object)
  • token (Object, nil) (defaults to: nil)
  • name (Object, nil) (defaults to: nil)
  • method (String) (defaults to: 'text')
  • units (Object, nil) (defaults to: nil)
  • visibility (String) (defaults to: 'open_visibility')
  • locale (Object, nil) (defaults to: nil)
  • skip_spec_refresh (Boolean) (defaults to: false)
  • link_to_product_line (Boolean) (defaults to: false)

Returns:

  • (Object)

Raises:

  • (ArgumentError)


120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'app/concerns/models/item_specification_helper.rb', line 120

def create_or_set_spec_value(grouping:, text_blurb:, token: nil, name: nil, method: 'text', units: nil, visibility: 'open_visibility', locale: nil, skip_spec_refresh: false, link_to_product_line: false)
  return if text_blurb.blank? # No point

  raise ArgumentError, 'Must specify token or name or both but not none' if token.blank? && name.blank?

  # If we have a name but no token, we derive it
  token ||= ProductSpecification.generate_token_from_name_and_grouping(name, grouping)
  token = token.to_sym
  # If we have a token but no name, we derive the name
  name ||= ProductSpecification.generate_name_from_token_and_grouping(token, grouping)

  attrs = { name: name, token: token, method: method, visibility: visibility, grouping: grouping }
  # if locale is specified but is en-US, we automatically will fall back to en, it makes it easier this way
  locale = :en if locale == :'en-US'
  localized_field_units = nil
  localized_field_text_blurb = nil
  %w[units text_blurb].each do |fn|
    suffix = locale.present? ? "_#{LocaleUtility.parameterized_locale(locale)}" : '_en'
    fnl = "#{fn}#{suffix}"
    localized_field_units = fnl.to_sym if fn == 'units'
    localized_field_text_blurb = fnl.to_sym if fn == 'text_blurb'
    attrs[fnl.to_sym] = binding.local_variable_get(fn.to_sym)
  end
  attrs[:grouping] = grouping
  attrs[:skip_spec_refresh] = true if skip_spec_refresh
  ps = direct_product_specifications.find_by(token:)
  ps ||= ProductSpecification.find_by(token: token, product_line_id: primary_product_line_id, product_category_id: product_category_id) if link_to_product_line
  if ps
    ps.do_not_compact_translation_container = true
    # Check if this localized value is really necessary? we check if it is the same as the fallback (in english only)
    if locale.present? && locale != :en && locale.to_s.start_with?('en') &&
       ps.text_blurb_en == attrs[localized_field_text_blurb] &&
       ps.units_en == attrs[localized_field_units]

      # Delete those localized attributes we don't need them
      attrs.delete(localized_field_text_blurb)
      attrs.delete(localized_field_units)
      # Then the localized attribute are simply the same as the fallback and can be removed
      logger.info 'Localized version of attributes is same as fallback, ignoring'
    else
      logger.debug("Updating product specification")
      ps.update(attrs)
    end

  elsif text_blurb.present? # in the context of this method, an empty value doesn't make sense
    # Build a new ProductSpecification and associate it with this item
    logger.debug("Creating product specification")

    ps = ProductSpecification.new(attrs)
    ps.do_not_compact_translation_container = true
    if link_to_product_line
      ps.product_line_id = primary_product_line_id
      ps.product_category_id = product_category_id
    else
      ps.items << self unless ps.items.include?(self)
    end
    ps.save!
  end
  ps
end

#finishObject

Finish.

Returns:

  • (Object)


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

def finish
  rendered_specifications.dig(:finish, :output)
end

#has_spec?(token) ⇒ Boolean

Whether has spec.

Parameters:

  • token (Object)

Returns:

  • (Boolean)


13
14
15
# File 'app/concerns/models/item_specification_helper.rb', line 13

def has_spec?(token)
  rendered_specifications[token.to_sym].present?
end

#is_dual_voltage?Boolean

Whether is dual voltage.

Returns:

  • (Boolean)


779
780
781
# File 'app/concerns/models/item_specification_helper.rb', line 779

def is_dual_voltage?
  spec_value(:dual_voltage) and spec_value(:dual_voltage).to_s.downcase.first != 'f'
end

#length(output_unit: :in) ⇒ Object

Length.

Parameters:

  • output_unit (Symbol) (defaults to: :in)

Returns:

  • (Object)


203
204
205
# File 'app/concerns/models/item_specification_helper.rb', line 203

def length(output_unit: :in)
  spec_value(:length, output_unit:)
end

#length=(val, unit: :in) ⇒ void

This method returns an undefined value.

Sets the length.

Parameters:

  • val (Object)
  • unit (Symbol) (defaults to: :in)


211
212
213
# File 'app/concerns/models/item_specification_helper.rb', line 211

def length=(val, unit: :in)
  create_or_set_spec_value(token: :length, name: 'Length', units: unit.to_s, grouping: 'Product Dimensions', text_blurb: val)
end

#length_formattedObject

Length formatted.

Returns:

  • (Object)


882
883
884
# File 'app/concerns/models/item_specification_helper.rb', line 882

def length_formatted
  spec_output(:length)
end

#linear_ftBigDecimal?

Returns the cable length in linear feet, or nil when the item is not a cable heating element

Returns:

  • (BigDecimal, nil)


865
866
867
868
869
870
871
# File 'app/concerns/models/item_specification_helper.rb', line 865

def linear_ft
  return unless get_self_and_kit_items.any?(&:is_cable_heating_element?)

  return if length.blank?

  length(output_unit: :ft)
end

#maximum_currentObject

Maximum current.

Returns:

  • (Object)


761
762
763
# File 'app/concerns/models/item_specification_helper.rb', line 761

def maximum_current
  spec_value(:maximum_current) || spec_value(:maximum_current_per_relay) if is_control? || is_power?
end

#num_polesObject

Num poles.

Returns:

  • (Object)


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

def num_poles
  spec_value(:numbers_of_poles) if is_relay_panel? || is_legacy_relay?
end

#ohmsObject

Ohms.

Returns:

  • (Object)


494
495
496
497
# File 'app/concerns/models/item_specification_helper.rb', line 494

def ohms
  o = spec_value(:ohms)
  o.is_a?(Numeric) ? o.round(4) : nil
end

#ohms=(val) ⇒ void

This method returns an undefined value.

Sets the ohms.

Parameters:

  • val (Object)


502
503
504
# File 'app/concerns/models/item_specification_helper.rb', line 502

def ohms=(val)
  create_or_set_spec_value(token: :ohms, name: 'Ohms', units: 'ohm', grouping: 'Electrical', text_blurb: val)
end

#ohms_law_calculatorObject

Ohms law calculator.

Returns:

  • (Object)


563
564
565
566
567
568
569
570
571
572
# File 'app/concerns/models/item_specification_helper.rb', line 563

def ohms_law_calculator
  @ohms_law_calculator ||= begin
    attrs = {}
    attrs[:resistance] = ohms if can_use_ohms_for_calculation?
    attrs[:voltage] = voltage if can_use_voltage_for_calculation?
    attrs[:power] = watts if can_use_watts_for_calculation?
    attrs[:current] = amps if can_use_amps_for_calculation?
    OhmsLawCalculator.new(attrs).calc
  end
end

#ohms_static?Boolean

Whether ohms static.

Returns:

  • (Boolean)


712
713
714
# File 'app/concerns/models/item_specification_helper.rb', line 712

def ohms_static?
  rendered_specifications.dig(:ohms, :static).to_b
end

#plan_groupingObject

Plan grouping.

Returns:

  • (Object)


748
749
750
# File 'app/concerns/models/item_specification_helper.rb', line 748

def plan_grouping
  spec_value(:plan_grouping)
end

#plan_grouping=(val) ⇒ void

This method returns an undefined value.

Sets the plan grouping.

Parameters:

  • val (Object)


755
756
757
# File 'app/concerns/models/item_specification_helper.rb', line 755

def plan_grouping=(val)
  create_or_set_spec_value(token: :plan_grouping, name: 'Plan Grouping', grouping: 'Logistics', visibility: 'internal', text_blurb: val)
end

#rendered_specificationsHash

Normalized view of rendered_product_specifications. Non-default locales
deserialize the Mobility container translation as a plain string-keyed
Hash (only the default locale comes back as a HashWithIndifferentAccess),
so bare symbol lookups/digs silently miss every spec — normalize once per
underlying hash instead of on every lookup.

Returns:

  • (Hash)


34
35
36
37
38
39
40
41
42
43
# File 'app/concerns/models/item_specification_helper.rb', line 34

def rendered_specifications
  rps = rendered_product_specifications
  return {} if rps.nil?

  unless @rendered_specifications_source.equal?(rps)
    @rendered_specifications_source = rps
    @rendered_specifications = rps.with_indifferent_access
  end
  @rendered_specifications
end

#set_spec_value(token, value) ⇒ Object

Finds a direct product spec and sets the value.

Parameters:

  • token (Object)
  • value (Object)

Returns:

  • (Object)


86
87
88
89
90
91
92
93
# File 'app/concerns/models/item_specification_helper.rb', line 86

def set_spec_value(token, value)
  token = token.to_sym
  ds = direct_product_specifications.where(token:).first
  logger.error("No direct_product_specifications present for #{sku} with token #{token}, cannot set spec value") unless ds
  ds&.update_attribute!(:text_blurb, value)
  update_rendered_product_specifications
  value
end

#spec(token) ⇒ Object

Retrieves a spec from the rendered product specification
Performs a little bit of value casting on the raw output before returning

Parameters:

  • token (Object)

Returns:

  • (Object)


21
22
23
24
25
26
# File 'app/concerns/models/item_specification_helper.rb', line 21

def spec(token)
  return unless (ps = rendered_specifications[token.to_sym])

  ps[:raw] = TypeCoercer.coerce(ps[:raw])
  ps
end

#spec_image(token) ⇒ Object

Spec image.

Parameters:

  • token (Object)

Returns:

  • (Object)


194
195
196
197
198
# File 'app/concerns/models/item_specification_helper.rb', line 194

def spec_image(token)
  return unless (iid = spec(token)&.dig(:image_id))

  Image.find(iid)
end

#spec_output(token, locale: Mobility.locale) ⇒ Object

returns the formatted output from the spec

Parameters:

  • token (Object)
  • locale (Object) (defaults to: Mobility.locale)

Returns:

  • (Object)


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

def spec_output(token, locale: Mobility.locale)
  Mobility.with_locale(locale) do
    spec(token)&.dig(:output)&.dup
  end
end

#spec_refresh_in_progressObject

Spec refresh in progress.

Returns:

  • (Object)


47
48
49
# File 'app/concerns/models/item_specification_helper.rb', line 47

def spec_refresh_in_progress
  BackgroundJobStatus.search(worker_klass: 'ItemAttributeWorker', args: id).present?
end

#spec_unit(token) ⇒ Object

Spec unit.

Parameters:

  • token (Object)

Returns:

  • (Object)


75
76
77
78
79
80
# File 'app/concerns/models/item_specification_helper.rb', line 75

def spec_unit(token)
  u = spec(token)&.dig(:units)&.dup
  # Sqft fudging to handle leftover bad data until it transitions over in all product_specifications.units
  u = 'sqft' if /Sq\.? ?Ft\.?/i.match?(u)
  u
end

#spec_value(token, output_unit: nil, precision: nil) ⇒ Object

returns the raw value from the spec

Parameters:

  • token (Object)
  • output_unit (Object, nil) (defaults to: nil)
  • precision (Object, nil) (defaults to: nil)

Returns:

  • (Object)


56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'app/concerns/models/item_specification_helper.rb', line 56

def spec_value(token, output_unit: nil, precision: nil)
  return unless (v = spec(token)&.dig(:raw)&.dup)

  current_unit_symbol = spec_unit(token)
  if current_unit_symbol.present? && output_unit.present? && current_unit_symbol.to_s != output_unit.to_s

    current_unit_val = RubyUnits::Unit.new("#{v} #{current_unit_symbol}")
    # We're using desired_unit for testing compatibility
    desired_unit = RubyUnits::Unit.new("1 #{output_unit}")
    # only convert if units are compatible
    v = current_unit_val.convert_to(output_unit.to_s).scalar.to_f if current_unit_val.compatible?(desired_unit)
  end
  v = v.round(precision) if precision.present? && v.respond_to?(:round)
  TypeCoercer.coerce(v)
end

#specs_for_identifier_labelObject

Specs for identifier label.

Returns:

  • (Object)


906
907
908
909
910
911
912
# File 'app/concerns/models/item_specification_helper.rb', line 906

def specs_for_identifier_label
  hsh = {}
  specifications_for_item_label.each do |rps|
    hsh[rps.name] = rps.output
  end
  hsh
end

#sqftObject

Sqft.

Returns:

  • (Object)


327
328
329
# File 'app/concerns/models/item_specification_helper.rb', line 327

def sqft
  area(output_unit: :sqft)
end

#sqft=(val) ⇒ void

This method returns an undefined value.

Sets the sqft.

Parameters:

  • val (Object)


334
335
336
# File 'app/concerns/models/item_specification_helper.rb', line 334

def sqft=(val)
  create_or_set_spec_value(token: :coverage, name: 'Coverage', units: 'sqft', grouping: 'Product Dimensions', text_blurb: val)
end

#token_specs_values_for_liquid(include_legacy: false) ⇒ Object

Renders a hash with a token spec value (possibly in multiple dimensions)
and its value. This hash can be used for liquid merges

Parameters:

  • include_legacy (Boolean) (defaults to: false)

Returns:

  • (Object)


934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
# File 'app/concerns/models/item_specification_helper.rb', line 934

def token_specs_values_for_liquid(include_legacy: false)
  options = {}
  options[:item_name] = name
  options[:detailed_description_html] = detailed_description_html
  options[:short_description] = short_description
  options[:feature_1] = feature_1
  options[:feature_2] = feature_2
  options[:feature_3] = feature_3
  options[:feature_4] = feature_4
  options[:feature_5] = feature_5

  rendered_product_specifications&.each do |k, _v|
    options[k] = spec_output(k)
    options["#{k}_formatted"] = spec_output(k) if include_legacy # This is for legacy
    if spec_unit(k)
      options["#{k}_raw"] = spec_value(k)
      options["#{k}_units"] = spec_unit(k)
    end
    supported_dim_units = %i[ft in m cm mm]
    supported_weight_units = %i[lb oz kg g]
    supported_coverage_units = %i[sqft sqm]
    current_unit = spec_unit(k)&.to_sym
    target_units = if supported_dim_units.include?(current_unit)
                     supported_dim_units
                   elsif supported_weight_units.include?(current_unit)
                     supported_weight_units
                   elsif supported_coverage_units.include?(current_unit)
                     supported_coverage_units
                   else
                     []
                   end
    # We iterate through each dimension unit we care to have a translation for and pull the value
    target_units.each do |unit|
      next unless (val = begin
        spec_value(k, output_unit: unit)
      rescue StandardError
        nil
      end)

      options["#{k}_#{unit}_raw"] = val
      options["#{k}_#{unit}_units"] = unit.to_s
    end
  end
  # Now combine computed methods
  ProductSpecification.methods_for_select.each do |method_string|
    method_symbolized = method_string.to_sym
    next if method_symbolized == :text

    begin
      options[method_symbolized.to_s] ||= send(method_symbolized)
    rescue StandardError
      # Ignore this
    end
  end
  options.with_indifferent_access.compact
end

#voltageObject

Voltage.

Returns:

  • (Object)


508
509
510
511
# File 'app/concerns/models/item_specification_helper.rb', line 508

def voltage
  v = spec_value(:voltage)
  v.is_a?(Numeric) ? v : nil
end

#voltage=(val) ⇒ void

This method returns an undefined value.

Sets the voltage.

Parameters:

  • val (Object)


516
517
518
# File 'app/concerns/models/item_specification_helper.rb', line 516

def voltage=(val)
  create_or_set_spec_value(token: :voltage, name: 'Voltage', units: 'V', grouping: 'Electrical', text_blurb: val)
end

#voltage_formattedObject

Voltage formatted.

Returns:

  • (Object)


888
889
890
# File 'app/concerns/models/item_specification_helper.rb', line 888

def voltage_formatted
  spec_output(:voltage)
end

#voltage_static?Boolean

Whether voltage static.

Returns:

  • (Boolean)


724
725
726
# File 'app/concerns/models/item_specification_helper.rb', line 724

def voltage_static?
  rendered_specifications.dig(:voltage, :static).to_b
end

#voltsObject

Volts.

Returns:

  • (Object)


522
523
524
525
# File 'app/concerns/models/item_specification_helper.rb', line 522

def volts
  logger.warn 'Deprecated method volts, use voltage instead'
  voltage
end

#volts=(val) ⇒ void

This method returns an undefined value.

Sets the volts.

Parameters:

  • val (Object)


530
531
532
533
# File 'app/concerns/models/item_specification_helper.rb', line 530

def volts=(val)
  logger.warn 'Deprecated method volts, use voltage instead'
  create_or_set_spec_value(token: :voltage, name: 'Voltage', units: 'V', grouping: 'Electrical', text_blurb: val)
end

#vopObject

Vop.

Returns:

  • (Object)


551
552
553
# File 'app/concerns/models/item_specification_helper.rb', line 551

def vop
  spec_value(:vop)
end

#wattsObject

Watts.

Returns:

  • (Object)


479
480
481
482
483
# File 'app/concerns/models/item_specification_helper.rb', line 479

def watts
  w = spec_value(:watts, output_unit: 'W') # Force watts in case this is expressed in kilowatts

  w.is_a?(Numeric) ? w : nil
end

#watts=(val) ⇒ void

This method returns an undefined value.

Sets the watts.

Parameters:

  • val (Object)


488
489
490
# File 'app/concerns/models/item_specification_helper.rb', line 488

def watts=(val)
  create_or_set_spec_value(token: :watts, name: 'Watts', units: 'W', grouping: 'Electrical', text_blurb: val)
end

#watts_formattedObject

Watts formatted.

Returns:

  • (Object)


894
895
896
# File 'app/concerns/models/item_specification_helper.rb', line 894

def watts_formatted
  spec_output(:watts)
end

#watts_per_linear_feet(precision: 2) ⇒ Object

Watts per linear feet.

Parameters:

  • precision (Integer) (defaults to: 2)

Returns:

  • (Object)


471
472
473
474
475
# File 'app/concerns/models/item_specification_helper.rb', line 471

def watts_per_linear_feet(precision: 2)
  return unless length && watts

  (BigDecimal(watts.to_s) / (length.to_f / 12)).truncate(precision)
end

#watts_per_sqftObject

Watts per sqft.

Returns:

  • (Object)


557
558
559
# File 'app/concerns/models/item_specification_helper.rb', line 557

def watts_per_sqft
  spec_value(:watts_per_sqft) || calculate_watts_per_sqft
end

#watts_per_sqft_at_3_5_in_spacingObject

Watts per sqft at 3 5 in spacing.

Returns:

  • (Object)


430
431
432
433
434
# File 'app/concerns/models/item_specification_helper.rb', line 430

def watts_per_sqft_at_3_5_in_spacing
  return unless watts && coverage_at_3_5_in

  (BigDecimal(watts.to_s) / coverage_at_3_5_in).truncate(1)
end

#watts_per_sqft_at_3_75_in_spacingObject

Watts per sqft at 3 75 in spacing.

Returns:

  • (Object)


438
439
440
441
442
# File 'app/concerns/models/item_specification_helper.rb', line 438

def watts_per_sqft_at_3_75_in_spacing
  return unless watts && coverage_at_3_75_in

  (BigDecimal(watts.to_s) / coverage_at_3_75_in).truncate(1)
end

#watts_per_sqft_at_3_in_spacingObject

Watts per sqft at 3 in spacing.

Returns:

  • (Object)


422
423
424
425
426
# File 'app/concerns/models/item_specification_helper.rb', line 422

def watts_per_sqft_at_3_in_spacing
  return unless watts && coverage_at_3_in

  (BigDecimal(watts.to_s) / coverage_at_3_in).truncate(1)
end

#watts_per_sqft_at_4_5_in_spacingObject

Watts per sqft at 4 5 in spacing.

Returns:

  • (Object)


454
455
456
457
458
# File 'app/concerns/models/item_specification_helper.rb', line 454

def watts_per_sqft_at_4_5_in_spacing
  return unless watts && coverage_at_4_5_in

  (BigDecimal(watts.to_s) / coverage_at_4_5_in).truncate(1)
end

#watts_per_sqft_at_4_in_spacingObject

Watts per sqft at 4 in spacing.

Returns:

  • (Object)


446
447
448
449
450
# File 'app/concerns/models/item_specification_helper.rb', line 446

def watts_per_sqft_at_4_in_spacing
  return unless watts && coverage_at_4_in

  (BigDecimal(watts.to_s) / coverage_at_4_in).truncate(1)
end

#watts_per_sqft_at_5_in_spacingObject

Watts per sqft at 5 in spacing.

Returns:

  • (Object)


462
463
464
465
466
# File 'app/concerns/models/item_specification_helper.rb', line 462

def watts_per_sqft_at_5_in_spacing
  return unless watts && coverage_at_5_in

  (BigDecimal(watts.to_s) / coverage_at_5_in).truncate(1)
end

#watts_static?Boolean

Whether watts static.

Returns:

  • (Boolean)


700
701
702
# File 'app/concerns/models/item_specification_helper.rb', line 700

def watts_static?
  rendered_specifications.dig(:watts, :static).to_b
end

#width(output_unit: :in) ⇒ Object

Width.

Parameters:

  • output_unit (Symbol) (defaults to: :in)

Returns:

  • (Object)


218
219
220
# File 'app/concerns/models/item_specification_helper.rb', line 218

def width(output_unit: :in)
  spec_value(:width, output_unit:)
end

#width=(val, unit: :in) ⇒ void

This method returns an undefined value.

Sets the width.

Parameters:

  • val (Object)
  • unit (Symbol) (defaults to: :in)


226
227
228
# File 'app/concerns/models/item_specification_helper.rb', line 226

def width=(val, unit: :in)
  create_or_set_spec_value(token: :width, name: 'Width', units: unit.to_s, grouping: 'Product Dimensions', text_blurb: val)
end

#width_by_length_textObject

Width by length text.

Returns:

  • (Object)


232
233
234
235
236
# File 'app/concerns/models/item_specification_helper.rb', line 232

def width_by_length_text
  return unless length && width

  "#{format('%.2g', width.to_f / 12)}′ x #{format('%d', length.to_f / 12)}"
end

#width_by_length_text_2Object

Width by length text 2.

Returns:

  • (Object)


240
241
242
243
244
# File 'app/concerns/models/item_specification_helper.rb', line 240

def width_by_length_text_2
  return unless length && width

  "#{format('%.2g', width.to_f / 12)}′ x #{format('%02d', length.to_f / 12)}"
end

#width_by_length_text_3String?

For larger rolls

Returns:

  • (String, nil)


248
249
250
251
252
# File 'app/concerns/models/item_specification_helper.rb', line 248

def width_by_length_text_3
  return unless length && width

  "#{format('%.2g', width.to_f / 12)}′ x #{format('%03d', length.to_f / 12)}"
end

#width_formattedObject

Width formatted.

Returns:

  • (Object)


876
877
878
# File 'app/concerns/models/item_specification_helper.rb', line 876

def width_formatted
  spec_output(:width)
end