Class: Item::HeatingElementPlan

Inherits:
Object
  • Object
show all
Defined in:
app/services/item/heating_element_plan.rb

Constant Summary collapse

SPECIAL_LEGEND_DESCRIPTIONS =
{ 'ot_sensor' => 'OT Sensor *', 'slab_sensor' => 'SCE-SLAB-SS-KIT Sensor #' }.freeze

Instance Attribute Summary collapse

Delegated Instance Attributes collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(line_items, options = {}) ⇒ HeatingElementPlan

Initialize with an LineItem result set of heating elements or pass
options:
cable_spacing: 3.0 if you want to append the cable spacing in inche to the width
heating_system: name of the heating system



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'app/services/item/heating_element_plan.rb', line 41

def initialize(line_items, options = {})
  @options = options
  self.all_line_items = line_items.without_children
  self.all_line_items_including_parents = line_items
  self.other_items = all_line_items.to_a - all_line_items.heating_elements.to_a # line_items.controls.to_a + line_items.accessories.to_a
  self.heating_system = @options[:heating_system] || 'TempZone Thin Cable' # use this as the default
  self.cable_spacing = @options[:cable_spacing]
  self.voltage = @options[:voltage] || 120 # use this as the default
  self.is_countertop = @options[:is_countertop] || false # use this as the default
  self.heating_elements_line_items = all_line_items.heating_elements.sort_for_plan

  self.installation_postal_code = @options[:installation_postal_code]
  self.electricity_rate = @options[:electricity_rate]
  self.installation_sqft = @options[:installation_sqft]
  self.locale = @options[:locale] || I18n.locale
  self.state_code = @options[:state_code]
  self.country_iso = @options[:country_iso]
  generate_heating_elements_lines
end

Instance Attribute Details

#all_line_itemsObject

Returns the value of attribute all_line_items.



6
7
8
# File 'app/services/item/heating_element_plan.rb', line 6

def all_line_items
  @all_line_items
end

#all_line_items_including_parentsObject

Returns the value of attribute all_line_items_including_parents.



6
7
8
# File 'app/services/item/heating_element_plan.rb', line 6

def all_line_items_including_parents
  @all_line_items_including_parents
end

#cable_spacingObject

Returns the value of attribute cable_spacing.



6
7
8
# File 'app/services/item/heating_element_plan.rb', line 6

def cable_spacing
  @cable_spacing
end

#country_isoObject

Returns the value of attribute country_iso.



6
7
8
# File 'app/services/item/heating_element_plan.rb', line 6

def country_iso
  @country_iso
end

#electricity_rateObject

Returns the value of attribute electricity_rate.



6
7
8
# File 'app/services/item/heating_element_plan.rb', line 6

def electricity_rate
  @electricity_rate
end

#heating_elements_line_itemsObject

Returns the value of attribute heating_elements_line_items.



6
7
8
# File 'app/services/item/heating_element_plan.rb', line 6

def heating_elements_line_items
  @heating_elements_line_items
end

#heating_elements_linesObject

Returns the value of attribute heating_elements_lines.



6
7
8
# File 'app/services/item/heating_element_plan.rb', line 6

def heating_elements_lines
  @heating_elements_lines
end

#heating_systemObject

Returns the value of attribute heating_system.



6
7
8
# File 'app/services/item/heating_element_plan.rb', line 6

def heating_system
  @heating_system
end

#installation_postal_codeObject

Returns the value of attribute installation_postal_code.



6
7
8
# File 'app/services/item/heating_element_plan.rb', line 6

def installation_postal_code
  @installation_postal_code
end

#installation_sqftObject

Returns the value of attribute installation_sqft.



6
7
8
# File 'app/services/item/heating_element_plan.rb', line 6

def installation_sqft
  @installation_sqft
end

#is_countertopObject

Returns the value of attribute is_countertop.



6
7
8
# File 'app/services/item/heating_element_plan.rb', line 6

def is_countertop
  @is_countertop
end

#localeObject

Returns the value of attribute locale.



6
7
8
# File 'app/services/item/heating_element_plan.rb', line 6

def locale
  @locale
end

#other_itemsObject

Returns the value of attribute other_items.



6
7
8
# File 'app/services/item/heating_element_plan.rb', line 6

def other_items
  @other_items
end

#state_codeObject

Returns the value of attribute state_code.



6
7
8
# File 'app/services/item/heating_element_plan.rb', line 6

def state_code
  @state_code
end

#voltageObject

Returns the value of attribute voltage.



6
7
8
# File 'app/services/item/heating_element_plan.rb', line 6

def voltage
  @voltage
end

Class Method Details

.get_room_best_location(rc) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'app/services/item/heating_element_plan.rb', line 21

def self.get_room_best_location(rc)
  res = {}
  if rc.opportunity&.installation_postal_code.present?
    res[:installation_postal_code] = rc.opportunity.installation_postal_code
  elsif (shippping_address = rc.orders.first&.shipping_address || rc.quotes.first&.shipping_address).present?
    res[:state_code] = shippping_address.state_code
    res[:country_iso] = shippping_address.country_iso
    res[:installation_postal_code] = shippping_address.zip
  else
    res[:state_code] = rc.opportunity.customer.state_code
    res[:country_iso] = rc.opportunity.customer.country_iso
    res[:installation_postal_code] = rc.opportunity.customer.zip
  end
  res
end

.new_from_room_configuration(rc, _options = {}) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
# File 'app/services/item/heating_element_plan.rb', line 9

def self.new_from_room_configuration(rc, _options = {})
  new(rc.line_items.goods,
      heating_system: rc.heating_system_type_name,
      cable_spacing: rc.installation_plan_cable_spacing || rc.line_items.primary_heating_element_spacing,
      voltage: rc.installation_plan_voltage_id,
      electricity_rate: rc.opportunity.electricity_rate,
      is_countertop: rc.is_countertop?,
      installation_sqft: rc.installation_sqft,
      locale: rc.opportunity.customer.locale,
      **get_room_best_location(rc))
end

