Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stock location sorters #2783

Merged
merged 2 commits into from
Oct 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Solidus 2.8.0 (master, unreleased)

### Core
kennyadsl marked this conversation as resolved.
Show resolved Hide resolved

- Implement stock location sorters [#2783](https://github.com/solidusio/solidus/pull/2783) ([aldesantis](https://github.com/aldesantis))

## Solidus 2.7.0 (2018-09-14)

### Major Changes
Expand Down
38 changes: 38 additions & 0 deletions core/app/models/spree/stock/location_sorter/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

module Spree
module Stock
module LocationSorter
# Stock location sorters are used to determine the order in which
# inventory units will be allocated when packaging a shipment.
#
# This allows you, for example, to allocate inventory from the default
# stock location first.
#
# @abstract To implement your own location sorter, subclass and
# implement {#sort}.
class Base
# @!attribute [r] stock_locations
# @return [Enumerable<Spree::StockLocation>]
# a collection of locations to sort
attr_reader :stock_locations

# Initializes the stock location sorter.
#
# @param stock_locations [Enumerable<Spree::StockLocation>]
# a collection of locations to sort
def initialize(stock_locations)
@stock_locations = stock_locations
end

# Sorts the stock locations.
#
# @return [Enumerable<Spree::StockLocation>]
# a collection of sorted stock locations
def sort
raise NotImplementedError
end
end
end
end
end
15 changes: 15 additions & 0 deletions core/app/models/spree/stock/location_sorter/default_first.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module Spree
module Stock
module LocationSorter
# This stock location sorter will give priority to the default stock
# location.
class DefaultFirst < Spree::Stock::LocationSorter::Base
def sort
stock_locations.order_default
end
end
end
end
end
14 changes: 14 additions & 0 deletions core/app/models/spree/stock/location_sorter/unsorted.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

module Spree
module Stock
module LocationSorter
# This stock location sorter will leave the stock locations unsorted.
class Unsorted < Spree::Stock::LocationSorter::Base
def sort
stock_locations
end
end
end
end
end
16 changes: 14 additions & 2 deletions core/app/models/spree/stock/simple_coordinator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def initialize(order, inventory_units = nil)
@order = order
@inventory_units = inventory_units || InventoryUnitBuilder.new(order).units
@splitters = Spree::Config.environment.stock_splitters
@stock_locations = Spree::StockLocation.active
@stock_locations = Spree::Config.stock.location_sorter_class.new(Spree::StockLocation.active).sort

@inventory_units_by_variant = @inventory_units.group_by(&:variant)
@desired = Spree::StockQuantities.new(@inventory_units_by_variant.transform_values(&:count))
Expand Down Expand Up @@ -85,7 +85,9 @@ def split_packages(initial_packages)
end

def allocate_inventory(availability_by_location)
availability_by_location.transform_values do |available|
sorted_availability = sort_availability(availability_by_location)

sorted_availability.transform_values do |available|
# Find the desired inventory which is available at this location
packaged = available & @desired

Expand All @@ -96,6 +98,16 @@ def allocate_inventory(availability_by_location)
end
end

def sort_availability(availability)
aldesantis marked this conversation as resolved.
Show resolved Hide resolved
sorted_availability = availability.sort_by do |stock_location_id, _|
@stock_locations.find_index do |stock_location|
stock_location.id == stock_location_id
end
end

Hash[sorted_availability]
end

def get_units(quantities)
# Change our raw quantities back into inventory units
quantities.flat_map do |variant, quantity|
Expand Down
6 changes: 6 additions & 0 deletions core/lib/spree/core/stock_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Core
class StockConfiguration
attr_writer :coordinator_class
attr_writer :estimator_class
attr_writer :location_sorter_class

def coordinator_class
@coordinator_class ||= '::Spree::Stock::SimpleCoordinator'
Expand All @@ -15,6 +16,11 @@ def estimator_class
@estimator_class ||= '::Spree::Stock::Estimator'
@estimator_class.constantize
end

def location_sorter_class
@location_sorter_class ||= '::Spree::Stock::LocationSorter::Unsorted'
@location_sorter_class.constantize
end
end
end
end
19 changes: 19 additions & 0 deletions core/spec/lib/spree/core/stock_configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
end
end
end

describe '#estimator_class' do
let(:stock_configuration) { described_class.new }
subject { stock_configuration.estimator_class }
Expand All @@ -37,4 +38,22 @@
end
end
end

describe '#location_sorter_class' do
let(:stock_configuration) { described_class.new }
subject { stock_configuration.location_sorter_class }

it "returns Spree::Stock::LocationSorter::Unsorted" do
is_expected.to be ::Spree::Stock::LocationSorter::Unsorted
end

context "with another constant name assiged" do
MySorter = Class.new
before { stock_configuration.location_sorter_class = MySorter.to_s }

it "returns the constant" do
is_expected.to be MySorter
end
end
end
end
20 changes: 20 additions & 0 deletions core/spec/models/spree/stock/location_sorter/default_first_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

require 'rails_helper'

module Spree
module Stock
module LocationSorter
RSpec.describe DefaultFirst, type: :model do
subject { described_class.new(stock_locations) }

let(:stock_locations) { OpenStruct.new(order_default: sorted_stock_locations) }
let(:sorted_stock_locations) { instance_double('Spree::StockLocation::ActiveRecord_Relation') }

it 'returns the default stock location first' do
expect(subject.sort).to eq(sorted_stock_locations)
end
end
end
end
end
19 changes: 19 additions & 0 deletions core/spec/models/spree/stock/location_sorter/unsorted_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

require 'rails_helper'

module Spree
module Stock
module LocationSorter
RSpec.describe Unsorted, type: :model do
subject { described_class.new(stock_locations) }

let(:stock_locations) { instance_double('Spree::StockLocation::ActiveRecord_Relation') }

it 'returns the original stock locations unsorted' do
expect(subject.sort).to eq(stock_locations)
end
end
end
end
end
5 changes: 5 additions & 0 deletions core/spec/models/spree/stock/simple_coordinator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ module Stock
subject.shipments
end

it 'uses the configured stock location sorter' do
expect(Spree::Config.stock).to receive(:location_sorter_class).and_call_original
subject.shipments
end

it 'builds shipments' do
expect(subject.shipments.size).to eq(1)
end
Expand Down
2 changes: 2 additions & 0 deletions guides/data/nav_tree.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ main:
href: "/developers/shipments/cartons.html"
- title: "Custom shipping calculators"
href: "/developers/shipments/custom-shipping-calculators.html"
- title: "Stock location sorters"
href: "/developers/shipments/stock-location-sorters.html"
- title: "Shipment setup examples"
href: "/developers/shipments/shipment-setup-examples.html"
- title: "Shipping method filters"
Expand Down
52 changes: 52 additions & 0 deletions guides/source/developers/shipments/stock-location-sorters.html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Stock location sorters

This article explains the purpose, interface and correct usage of custom stock location sorters.

Your app's stock location sorter defines the order in which stock locations are used to allocate
inventory when creating packages for an order. The sorter is called by `Spree::Stock::SimpleCoordinator`
when allocating inventory for an order.

## Pre-configured sorters

Currently, we only have two sorters, which you should use unless you need custom logic:

- [Unsorted](https://github.com/solidusio/solidus/blob/master/core/app/models/spree/stock/sorter/unsorted.rb),
which allocates inventory from stock locations as they are returned from the DB.
- [Default first](https://github.com/solidusio/solidus/blob/master/core/app/models/spree/stock/sorter/default_first.rb),
which allocates inventory from the default stock location first.

## Custom sorter API

A custom sorter should inherit from `Spree::Stock::LocationSorter::Base` and implement a `sort` method
which accepts a `Spree::StockLocation::ActiveRecord_Relation` and returns an enumerable of stock
locations. Note that the return value does not have to be an AR relation.

Here's an example that sorts stock locations by a custom `priority` attribute:

```ruby
class Spree::Stock::LocationSorter::Priority < Spree::Stock::LocationSorter::Base
def sort
stock_locations.order(priority: :asc)
end
end
```

### Switching the sorter

Once you have created the logic for the new sorter, you need to register it so that it's used by
`Spree::Stock::SimpleCoordinator`.

For example, you can register it in your `/config/application.rb` initializer:

```ruby
# /config/application.rb
module MyStore
class Application < Rails::Application
# ...

initializer 'spree.register.stock_location_sorter' do |app|
app.config.spree.stock.location_sorter_class = 'Spree::Stock::LocationSorter::Priority'
end
end
end
```