Class: Packaging

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable
Defined in:
app/models/packaging.rb

Overview

== Schema Information

Table name: packagings
Database name: primary

id :integer not null, primary key
number_items :integer
created_at :datetime
updated_at :datetime
store_item_id :integer
warehouse_package_id :integer

Indexes

index_packagings_on_store_item_id (store_item_id)
index_packagings_on_warehouse_package_id (warehouse_package_id)

Foreign Keys

packagings_warehouse_package_id_fk (warehouse_package_id => warehouse_packages.id) ON DELETE => cascade

Constant Summary collapse

DEFAULT_BOX_SIZE =
[20, 10, 6]
PER_PACKAGE_WEIGHT_LIMIT =
70.0
LTL_FREIGHT_WASTED_SPACE_IN_PALLETS_FACTOR =

wasted space factor in packing the pallet compared to perfect packing

0.75
LTL_FREIGHT_MAX_PALLET_HEIGHT_IN_FT =

max of 5 feet or 60" per pallet

5.0
LTL_FREIGHT_MIN_PALLET_HEIGHT_IN_INCHES =

here we want every pallet to realistically be minimum 24 inches high

24.0
CRATE_PADDING =
3.5

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#number_itemsObject (readonly)



36
# File 'app/models/packaging.rb', line 36

validates :warehouse_package_id, :store_item, :number_items, presence: true

#warehouse_package_idObject (readonly)



36
# File 'app/models/packaging.rb', line 36

validates :warehouse_package_id, :store_item, :number_items, presence: true

Class Method Details

.add_to_packages_hash(packages_hash, packaging, num_items) ⇒ Object



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
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'app/models/packaging.rb', line 331

def self.add_to_packages_hash(packages_hash, packaging, num_items)
  # get the info from the packaging
  store_item = packaging.store_item
  # puts "store_item.sku: #{store_item.sku}"
  warehouse_package_id = packaging.warehouse_package_id
  # puts "warehouse_package_id: #{warehouse_package_id}"
  num_items_per_package = packaging.number_items
  # puts "num_items_per_package: #{num_items_per_package}"
  per_store_item_usage = (1.0 / num_items_per_package).to_f
  # puts "per_store_item_usage: #{per_store_item_usage}"
  # add an entry unless it exists
  packages_hash[warehouse_package_id] = [] unless packages_hash[warehouse_package_id]
  # see if we can fill up any existing package
  num_items_to_pack_in_existing_packages = 0
  # find any package entries that can take at least one item
  packages_hash[warehouse_package_id].each do |package_entry|
    # puts "package_entry: #{package_entry.inspect}"
    next unless package_entry[:fraction_leftover] >= per_store_item_usage

    # puts "package_entry[:fraction_leftover]: #{package_entry[:fraction_leftover]}"
    # take integral number that can fit in leftover space
    num_items_to_pack_in_this_package = (package_entry[:fraction_leftover] / per_store_item_usage).floor
    num_items_to_pack_in_this_package = num_items if num_items_to_pack_in_this_package > num_items
    # puts "num_items_to_pack_in_this_package: #{num_items_to_pack_in_this_package}"
    num_items_to_pack_in_existing_packages += num_items_to_pack_in_this_package
    # puts "num_items_to_pack_in_existing_packages: #{num_items_to_pack_in_existing_packages}"
    # update store items list, weight and fraction leftover
    package_entry[:store_items] << { store_item.id => { number: num_items_to_pack_in_this_package, per_store_item_usage: per_store_item_usage } }
    package_entry[:weight] += (num_items_to_pack_in_this_package * store_item.shipping_weight)
    package_entry[:fraction_leftover] -= (num_items_to_pack_in_this_package * per_store_item_usage).to_f
    # puts "added items: package_entry: #{package_entry.inspect}"
  end
  # figure out how many new packages we need, rounding up
  number_new_packages = ((num_items - num_items_to_pack_in_existing_packages) / num_items_per_package).ceil
  # puts "number_new_packages: #{number_new_packages}"
  num_items_remaining = (num_items - num_items_to_pack_in_existing_packages)
  # puts "num_items_remaining: #{num_items_remaining}"
  # create a package_entry for each one
  if number_new_packages > 1
    (number_new_packages - 1).times do |_i|
      packages_hash[warehouse_package_id] << { store_items: [{ store_item.id => { number: num_items_per_package, per_store_item_usage: per_store_item_usage } }], weight: num_items_per_package * store_item.shipping_weight,
                                               fraction_leftover: 0.0 }
      num_items_remaining -= num_items_per_package
      # puts "added items: package_entry: #{packages_hash[warehouse_package_id].last.inspect}"
      # puts "num_items_remaining: #{num_items_remaining}"
    end
  end
  # for the last package figure out remainder
  return unless num_items_remaining > 0

  packages_hash[warehouse_package_id] << { store_items: [{ store_item.id => { number: num_items_remaining, per_store_item_usage: per_store_item_usage } }], weight: (num_items_remaining * store_item.shipping_weight),
                                           fraction_leftover: (1.0 - (num_items_remaining * per_store_item_usage).to_f) }
  # puts "added items: package_entry: #{packages_hash[warehouse_package_id].last.inspect}"