Instance Method Details

#air_sensorsObject



428
429
430
# File 'app/services/item/heating_element_plan.rb', line 428

def air_sensors
  line_items_for_sku(%w[AIR-SS AIR-SS-2 SCPM-SS-120])
end

#alternative_volts_explanationObject

make a hash of all alternative volts which we can display on the install plan



287
288
289
290
291
292
293
294
295
296
297
# File 'app/services/item/heating_element_plan.rb', line 287

def alternative_volts_explanation
  volts = {}
  symbol = 1
  heating_elements_lines.select { |hel| hel.alternative_volts.present? }.each do |hel|
    unless volts.value?(hel.alternative_volts)
      volts[symbol] = hel.alternative_volts
      symbol += 1
    end
  end
  volts
end

#breaker_rulesObject



577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
# File 'app/services/item/heating_element_plan.rb', line 577

def breaker_rules
  rules = []
  if ['TempZone Flex Roll', 'Environ Flex Roll', 'TempZone Easy Mat', 'Environ Easy Mat', 'TempZone Cable', 'TempZone Ruler Cable', 'TempZone Thin Cable',
      'TempZone Shower Mat', 'TempZone Custom Mat', 'Shower Mat Bench'].include?(heating_system)
    if relays.present?
      breakers = {}
      heating_elements_lines.each do |hel|
        bs = "#{breaker_size(hel.amps)} Amp #{hel.volts}V GFI Heater Circuit"
        new_qty = (breakers[bs] || 0) + 1
        breakers[bs] = new_qty
      end
      breakers.each_pair { |key, value| rules << "#{value} x #{key}" }
      rules << "#{controls.sum(:quantity)} x 15 Amp 120V Non-GFI Control Circuit" unless controls.empty?
    elsif (num_power_modules = power_modules.sum(:quantity)).positive?
      # How many switching thermostats do we have as well?
      num_tstats = current_switching_thermostats.sum(&:quantity)
      total_switching_relays = num_tstats + num_power_modules
      if total_switching_relays == heating_elements_lines.length
        # SPECIAL CASE: If total_switching_relays matches number of heating elements, then assume 1 per switching relay and show each unit's breaker based on the heating element's amps.
        breakers = {}
        heating_elements_lines.each do |hel|
          bs = "#{breaker_size(hel.amps)} Amp #{hel.volts}V Non-GFI Heater Circuit"
          new_qty = (breakers[bs] || 0) + 1
          breakers[bs] = new_qty
        end
        breakers.each_pair do |key, value|
          rules << "#{value} x #{key}"
        end
      else
        rules << "#{total_switching_relays} x 20 Amp #{heating_system_voltage}V Non-GFI Circuit"
      end
    elsif integration_kits.sum(:quantity) == 1
      # if there is just 1 integration kit then show breaker info based on total heating load
      rules << "1 x #{breaker_size(total_amps, 30)} Amp #{heating_system_voltage}V GFI Circuit"
    elsif integration_kits.sum(:quantity) > 1
      rules << "#{integration_kits.sum(:quantity)} x 30 Amp #{integration_kits.first.item.voltage}V GFI Circuit"
    elsif current_switching_thermostats.present?
      if (num_tstats = current_switching_thermostats.sum(&:quantity)) > 1
        if num_tstats == heating_elements_lines.length
          # SPECIAL CASE: If multiple thermostats matches number of heating elements, then assume 1 per tstat and show each unit's breaker based on the heating element's amps.
          breakers = {}
          heating_elements_lines.each do |hel|
            bs = "#{breaker_size(hel.amps)} Amp #{hel.volts}V Non-GFI Heater Circuit"
            new_qty = (breakers[bs] || 0) + 1
            breakers[bs] = new_qty
          end
          breakers.each_pair do |key, value|
            rules << "#{value} x #{key}"
          end
        else
          # If multiple thermostats, then show qty of breakers = qty of thermostats, and show each unit need 20 Amps breaker, since we are assuming maximum load.
          rules << "#{num_tstats} x 20 Amp #{heating_system_voltage}V Non-GFI Circuit"
        end
      else
        # else if single thermostat then show 1 breaker and use regular breaker size rule for total heating elements, upto max of 20 Amps.
        rules << "1 x #{breaker_size(total_amps, 20)} Amp #{heating_system_voltage}V Non-GFI Circuit"
      end
    else # no controls, so use generic sentence
      rules << 'The warming system must be electrically grounded and protected by a GFCI'
    end
  elsif ['Slab Heat Mat', 'Slab Heat Cable'].include?(heating_system)
    if relays.present?
      breakers = {}
      heating_elements_lines.each do |hel|
        bs = "#{breaker_size(hel.amps)} Amp #{hel.volts}V GFI Heater Circuit"
        new_qty = (breakers[bs] || 0) + 1
        breakers[bs] = new_qty
      end
      breakers.each_pair { |key, value| rules << "#{value} x #{key}" }
      rules << "#{controls.sum(:quantity)} x 15 Amp 120V Non-GFI Control Circuit" unless controls.empty?
    elsif (num_power_modules = power_modules.sum(:quantity)).positive? # if a power modulator is present
      # How many switching thermostats do we have as well?
      num_tstats = current_switching_thermostats.sum(&:quantity)
      total_switching_relays = num_tstats + num_power_modules
      rules << "#{total_switching_relays} x 20 Amp #{heating_system_voltage}V Non-GFI Circuit"
    elsif controls.empty? # no relays, no controls, so use generic sentence
      rules << 'The warming system must be electrically grounded and protected by a GFCI'
    else # no relays, but there are controls
      rules << "#{controls.sum(:quantity)} x #{breaker_size(total_amps)} Amp #{heating_system_voltage}V Non-GFI Circuit" unless controls.empty?
    end
  elsif ['Snow Melt Mat', 'Snow Melt PowerMat', 'Snow Melt OmniMat', 'Snow Melt EcoMat', 'Snow Melt Cable'].include?(heating_system)
    if relays.present?
      breakers = {}
      heating_elements_lines.each do |hel|
        bs = "#{breaker_size(hel.amps)} Amp #{hel.volts}V GFEP Heater Circuit"
        breakers[bs] ||= 0
        breakers[bs] += 1
      end
      breakers.each_pair { |key, value| rules << "#{value} x #{key}" }
      rules << "#{controls.sum(:quantity)} x 15 Amp 120V Non-GFI Control Circuit" unless controls.empty?
    elsif has_item?(ItemConstants::POWER_MODULATORS) # if a power modulator is present
      power_modulator_count = line_items_for_sku(ItemConstants::POWER_MODULATORS).sum(:quantity)
      rules << "#{power_modulator_count} x 120V Non-GFI Control Circuit"
      rules << "1 x #{heating_system_voltage}V Non-GFI Circuit per contactor"
    elsif controls.empty? # no relays, no controls, so qty of breakers = qty of heating elements
      breakers = {}
      heating_elements_lines.each do |hel|
        bs = "#{breaker_size(hel.amps)} Amp #{hel.volts}V GFEP Heater Circuit"
        new_qty = (breakers[bs] || 0) + 1
        breakers[bs] = new_qty
      end
      breakers.each_pair { |key, value| rules << "#{value} x #{key}" }
    else # no relays, but there are controls
      rules << "#{controls.sum(:quantity)} x #{breaker_size(total_amps)} Amp #{heating_system_voltage}V GFEP Circuit" unless controls.empty?
    end
  elsif /roof|pipe/i.match?(heating_system)
    number_of_power_kits = line_items_for_sku(%w[SR-PWR-KIT PT-PWR-KIT-H]).sum(:quantity)
    if has_item?('SR-PLUG-KIT') # if a plug-in kit is present
      number_of_kits = line_items_for_sku('SR-PLUG-KIT').sum(:quantity)
      rules << "#{number_of_kits} x 15 AMP 120V Non-GFI Circuit (1 outlet required per plug-in kit)"
    elsif relays.present?
      rules << "#{controls.sum(:quantity)} x 15 Amp 120V Non-GFI Control Circuit" unless controls.empty?
      if [120, 240].include?(heating_system_voltage)
        rules << if number_of_power_kits.zero?
                   "#{heating_system_voltage}V"
                 else
                   "#{number_of_power_kits} x #{heating_system_voltage}V GFEP Heater Circuit"
                 end
        rules << "Refer to electrical plan's circuit breaker size vs cable length table"
      end
    elsif heating_system.match(/roof/i) && has_item?(ItemConstants::POWER_MODULATORS) # if r&g and a power modulator is present
      power_modulator_count = line_items_for_sku(ItemConstants::POWER_MODULATORS).sum(:quantity)
      rules << "#{power_modulator_count} x 120V Non-GFI Control Circuit"
      rules << "1 x #{heating_system_voltage}V Non-GFI Circuit per contactor"
    elsif controls.empty? # no relays, no controls, so qty of breakers = qty of heating elements
      rules << if number_of_power_kits.zero?
                 "#{heating_system_voltage}V"
               else
                 "#{number_of_power_kits} x #{heating_system_voltage}V GFEP Heater Circuit"
               end
      rules << "Refer to electrical plan's circuit breaker size vs cable length table"
    elsif /pipe/i.match?(heating_system) # no relays, but there are controls
      rules << "Refer to electrical plan's circuit breaker size vs cable length table"
    else
      unless controls.empty?
        rules << "#{controls.sum(:quantity)} x #{roof_and_gutter_breaker_size(heating_system_voltage,
                                                                              total_length)} Amp #{heating_system_voltage}V GFEP Circuit"
      end
    end
  else
    []
  end
  rules
