seshbot-packing 0.8.1 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/lib/seshbot/packing.rb +1 -0
- data/lib/seshbot/packing/line_item.rb +19 -0
- data/lib/seshbot/packing/package.rb +47 -36
- data/lib/seshbot/packing/recipe.rb +3 -57
- data/lib/seshbot/packing/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c1693c794a4be401fe157bd5a6385e1375e37c738e8919a168cc881ec262f9ec
|
4
|
+
data.tar.gz: 44c25e149bc8dba14cc2cc49f309b5e3ba5901bf4a636f89787ec5e946e28502
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 59f4cb5bad8c401ef6dde61443937c3bbce68f37f659aef074175a067fd6ea91b346531bc1c809d7b2788a30969eb14c92d8f716fc382d56ac26560cec6c93fd
|
7
|
+
data.tar.gz: c9536fe10e717e4841e8521f5986e467cc0e2b029c3a7512cee255f61941f0d0a5041c6ff0a9a047c218691d9b47ef69b14da6df7227f304cc4eb015f0ba1afa
|
data/Gemfile.lock
CHANGED
data/lib/seshbot/packing.rb
CHANGED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Seshbot
|
4
|
+
module Packing
|
5
|
+
# :nodocs:
|
6
|
+
class LineItem
|
7
|
+
attr_accessor :sku, :quantity, :price
|
8
|
+
|
9
|
+
def initialize(sku, quantity, price=0)
|
10
|
+
raise 'SKU must be a string' if sku.class != String
|
11
|
+
raise 'Quantity must be an integer' if quantity.class != Integer
|
12
|
+
|
13
|
+
@sku = sku
|
14
|
+
@quantity = quantity
|
15
|
+
@price = price
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -5,35 +5,46 @@ module Seshbot
|
|
5
5
|
module Packing
|
6
6
|
class Error < StandardError; end
|
7
7
|
class << self
|
8
|
+
@@logger = nil
|
8
9
|
def logger
|
9
|
-
|
10
|
+
if @@logger.nil?
|
11
|
+
@@logger = Logger.new(STDERR)
|
12
|
+
@@logger.level = 1
|
13
|
+
end
|
14
|
+
@@logger
|
10
15
|
end
|
11
16
|
|
12
17
|
def filter_by_sku(items, sku_fragment, inverse: false)
|
13
18
|
items.select do |i|
|
14
|
-
matches = i
|
19
|
+
matches = i.sku.include?(sku_fragment)
|
15
20
|
inverse ? !matches : matches
|
16
21
|
end
|
17
22
|
end
|
18
23
|
|
19
24
|
def substitute_sku(items, from, to)
|
20
|
-
items.
|
21
|
-
items
|
25
|
+
items.map { |i| Seshbot::Packing::LineItem.new(i.sku.gsub(from, to), i.quantity) }
|
22
26
|
end
|
23
27
|
|
24
28
|
def count_product_types(items)
|
25
29
|
result = Hash.new(0)
|
26
30
|
# create a hash were the keys are sku_fragment, and the values are the summed quantities
|
27
31
|
items.each do |item|
|
28
|
-
size = packaged_size(item
|
29
|
-
result[size] += item
|
32
|
+
size = packaged_size(item.sku)
|
33
|
+
result[size] += item.quantity
|
30
34
|
end
|
31
|
-
result
|
35
|
+
result.map { |k,v| Seshbot::Packing::LineItem.new("BUND-#{k}", v) }
|
32
36
|
end
|
33
37
|
|
34
38
|
def packaged_size(sku)
|
35
|
-
|
36
|
-
|
39
|
+
size_regex = /-(K?[LUP]\d{2}|[BCM]\d{3})/
|
40
|
+
match = sku.match(size_regex)
|
41
|
+
raise "Cannot determine packaged size for SKU: #{sku}" if match.nil? || match.captures.empty?
|
42
|
+
|
43
|
+
# this is everything inside the (..) in the regex (everything except the leading '-')
|
44
|
+
result = match.captures[0]
|
45
|
+
# HACK! for legacy reasons, we don't consider K to be part of the size.
|
46
|
+
result.gsub!(/^K/, '')
|
47
|
+
|
37
48
|
if result == "B502"
|
38
49
|
result = "B306"
|
39
50
|
end
|
@@ -45,20 +56,19 @@ module Seshbot
|
|
45
56
|
result
|
46
57
|
end
|
47
58
|
|
48
|
-
def bundle_items(items, fulfilled_at: nil)
|
59
|
+
def bundle_items(recipes, items, fulfilled_at: nil)
|
49
60
|
logger.debug "***** BUNDLING"
|
50
61
|
logger.debug "Bundling Items: #{items}. Fulfilled at: #{fulfilled_at || '(no fulfillment date)'}"
|
51
62
|
if !fulfilled_at.nil? && fulfilled_at < DateTime.new(2020, 1, 14, 8, 0, 0, Time.new.zone)
|
52
|
-
return items
|
63
|
+
return items
|
53
64
|
end
|
54
65
|
|
55
66
|
# get the cans first, and separate them out
|
56
67
|
cans = filter_by_sku(items, '-C')
|
57
68
|
unless cans.empty?
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
new_can_items = product_type_counts.map { |k, v| { 'sku' => "BUND-#{k}", 'quantity' => v, 'price' => 0 } }
|
69
|
+
bundle_items_by_type = count_product_types(cans)
|
70
|
+
bundle_items_by_type = unpack(recipes, bundle_items_by_type)
|
71
|
+
new_can_items = pack(recipes, bundle_items_by_type)
|
62
72
|
# separate out all the C324s
|
63
73
|
separated_c324s = filter_by_sku(new_can_items, '-C324')
|
64
74
|
logger.debug "Cans: #{cans}"
|
@@ -76,16 +86,15 @@ module Seshbot
|
|
76
86
|
logger.debug " - remaining items: #{remaining_items}"
|
77
87
|
|
78
88
|
# substitute C's for B's
|
79
|
-
substitute_sku(remaining_items, '-C', '-B')
|
89
|
+
remaining_items = substitute_sku(remaining_items, '-C', '-B')
|
80
90
|
logger.debug " - skus updated to: #{remaining_items}"
|
81
91
|
|
82
92
|
# get a hash of {pack_6: 5, pack_12: 1}
|
83
|
-
|
93
|
+
bundle_items_by_type = count_product_types(remaining_items)
|
84
94
|
# dismantle packages into individual units (e.g., {pack_6: 7})
|
85
|
-
|
95
|
+
bundle_items_by_type = unpack(recipes, bundle_items_by_type)
|
86
96
|
# repackage into the 'best' packaging we can figure out (e.g., {pack_12: 2})
|
87
|
-
|
88
|
-
new_remaining_items = product_type_counts.map { |k,v| { "sku" => "BUND-#{k}", "quantity" => v, "price" => 0} }
|
97
|
+
new_remaining_items = pack(recipes, bundle_items_by_type)
|
89
98
|
|
90
99
|
separated_c324s ||= []
|
91
100
|
merged_items = new_remaining_items + separated_c324s
|
@@ -95,45 +104,47 @@ module Seshbot
|
|
95
104
|
merged_items
|
96
105
|
end
|
97
106
|
|
107
|
+
def unpack(recipes, items)
|
98
108
|
|
99
|
-
|
100
|
-
final_result = product_type_counts
|
109
|
+
final_result = items
|
101
110
|
|
102
|
-
|
103
|
-
new_result = pack_single_step(final_result, unpacking: true)
|
104
|
-
break if new_result == final_result
|
111
|
+
loop do
|
112
|
+
new_result = pack_single_step(recipes, final_result, unpacking: true)
|
113
|
+
break if new_result.map { |li| [li.sku, li.quantity] }.sort == final_result.map { |li| [li.sku, li.quantity] }.sort
|
105
114
|
|
106
115
|
final_result = new_result
|
107
116
|
end
|
108
117
|
final_result
|
109
118
|
end
|
110
119
|
|
111
|
-
def pack_single_step(
|
120
|
+
def pack_single_step(recipes, items, unpacking:)
|
112
121
|
result = Hash.new(0)
|
113
|
-
|
114
|
-
|
122
|
+
items.each do |item|
|
123
|
+
sku_size = packaged_size(item.sku)
|
124
|
+
recipe = Recipe::find_best_recipe(recipes, sku_size, item.quantity, unpacking: unpacking)
|
115
125
|
if recipe.nil?
|
116
|
-
result[
|
126
|
+
result[sku_size] += item.quantity
|
117
127
|
next
|
118
128
|
end
|
119
129
|
|
120
|
-
recipe_quantities = Recipe::apply_recipe(recipe,
|
130
|
+
recipe_quantities = Recipe::apply_recipe(recipe, item.quantity)
|
121
131
|
result[recipe["input_fragment"]] += recipe_quantities[:input_quantity]
|
122
132
|
result[recipe["output_fragment"]] += recipe_quantities[:output_quantity]
|
123
133
|
end
|
124
134
|
result.delete_if { |k,v| v == 0}
|
125
|
-
result
|
135
|
+
result.map { |k,v| Seshbot::Packing::LineItem.new("BUND-#{k}", v) }
|
126
136
|
end
|
127
137
|
|
128
138
|
|
129
|
-
def pack(
|
130
|
-
final_result =
|
139
|
+
def pack(recipes, items)
|
140
|
+
final_result = items
|
131
141
|
|
132
142
|
# keep trying to 'pack' until it stabilises (e.g., 6x6pack => 1x24pack+2x6pack => 1x24pack+1x12pack)
|
133
|
-
|
134
|
-
new_result = pack_single_step(final_result, unpacking: false)
|
143
|
+
loop do
|
144
|
+
new_result = pack_single_step(recipes, final_result, unpacking: false)
|
135
145
|
# no changes, break out
|
136
|
-
break if new_result == final_result
|
146
|
+
break if new_result.map { |li| [li.sku, li.quantity] }.sort == final_result.map { |li| [li.sku, li.quantity] }.sort
|
147
|
+
|
137
148
|
final_result = new_result
|
138
149
|
end
|
139
150
|
final_result
|
@@ -2,61 +2,8 @@ module Seshbot
|
|
2
2
|
module Packing
|
3
3
|
module Recipe
|
4
4
|
class << self
|
5
|
-
def
|
6
|
-
|
7
|
-
"bottles_twenty_four_pack_4"=>{
|
8
|
-
"input_fragment"=>"B306",
|
9
|
-
"input_quantity"=>4,
|
10
|
-
"output_fragment"=>"B324",
|
11
|
-
"output_quantity"=>1
|
12
|
-
},
|
13
|
-
"bottles_twenty_four_pack_3"=>{
|
14
|
-
"input_fragment"=>"B306",
|
15
|
-
"input_quantity"=>3,
|
16
|
-
"output_fragment"=>"B318",
|
17
|
-
"output_quantity"=>1
|
18
|
-
},
|
19
|
-
"bottles_twelve_pack"=>{
|
20
|
-
"input_fragment"=>"B306",
|
21
|
-
"input_quantity"=>2,
|
22
|
-
"output_fragment"=>"B312",
|
23
|
-
"output_quantity"=>1
|
24
|
-
},
|
25
|
-
"bottles_six_pack"=>{
|
26
|
-
"input_fragment"=>"B301",
|
27
|
-
"input_quantity"=>6,
|
28
|
-
"output_fragment"=>"B306",
|
29
|
-
"output_quantity"=>1
|
30
|
-
},
|
31
|
-
"cans_twenty_four_pack_4"=>{
|
32
|
-
"input_fragment"=>"C306",
|
33
|
-
"input_quantity"=>4,
|
34
|
-
"output_fragment"=>"C324",
|
35
|
-
"output_quantity"=>1
|
36
|
-
},
|
37
|
-
"cans_twenty_four_pack_3"=>{
|
38
|
-
"input_fragment"=>"C306",
|
39
|
-
"input_quantity"=>3,
|
40
|
-
"output_fragment"=>"C318",
|
41
|
-
"output_quantity"=>1
|
42
|
-
},
|
43
|
-
"cans_twelve_pack"=>{
|
44
|
-
"input_fragment"=>"C306",
|
45
|
-
"input_quantity"=>2,
|
46
|
-
"output_fragment"=>"C312",
|
47
|
-
"output_quantity"=>1
|
48
|
-
},
|
49
|
-
"cans_six_pack"=>{
|
50
|
-
"input_fragment"=>"C301",
|
51
|
-
"input_quantity"=>6,
|
52
|
-
"output_fragment"=>"C306",
|
53
|
-
"output_quantity"=>1
|
54
|
-
}
|
55
|
-
}
|
56
|
-
end
|
57
|
-
|
58
|
-
def find_best_recipe(sku_fragment, qty, unpacking: false)
|
59
|
-
recipes = find_recipes(sku_fragment, qty, unpacking: unpacking)
|
5
|
+
def find_best_recipe(recipes_hash, sku_fragment, qty, unpacking: false)
|
6
|
+
recipes = find_recipes(recipes_hash, sku_fragment, qty, unpacking: unpacking)
|
60
7
|
# we want the recipe that is 'best' (packaging into as few items as possible, or unpacaging into as many as possible)
|
61
8
|
best_recipe_quantities = nil
|
62
9
|
recipes.each do |name, recipe|
|
@@ -77,8 +24,7 @@ module Seshbot
|
|
77
24
|
best_recipe_quantities[:recipe] if best_recipe_quantities
|
78
25
|
end
|
79
26
|
|
80
|
-
def find_recipes(sku_fragment, qty, unpacking: false)
|
81
|
-
recipes = get_recipes
|
27
|
+
def find_recipes(recipes, sku_fragment, qty, unpacking: false)
|
82
28
|
recipes = reverse_recipes(recipes) if unpacking
|
83
29
|
recipes.select do |name, recipe|
|
84
30
|
recipe["input_fragment"] == sku_fragment && qty >= recipe["input_quantity"]
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: seshbot-packing
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shaun
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-03-31 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -28,6 +28,7 @@ files:
|
|
28
28
|
- bin/console
|
29
29
|
- bin/setup
|
30
30
|
- lib/seshbot/packing.rb
|
31
|
+
- lib/seshbot/packing/line_item.rb
|
31
32
|
- lib/seshbot/packing/package.rb
|
32
33
|
- lib/seshbot/packing/recipe.rb
|
33
34
|
- lib/seshbot/packing/version.rb
|