end

.by_store_idActiveRecord::Relation<Packaging>

A relation of Packagings that are by store id. Active Record Scope

Returns:

See Also:



42
# File 'app/models/packaging.rb', line 42

scope :by_store_id, ->(store_id) { joins(:store_item).where(store_items: { store_id: store_id }) }

.consolidate_package_entries(package_entries_arr, si_cache: {}) ⇒ Object



241
242
243
244
245
246
247
248
249
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
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
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
328
329
# File 'app/models/packaging.rb', line 241

def self.consolidate_package_entries(package_entries_arr, si_cache: {})
  # puts "consolidate_package_entries: package_entries_arr: #{package_entries_arr.inspect}"
  # take smallest package entry out of the array
  return if package_entries_arr.length == 1

  smallest_package_entry = package_entries_arr.delete_at(0)
  smallest_package = smallest_package_entry.first
  smallest_package_entry_packages = smallest_package_entry.last
  # puts "000000000 smallest_package: #{smallest_package.description}"
  # puts "000000000 smallest_package_entry_packages: #{smallest_package_entry_packages.inspect}"
  total_available_package_usage = package_entries_arr.sum { |peg| smallest_package.fits_inside?(peg.first) ? peg.last.sum { |pei| pei[:fraction_leftover] } : 0.0 }
  # puts "000000000 total_available_package_usage: #{total_available_package_usage}"
  consolidated_smallest_package_away = false
  smallest_package_entry_packages.sort_by { |s| s[:fraction_leftover] }.each do |spe|
    # puts "111111111 working on distributing items in: spe[:store_items].inspect: #{spe[:store_items].inspect}"
    spe[:store_items].each do |si|
      small_package_usage = si.values.first[:number].to_f * si.values.first[:per_store_item_usage]
      # puts "222222222 looking at store item: #{si.inspect}, small_package_usage: #{small_package_usage}"
      si_entry = si.values.first
      store_item_id = si.keys.first
      store_item = si_cache[store_item_id] || StoreItem.find(store_item_id)
      per_store_item_usage = si_entry[:per_store_item_usage]
      # see if we can even consolidate away this package
      next unless small_package_usage <= total_available_package_usage

      # puts "222222222 small_package_usage <= total_available_package_usage: #{small_package_usage} <= #{total_available_package_usage}"
      # iterate over the array
      package_entries_arr.each_with_index do |package_entry, _i|
        package = package_entry.first
        package_entry_packages = package_entry.last
        # puts "333333333 i: #{i}, package: #{package.description}"
        # puts "333333333 i: #{i}, package_entry_packages: #{package_entry_packages.inspect}"
        # see if we can fit the smallest package into the next biggest package
        # puts "333333333 smallest_package.fits_inside?(package): #{smallest_package.fits_inside?(package)}"
        next unless smallest_package.fits_inside?(package)

        # it fits inside
        available_package_usage = package_entry_packages.sum { |pei| pei[:fraction_leftover] }
        # puts "333333333 available_package_usage: #{available_package_usage}"
        next unless small_package_usage <= available_package_usage

        # puts "444444444 small_package_usage <= available_package_usage: #{small_package_usage} <= #{available_package_usage}"
        # see if we can move any items into the bigger package
        package_entry_packages.each_with_index do |pei, _j|
          # puts "444444444 looking at package entry #{i}, #{j}: #{pei.inspect}"
          next unless pei[:fraction_leftover] >= per_store_item_usage

          # puts "555555555 pei[:fraction_leftover] >= #{per_store_item_usage}"
          # take integral number that can fit in leftover space
          num_items_to_pack_in_this_package = (pei[:fraction_leftover] / per_store_item_usage).floor
          num_items_to_pack_in_this_package = si_entry[:number] if num_items_to_pack_in_this_package > si_entry[:number]
          # puts "555555555 num_items_to_pack_in_this_package: #{num_items_to_pack_in_this_package}"
          # update store items list, weight and fraction leftover
          psi = pei[:store_items].detect { |s| s.keys.first == store_item_id }
          if psi
            # puts "666666666 found store item entry in this package, adding to it"
            psi.values.first[:number] += num_items_to_pack_in_this_package
          else
            # puts "666666666 did not find store item entry in this package, creating it"
            pei[:store_items] << { store_item.id => { number: num_items_to_pack_in_this_package, per_store_item_usage: per_store_item_usage } }
          end
          pei[:weight] += (num_items_to_pack_in_this_package * store_item.shipping_weight)
          pei[:fraction_leftover] -= (num_items_to_pack_in_this_package * per_store_item_usage).to_f
          # puts "555555555 added items: pei: #{pei.inspect}"
          si_entry[:number] -= num_items_to_pack_in_this_package
          spe[:weight] -= (num_items_to_pack_in_this_package * store_item.shipping_weight)
          spe[:fraction_leftover] += (num_items_to_pack_in_this_package * per_store_item_usage).to_f
          # puts "555555555 removing items from smaller package, store_item entry: #{spe.inspect}"
          next unless si_entry[:number] == 0

          spe[:store_items].delete(si)
          # puts "777777777 no more store items in entry, removing entry: #{spe[:store_items].inspect}"
          next unless spe[:store_items].empty?

          smallest_package_entry_packages.delete(spe)
          # puts "888888888 no more items in package, removing package: #{smallest_package_entry_packages.inspect}"
          if smallest_package_entry_packages.empty?
            consolidated_smallest_package_away = true
            # puts "999999999 consolidated_smallest_package_away = true"
          end
        end
      end
    end
  end
  Packaging.consolidate_package_entries(package_entries_arr, si_cache: si_cache)
  return if consolidated_smallest_package_away

  package_entries_arr << smallest_package_entry