end

#breaker_size(amps = total_amps, max_amps = 50) ⇒ Object



463
464
465
466
467
468
469
470
471
# File 'app/services/item/heating_element_plan.rb', line 463

def breaker_size(amps = total_amps, max_amps = 50)
  case amps
  when 0..12 then 15
  when 12.01..16 then [20, max_amps].min
  when 16.01..24 then [30, max_amps].min
  when 24.01..32 then [40, max_amps].min
  when 32.01..40 then [50, max_amps].min
  end
end

#build_from_line_item(line_item, qty = nil, heating_element_number = nil) ⇒ Object



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'app/services/item/heating_element_plan.rb', line 250

def build_from_line_item(line_item, qty = nil, heating_element_number = nil)
  item = line_item.item
  multiplier = (qty || line_item.quantity || 1).to_f
  # Self healing in case specs are missing
  if item.voltage.blank? || item.amps.blank? || item.watts.blank? || item.ohms.blank?
    item.update_rendered_product_specifications(tokens: %w[voltage amps watts ohms])
    item.reload
  end
  begin
    element_pole_assignments = if line_item.room_configuration && heating_element_number
                                 line_item.room_configuration.element_pole_assignments.ordered.where(heating_element_number: heating_element_number,
                               heating_element_item_id: line_item.item_id)
                               else
                                 ElementPoleAssignment.none
                               end
    Item::HeatingElementLine.new(
      sku: item.sku,
      width: cable_spacing || item.width,
      length: (item.length.nil? ? nil : item.length * multiplier),
      volts: item.voltage,
      alternative_volts: item.alternative_volts.present? ? item.alternative_volts.join(', ') : nil,
      amps: (item.amps.to_f * multiplier).round(2),
      watts: (item.watts.to_f * multiplier),
      btu_per_hr: (item.watts.to_f * multiplier) * RoomConfiguration::BTU_PER_HOUR_PER_WATT,
      watts_per_linear_feet: item.watts_per_linear_feet,
      cold_lead_length: item.cold_lead_length,
      ohms: item.ohms&.round(2),
      poles: element_pole_assignments.map(&:pole_number).join(', '),
      relay: element_pole_assignments.first&.relay_identifier,
      heating_element_number: heating_element_number
    )
  rescue StandardError => e
    ErrorReporting.error(e, { line_item_id: line_item.id, item_id: item.id, item_sku: item.sku })
  end
