Module: Models::Pickable

Extended by:
ActiveSupport::Concern
Included in:
Order, Quote, RoomConfiguration
Defined in:
app/concerns/models/pickable.rb

Overview

Mixin for resources whose LineItem children are picked from a
Catalog (orders, quotes, room configurations, etc.).

Hosts are expected to expose line_items, customer, currency, and
the total columns that the SQL aggregator writes to. Catalog coherence
is enforced both per-line (through LineItem#catalog_item_matches_resource_catalog)
and at the resource level when the pick catalog is flipped.

Has many collapse

Instance Method Summary collapse

Instance Method Details

#all_my_publications {|ActiveRecord::Relation<Publication>| ... } ⇒ ActiveRecord::Relation<Publication>

Collects publications referenced by all line items, scoped to the resource's store.

Yields:

  • (ActiveRecord::Relation<Publication>)

    relation to further filter

Yield Returns:

  • (ActiveRecord::Relation<Publication>)

    filtered relation

Returns:

  • (ActiveRecord::Relation<Publication>)

    publications for this resource



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

def all_my_publications
  pubs = nil
  line_items.each do |li|
    item_pubs = li.item.all_my_publications
    pubs = pubs.nil? ? item_pubs : (pubs | item_pubs) # This builds up an or condition
  end
  pubs = pubs.in_store(store.id) # Only publications relevant to what the order's store should be used regardless of locale
  pubs = yield(pubs) if block_given?
  pubs
end

#append_suggested_items(suggested_items_list, containing_resource) ⇒ Hash{Itemizable => Array<LineItem>}

Returns the updated accumulator.

Parameters:

  • suggested_items_list (Hash{Itemizable => Array<LineItem>})

    accumulator for suggested items

  • containing_resource (Itemizable)

    resource that will contain the suggested items

Returns:



404
405
406
407
408
# File 'app/concerns/models/pickable.rb', line 404

def append_suggested_items(suggested_items_list, containing_resource)
  suggested_items_list.tap do |suggested_items_list|
    append_suggested_materials(suggested_items_list, containing_resource)
  end
end

#append_suggested_materials(suggested_items_list, _containing_resource = nil) ⇒ Hash{Itemizable => Array<LineItem>}

Returns the updated accumulator.

Parameters:

  • suggested_items_list (Hash{Itemizable => Array<LineItem>})

    accumulator for suggested items

  • _containing_resource (Itemizable, nil) (defaults to: nil)

    unused legacy parameter, kept for API compatibility

Returns:



413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
# File 'app/concerns/models/pickable.rb', line 413

def append_suggested_materials(suggested_items_list, _containing_resource = nil)
  suggested_items_list.tap do |suggested_items_list|
    catalog = customer.catalog
    # Now figure out the suggested items
    material_alerts = get_material_alerts
    material_alerts.select { |ma| ma.items.any? and (ma.recommended_qty > ma.actual_qty) }.each do |ma|
      logger.debug "append_suggested_materials, ma.group_name: #{ma.group_name}"
      logger.debug "append_suggested_materials, ma.items.map{|i| i.sku}: #{ma.items.map(&:sku).inspect}"
      ma.items.each do |item|
        ci = catalog.catalog_items.by_skus(item.sku).first
        next unless ci

        # re-apply stock thresholds and customer filters for quote pdf
        stock_threshold_ok = false
        stock_threshold_ok = true if ci.item.suggested_item_applies_to(customer) && (ci.qty_available > item.suggested_item_stock_threshold)
        add_item = true
        if stock_threshold_ok && (ma.group_name.to_s == 'Installation Kits')
          # ugh, hacky but this is the y to accomodate all the weird patterns for suggested add ons in the quote
          add_item = false
          logger.debug "append_suggested_materials, item.sku: #{item.sku}, add_item: #{add_item}"
        end
        next unless add_item

        ci_discount = [customer.pricing_program_discount, ci.max_discount].min
        discount_unit_price = (ci.amount * (1 - (ci_discount / 100)).to_f).round(2)
        suggested_items_list ||= {}
        suggested_items_list[self] ||= []
        suggested_items_list[self] << LineItem.new(item: item, resource: self, catalog_item_id: ci.id, description: ci.public_name, quantity: (ma.recommended_qty - ma.actual_qty), price: ci.amount, discounted_price: discount_unit_price)
      end
    end
  end
end

#control_capacity(product_line_url = nil) ⇒ Integer

Returns total control capacity across matching control lines.

Parameters:

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

    restrict to controls for this product line URL

Returns:

  • (Integer)

    total control capacity across matching control lines



147
148
149
150
151
152
153
154
# File 'app/concerns/models/pickable.rb', line 147

def control_capacity(product_line_url = nil)
  lines = if product_line_url.present?
            [line_items.controls.by_product_line_url(product_line_url), line_items.integration_kits.by_product_line_url(product_line_url), line_items.powers.by_product_line_url(product_line_url)].flatten
          else
            [line_items.controls, line_items.integration_kits, line_items.powers].flatten
          end
  lines.sum { |li| li.quantity * li.item.control_capacity }
end

#determine_catalog_for_pickingCatalog

Returns catalog that line items should be picked from.

Returns:

  • (Catalog)

    catalog that line items should be picked from



375
376
377
# File 'app/concerns/models/pickable.rb', line 375

def determine_catalog_for_picking
  try(:is_store_transfer?) ? from_store.primary_catalog : customer.catalog
end

#determine_skus_to_filterArray<String>?

Returns SKUs available in the target store's primary catalog,
or nil when this is not a store transfer.

Returns:

  • (Array<String>, nil)

    SKUs available in the target store's primary catalog,
    or nil when this is not a store transfer



381
382
383
# File 'app/concerns/models/pickable.rb', line 381

def determine_skus_to_filter
  try(:is_store_transfer?) ? to_store.primary_catalog.items.pluck(:sku) : nil
end

#discounted_shipping_totalBigDecimal

Returns sum of discounted totals for active shipping lines.

Returns:

  • (BigDecimal)

    sum of discounted totals for active shipping lines



276
277
278
# File 'app/concerns/models/pickable.rb', line 276

def discounted_shipping_total
  line_items.active_shipping_lines.sum(&:total)
end

#electrical_heating_elementsActiveRecord::Relation<LineItem>

Returns goods lines for electrical heating elements.

Returns:

  • (ActiveRecord::Relation<LineItem>)

    goods lines for electrical heating elements



99
100
101
# File 'app/concerns/models/pickable.rb', line 99

def electrical_heating_elements
  line_items.goods.joins(item: :primary_product_line).heating_elements.order('product_lines.lineage_expanded')
end

#fix_catalogvoid

This method returns an undefined value.



370
371
372
# File 'app/concerns/models/pickable.rb', line 370

def fix_catalog
  CatalogItem::Remapper.new(self).fix_all
end

#get_material_alerts(for_www: false, for_cart: false, _cache: true) ⇒ Array<MaterialAlert>

Returns persisted material alerts for this resource.
Uses database persistence instead of Rails.cache to avoid serialization issues.

Parameters:

  • for_www (Boolean) (defaults to: false)

    whether alerts are for www display (affects some alert messages)

  • for_cart (Boolean) (defaults to: false)

    whether alerts are for cart display

  • _cache (Boolean) (defaults to: true)

    whether to use persistence (always true now, kept for API compatibility)

Returns:

  • (Array<MaterialAlert>)

    array of material alerts with items eager-loaded



453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
# File 'app/concerns/models/pickable.rb', line 453

def get_material_alerts(for_www: false, for_cart: false, _cache: true)
  return [] unless respond_to?(:line_items)

  # Eager load items to avoid N+1 during signature calculation (which calls line_item.sku)
  current_signature = line_items.includes(:item).signature
  if current_signature.nil?
    ErrorReporting.warning("Material alerts: line_items.signature returned nil for #{self.class.name} #{id}")
    return []
  end

  # Check for existing valid alerts with matching signature
  existing = MaterialAlert.for_resource(self, signature: current_signature)
  return existing.to_a if existing.exists?

  # Generate fresh alerts and persist them
  options = { for_www: for_www, for_cart: for_cart }
  check_results = Item::Materials::Check.new.perform_checks_raw(
    container: self,
    options: options
  )

  MaterialAlert.regenerate_for(self, check_results: check_results, signature: current_signature)
end

#has_custom_products?Boolean

Returns whether any line item is a custom product.

Returns:

  • (Boolean)

    whether any line item is a custom product



89
90
91
# File 'app/concerns/models/pickable.rb', line 89

def has_custom_products?
  line_items.joins(:item).where(items: { product_category_id: ProductCategory.custom_product_ids }).exists?
end

#invalidate_material_alerts!Integer

Invalidates (deletes) all cached material alerts for this resource.
Call this when line items change significantly outside normal flow,
such as during order split operations.

Returns:

  • (Integer)

    number of deleted alerts



482
483
484
# File 'app/concerns/models/pickable.rb', line 482

def invalidate_material_alerts!
  MaterialAlert.invalidate_for(self)
end

#line_discountsActiveRecord::Associations::CollectionProxy<LineDiscount>

Returns:

  • (ActiveRecord::Associations::CollectionProxy<LineDiscount>)


27
# File 'app/concerns/models/pickable.rb', line 27

has_many :line_discounts, through: :line_items

#line_itemsActiveRecord::Associations::CollectionProxy<LineItem>

Returns:

  • (ActiveRecord::Associations::CollectionProxy<LineItem>)


19
20
21
22
23
24
# File 'app/concerns/models/pickable.rb', line 19

has_many             :line_items,
as: :resource,
dependent: :destroy,
extend: LineItemExtension,
autosave: true,
inverse_of: :resource

#line_items_grouped_by_room_configuration(ungrouped_name: nil, group_by_item_category: true, list_empty_group: true, include_shipping_lines: true, sort_by_product_category_priority: false, exclude_unnamed_empty_group: true) ⇒ Hash{RoomConfiguration, String => Array<LineItem>}

Groups parent line items by room configuration, with optional category grouping
and sorting.

Parameters:

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

    label for the catch-all group

  • group_by_item_category (Boolean) (defaults to: true)

    whether to further group lines by category

  • list_empty_group (Boolean) (defaults to: true)

    whether to include empty groups

  • include_shipping_lines (Boolean) (defaults to: true)

    whether shipping lines appear in the group

  • sort_by_product_category_priority (Boolean) (defaults to: false)

    whether to sort by category priority

  • exclude_unnamed_empty_group (Boolean) (defaults to: true)

    whether to drop an empty ungrouped bucket

Returns:



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'app/concerns/models/pickable.rb', line 212

def line_items_grouped_by_room_configuration(ungrouped_name: nil,
                                             group_by_item_category: true,
                                             list_empty_group: true,
                                             include_shipping_lines: true,
                                             sort_by_product_category_priority: false,
                                             exclude_unnamed_empty_group: true)
  ungrouped_name ||= 'Ungrouped items'
  items = {}
  if instance_of? RoomConfiguration
    l_items = if include_shipping_lines
                line_items.parents_only.with_associations
              else
                line_items.parents_only.with_associations.reject(&:is_shipping?)
              end
    items[name] = l_items.to_a
  else
    rc_ids = []
    room_configurations.order(:name).each do |rc|
      rc_ids << rc.id
      items[rc] ||= [] # Required to set order
    end
    items[ungrouped_name] = []

    line_items.parents_only
              .includes(:item, :line_discounts, :inventory_commits, catalog_item: :store_item)
              .reject(&:is_shipping?).each do |li|
      rc = (li.resource.is_a?(RoomConfiguration) ? li.resource : li.room_configuration)
      group_id = (!rc.nil? && rc_ids.include?(rc.id) ? rc : ungrouped_name)
      items[group_id] ||= []
      items[group_id] << li
    end
  end

  items.each { |key, lines| items[key] = LineItem.group_by_category(lines) } if group_by_item_category
  if sort_by_product_category_priority
    items.each do |key, lines|
      items[key] = lines.sort_by do |li|
        [(begin
          li.item.product_category.priority
        rescue StandardError
          99
        end), li.item.sku]
      end
    end
  end
  items.delete_if { |_key, lines| lines.empty? } unless list_empty_group
  items.delete_if { |key, lines| key == ungrouped_name && lines.empty? } if exclude_unnamed_empty_group
  items
end

#lines_match_pick_catalog_on_flipvoid

This method returns an undefined value.

Rejects a flip that would leave a persisted goods line in the wrong catalog.
Queries the DB (not the cached association) so a legitimate flip through
Catalog::SwitchItemizableCatalog — which remaps + saves the lines before the
resource is saved — sees them already coherent, while a raw currency/customer
change with un-remapped lines is rejected and rolled back.



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'app/concerns/models/pickable.rb', line 66

def lines_match_pick_catalog_on_flip
  return if try(:is_store_transfer?)
  return if is_a?(Order) && order_type == Order::CREDIT_ORDER # credits keep the original cross-catalog item

  target = begin
    determine_catalog_for_picking
  rescue StandardError
    nil
  end
  return if target&.id.blank?

  # `goods` (not `non_shipping`) to mirror LineItem#catalog_item_matches_resource_catalog,
  # which only enforces goods lines (is_goods? == tax_class 'g').
  stranded = line_items.goods.joins(:catalog_item)
                       .where(CatalogItem[:catalog_id].not_eq(target.id)).count
  return if stranded.zero?

  errors.add(:base,
             "#{stranded} line item(s) are not in catalog #{target.id} (#{target.name}) — " \
             'use Catalog::SwitchItemizableCatalog to change catalog/currency')
end

#meets_custom_products_threshold?Boolean

Returns whether custom products meet the threshold for a custom order agreement.

Returns:

  • (Boolean)

    whether custom products meet the threshold for a custom order agreement



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

def meets_custom_products_threshold?
  has_custom_products? && subtotal >= OrderConstants::CUSTOM_ORDER_AGREEMENT_THRESHOLD
end

#pricing_program_discount_factorBigDecimal

Resolves the effective pricing-program discount as a decimal factor.
Prefers the resource's own discount, then its quote, then its customer.

Returns:

  • (BigDecimal)

    discount factor (e.g. 0.10 for 10%)



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'app/concerns/models/pickable.rb', line 284

def pricing_program_discount_factor
  effective_discount = (begin
    pricing_program_discount
  rescue StandardError
    nil
  end)
  effective_discount ||= begin
    quote.pricing_program_discount
  rescue StandardError
    nil
  end
  # Order's quote will have precedence over customers
  effective_discount ||= begin
    customer.pricing_program_discount
  rescue StandardError
    nil
  end

  (effective_discount.to_f / 100).round(2)
end

#purge_empty_line_itemsvoid

This method returns an undefined value.



157
158
159
160
# File 'app/concerns/models/pickable.rb', line 157

def purge_empty_line_items
  logger.info "#{self.class.name}:#{id} purge_empty_line_items called"
  line_items.select { |li| li.quantity == 0 }.each(&:destroy)
end

#soft_recalc(_reset_to_catalog_price = true) ⇒ Boolean

Admin helper to recompute totals while temporarily disabling auto-coupons.

Parameters:

  • _reset_to_catalog_price (Boolean) (defaults to: true)

    unused legacy parameter, kept for API compatibility

Returns:

  • (Boolean)

    result of the final save!



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'app/concerns/models/pickable.rb', line 166

def soft_recalc(_reset_to_catalog_price = true)
  prev_value = begin
    disable_auto_coupon
  rescue StandardError
    nil
  end
  unless is_a? RoomConfiguration
    reload
    self.disable_auto_coupon = true
    self.line_total = nil
    self.tax_total = nil
    self.total = nil
  end
  save!
  update_attribute(:disable_auto_coupon, prev_value) unless is_a? RoomConfiguration
end

#subtotal(limit_to_types = []) ⇒ BigDecimal

Returns sum of line totals for active non-shipping lines.

Parameters:

  • limit_to_types (Array<Symbol>) (defaults to: [])

    optional line-item scopes to restrict to

Returns:

  • (BigDecimal)

    sum of line totals for active non-shipping lines



264
265
266
267
268
269
270
271
272
273
# File 'app/concerns/models/pickable.rb', line 264

def subtotal(limit_to_types = [])
  l_its = line_items.active_non_shipping_lines
  if limit_to_types.present? && limit_to_types.any?
    l_its = []
    limit_to_types.each do |ltype|
      l_its += line_items.send(ltype)
    end
  end
  l_its.sum(&:total)
end

#subtotal_after_trade_discount_without_shippingBigDecimal

Returns subtotal after trade discount for non-shipping lines.

Returns:

  • (BigDecimal)

    subtotal after trade discount for non-shipping lines



191
192
193
194
195
# File 'app/concerns/models/pickable.rb', line 191

def subtotal_after_trade_discount_without_shipping
  # Use line_discounts for exact totals (prevents rounding errors)
  # This matches LineItem#discounted_total and SQL calculate_itemizable_total function
  line_items.non_shipping.includes(:line_discounts).to_a.sum(&:discounted_total)
end

#subtotal_discounted_without_shippingBigDecimal

Returns discounted subtotal of non-shipping lines.

Returns:

  • (BigDecimal)

    discounted subtotal of non-shipping lines



184
185
186
187
188
# File 'app/concerns/models/pickable.rb', line 184

def subtotal_discounted_without_shipping
  # Use line_discounts for exact totals (prevents rounding errors)
  # This matches LineItem#discounted_total and SQL calculate_itemizable_total function
  line_items.non_shipping.includes(:line_discounts).to_a.sum(&:discounted_total)
end

#subtotal_msrp_without_shippingBigDecimal

Returns MSRP subtotal of non-shipping lines.

Returns:

  • (BigDecimal)

    MSRP subtotal of non-shipping lines



198
199
200
# File 'app/concerns/models/pickable.rb', line 198

def subtotal_msrp_without_shipping
  BigDecimal(line_items.non_shipping.sum('(price * quantity)'))
end

#switching_pick_catalog?Boolean

Returns whether this save changes the catalog the resource picks
from (a currency change, or — for orders — a customer reassignment).

Returns:

  • (Boolean)

    whether this save changes the catalog the resource picks
    from (a currency change, or — for orders — a customer reassignment)



53
54
55
56
57
# File 'app/concerns/models/pickable.rb', line 53

def switching_pick_catalog?
  return true if will_save_change_to_currency?

  has_attribute?(:customer_id) && will_save_change_to_customer_id?
end

#synchronize_lines(target = nil, save_targets = false, _do_not_recalc_shipping = false) ⇒ Array<String>

Synchronizes parent line items between this resource and its related targets.

Parameters:

  • target (Itemizable, Array<Itemizable>, nil) (defaults to: nil)

    target(s) to sync into;
    falls back to synchronization_targets when nil

  • save_targets (Boolean) (defaults to: false)

    whether to persist changes on each target

  • _do_not_recalc_shipping (Boolean) (defaults to: false)

    unused legacy parameter, kept for API compatibility

Returns:

  • (Array<String>)

    human-readable log of applied changes



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'app/concerns/models/pickable.rb', line 312

def synchronize_lines(target = nil, save_targets = false, _do_not_recalc_shipping = false)
  lines_changed = []

  targets = [target || try(:synchronization_targets)].flatten
  # Rooms push to their quotes, quotes and orders push to their room
  targets.each do |i|
    # The lines we match against varies, a room configuration will sync into quotes and order and the lines need to be filtered to be just those of the room configuration, but from a quote or order to room there's no need to do a filter
    target_lines = []
    rc_id_filter = nil
    if i.is_a? RoomConfiguration
      rc_id_filter = i.id
      target_lines = i.line_items.active_parent_lines
      source_lines = line_items.active_parent_lines.select { |li| li.room_configuration_id == i.id }
    else
      rc_id_filter = id
      source_lines = line_items.active_parent_lines
      target_lines = i.line_items.active_parent_lines.select { |lir| lir.room_configuration_id == id }
    end
    # Try to match these room lines in the itemizable to our room
    merged_line_ids = []
    change_detected = false
    target_lines.each do |lir|
      # iterate through each target line and find it in the source lines. beware of what was already matched.
      matched_line_in_source = source_lines.find do |li|
        li.catalog_item_id == lir.catalog_item_id and merged_line_ids.exclude?(li.id)
      end

      if matched_line_in_source
        # update quantities if different
        unless lir.quantity == matched_line_in_source.quantity
          lir.quantity = matched_line_in_source.quantity
          lines_changed << "#{i.id}[#{lir.id}M]"
          change_detected = true
        end
        # Keep track of lines we processed
        merged_line_ids << matched_line_in_source.id
      else
        lines_changed << "#{i.id}[#{lir.id}-]"
        lir.destroy
        change_detected = true
      end
    end
    # Anything unmatched is added
    source_lines.reject { |li| merged_line_ids.include? li.id }.each do |new_line|
      change_detected = true
      attrs = new_line.attributes.except('id', 'resource_id', 'resource_type', 'delivery_id', 'created_at', 'updated_at')
      attrs['room_configuration_id'] = rc_id_filter
      i.line_items.build(attrs)
      lines_changed << "#{i.id}[#{new_line.id}+]"
    end
    i.save if save_targets
    i.refresh_plans(delay: 10.seconds) if i.is_a?(RoomConfiguration) && i.respond_to?(:refresh_plans) && change_detected
  end

  lines_changed
end

#total_amps_by_product_lineHash{String => Integer}

Returns total amps per product line.

Returns:

  • (Hash{String => Integer})

    total amps per product line



141
142
143
# File 'app/concerns/models/pickable.rb', line 141

def total_amps_by_product_line
  total_spec_by_product_line(electrical_heating_elements, 'amps')
end

#total_coverage_by_product_lineHash{String => Integer}

Returns total coverage per product line.

Returns:

  • (Hash{String => Integer})

    total coverage per product line



124
125
126
127
128
# File 'app/concerns/models/pickable.rb', line 124

def total_coverage_by_product_line
  total_spec_by_product_line(electrical_heating_elements, 'coverage').merge(
    total_spec_by_product_line(underlayments, 'coverage')
  )
end

#total_linear_feet_by_product_lineHash{String => Integer}

Returns total linear feet per product line.

Returns:

  • (Hash{String => Integer})

    total linear feet per product line



131
132
133
# File 'app/concerns/models/pickable.rb', line 131

def total_linear_feet_by_product_line
  total_spec_by_product_line(electrical_heating_elements, 'length', 1.to_f / 12)
end

#total_spec_by_product_line(scope, spec, multiplier = 1) ⇒ Hash{String => Integer}

Sums a numeric product spec across the given scope, grouped by product line.

Parameters:

  • scope (ActiveRecord::Relation<LineItem>)

    line items to aggregate

  • spec (String)

    product specification key (e.g. coverage, watts)

  • multiplier (Numeric) (defaults to: 1)

    factor to apply to each row (defaults to 1)

Returns:

  • (Hash{String => Integer})

    spec total per product line lineage



114
115
116
117
118
119
120
121
# File 'app/concerns/models/pickable.rb', line 114

def total_spec_by_product_line(scope, spec, multiplier = 1)
  r = scope.where(parent_id: nil).pluck(Arel.sql("product_lines.lineage_expanded,items.rendered_product_specifications->'#{spec}'->'raw',quantity")).select { |e| e[1] && e[2] }
  r.each_with_object({}) do |r, hsh|
    hsh[r[0]] ||= 0.0
    hsh[r[0]] += (r[1].to_f * r[2].to_f) * multiplier
    hsh[r[0]] = hsh[r[0]].round
  end
end

#total_watts_by_product_lineHash{String => Integer}

Returns total watts per product line.

Returns:

  • (Hash{String => Integer})

    total watts per product line



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

def total_watts_by_product_line
  total_spec_by_product_line(electrical_heating_elements, 'watts')
end

#underlaymentsActiveRecord::Relation<LineItem>

Returns goods lines for underlayments.

Returns:

  • (ActiveRecord::Relation<LineItem>)

    goods lines for underlayments



104
105
106
# File 'app/concerns/models/pickable.rb', line 104

def underlayments
  line_items.goods.joins(item: :primary_product_line).underlayments.order('product_lines.lineage_expanded')
end