end

.consolidate_packages(packages) ⇒ Object



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

def self.consolidate_packages(packages)
  consolidated_packages = {}
  wp_by_id = WarehousePackage.where(id: packages.keys).index_by(&:id)
  package_entries_arr = packages.collect { |id, data| [wp_by_id[id], data] }.sort_by { |parr| parr.first.volume }
  # Batch-load all StoreItem records referenced in package data for consolidation
  all_si_ids = packages.values.flat_map { |data_arr| data_arr.flat_map { |d| d[:store_items].map { |si| si.keys.first } } }.uniq
  si_cache = StoreItem.where(id: all_si_ids).index_by(&:id)
  Packaging.consolidate_package_entries(package_entries_arr, si_cache: si_cache)
  package_entries_arr.each do |package_entry|
    consolidated_packages[package_entry.first.id] = package_entry.last
  end
  consolidated_packages
end

.fit_store_items_into_respective_packaging_legacy(packagings_by_store_item_id, number_by_store_item_id) ⇒ Object



386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
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
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
# File 'app/models/packaging.rb', line 386

def self.fit_store_items_into_respective_packaging_legacy(packagings_by_store_item_id, number_by_store_item_id)
  packages = {}
  packagings_by_store_item_id.each do |store_item_id, packagings|
    # basically for each store item, you fill the larger boxes until you have a small enough number to fit in the smaller boxes
    num_store_items = number_by_store_item_id[store_item_id]
    # get the packagings array and reverse sort in descending order of number_items that fit per package, we should end up at 1 item per package for the last one
    sorted_packagings = packagings.sort_by { |p| p.number_items }.reverse
    sorted_packagings.each_index do |i|
      # puts "!!!!! #{store_item.sku}, packagings: #{sorted_packagings[i].warehouse_package.description}: #{sorted_packagings[i].number_items}"
      if sorted_packagings[i].number_items == 1
        # special case for package that can fit only one item
        # puts "111111 package #{sorted_packagings[i].warehouse_package.description} can handle only 1 of these items (please make sure: package.number_items: #{sorted_packagings[i].number_items} == 1?)"
        if num_store_items > 1
          # here rather than use more than a single of the current package, put them all into the previous (presumably bigger) package
          # puts "121212 we have more than one item to deal with"
          if i > 0 && packages[sorted_packagings[i - 1].warehouse_package_id]
            # we have a larger package to put them in
            # puts "131313 we have a larger package to put them in: #{sorted_packagings[i-1].warehouse_package.description} which holds #{sorted_packagings[i-1].number_items}"
            Packaging.add_to_packages_hash(packages, sorted_packagings[i - 1], num_store_items)
            # puts "141414 Added package: #{sorted_packagings[i-1].warehouse_package_id} => #{packages[sorted_packagings[i-1].warehouse_package_id].inspect}"
          else
            # unless this is the largest package, whereupon we need as many as we need
            # puts "151515 we do NOT have a larger package to put them in"
            Packaging.add_to_packages_hash(packages, sorted_packagings[i], num_store_items)
            # puts "161616 Added package: #{sorted_packagings[i].warehouse_package_id} => #{packages[sorted_packagings[i].warehouse_package_id].inspect}"
          end
        elsif num_store_items == 1
          # here we are going for a smaller package since it fits the one item rather than using a larger package
          # puts "171717 we have only one item to deal with"
          Packaging.add_to_packages_hash(packages, sorted_packagings[i], 1)
          # puts "181818 Added package: #{sorted_packagings[i].warehouse_package_id} => #{packages[sorted_packagings[i].warehouse_package_id].inspect}"
        end
        break
      else
        finished = false
        until finished
          # get the number of packages needed by dividing the number of items by the number that fit per package
          num_packages = (num_store_items / sorted_packagings[i].number_items).floor
          # puts "111 #{num_packages} of this package are required"
          if num_packages > 0
            # this means we can at least fill one of these packages so add this package entry to packages with the number needed
            # get the number left over after filling the packages
            num_store_items %= sorted_packagings[i].number_items
            # puts "222 After filling #{num_packages} of this package, #{num_store_items} are left over"
            Packaging.add_to_packages_hash(packages, sorted_packagings[i], num_packages * sorted_packagings[i].number_items)
            # puts "333 Added package: #{sorted_packagings[i].warehouse_package_id} => #{packages[sorted_packagings[i].warehouse_package_id].inspect}"
            finished = true if num_store_items == 0

          elsif num_store_items.positive? # we have less items than fit in the current box and at least one item to deal with
            # puts "444 We don't fill at least one of this package"
            # see if these fit in the next smaller package, if it exists
            if sorted_packagings[i + 1] and num_store_items <= sorted_packagings[i + 1].number_items
              # puts "555 We can fit #{num_store_items} items into package #{sorted_packagings[i+1].warehouse_package.description} which can handle #{sorted_packagings[i+1].number_items} of these items"
              # yes, so let iteration on the next sorted_packagings handle this by setting finished true
              finished = true
            end
            unless finished
              # no or this is the smallest box, so just use the current box
              # puts "888 No smaller packages to use, so we will fit #{num_store_items} items into package #{sorted_packagings[i].warehouse_package.description} which can handle #{sorted_packagings[i].number_items} of these items"
              Packaging.add_to_packages_hash(packages, sorted_packagings[i], num_store_items)
              # puts "999 Added package: #{sorted_packagings[i].warehouse_package_id} => #{packages[sorted_packagings[i].warehouse_package_id].inspect}"
              # puts "101010 We are done with this item"
              finished = true
            end
            break
          else
            finished = true
            break
          end
        end
      end
    end
  end
  packages