end

#cold_leadsObject



436
437
438
# File 'app/services/item/heating_element_plan.rb', line 436

def cold_leads
  line_items_for_sku('COLDLEAD')
end

#cold_leads_lengthObject



449
450
451
# File 'app/services/item/heating_element_plan.rb', line 449

def cold_leads_length
  cold_leads.sum(:quantity)
end

#control_voltageObject



400
401
402
# File 'app/services/item/heating_element_plan.rb', line 400

def control_voltage
  relays.present? ? 120 : heating_system_voltage
end

#controlsObject

Alias for All_line_items#controls

Returns:

  • (Object)

    All_line_items#controls

See Also:



410
# File 'app/services/item/heating_element_plan.rb', line 410

delegate :controls, to: :all_line_items

#current_switching_thermostatsObject



549
550
551
# File 'app/services/item/heating_element_plan.rb', line 549

def current_switching_thermostats
  all_line_items.smartstats + all_line_items.easystats + all_line_items.oj_tstats
end

#display_cutting_info?Boolean

Returns:

  • (Boolean)


509
510
511
# File 'app/services/item/heating_element_plan.rb', line 509

def display_cutting_info?
  ['TempZone Flex Roll', 'Environ Flex Roll'].include?(heating_system)
end

#electrical_consumptionObject



738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
# File 'app/services/item/heating_element_plan.rb', line 738

def electrical_consumption
  return nil if heating_elements_line_items.any? { |he| he.item.is_self_regulating_cable? }
  return nil unless total_watts&.positive?

  avg_rate = ElectricityRate.get_average_for_locale(locale) # I don't see a way to determine the locale to use if no zip code
  disclaimer = "This calculation is based on the national average of $#{avg_rate} per kWh, but consumption may vary based on individual conditions."
  if electricity_rate.present?
    avg_rate = electricity_rate.round(4)
    disclaimer = "This calculation is based on the rate of $#{avg_rate} per kWh, but consumption may vary based on individual conditions."
  elsif installation_postal_code.present?
    result = ElectricityRate.get_from_postal_code(installation_postal_code)
    if result[:status] == :ok && result[:average_rate].present?
      avg_rate = result[:average_rate].round(4)
      disclaimer = "This calculation is based on the average rate of $#{avg_rate} per kWh for zip/postal code #{installation_postal_code}, but consumption may vary based on individual conditions."
    end
  elsif (state_rate = ElectricityRate.get_average_rate_for_country_iso_state_code(country_iso, state_code))
    avg_rate = state_rate
    disclaimer = "This calculation is based on the average rate of $#{avg_rate} per kWh for state/province #{state_code}, but consumption may vary based on individual conditions."
  end

  # 2.3 = thermal inertia
  # 1 = number of hours operation
  if heating_system.index(/roof|snow|pipe/i)
    # For snow melt we typically don't account for thermal inertia, because these systems can be running non-stop (Anatoliy 3/16/17)
    coefficient = 1
  else
    coefficient = 0.4348
    disclaimer << ' Operating cost is estimated based on the average thermal inertia due to the thermostat cycling the power during the heating period.'
  end
  wpsqft = watts_per_sqft(true)

  consumption_rate = ((avg_rate * coefficient * total_watts) / 1000).round(2)
  {
    rate: consumption_rate,
    coefficient:,
    avg_rate:,
    total_watts:,
    disclaimer:
  }
end

#electrical_plan_controlsObject



440
441
442
443
# File 'app/services/item/heating_element_plan.rb', line 440

def electrical_plan_controls
  # here we really want parents_only i.e. no kit compnents, I believe, per Anatoliy's request to not display the Zone braker twice
  all_line_items_including_parents.electrical_plan_controls.parents_only + line_items_for_sku('SR-PLUG-KIT')
end

#electrical_rough_in_kitsObject



553
554
555
# File 'app/services/item/heating_element_plan.rb', line 553

def electrical_rough_in_kits
  all_line_items_including_parents.joins(:item).where(items: { sku: 'FHE-ROUGH-IN-KIT-03' })
end

#extra_instructionsObject



722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
# File 'app/services/item/heating_element_plan.rb', line 722

def extra_instructions
  instructions = []
  # Cold lead
  if cold_leads_length.positive?
    instructions << 'COLD LEAD:'
    instructions << "#{cold_leads_length}' of additional cold lead"
  end

  # Aerial Sensor
  if air_sensors.present?
    instructions << 'AERIAL SENSOR:'
    instructions << 'Provided, but not shown on drawing. Location per installer\'s discretion. WarmlyYours recommends it to be in an area most indicative of snow conditions.'
  end
  instructions
end

#first_heating_element_voltageObject



541
542
543
# File 'app/services/item/heating_element_plan.rb', line 541

def first_heating_element_voltage
  heating_elements_lines.first&.volts
end

#floor_loadObject



387
388
389
390
# File 'app/services/item/heating_element_plan.rb', line 387

def floor_load
  # needs to be 1000.0 so both values are Float, else can lead to it being 0
  (total_watts / 1000.0).round(1)
end

#flooring_labelObject



573
574
575
# File 'app/services/item/heating_element_plan.rb', line 573

def flooring_label
  is_outdoor? || is_countertop? ? 'Surface Type' : 'Flooring'
end

#generate_heating_elements_linesObject



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'app/services/item/heating_element_plan.rb', line 180

def generate_heating_elements_lines
  self.heating_elements_lines = []
  heating_element_number = 0
  heating_elements_line_items.each do |he_line_item|
    if he_line_item.item.spec_value(:plan_grouping) == 'group_qty_to_length'
      # Group items by sku on plan
      # Build heating element line will use implicit line item quantity multiplier
      heating_elements_lines << build_from_line_item(he_line_item)
    else
      # one line per quantity will split the line items rendering one in the table for each qty
      he_line_item.quantity.times do
        heating_element_number += 1
        # Force quantity multiplier to one since we're splitting the rendering
        heating_elements_lines << build_from_line_item(he_line_item, 1, heating_element_number)
      end
    end
  end
