Module: LineItemExtension

Defined in:
app/models/line_item_extension.rb

Overview

Association extension for has_many :line_items collections (mixed in
wherever the Models::Pickable concern declares line_items).

Adds collection-level helpers that the rest of the ERP relies on
without paying an N+1: heating-system aggregations
(#primary_heating_system_product_line, #primary_heating_element_spacing),
active-line filtering (#active_lines, #active_parent_lines,
#active_goods_lines, #active_services_lines, #active_shipping_lines),
preloads for the coupon engine (#with_discount_preloads), and a
stable MD5 #signature used to detect "is this packing list still
the same one we already shipped?" for shipment matching.

Instance Method Summary collapse

Instance Method Details

#active_goods_linesObject



128
129
130
# File 'app/models/line_item_extension.rb', line 128

def active_goods_lines
  active_lines.select(&:is_goods?)
end

#active_linesObject



79
80
81
82
# File 'app/models/line_item_extension.rb', line 79

def active_lines
  # uniq is a safety in case multiple of the same lines are added to the collection
  to_a.reject { |li| li.destroyed? or li.marked_for_destruction? or li.frozen? }.uniq
end

#active_lines_for_packagingObject



102
103
104
105
106
107
108
109
110
# File 'app/models/line_item_extension.rb', line 102

def active_lines_for_packaging
  active_lines.select do |li|
    li.is_goods? &&
      (
        (li.children_count.to_i.zero? && !li.parent&.catalog_item&.pack_at_kit_level&.to_b) ||
        li.catalog_item.pack_at_kit_level.to_b
      )
  end
end

#active_lines_without_kit_parentsObject



98
99
100
# File 'app/models/line_item_extension.rb', line 98

def active_lines_without_kit_parents
  active_lines.reject { |li| li.children_count.present? and li.children_count > 0 }
end

#active_non_shipping_linesObject



120
121
122
# File 'app/models/line_item_extension.rb', line 120

def active_non_shipping_lines
  active_lines.reject(&:is_shipping?)
end

#active_parent_linesObject



84
85
86
# File 'app/models/line_item_extension.rb', line 84

def active_parent_lines
  active_lines.reject { |li| li.parent_id.present? }
end

#active_services_linesObject



132
133
134
# File 'app/models/line_item_extension.rb', line 132

def active_services_lines
  active_lines.select(&:is_service?)
end

#active_shipping_linesObject



124
125
126
# File 'app/models/line_item_extension.rb', line 124

def active_shipping_lines
  active_lines.select(&:is_shipping?)
end

#active_tax_unclassed_linesObject



136
137
138
# File 'app/models/line_item_extension.rb', line 136

def active_tax_unclassed_lines
  active_lines.select(&:is_tax_unclassed?)
end

#any_require_cable_spacing?Boolean

Returns:

  • (Boolean)


58
59
60
61
# File 'app/models/line_item_extension.rb', line 58

def any_require_cable_spacing?
  # see if ant line items require cable spacing
  heating_elements.any? { |li| li.item&.is_cable_system? }
end

#changed_for_shipping?Boolean

Returns:

  • (Boolean)


75
76
77
# File 'app/models/line_item_extension.rb', line 75

def changed_for_shipping?
  (active_goods_lines + active_services_lines).any? { |li| li.destroyed? or li.marked_for_destruction? or li.new_record? or li.quantity_changed? or li.catalog_item_id_changed? }
end

#goods_changed?Boolean

Returns:

  • (Boolean)


148
149
150
# File 'app/models/line_item_extension.rb', line 148

def goods_changed?
  lines_changed.any?(&:is_goods?)
end

#grouped_by_categoryObject



67
68
69
# File 'app/models/line_item_extension.rb', line 67

def grouped_by_category
  product_category_sorted.group_by(&:category_name)
end

#has_goods_lines?Boolean

Returns:

  • (Boolean)


144
145
146
# File 'app/models/line_item_extension.rb', line 144

def has_goods_lines?
  active_goods_lines.present?
end

#has_goods_without_shipping?Boolean

Returns:

  • (Boolean)


152
153
154
# File 'app/models/line_item_extension.rb', line 152

def has_goods_without_shipping?
  (has_goods_lines? and !has_shipping_line?)
end

#has_shipping_line?Boolean

Returns:

  • (Boolean)


140
141
142
# File 'app/models/line_item_extension.rb', line 140

def has_shipping_line?
  active_shipping_lines.present?
end

#has_underlayment?Boolean

Returns:

  • (Boolean)


63
64
65
# File 'app/models/line_item_extension.rb', line 63

def has_underlayment?
  to_a.map(&:item_id).intersect?(Item.underlayments.ids)
end

#lines_changedObject



112
113
114
# File 'app/models/line_item_extension.rb', line 112

def lines_changed
  to_a.select { |li| li.changed? or li.destroyed? or li.marked_for_destruction? }
end

#lines_changed?Boolean

Returns:

  • (Boolean)


116
117
118
# File 'app/models/line_item_extension.rb', line 116

def lines_changed?
  to_a.any? { |li| li.changed? or li.destroyed? or li.marked_for_destruction? or li.new_record? }
end

#primary_heating_element_spacingObject



45
46
47
48
49
50
51
52
53
54
55
56
# File 'app/models/line_item_extension.rb', line 45

def primary_heating_element_spacing
  # just count the occurents of line items for a given heating_element_spacing
  he_spacings = heating_elements.filter_map { |li| li.item&.spec(:heating_element_spacing)&.dig(:raw) }
  if he_spacings.empty?
    nil
  else
    freq = he_spacings.each_with_object(Hash.new(0)) do |v, h|
      h[v] += 1
    end
    he_spacings.max_by { |v| freq[v] }
  end
end

#primary_heating_system_product_lineObject



18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'app/models/line_item_extension.rb', line 18

def primary_heating_system_product_line
  # just count the occurrences of line items for a given heating system type
  hs_product_lines = heating_elements.parents_only.select { |li| li.item.primary_product_line&.get_first_heating_system_type.present? }.filter_map { |li| li.item.primary_product_line.get_first_heating_system_type }
  hs_product_lines = heating_elements.select { |li| li.item.primary_product_line&.get_first_heating_system_type.present? }.filter_map { |li| li.item.primary_product_line.get_first_heating_system_type } if hs_product_lines.empty?
  if hs_product_lines.empty?
    nil
  else
    freq = hs_product_lines.each_with_object(Hash.new(0)) do |v, h|
      h[v] += 1
    end
    hs_product_lines.max_by { |v| freq[v] }
  end
end

#primary_heating_system_voltageObject



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

def primary_heating_system_voltage
  # just count the occurents of line items for a given heating system voltage
  hs_volts = heating_elements.filter_map { |li| li.item&.voltage }
  if hs_volts.empty?
    nil
  else
    freq = hs_volts.each_with_object(Hash.new(0)) do |v, h|
      h[v] += 1
    end
    hs_volts.max_by { |v| freq[v] }
  end
end

#signatureObject



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

def signature
  Delivery.md5_hash_items(active_lines)
end

#ungroupedObject



71
72
73
# File 'app/models/line_item_extension.rb', line 71

def ungrouped
  where(room_configuration_id: nil)
end

#versionObject



14
15
16
# File 'app/models/line_item_extension.rb', line 14

def version
  all.maximum(:updated_at).to_i
end

#with_discount_preloadsObject

Preload associations commonly needed for coupon qualification and discount calculations.
This prevents N+1 queries when:

  • item: checking item specifications (sqft, linear_ft), product line, etc.
  • catalog_item: calculating msrp_total
  • line_discounts: checking if discount applies to line_item, calculating discounted_total
    Returns a relation - call .active_parent_lines after this to get the filtered array.


94
95
96
# File 'app/models/line_item_extension.rb', line 94

def with_discount_preloads
  includes(:item, :catalog_item, :line_discounts)
end