Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add an allocator class to extend the Solidus initial allocation logic
With this PR we add the possibility to change the stock allocation logic using a custom class without override the allocate_inventory method in SimpleCoordinator.
- Loading branch information
1 parent
fac5b35
commit f6c7c3e
Showing
8 changed files
with
295 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# frozen_string_literal: true | ||
|
||
module Spree | ||
module Stock | ||
module Allocator | ||
class Base | ||
attr_reader :availability | ||
|
||
def initialize(availability) | ||
@availability = availability | ||
end | ||
|
||
def allocate_inventory(_desired) | ||
raise NotImplementedError | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
# frozen_string_literal: true | ||
|
||
module Spree | ||
module Stock | ||
module Allocator | ||
class OnHandFirst < Spree::Stock::Allocator::Base | ||
def allocate_inventory(desired) | ||
# Allocate any available on hand inventory | ||
on_hand = allocate_on_hand(desired) | ||
desired -= on_hand.values.sum if on_hand.present? | ||
|
||
# Allocate remaining desired inventory from backorders | ||
backordered = allocate_backordered(desired) | ||
desired -= backordered.values.sum if backordered.present? | ||
|
||
# If all works at this point desired must be empty | ||
[on_hand, backordered, desired] | ||
end | ||
|
||
protected | ||
|
||
def allocate_on_hand(desired) | ||
allocate(availability.on_hand_by_stock_location_id, desired) | ||
end | ||
|
||
def allocate_backordered(desired) | ||
allocate(availability.backorderable_by_stock_location_id, desired) | ||
end | ||
|
||
def allocate(availability_by_location, desired) | ||
availability_by_location.transform_values do |available| | ||
# Find the desired inventory which is available at this location | ||
packaged = available & desired | ||
# Remove found inventory from desired | ||
desired -= packaged | ||
packaged | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
146 changes: 146 additions & 0 deletions
146
core/spec/models/spree/stock/allocator/on_hand_first_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'rails_helper' | ||
|
||
module Spree | ||
module Stock | ||
module Allocator | ||
RSpec.describe OnHandFirst, type: :model do | ||
subject { described_class.new(availability) } | ||
|
||
let(:availability) { double(Spree::Stock::Availability) } | ||
|
||
let!(:default_stock_location) { create(:stock_location, default: true, backorderable_default: false) } | ||
let!(:backorderable_stock_location) { create(:stock_location) } | ||
|
||
let(:first_variant) { create(:variant) } | ||
let(:second_variant) { create(:variant) } | ||
|
||
let(:desired_quantities) do | ||
quantities = {} | ||
quantities[first_variant] = first_variant_desired | ||
quantities[second_variant] = second_variant_desired | ||
quantities | ||
end | ||
|
||
let(:desired) { Spree::StockQuantities.new(desired_quantities) } | ||
|
||
describe '#allocate_inventory' do | ||
let(:default_on_hand_availability) do | ||
quantities = {} | ||
quantities[first_variant] = first_variant_default_availability | ||
quantities[second_variant] = second_variant_default_availability | ||
quantities | ||
end | ||
|
||
let(:dropship_on_hand_availability) do | ||
quantities = {} | ||
quantities[first_variant] = first_variant_dropship_availability | ||
quantities[second_variant] = second_variant_dropship_availability | ||
quantities | ||
end | ||
|
||
let(:on_hand_by_stock_location_id) do | ||
availability = {} | ||
availability[default_stock_location.id] = Spree::StockQuantities.new(default_on_hand_availability) | ||
availability[backorderable_stock_location.id] = Spree::StockQuantities.new(dropship_on_hand_availability) | ||
availability | ||
end | ||
|
||
let(:dropship_backorderable_availability) do | ||
quantities = {} | ||
quantities[first_variant] = Float::INFINITY | ||
quantities[second_variant] = Float::INFINITY | ||
quantities | ||
end | ||
|
||
let(:backorderable_by_stock_location_id) do | ||
availability = {} | ||
availability[backorderable_stock_location.id] = Spree::StockQuantities.new(dropship_backorderable_availability) | ||
availability | ||
end | ||
|
||
before do | ||
allow(availability).to receive(:on_hand_by_stock_location_id) | ||
.and_return(on_hand_by_stock_location_id) | ||
|
||
allow(availability).to receive(:backorderable_by_stock_location_id) | ||
.and_return(backorderable_by_stock_location_id) | ||
end | ||
|
||
context 'when default stock location has enough items' do | ||
let(:first_variant_default_availability) { 100 } | ||
let(:second_variant_default_availability) { 100 } | ||
let(:first_variant_dropship_availability) { 0 } | ||
let(:second_variant_dropship_availability) { 0 } | ||
let(:first_variant_desired) { 30 } | ||
let(:second_variant_desired) { 5 } | ||
|
||
it 'allocates all the desired units on the default stock location' do | ||
on_hand_packages, backordered_packages, leftover = subject.allocate_inventory(desired) | ||
|
||
expect(on_hand_packages[default_stock_location.id][first_variant]).to eq(30) | ||
expect(on_hand_packages[default_stock_location.id][second_variant]).to eq(5) | ||
expect(on_hand_packages[backorderable_stock_location.id][first_variant]).to eq(0) | ||
expect(on_hand_packages[backorderable_stock_location.id][second_variant]).to eq(0) | ||
|
||
expect(backordered_packages[backorderable_stock_location.id][first_variant]).to eq(0) | ||
expect(backordered_packages[backorderable_stock_location.id][second_variant]).to eq(0) | ||
|
||
expect(leftover[first_variant]).to eq(0) | ||
expect(leftover[second_variant]).to eq(0) | ||
end | ||
end | ||
|
||
context 'when default stock location hasn\'t enough items' do | ||
let(:first_variant_default_availability) { 10 } | ||
let(:second_variant_default_availability) { 10 } | ||
|
||
let(:first_variant_desired) { 15 } | ||
let(:second_variant_desired) { 5 } | ||
|
||
context 'when dropship stock location has enough items' do | ||
let(:first_variant_dropship_availability) { 20 } | ||
let(:second_variant_dropship_availability) { 0 } | ||
|
||
it 'allocates all the desired units on the stock locations while stocks last' do | ||
on_hand_packages, backordered_packages, leftover = subject.allocate_inventory(desired) | ||
|
||
expect(on_hand_packages[default_stock_location.id][first_variant]).to eq(10) | ||
expect(on_hand_packages[default_stock_location.id][second_variant]).to eq(5) | ||
expect(on_hand_packages[backorderable_stock_location.id][first_variant]).to eq(5) | ||
expect(on_hand_packages[backorderable_stock_location.id][second_variant]).to eq(0) | ||
|
||
expect(backordered_packages[backorderable_stock_location.id][first_variant]).to eq(0) | ||
expect(backordered_packages[backorderable_stock_location.id][second_variant]).to eq(0) | ||
|
||
expect(leftover[first_variant]).to eq(0) | ||
expect(leftover[second_variant]).to eq(0) | ||
end | ||
end | ||
|
||
context 'when dropship stock location hasn\'t enough items' do | ||
let(:first_variant_dropship_availability) { 2 } | ||
let(:second_variant_dropship_availability) { 0 } | ||
|
||
it 'allocates all the desired units on the stock locations while stocks last' do | ||
on_hand_packages, backordered_packages, leftover = subject.allocate_inventory(desired) | ||
|
||
expect(on_hand_packages[default_stock_location.id][first_variant]).to eq(10) | ||
expect(on_hand_packages[default_stock_location.id][second_variant]).to eq(5) | ||
expect(on_hand_packages[backorderable_stock_location.id][first_variant]).to eq(2) | ||
expect(on_hand_packages[backorderable_stock_location.id][second_variant]).to eq(0) | ||
|
||
expect(backordered_packages[backorderable_stock_location.id][first_variant]).to eq(3) | ||
expect(backordered_packages[backorderable_stock_location.id][second_variant]).to eq(0) | ||
|
||
expect(leftover[first_variant]).to eq(0) | ||
expect(leftover[second_variant]).to eq(0) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
58 changes: 58 additions & 0 deletions
58
guides/source/developers/shipments/stock-allocator.html.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# Stock allocator | ||
|
||
This article explains the concept of a stock allocator and its usage. | ||
|
||
During the checkout process, after the delivery step, the order stocks the ordered stock items | ||
to ship them. | ||
|
||
The stock allocator defines the logic with which these packages are created. | ||
|
||
The allocator is called by `Spree::Stock::SimpleCoordinator` when allocating inventory for an order. | ||
|
||
## Pre-configured allocator | ||
|
||
Currently, we only have one allocator, which you should use unless you need custom logic: | ||
|
||
- [On-hand First](https://github.com/solidusio/solidus/blob/master/core/app/models/spree/stock/allocator/on_hand_first.rb), | ||
which allocates inventory using Solidus' pre-existing logic. | ||
|
||
Examples: | ||
- Someone orders a product which doesn't have stock items on hand, but is backorderable: | ||
- The order stocks the inventory and create one backordered shipment. | ||
- Someone orders a product which has on hand stock items and it's backorderable: | ||
- If the ordered quantity doesn't exceed the availability, the order stocks the inventory | ||
and creates one `on_hand` shipment. | ||
- Otherwise, if the order exceeds the availability, the order stocks the inventory | ||
and create two shipments, one `on_hand` up to the number of available stock items and one | ||
backordered for the rest. | ||
|
||
## Custom allocator API | ||
|
||
A custom allocator should inherit from `Spree::Stock::Allocator::Base` and implement an | ||
`allocate_inventory` method which accepts a `Spree::StockQuantities` and returns the packages | ||
splitted with the allocator's logic. | ||
|
||
```ruby | ||
class Spree::Stock::Allocator::CustomAllocator < Spree::Stock::Allocator::Base | ||
def allocate_inventory(desired) | ||
# Some code to allocate packages with different logic | ||
end | ||
end | ||
``` | ||
|
||
### Switching the allocator | ||
|
||
Once you have created the logic for the new allocator, you need to register it so that it's used by | ||
`Spree::Stock::SimpleCoordinator`. | ||
|
||
For example, you can register it in your `/config/initializer/spree.rb` initializer: | ||
|
||
```ruby | ||
# /config/initializer/spree.rb | ||
Spree.config do |config| | ||
# ... | ||
|
||
config.stock.allocator_class = 'Spree::Stock::Allocator::CustomAllocator' | ||
end | ||
end | ||
``` |