end

#has_item?(sku) ⇒ Boolean

Returns:

  • (Boolean)


416
417
418
# File 'app/services/item/heating_element_plan.rb', line 416

def has_item?(sku)
  line_items_for_sku(sku).present?
end

#heating_elements_table(with_cold_lead: false, with_ohms: false, with_pole_assignment: false) ⇒ Object



329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'app/services/item/heating_element_plan.rb', line 329

def heating_elements_table(with_cold_lead: false, with_ohms: false, with_pole_assignment: false)
  table = []
  heating_elements_lines.each_with_index do |hel, i|
    row = {}
    row[:row_number] = i + 1
    heating_elements_table_column_headers(with_cold_lead: with_cold_lead, with_ohms: with_ohms, with_row_number: false, with_pole_assignment: with_pole_assignment).each_key do |attrib|
      row[attrib] = if attrib == :volts
                      hel.volts_with_alternative_volts(alternative_volts_explanation)
                    else
                      hel.send(attrib)
                    end
    end
    table << row
  end
  table
end

#heating_elements_table_column_headers(with_cold_lead: false, with_ohms: false, with_row_number: true, with_pole_assignment: false) ⇒ Object



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'app/services/item/heating_element_plan.rb', line 303

def heating_elements_table_column_headers(with_cold_lead: false, with_ohms: false, with_row_number: true, with_pole_assignment: false)
  headers = {}
  headers[:row_number] = '#' if with_row_number
  unless /TempZone Custom Mat/i.match?(heating_system)
    headers[:imperial_width] = (cable_spacing.present? ? 'Spacing' : 'Width') unless /roof|pipe/i.match?(heating_system)
    headers[:imperial_length] = 'Length'
  end
  headers[:imperial_cold_lead_length] = 'Lead' unless /roof|pipe/i.match?(heating_system)
  headers[:volts] = 'Volts'
  if /roof|pipe/i.match?(heating_system)
    headers[:watts_per_linear_feet] = 'Watts Per Ft'
  else
    headers[:watts] = 'Watts'
    headers[:btu_per_hr] = 'BTU/h'
    headers[:amps] = 'Amps'
    headers[:ohms] = 'Ohms'
  end

  if with_pole_assignment
    headers[:poles] = 'Poles'
    headers[:relay] = 'Relay'
  end

  headers
end

#heating_elements_table_footersObject



346
347
348
349
350
351
352
353
354
355
# File 'app/services/item/heating_element_plan.rb', line 346

def heating_elements_table_footers
  return {} if heating_elements_lines.length <= 1 # don't add a footer if there's only 1 row

  footers = {}
  footers[:imperial_length] = UnitConversions.inches_to_feetinches(total_length) if heating_system.index(/roof|pipe/i)
  footers[:watts] = total_watts unless heating_system.index(/roof|pipe/i)
  footers[:btu_per_hr] = total_btu_per_hr unless heating_system.index(/roof|pipe/i)
  footers[:amps] = total_amps unless heating_system.index(/roof|pipe/i)
  footers
end

#heating_system_nameObject



517
518
519
520
521
522
523
524
525
526
527
528
# File 'app/services/item/heating_element_plan.rb', line 517

def heating_system_name
  case heating_system
  when 'TempZone Thin Cable'
    'TempZone 3W Cable'
  when 'TempZone Cable'
    'TempZone 3.7W Cable'
  when 'TempZone Ruler Cable'
    'TempZone 3.7W Ruler Cable'
  else
    heating_system
  end
end

#heating_system_voltageObject



545
546
547
# File 'app/services/item/heating_element_plan.rb', line 545

def heating_system_voltage
  voltage || first_heating_element_voltage
end

#heating_system_with_voltageObject



530
531
532
533
534
535
536
537
538
539
# File 'app/services/item/heating_element_plan.rb', line 530

def heating_system_with_voltage
  addendum_for_mixed_systems = ''
  addendum_for_mixed_systems = ' & Cable' if (heating_system_name.to_s.downcase.index('mat') || heating_system_name.to_s.downcase.index('roll')) && heating_elements_line_items.any? do |li|
    li.item.is_cable_system?
  end
  addendum_for_mixed_systems = ' & Mat/Roll' if heating_system_name.to_s.downcase.index('cable') && !heating_system_name.to_s.downcase.index('roof') && !heating_system_name.to_s.downcase.index('pipe') && heating_elements_line_items.any? do |li|
                                                  !li.item.is_cable_system?
                                                end
  "#{heating_system_name}#{addendum_for_mixed_systems} #{voltage}V"
end

#hide_flip_turn?Boolean

Returns:

  • (Boolean)


513
514
515
# File 'app/services/item/heating_element_plan.rb', line 513

def hide_flip_turn?
  [nil, 'Environ Flex Roll'].include?(heating_system)
end

#highlighted_columnsObject



299
300
301
# File 'app/services/item/heating_element_plan.rb', line 299

def highlighted_columns
  %i[voltage watts btu_per_hr amps ohms]
end

#instructions(include_electrical_plan_rules = false, include_installation_plan_rules = false) ⇒ Object



779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
# File 'app/services/item/heating_element_plan.rb', line 779

