seshbot-packing 0.7.0 → 0.8.5
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/.gitignore +1 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile.lock +1 -1
- data/README.md +36 -6
- data/lib/seshbot/packing/package.rb +81 -24
- data/lib/seshbot/packing/recipe.rb +3 -57
- data/lib/seshbot/packing/version.rb +1 -1
- data/seshbot-packing.gemspec +6 -6
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1df1737c878348a458089c20c950a5a2c0a8fdd501736904f8a4c89cc83e92ad
|
4
|
+
data.tar.gz: bb96a3429acfbda5e066652600df576f6cf3a0a4e303ee3c6a68a1841eb984e8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8557f78b9e1092f5ea8d776399d2ebc9b1163cd184a45e28482abf2635788b57e27f7dc6fd7d32e7b6b013eb62f86dddb71f3ff64c77f74e71c36278a24e6403
|
7
|
+
data.tar.gz: 52d32265eedae66704b42a43a6183a47bf4268007b463a6cc2529cc291b09c10ef5353b15b725add93931d7f273053fbfc46700a70e43091218b8106017f9e4d
|
data/.gitignore
CHANGED
data/CHANGELOG.md
ADDED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,9 +1,5 @@
|
|
1
1
|
# Seshbot::Packing
|
2
2
|
|
3
|
-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/seshbot/packing`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
|
-
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
6
|
-
|
7
3
|
## Installation
|
8
4
|
|
9
5
|
Add this line to your application's Gemfile:
|
@@ -20,9 +16,43 @@ Or install it yourself as:
|
|
20
16
|
|
21
17
|
$ gem install seshbot-packing
|
22
18
|
|
23
|
-
##
|
19
|
+
## Methods
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
# Bundle Items
|
23
|
+
## this is the main method, it calls most other methods to get its result
|
24
|
+
items = [{ 'sku' => 'B-B306', 'quantity' => 2, 'price' => 1 }]
|
25
|
+
Seshbot::Packing.bundle_items(items)
|
26
|
+
#=> [{ 'sku' => 'BUND-B312', 'quantity' => 1, 'price' => 0 }]
|
27
|
+
|
28
|
+
# Count Product Types
|
29
|
+
## groups together everythign after the `-` and counts them
|
30
|
+
items = [{"sku" => "A-B306", "quantity" => 2}, {"sku" => "B-B306", "quantity" => 1}]
|
31
|
+
Seshbot::Packing.count_product_types(items)
|
32
|
+
#=> {"B306"=>3}
|
33
|
+
|
34
|
+
# Separate SKU
|
35
|
+
## returns the hashes where the sku value matches the given string
|
36
|
+
items = [{ 'sku' => 'A-C306', 'quantity' => 4, 'price' => 1 },
|
37
|
+
{ 'sku' => 'A-B306', 'quantity' => 4, 'price' => 1 }]
|
38
|
+
Seshbot::Packing.filter_by_sku(items, '-C')
|
39
|
+
#=> [{ 'sku' => 'A-C306', 'quantity' => 4, 'price' => 1 }]
|
40
|
+
## also takes a named argument
|
41
|
+
Seshbot::Packing.filter_by_sku(items, '-C', inverse: true)
|
42
|
+
#=> [{ 'sku' => 'A-B306', 'quantity' => 4, 'price' => 1 }]
|
43
|
+
|
44
|
+
# Substitute SKU
|
45
|
+
## replaces an sku in a hash
|
46
|
+
Seshbot::Packing.substitute_sku(items, '-B', '-C')
|
47
|
+
#=> items = [{"sku" => "A-C306", "quantity" => 2}, {"sku" => "B-C306", "quantity" => 1}]
|
48
|
+
```
|
49
|
+
|
24
50
|
|
25
|
-
|
51
|
+
## C324s
|
52
|
+
As of v0.8.0, C324s will be treated differently. If there are any C324s that can exist, they will stay as C324s, and not converted to bottles.
|
53
|
+
eg.
|
54
|
+
In: 1x C312, 1x C312, 1x C306, 1x B306
|
55
|
+
Out: 1x C324, 1x B312
|
26
56
|
|
27
57
|
## Development
|
28
58
|
|
@@ -1,13 +1,32 @@
|
|
1
|
-
require
|
1
|
+
require 'logger'
|
2
|
+
require 'date'
|
3
|
+
|
2
4
|
module Seshbot
|
3
5
|
module Packing
|
4
6
|
class Error < StandardError; end
|
5
|
-
|
6
7
|
class << self
|
8
|
+
def logger
|
9
|
+
logger = Logger.new(STDERR)
|
10
|
+
logger.level = 1
|
11
|
+
logger
|
12
|
+
end
|
13
|
+
|
14
|
+
def filter_by_sku(items, sku_fragment, inverse: false)
|
15
|
+
items.select do |i|
|
16
|
+
matches = i['sku'].include?(sku_fragment)
|
17
|
+
inverse ? !matches : matches
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def substitute_sku(items, from, to)
|
22
|
+
items.each { |i| i['sku'].gsub!(from, to) }
|
23
|
+
items
|
24
|
+
end
|
25
|
+
|
7
26
|
def count_product_types(items)
|
8
27
|
result = Hash.new(0)
|
9
28
|
# create a hash were the keys are sku_fragment, and the values are the summed quantities
|
10
|
-
items.each do |item|
|
29
|
+
items.each do |item|
|
11
30
|
size = packaged_size(item["sku"])
|
12
31
|
result[size] += item["quantity"]
|
13
32
|
end
|
@@ -15,8 +34,15 @@ module Seshbot
|
|
15
34
|
end
|
16
35
|
|
17
36
|
def packaged_size(sku)
|
18
|
-
|
19
|
-
|
37
|
+
size_regex = /-(K?[LUP]\d{2}|[BCM]\d{3})/
|
38
|
+
match = sku.match(size_regex)
|
39
|
+
raise "Cannot determine packaged size for SKU: #{sku}" if match.nil? || match.captures.empty?
|
40
|
+
|
41
|
+
# this is everything inside the (..) in the regex (everything except the leading '-')
|
42
|
+
result = match.captures[0]
|
43
|
+
# HACK! for legacy reasons, we don't consider K to be part of the size.
|
44
|
+
result.gsub!(/^K/, '')
|
45
|
+
|
20
46
|
if result == "B502"
|
21
47
|
result = "B306"
|
22
48
|
end
|
@@ -24,46 +50,77 @@ module Seshbot
|
|
24
50
|
result = "B312"
|
25
51
|
end
|
26
52
|
raise "Cannot determine packaged size for SKU: #{sku}" if result.nil?
|
27
|
-
|
53
|
+
|
28
54
|
result
|
29
55
|
end
|
30
56
|
|
31
|
-
def bundle_items(items, fulfilled_at: nil)
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
return
|
57
|
+
def bundle_items(recipes, items, fulfilled_at: nil)
|
58
|
+
logger.debug "***** BUNDLING"
|
59
|
+
logger.debug "Bundling Items: #{items}. Fulfilled at: #{fulfilled_at || '(no fulfillment date)'}"
|
60
|
+
if !fulfilled_at.nil? && fulfilled_at < DateTime.new(2020, 1, 14, 8, 0, 0, Time.new.zone)
|
61
|
+
return items.map { |item| { 'sku' => item.sku, 'quantity' => item.quantity, 'price' => item.price } }
|
36
62
|
end
|
37
63
|
|
38
|
-
# get
|
39
|
-
|
64
|
+
# get the cans first, and separate them out
|
65
|
+
cans = filter_by_sku(items, '-C')
|
66
|
+
unless cans.empty?
|
67
|
+
product_type_counts = count_product_types(cans)
|
68
|
+
product_type_counts = unpack(recipes, product_type_counts)
|
69
|
+
product_type_counts = pack(recipes, product_type_counts)
|
70
|
+
new_can_items = product_type_counts.map { |k, v| { 'sku' => "BUND-#{k}", 'quantity' => v, 'price' => 0 } }
|
71
|
+
# separate out all the C324s
|
72
|
+
separated_c324s = filter_by_sku(new_can_items, '-C324')
|
73
|
+
logger.debug "Cans: #{cans}"
|
74
|
+
logger.debug " - Removed C324s: #{separated_c324s}"
|
40
75
|
|
41
|
-
|
42
|
-
|
76
|
+
remaining_cans = filter_by_sku(new_can_items, '-C324', inverse: true)
|
77
|
+
logger.debug " - Remaining skus: #{remaining_cans}"
|
78
|
+
end
|
43
79
|
|
80
|
+
# merge the remaining with the original leftover
|
81
|
+
non_cans = filter_by_sku(items, '-C', inverse: true)
|
82
|
+
remaining_cans ||= []
|
83
|
+
remaining_items = non_cans + remaining_cans
|
84
|
+
logger.debug "Substituting:"
|
85
|
+
logger.debug " - remaining items: #{remaining_items}"
|
86
|
+
|
87
|
+
# substitute C's for B's
|
88
|
+
substitute_sku(remaining_items, '-C', '-B')
|
89
|
+
logger.debug " - skus updated to: #{remaining_items}"
|
90
|
+
|
91
|
+
# get a hash of {pack_6: 5, pack_12: 1}
|
92
|
+
product_type_counts = count_product_types(remaining_items)
|
93
|
+
# dismantle packages into individual units (e.g., {pack_6: 7})
|
94
|
+
product_type_counts = unpack(recipes, product_type_counts)
|
44
95
|
# repackage into the 'best' packaging we can figure out (e.g., {pack_12: 2})
|
45
|
-
product_type_counts = pack(product_type_counts)
|
96
|
+
product_type_counts = pack(recipes, product_type_counts)
|
97
|
+
new_remaining_items = product_type_counts.map { |k,v| { "sku" => "BUND-#{k}", "quantity" => v, "price" => 0} }
|
46
98
|
|
47
|
-
|
48
|
-
|
99
|
+
separated_c324s ||= []
|
100
|
+
merged_items = new_remaining_items + separated_c324s
|
101
|
+
logger.debug "Bundled result:"
|
102
|
+
logger.debug " - IN: #{items}"
|
103
|
+
logger.debug " - OUT: #{merged_items}"
|
104
|
+
merged_items
|
49
105
|
end
|
50
106
|
|
51
107
|
|
52
|
-
def unpack(product_type_counts)
|
108
|
+
def unpack(recipes, product_type_counts)
|
53
109
|
final_result = product_type_counts
|
54
110
|
|
55
111
|
while true
|
56
|
-
new_result = pack_single_step(final_result, unpacking: true)
|
112
|
+
new_result = pack_single_step(recipes, final_result, unpacking: true)
|
57
113
|
break if new_result == final_result
|
114
|
+
|
58
115
|
final_result = new_result
|
59
116
|
end
|
60
117
|
final_result
|
61
118
|
end
|
62
119
|
|
63
|
-
def pack_single_step(product_type_counts, unpacking:)
|
120
|
+
def pack_single_step(recipes, product_type_counts, unpacking:)
|
64
121
|
result = Hash.new(0)
|
65
122
|
product_type_counts.each do |sku_fragment, qty|
|
66
|
-
recipe = Recipe::find_best_recipe(sku_fragment, qty, unpacking: unpacking)
|
123
|
+
recipe = Recipe::find_best_recipe(recipes, sku_fragment, qty, unpacking: unpacking)
|
67
124
|
if recipe.nil?
|
68
125
|
result[sku_fragment] += qty
|
69
126
|
next
|
@@ -78,12 +135,12 @@ module Seshbot
|
|
78
135
|
end
|
79
136
|
|
80
137
|
|
81
|
-
def pack(product_type_counts)
|
138
|
+
def pack(recipes, product_type_counts)
|
82
139
|
final_result = product_type_counts
|
83
140
|
|
84
141
|
# keep trying to 'pack' until it stabilises (e.g., 6x6pack => 1x24pack+2x6pack => 1x24pack+1x12pack)
|
85
142
|
while true
|
86
|
-
new_result = pack_single_step(final_result, unpacking: false)
|
143
|
+
new_result = pack_single_step(recipes, final_result, unpacking: false)
|
87
144
|
# no changes, break out
|
88
145
|
break if new_result == final_result
|
89
146
|
final_result = new_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"]
|
data/seshbot-packing.gemspec
CHANGED
@@ -1,20 +1,20 @@
|
|
1
1
|
require_relative 'lib/seshbot/packing/version'
|
2
2
|
|
3
3
|
Gem::Specification.new do |spec|
|
4
|
-
spec.name =
|
4
|
+
spec.name = 'seshbot-packing'
|
5
5
|
spec.version = Seshbot::Packing::VERSION
|
6
|
-
spec.authors = [
|
7
|
-
spec.email = [
|
8
|
-
|
6
|
+
spec.authors = ['Shaun']
|
7
|
+
spec.email = ['shaun@brewkeeper.com.au']
|
8
|
+
spec.license = 'MIT'
|
9
9
|
spec.summary = %q{Receives, calculates, returns skus with new quantities}
|
10
|
-
spec.homepage =
|
10
|
+
spec.homepage = 'https://gitlab.com/brewkeeper/seshbot-packing'
|
11
11
|
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
12
12
|
|
13
13
|
# spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
14
14
|
|
15
15
|
spec.metadata["homepage_uri"] = spec.homepage
|
16
16
|
spec.metadata["source_code_uri"] = "https://gitlab.com/brewkeeper/seshbot-packing"
|
17
|
-
spec.metadata["changelog_uri"] = "https://gitlab.com/brewkeeper/seshbot-packing
|
17
|
+
spec.metadata["changelog_uri"] = "https://gitlab.com/brewkeeper/seshbot-packing/-/blob/master/CHANGELOG.md"
|
18
18
|
|
19
19
|
# Specify which files should be added to the gem when it is released.
|
20
20
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
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.
|
4
|
+
version: 0.8.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shaun
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-03-29 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -19,6 +19,7 @@ extra_rdoc_files: []
|
|
19
19
|
files:
|
20
20
|
- ".gitignore"
|
21
21
|
- ".travis.yml"
|
22
|
+
- CHANGELOG.md
|
22
23
|
- CODE_OF_CONDUCT.md
|
23
24
|
- Gemfile
|
24
25
|
- Gemfile.lock
|
@@ -32,11 +33,12 @@ files:
|
|
32
33
|
- lib/seshbot/packing/version.rb
|
33
34
|
- seshbot-packing.gemspec
|
34
35
|
homepage: https://gitlab.com/brewkeeper/seshbot-packing
|
35
|
-
licenses:
|
36
|
+
licenses:
|
37
|
+
- MIT
|
36
38
|
metadata:
|
37
39
|
homepage_uri: https://gitlab.com/brewkeeper/seshbot-packing
|
38
40
|
source_code_uri: https://gitlab.com/brewkeeper/seshbot-packing
|
39
|
-
changelog_uri: https://gitlab.com/brewkeeper/seshbot-packing
|
41
|
+
changelog_uri: https://gitlab.com/brewkeeper/seshbot-packing/-/blob/master/CHANGELOG.md
|
40
42
|
post_install_message:
|
41
43
|
rdoc_options: []
|
42
44
|
require_paths:
|