end

.fit_store_items_into_respective_packaging_new(packagings_by_store_item_id, number_by_store_item_id) ⇒ Object



213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'app/models/packaging.rb', line 213

def self.fit_store_items_into_respective_packaging_new(packagings_by_store_item_id, number_by_store_item_id)
  packages = {}
  packagings_by_store_item_id.each do |store_item_id, packagings|
    # basically for each store item, you fill the larger boxes until you have a small enough number to fit in the smaller boxes
    num_store_items = number_by_store_item_id[store_item_id]
    # get the packagings array and reverse sort in descending order of number_items that fit per package, we should end up at 1 item per package for the last one
    sorted_packagings = packagings.sort_by { |p| p.number_items }.reverse
    largest_packaging = sorted_packagings.first
    Packaging.add_to_packages_hash(packages, largest_packaging, num_store_items)
    # #puts "Added package: #{largest_packaging.warehouse_package_id} => #{packages[largest_packaging.warehouse_package_id].inspect}"
  end
  packages
end

.get_freight_pallet_packages_hash_from(line_items) ⇒ Object

Delegates to Shipping::FreightPalletCalculator (the authoritative implementation).
Kept for backward compatibility with any callers outside DeterminePackaging.



464
465
466
# File 'app/models/packaging.rb', line 464