def instructions(include_electrical_plan_rules = false, include_installation_plan_rules = false)
  inst = []
  inst << 'All power leads travel back to thermostat or junction box.' if ['TempZone Flex Roll', 'Environ Flex Roll', 'Environ Easy Mat',
                                                                           'TempZone Easy Mat', 'TempZone Cable', 'TempZone Ruler Cable', 'TempZone Thin Cable', 'Slab Heat Cable', 'Slab Heat Mat', 'TempZone Shower Mat', 'Shower Mat Bench'].include?(heating_system)
  inst << 'All power leads travel back to the controller, relay panel or junction box.' if heating_system.index(/roof|snow|pipe/i)
  if include_installation_plan_rules == true
    inst << 'Always start warming cable down unless noted otherwise.' if ['TempZone Flex Roll', 'Environ Flex Roll'].include?(heating_system)
    inst << 'Cut only mesh, never cut warming cable.' if ['TempZone Flex Roll', 'Slab Heat Mat', 'Snow Melt Mat', 'Snow Melt PowerMat', 'Snow Melt OmniMat', 'Snow Melt EcoMat'].include?(heating_system)
    inst << 'Cut only silver material, never cut warming cable.' if ['Environ Flex Roll'].include?(heating_system)
    inst << 'Never cut heating rolls.' if ['Environ Easy Mat', 'TempZone Easy Mat', 'TempZone Shower Mat',
                                           'Shower Mat Bench'].include?(heating_system)
    inst << 'Never cut heating cable.' if ['TempZone Cable', 'TempZone Ruler Cable', 'TempZone Thin Cable', 'Snow Melt Cable', 'Snow Melt Cable'].include?(heating_system)
    inst << 'Never wire multiple rolls or cables together.' # all systems
    inst << 'Verify sum of panels equals roll length for each roll.' if ['TempZone Flex Roll', 'Environ Flex Roll', 'Slab Heat Mat',
                                                                         'Snow Melt Mat', 'Snow Melt PowerMat', 'Snow Melt OmniMat', 'Snow Melt EcoMat'].include?(heating_system)
  end
  inst << 'Keep the bus wires separated. The bus wires will short if they touch each other.' if heating_system.index(/roof|pipe/i)
  inst << 'Keep components and ends of heating cable dry before installation.' if heating_system.index(/roof|pipe/i)
  inst << 'The heating cable should not be embedded in insulation or roofing material.' if heating_system.index(/roof/i)
  inst << 'Do not twist cable during installation.' if heating_system.index(/roof|pipe/i)
  if include_electrical_plan_rules == true
    inst << 'Always test rolls/cables out of the box with an Ohm meter and Mega-Ohm meter (optional). See manual for other required tests.' # all systems
    unless heating_system.index(/roof|pipe|snow/i)
      inst << 'Integration Kit: Can be installed on the side or inside of the separate electrical box or a distribution panel, according to NEC and local code requirements.' if integration_kits.present?
      if has_item?(%w[UWG4-4999 UWG4-4999-WY UWG5-4999-WY UWG4-4999-B UDG4-4999 UDG4-4999-WY UDG4-4999-B UDG-4999 UTN4-4999 USG-4000 USG5-4000]) # if an OJ tstat is present
        tstat_box_size = conduit_box_size = '3"x2"x3-1/2" single gang'
      else
        tstat_box_size = '4"x4" square'
        conduit_box_size = '4" double gang'
      end
      if thermostats.any? && integration_kits.blank?
        inst << "Thermostat: Install a #{tstat_box_size}, 2-1/8\" deep electrical box according to NEC and local code requirements. Electrical boxes should be located on interior walls, commonly 60\" from the floor."
      end
      inst << "Power lead Conduit: The shielded power lead can be installed with or without electrical conduit depending on local code requirements. In either case, remove one of the knock-outs in the #{conduit_box_size} box to route the lead. If electrical conduit is not required by code, install a wire collar to secure the leads where they enter the box."
      if sensors.present?
        inst << 'Floor Sensor: A floor sensor comes with our floor heating systems. It can be installed in a conduit separate from the electrical power lead. If local code requires the low voltage sensor wire be housed in conduit, it must use a separate conduit from the power leads (high voltage).'
      end
      if power_modules.present?
        inst << 'Power module(s) must be wired to the main Thermostat using low-voltage field wiring cable, recommended min. 20 AWG. Connect wiring from A&B terminals on the front of the thermostat base to C&D on the front of the power module base. (Keep polarity: A to C and B to D). Maximum distance between thermostat and power modules is 80 ft (25 m.) See the diagrams in the power module box.'
      end
    end
  end
  inst
end

#integration_kitsObject

Alias for All_line_items#integration_kits

Returns:

  • (Object)

    All_line_items#integration_kits

See Also:



406
# File 'app/services/item/heating_element_plan.rb', line 406

delegate :integration_kits, to: :all_line_items

#is_countertop?Boolean

Returns:

  • (Boolean)


561
562
563
# File 'app/services/item/heating_element_plan.rb', line 561

def is_countertop?
  is_countertop == true
end

#is_environ?Boolean

Returns:

  • (Boolean)


228
229
230
# File 'app/services/item/heating_element_plan.rb', line 228

def is_environ?
  ['Environ Flex Roll', 'Environ Easy Mat'].include?(heating_system)
end

#is_indoor_floor_heating_system?Boolean

Returns:

  • (Boolean)


223
224
225
226
# File 'app/services/item/heating_element_plan.rb', line 223

def is_indoor_floor_heating_system?
  ['TempZone Flex Roll', 'TempZone Easy Mat', 'TempZone Shower Mat', 'Shower Mat Bench', 'TempZone Cable', 'TempZone Ruler Cable', 'TempZone Thin Cable', 'Environ Flex Roll',
   'Environ Easy Mat'].include?(heating_system)
end

#is_outdoor?Boolean

Returns:

  • (Boolean)


557
558
559
# File 'app/services/item/heating_element_plan.rb', line 557

def is_outdoor?
  heating_system.index(/roof|snow|pipe/i) != nil
end

#is_pipe_freeze?Boolean

Returns:

  • (Boolean)


565
566
567
# File 'app/services/item/heating_element_plan.rb', line 565

def is_pipe_freeze?
  heating_system.index(/pipe/i) != nil
end

#legendObject



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'app/services/item/heating_element_plan.rb', line 199

def legend
  symbols = case heating_system
            when /TempZone Flex Roll|Environ Flex Roll|TempZone Shower Mat|Shower Mat Bench/i
              %w[start end roll_number]
            when /Snow Melt Mat|TempZone Easy Mat|Slab Heat Mat/i
              %w[start end mat_number]
            when /TempZone Cable|TempZone Ruler Cable|TempZone Thin Cable|Snow Melt Cable|Slab Heat Cable/i
              %w[start end cable_number halfway_mark]
            when /roof|pipe/i
              %w[start end cable_number]
            when 'Environ Easy Mat'
              ['roll_number']
            else
              []
            end
  symbols << 'thermostat' if all_line_items.thermostats.present?
  symbols << 'relay' if relays.present? && is_indoor_floor_heating_system?
  symbols << 'power_module' if all_line_items.power_modules.present?
  symbols << 'integration_kit' if integration_kits.present?
  symbols << 'ot_sensor' if ot_sensors.present?
  symbols << 'slab_sensor' if has_item?('SLAB-SS') || has_item?('SCE-SLAB-SS-56')
  symbols
end

#line_items_for_sku(sku) ⇒ Object



420
421
422
# File 'app/services/item/heating_element_plan.rb', line 420

def line_items_for_sku(sku)
  all_line_items.joins(:item).where(items: { sku: })
end

#number_of_circuitsObject



453
454
455
456
457
458
459
460
461
# File 'app/services/item/heating_element_plan.rb', line 453

def number_of_circuits
  circuits = 0
  if integration_kits.sum(:quantity).positive?
    circuits = integration_kits.sum(:quantity)
  else
    electrical_plan_controls.each { |c| circuits += (c.item.spec_value(:circuits_required).to_i * c.quantity) }
  end
  circuits
end

#ot_sensorsObject



432
433
434
# File 'app/services/item/heating_element_plan.rb', line 432

def ot_sensors
  line_items_for_sku(['SCP-120', *ItemConstants::POWER_MODULATORS])
end

#other_items_tableObject



357
358
359
360
361
362
363
364
365
366
367
368
# File 'app/services/item/heating_element_plan.rb', line 357

def other_items_table
  oi_table = []
  other_items.each do |line_item|
    item_name = begin
      "#{line_item.item.sku} - #{line_item.item.name}"
    rescue StandardError
      'n/a'
    end
    oi_table << [item_name, line_item.quantity]
  end
  oi_table
end

#power_kitsObject



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

def power_kits
  line_items_for_sku('SR-PWR-KIT')
end

#power_modulesObject

Alias for All_line_items#power_modules

Returns:

  • (Object)

    All_line_items#power_modules

See Also:



404
# File 'app/services/item/heating_element_plan.rb', line 404

delegate :power_modules, to: :all_line_items

#relaysObject



412
413
414
# File 'app/services/item/heating_element_plan.rb', line 412

def relays
  all_line_items.relay_panels
end

#requires_breaker_size_vs_circuit_length_table?Boolean

Returns:

  • (Boolean)


501
502
503
504
505
506
507
# File 'app/services/item/heating_element_plan.rb', line 501

def requires_breaker_size_vs_circuit_length_table?
  if heating_system.match(/roof|pipe/i) && (has_item?(ItemConstants::PIPE_TRACING_CABLE_5_WATT_PER_FT_SPOOL_SKUS) || has_item?(ItemConstants::PIPE_TRACING_CABLE_8_WATT_PER_FT_SPOOL_SKUS) || has_item?(ItemConstants::PIPE_TRACING_CABLE_10_WATT_PER_FT_SPOOL_SKUS))
    return true
  end

  false
end

#roof_and_gutter_breaker_recommendations(volts) ⇒ Object



493
494
495
496
497
498
499
# File 'app/services/item/heating_element_plan.rb', line 493

def roof_and_gutter_breaker_recommendations(volts)
  if volts == 120
    "<table class='rg-breakers'><tr><th>Max Length</th><th>Breaker</th></tr><tr><td>107 ft</td><td>15 A 120V GFEP</td></tr><tr><td>142 ft</td><td>20 A 120V GFEP</td></tr><tr><td>214 ft</td><td>30 A 120V GFEP</td></tr></table>"
  elsif volts == 240
    "<table class='rg-breakers'><tr><th>Max Length</th><th>Breaker</th></tr><tr><td>214 ft</td><td>15 A 240V GFEP</td></tr><tr><td>286 ft</td><td>20 A 240V GFEP</td></tr><tr><td>429 ft</td><td>30 A 240V GFEP</td></tr></table>"
  end
end

#roof_and_gutter_breaker_size(volts, length) ⇒ Object



473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
# File 'app/services/item/heating_element_plan.rb', line 473

def roof_and_gutter_breaker_size(volts, length)
  # length will be in inches, so lets convert it to feet
  length /= 12
  if volts == 120
    case length
    when 0..107 then 15
    when 108..142 then 20
    else 30
    end
  elsif volts == 240
    case length
    when 0..214 then 15
    when 215..286 then 20
    else 30
    end
  else # not set up for that voltage, so just give the max
    30
  end
end

#room_labelObject



569
570
571
# File 'app/services/item/heating_element_plan.rb', line 569

def room_label
  is_outdoor? ? 'Area' : 'Room'
end

#sensorsObject



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

def sensors
  all_line_items.has_floor_sensor
end

#shorten_text(text, num_chars = 32) ⇒ Object



824
825
826
827
828
# File 'app/services/item/heating_element_plan.rb', line 824

def shorten_text(text, num_chars = 32)
  res = text
  res = "#{text.to_s[0..(num_chars - 4)]} ..." if text.to_s.length > num_chars
  res
end

#slab_heat_cable_watts_per_sqftObject



169
170
171
172
173
174
175
176
177
178
# File 'app/services/item/heating_element_plan.rb', line 169

def slab_heat_cable_watts_per_sqft
  {
    '3': 23,
    '3.0': 23,
    '4': 17,
    '4.0': 17,
    '5': 15,
    '5.0': 15
  }.fetch((cable_spacing || 3.0).round(2).to_s.to_sym, 23)
end

#snow_melt_cable_watts_per_sqftObject



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'app/services/item/heating_element_plan.rb', line 153