def self.get_freight_pallet_packages_hash_from(line_items)
  Shipping::FreightPalletCalculator.call(line_items)
end

.get_packaging_for_line_items(line_items:, store: nil, is_freight: false) ⇒ Object



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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
118
119
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
180
181
182
183
184
185
186
187
188
189
# File 'app/models/packaging.rb', line 44

def self.get_packaging_for_line_items(line_items:, store: nil, is_freight: false)
  # First only goods to be sure
  line_items = line_items.select(&:is_goods?)

  # Short circuit this for freight
  return Packaging.get_freight_pallet_packages_hash_from(line_items) if is_freight

  # puts "get_packaging_for_line_items: store: #{store.id}"
  # puts "get_packaging_for_line_items: line_items.map{|li| [li.sku, li.quantity]}: #{line_items.map{|li| [li.sku, li.quantity]}}"

  # Here are the assumptions for this rather complex calculation:
  #
  # UPS (and other carriers) shipping charges are focused on weight and number of packages, two packages of 5 lbs will cost about twice a single package of 10 lbs, regardless of package dimensions, as long as the packages are not oversize (as UPS defines it: lxwxh > 5196 cubic inches; also if l+2(h+w) > 130 inches then a large item surcharge is applied)
  # So, to get an accurate rating, you need to take into account the way the warehouse will package the items, paying special attention to oversize items, and you can then simplify by combining non oversize items into one package
  #
  # here is how this calculation will proceed:
  # - take all oversize items grouped by packaging and fit them into their respective packages by using the number_items attribute
  # - consolidate, assuming if one package can fit into another, so can its store_items, proportional to the number_items it has in its packagings
  # - if there are any non-oversize items, find the item with the largest minimum package (that is the smallest package by volume in the packaging set for a store item), and assume all remaining items fit into the store_item's packaging
  ship_weights = []
  ship_dimensions = []
  os_store_items_list = line_items.find_all { |li| Packaging.use_shipping_dimensions?(li) }.collect { |li| li.store_item.id }.uniq.sort
  non_os_store_items_list = line_items.find_all { |li| Packaging.use_shipping_dimensions?(li) != true }.collect { |li| li.store_item.id }.uniq.sort
  # puts "os_store_items_list: #{os_store_items_list.inspect}"
  # puts "non_os_store_items_list: #{non_os_store_items_list.inspect}"
  os_packagings_by_store_item_id = {}

  unless os_store_items_list.empty?
    packagings = Packaging.includes(:warehouse_package).where(store_item_id: os_store_items_list)
    packagings = packagings.by_store_id(store&.id) if store
    os_packagings_by_store_item_id = packagings.sort_by { |p| p.warehouse_package.volume }.reverse.group_by(&:store_item_id)
  end
  non_os_packagings_by_store_item_id = {}
  unless non_os_store_items_list.empty?
    packagings = Packaging.includes(:warehouse_package).where(store_item_id: non_os_store_items_list)
    packagings = packagings.by_store_id(store&.id) if store
    non_os_packagings_by_store_item_id = packagings.sort_by { |p| p.warehouse_package.volume }.reverse.group_by(&:store_item_id)
  end
  # puts "os_packagings_by_store_item_id: #{os_packagings_by_store_item_id.inspect}"
  # puts "non_os_packagings_by_store_item_id: #{non_os_packagings_by_store_item_id.inspect}"
  os_number_by_store_item_id = {}
  non_os_number_by_store_item_id = {}
  line_items.each do |li|
    store_item_id = li.store_item.id
    if os_packagings_by_store_item_id[store_item_id]
      os_number_by_store_item_id[store_item_id] = 0 unless os_number_by_store_item_id[store_item_id]
      os_number_by_store_item_id[store_item_id] += li.quantity.abs
    end
    if non_os_packagings_by_store_item_id[store_item_id]
      non_os_number_by_store_item_id[store_item_id] = 0 unless non_os_number_by_store_item_id[store_item_id]
      non_os_number_by_store_item_id[store_item_id] += li.quantity.abs
    end
  end
  # puts "os_number_by_store_item_id: #{os_number_by_store_item_id.inspect}"
  # puts "non_os_number_by_store_item_id: #{non_os_number_by_store_item_id.inspect}"
  unless os_packagings_by_store_item_id.empty?
    # - take all oversize items grouped by packaging and fit them into their respective packages by using the number_items attribute
    # we have two strategies, try both and take the one that minimizes the number of packages and the volumes of those
    os_packages_1 = Packaging.fit_store_items_into_respective_packaging_new(os_packagings_by_store_item_id, os_number_by_store_item_id)
    os_packages_2 = Packaging.fit_store_items_into_respective_packaging_legacy(os_packagings_by_store_item_id, os_number_by_store_item_id)
    # - consolidate, assuming if one package can fit into another, so can its store_items, proportional to the number_items it has in its packagings
    # puts "!!!!!!!!!!!!!!!!!!os_packages_1: #{os_packages_1.inspect}"
    # puts "!!!!!!!!!!!!!!!!!!os_packages_2: #{os_packages_2.inspect}"
    consolidated_os_packages_1 = Packaging.consolidate_packages(os_packages_1)
    consolidated_os_packages_2 = Packaging.consolidate_packages(os_packages_2)
    # puts "!!!!!!!!!!!!!!!!!!consolidated_os_packages_1: #{consolidated_os_packages_1.inspect}"
    # puts "!!!!!!!!!!!!!!!!!!consolidated_os_packages_2: #{consolidated_os_packages_2.inspect}"
    # Batch-load all WarehousePackage records needed for both consolidation strategies
    all_wp_ids = (consolidated_os_packages_1.keys + consolidated_os_packages_2.keys).uniq
    wp_by_id = WarehousePackage.where(id: all_wp_ids).index_by(&:id)
    ship_weights_1 = []
    ship_dimensions_1 = []
    volume_1 = 0.0
    consolidated_os_packages_1.each do |package_id, packages|
      warehouse_package = wp_by_id[package_id]
      packages.each do |package_data|
        ship_weights_1 << package_data[:weight]
        ship_dimensions_1 << [warehouse_package.length, warehouse_package.width, warehouse_package.height]
        volume_1 += warehouse_package.volume
      end
    end
    ship_weights_2 = []
    ship_dimensions_2 = []
    volume_2 = 0.0
    consolidated_os_packages_2.each do |package_id, packages|
      warehouse_package = wp_by_id[package_id]
      packages.each do |package_data|
        ship_weights_2 << package_data[:weight]
        ship_dimensions_2 << [warehouse_package.length, warehouse_package.width, warehouse_package.height]
        volume_2 += warehouse_package.volume
      end
    end
    if ship_weights_2.length > ship_weights_1.length
      # puts "taking ship_weights_1 by number of packages: #{ship_weights_2.length} > #{ship_weights_1.length}"
      ship_weights.concat(ship_weights_1)
      ship_dimensions.concat(ship_dimensions_1)
    elsif ship_weights_1.length > ship_weights_2.length
      # puts "taking ship_weights_2 by number of packages: #{ship_weights_1.length} > #{ship_weights_2.length}"
      ship_weights.concat(ship_weights_2)
      ship_dimensions.concat(ship_dimensions_2)
    elsif volume_2 >= volume_1 # equal numbers, take that with least volume
      # puts "equal number of packages: #{ship_weights_1.length}"
      ship_weights.concat(ship_weights_1)
      ship_dimensions.concat(ship_dimensions_1)
    # puts "taking ship_weights_1 by volume: #{volume_2} >= #{volume_1}"
    elsif volume_1 > volume_2
      # puts "taking ship_weights_2 by volume: #{volume_1} > #{volume_2}"
      ship_weights.concat(ship_weights_2)
      ship_dimensions.concat(ship_dimensions_2)
    end
  end
  unless non_os_packagings_by_store_item_id.empty?
    # - if there are any non-oversize items, find the item with the largest minimum package (that is the smallest package by volume in the packaging set for a store item), and assume all remaining items fit into the store_item's packaging
    # choose first available packaging's smallest warehouse package
    largest_minimum_non_os_package = non_os_packagings_by_store_item_id.first[1].sort_by { |p| p.warehouse_package.volume }.first.warehouse_package
    # iterate over all store_items
    non_os_packagings_by_store_item_id.each do |_store_item, packagings|
      # get smallest package in the packagings and see if it is larger than what we have so far
      smallest_packaging = packagings.sort_by { |p| p.warehouse_package.volume }.first
      largest_minimum_non_os_package = smallest_packaging.warehouse_package if largest_minimum_non_os_package.volume < smallest_packaging.warehouse_package.volume
    end
    non_os_si_by_id = StoreItem.where(id: non_os_number_by_store_item_id.keys).index_by(&:id)
    non_os_store_items_ship_weight = non_os_number_by_store_item_id.sum { |sid, num| non_os_si_by_id[sid].shipping_weight * num }
    if non_os_store_items_ship_weight <= PER_PACKAGE_WEIGHT_LIMIT
      # use this as our nominal maximum, though UPS per package limit is 150 LBS, we use 70 lbs for PER_PACKAGE_WEIGHT_LIMIT to support Purolator's maximum
      ship_dimensions << [largest_minimum_non_os_package.length, largest_minimum_non_os_package.width, largest_minimum_non_os_package.height]
      ship_weights << non_os_store_items_ship_weight
    else
      # simply divide into equal number of boxes
      num_boxes = (non_os_store_items_ship_weight / PER_PACKAGE_WEIGHT_LIMIT).ceil
      weight_per_box = non_os_store_items_ship_weight / num_boxes
      num_boxes.times do |_i|
        ship_dimensions << [largest_minimum_non_os_package.length, largest_minimum_non_os_package.width, largest_minimum_non_os_package.height]
        ship_weights << weight_per_box
      end
    end
    # puts "non_os_packagings_by_store_item_id: #{non_os_packagings_by_store_item_id.inspect}"
    # puts "largest_minimum_non_os_package: #{largest_minimum_non_os_package.inspect}"
    # puts "{:weights => ship_weights, :dimensions => ship_dimensions}: #{{:weights => ship_weights, :dimensions => ship_dimensions}.inspect}"
  end
  if ship_weights.empty? && ship_dimensions.empty?
    ship_weights = line_items.map { |li| li.item.base_weight }
    ship_dimensions = line_items.map { |li| li.item.shipping_dimensions }
  end
  { weights: ship_weights, dimensions: ship_dimensions, container_types: ship_weights.size.times.map { |_t| Shipment.container_types.keys.first } } # Shipment.container_types.keys.first is 'carton'