def snow_melt_cable_watts_per_sqft
  {
    '2.5': 'Up to 60',
    '3': 'Up to 50',
    '3.0': 'Up to 50',
    '3.5': 'Up to 43',
    '4': 'Up to 38.5',
    '4.0': 'Up to 38.5',
    '4.5': 'Up to 33',
    '5': 'Up to 31.5',
    '5.0': 'Up to 31.5',
    '6': 'Up to 25',
    '6.0': 'Up to 25'
  }.fetch((cable_spacing || 3.0).round(2).to_s.to_sym, 50)
end

#special_instructionsObject

TODO: Is this needed? We already put that in the instructions if an AIR-SS is present.



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'app/services/item/heating_element_plan.rb', line 233

def special_instructions
  instructions = []
  if ot_sensors.present?
    instructions << "* Installers may locate the OT Sensor at their discretion.
    Placement shown is a recommendation only. Sensor must be inside metal conduit
    (capped off for replaceability) between two passes of heat cable."
  end
  if has_item?('SLAB-SS') || has_item?('SCE-SLAB-SS-56')
    instructions << "# Installers may locate Slab Sensor at their discretion.
    Placement shown is a recommendation only. Sensor should be located between
    two passes of Heating Cable where snow and ice problems normally occur.
    Sensor must be embedded horizontally with its top flush with the surroundings
    with the help of the accompanying installation plate."
  end
  instructions
end

#tempzone_thick_cable_watts_per_sqftObject



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'app/services/item/heating_element_plan.rb', line 136

def tempzone_thick_cable_watts_per_sqft
  {
    '2.5': 17.76,
    '2.9': 15.3,
    '3': 14.8,
    '3.0': 14.8,
    '3.13': 14.2,
    '3.5': 12.7,
    '3.75': 11.84,
    '4': 11.1,
    '4.0': 11.1,
    '4.75': 9.3,
    '5': 8.9,
    '5.0': 8.9
  }.fetch((cable_spacing || 3.0).round(2).to_s.to_sym, 14.8)
end

#tempzone_thin_cable_watts_per_sqftObject



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'app/services/item/heating_element_plan.rb', line 119

def tempzone_thin_cable_watts_per_sqft
  {
    '2.5': 14.4,
    '2.9': 12.4,
    '3': 12,
    '3.0': 12,
    '3.13': 11.5,
    '3.5': 10.3,
    '3.75': 9.6,
    '4': 8.7,
    '4.0': 8.7,
    '4.75': 7.6,
    '5': 7.2,
    '5.0': 7.2
  }.fetch((cable_spacing || 3.0).round(2).to_s.to_sym, 12)
end

#thermostatsObject

Alias for All_line_items#thermostats

Returns:

  • (Object)

    All_line_items#thermostats

See Also:



408
# File 'app/services/item/heating_element_plan.rb', line 408

delegate :thermostats, to: :all_line_items

#total_ampsObject



392
393
394
# File 'app/services/item/heating_element_plan.rb', line 392

def total_amps
  heating_elements_lines.to_a.sum { |he| he.amps || 0 }.round(2)
end

#total_btu_per_hrObject



383
384
385
# File 'app/services/item/heating_element_plan.rb', line 383

def total_btu_per_hr
  heating_elements_lines.to_a.sum { |he| he.btu_per_hr || 0 }.round
end

#total_lengthObject



396
397
398
# File 'app/services/item/heating_element_plan.rb', line 396

def total_length
  heating_elements_lines.to_a.sum { |he| he.length || 0 }
end

#total_wattsObject



370
371
372
373
374
375
376
377
378
379
380
381
# File 'app/services/item/heating_element_plan.rb', line 370

def total_watts
  heating_elements_lines.to_a.sum do |he|
    value = if he.respond_to?(:watts)
              he.watts
            elsif he.is_a?(Hash)
              he[:watts] || he['watts']
            else
              0
            end
    (value || 0).to_f
  end.round
end

#watts_per_sqft(watts_only = false) ⇒ Object



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'app/services/item/heating_element_plan.rb', line 61

def watts_per_sqft(watts_only = false)
  watts = case heating_system
          when /TempZone Flex Roll|TempZone Shower Mat|TempZone Easy Mat|Shower Mat Bench/i
            15
          when 'TempZone Custom Mat'
            watts_only ? 12.5 : '12-15'
          when /Environ Flex Roll|Environ Easy Mat/i
            12
          when 'Snow Melt Mat'
            if cable_spacing.to_f.round == 4
              'Up to 38.5'
            else
              (cable_spacing.to_f.round == 5 ? 'Up to 31.5' : 'Up to 50')
            end
          when 'Snow Melt PowerMat'
            'Up to 50'
          when 'Snow Melt OmniMat'
            'Up to 38.5'
          when 'Snow Melt EcoMat'
            'Up to 31.5'
          when 'Slab Heat Mat'
            20
          when 'TempZone Thin Cable'
            tempzone_thin_cable_watts_per_sqft
          when 'TempZone Cable'
            tempzone_thick_cable_watts_per_sqft
          when 'TempZone Ruler Cable'
            tempzone_thick_cable_watts_per_sqft
          when 'Snow Melt Cable'
            snow_melt_cable_watts_per_sqft
          when 'Slab Heat Cable'
            slab_heat_cable_watts_per_sqft
          when /roof/i
            if watts_only
              heating_system_voltage == 208 ? 6.5 : 7.5
            else
              heating_system_voltage == 208 ? '4-9' : '5-10'
            end
          when /pipe/i
            if has_item?(ItemConstants::PIPE_TRACING_CABLE_5_WATT_PER_FT_SPOOL_SKUS)
              5
            elsif has_item?(ItemConstants::PIPE_TRACING_CABLE_8_WATT_PER_FT_SPOOL_SKUS)
              8
            elsif has_item?(ItemConstants::PIPE_TRACING_CABLE_10_WATT_PER_FT_SPOOL_SKUS)
              10
            end
          end
  return watts if watts_only || watts.nil?

  text = if /roof|pipe/i.match?(heating_system)
           "#{watts} W/Ft."
         else
           "#{watts} W/Sq.ft."
         end
  text += " @ #{cable_spacing}\" spacing" if !is_environ? && cable_spacing
  text
end