end

.use_shipping_dimensions?(li) ⇒ Boolean

Returns:

  • (Boolean)


191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'app/models/packaging.rb', line 191

def self.use_shipping_dimensions?(li)
  return true if li.item&.dropship # use dropship item shipping dimensions

  # Here we are using the shipping_dimensions method on line items when this test passes*, to avoid unrealistic scenarios of 200 items in a single regular box.
  # * Note this test should apply to items that have sizable shipping dimensions
  q = li.quantity.abs.to_i
  logger.debug "use_shipping_dimensions, q: #{q}, li.item.shipping_dimensions: #{begin
    li.item.shipping_dimensions.inspect
  rescue StandardError
    'n/a'
  end}, li.item.shipping_dimensions.inject(:*): #{begin
    li.item.shipping_dimensions.inject(:*)
  rescue StandardError
    'n/a'
  end}"
  if li.oversize? or (li.item and li.item.shipping_dimensions and (li.item.shipping_dimensions.inject(:*) > 250.0 && li.item.shipping_dimensions.any? { |dim| dim > 20.0 }))
    true
  else
    false
  end
end

Instance Method Details

#check_dimensionsObject (protected)



609
610
611
612
# File 'app/models/packaging.rb', line 609

def check_dimensions
  errors.add(:base, 'Warehouse package dimensions are smaller the shipping dimensions defined for this item') if warehouse_package && !warehouse_package.can_contain_dimensions?(store_item.shipping_dimensions, 0.1)
  errors.empty?
end

#enqueue_packing_reimportObject (protected)



605
606
607
# File 'app/models/packaging.rb', line 605

def enqueue_packing_reimport
  PackagingImportWorker.perform_async(id)
end

#store_itemStoreItem

Returns:

See Also:

Validations:



33
# File 'app/models/packaging.rb', line 33

belongs_to :store_item, inverse_of: :packagings, optional: true

#warehouse_packageWarehousePackage



32
# File 'app/models/packaging.rb', line 32

belongs_to :warehouse_package, optional: true, counter_cache: true