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

[RFC] Add FulfillmentChanger #2070

Merged
merged 6 commits into from
Jul 28, 2017
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,7 +1,11 @@
## Solidus 2.4.0 (master, unreleased)

- Change HTTP Status code for Api::ShipmentsController#transfer_to_* to be always 202 Accepted rather than 201 Created or 500.
Speed up changing fulfilment for parts of a shipment [\#2070](https://github.com/solidusio/solidus/pull/2070) ([mamhoff](https://github.com/mamhoff))

- Customized responders have been removed. They are available in the `solidus_responders` extension


## Solidus 2.3.0 (unreleased)

- Rails 5.1 [\#1895](https://github.com/solidusio/solidus/pull/1895) ([jhawthorn](https://github.com/jhawthorn))
Expand Down
23 changes: 17 additions & 6 deletions api/app/controllers/spree/api/shipments_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,26 @@ def remove
end

def transfer_to_location
@stock_location = Spree::StockLocation.find(params[:stock_location_id])
@original_shipment.transfer_to_location(@variant, @quantity, @stock_location)
render json: { success: true, message: Spree.t(:shipment_transfer_success) }, status: 201
@desired_stock_location = Spree::StockLocation.find(params[:stock_location_id])
@desired_shipment = @original_shipment.order.shipments.build(stock_location: @desired_stock_location)
transfer_to_shipment
end

def transfer_to_shipment
@target_shipment = Spree::Shipment.find_by!(number: params[:target_shipment_number])
@original_shipment.transfer_to_shipment(@variant, @quantity, @target_shipment)
render json: { success: true, message: Spree.t(:shipment_transfer_success) }, status: 201
@desired_shipment ||= Spree::Shipment.find_by!(number: params[:target_shipment_number])

fulfilment_changer = Spree::FulfilmentChanger.new(
current_shipment: @original_shipment,
desired_shipment: @desired_shipment,
variant: @variant,
quantity: @quantity
)

if fulfilment_changer.run!
render json: { success: true, message: Spree.t(:shipment_transfer_success) }, status: :accepted
else
render json: { success: false, message: fulfilment_changer.errors.full_messages.to_sentence }, status: :accepted
end
end

private
Expand Down
17 changes: 16 additions & 1 deletion api/spec/requests/spree/api/shipments_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,21 @@
end
end

context "for an unsuccessful transfer" do
before do
source_shipment
variant
stock_location.stock_items.update_all(backorderable: false)
end

it "returns the correct message" do
subject
expect(response).to be_accepted
expect(parsed_response["success"]).to be false
expect(parsed_response["message"]).to eq("Desired shipment not enough stock in desired stock location")
end
end

context "if the source shipment can not be found" do
let(:stock_location_id) { 9999 }

Expand Down Expand Up @@ -422,7 +437,7 @@

it "returns the correct message" do
subject
expect(response).to be_success
expect(response).to be_accepted
expect(parsed_response["success"]).to be true
expect(parsed_response["message"]).to eq("Variants successfully transferred")
end
Expand Down
8 changes: 6 additions & 2 deletions backend/app/assets/javascripts/spree/backend/shipments.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,12 @@ var ShipmentSplitItemView = Backbone.View.extend({
}
jqXHR.error(function(msg) {
alert(Spree.t("split_failed"));
}).done(function() {
window.Spree.advanceOrder();
}).done(function(response) {
if (response.success) {
window.Spree.advanceOrder();
} else {
alert(response.message);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use show_flash instead of an alert here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using show_flash resulted in quite a lot of testing trouble for some reason: Locked Sqlite databases, hung phantomjs, weird things. Can we move that to a separate PR?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

};
});
},

Expand Down
13 changes: 7 additions & 6 deletions backend/spec/features/admin/orders/order_details_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@
expect(order.shipments.first.stock_location.id).to eq(stock_location2.id)
end

it 'should allow me to split more than I have if available there' do
it 'should not allow me to split more than I had in the original shipment' do
expect(order.shipments.first.stock_location.id).to eq(stock_location.id)

within_row(1) { click_icon 'arrows-h' }
Expand All @@ -213,7 +213,7 @@

expect(order.shipments.count).to eq(1)
expect(order.shipments.last.backordered?).to eq(false)
expect(order.shipments.first.inventory_units_for(product.master).count).to eq(5)
expect(order.shipments.first.inventory_units_for(product.master).count).to eq(2)
expect(order.shipments.first.stock_location.id).to eq(stock_location2.id)
end

Expand All @@ -222,7 +222,7 @@

within_row(1) { click_icon 'arrows-h' }

accept_alert "Unable to complete split" do
accept_alert "Quantity must be greater than 0" do
complete_split_to(stock_location2, quantity: 0)
end

Expand All @@ -232,7 +232,7 @@

fill_in 'item_quantity', with: -1

accept_alert "Unable to complete split" do
accept_alert "Quantity must be greater than 0" do
click_icon :ok
end

Expand Down Expand Up @@ -265,7 +265,7 @@
within_row(1) { click_icon 'arrows-h' }
complete_split_to(stock_location2, quantity: 2)

accept_alert "Unable to complete split"
accept_alert "Desired shipment not enough stock in desired stock location"

expect(order.shipments.count).to eq(1)
expect(order.shipments.first.inventory_units_for(product.master).count).to eq(2)
Expand Down Expand Up @@ -364,7 +364,7 @@
within(all('.stock-contents', count: 2).first) do
within_row(1) { click_icon 'arrows-h' }

accept_alert("Unable to complete split") do
accept_alert("Desired shipment not enough stock in desired stock location") do
complete_split_to(@shipment2, quantity: 200)
end
end
Expand Down Expand Up @@ -404,6 +404,7 @@

context 'receiving shipment can backorder' do
it 'should add more to the backorder' do
@shipment1.inventory_units.update_all(state: :on_hand)
product.master.stock_items.last.update_column(:backorderable, true)
product.master.stock_items.last.update_column(:count_on_hand, 0)
expect(@shipment2.reload.backordered?).to eq(false)
Expand Down
125 changes: 125 additions & 0 deletions core/app/models/spree/fulfilment_changer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
module Spree
# Service class to change fulfilment of inventory units of a particular variant
# to another shipment. The other shipment would typically have a different
# shipping method, stock location or delivery date, such that we actually change
# the planned fulfilment for the items in question.
#
# Can be used to merge shipments by moving all items to another shipment, because
# this class will delete any empty original shipment.
#
# @attr [Spree::Shipment] current_shipment The shipment we transfer units from
# @attr [Spree::Shipment] desired_shipment The shipment we want to move units onto
# @attr [Spree::StockLocation] current_stock_location The stock location of the current shipment
# @attr [Spree::StockLocation] desired_stock_location The stock location of the desired shipment
# @attr [Spree::Variant] variant We only move units that represent this variant
# @attr [Integer] quantity How many units we want to move
#
class FulfilmentChanger
include ActiveModel::Validations

attr_accessor :current_shipment, :desired_shipment
attr_reader :variant, :quantity, :current_stock_location, :desired_stock_location

def initialize(current_shipment:, desired_shipment:, variant:, quantity:)
@current_shipment = current_shipment
@desired_shipment = desired_shipment
@current_stock_location = current_shipment.stock_location
@desired_stock_location = desired_shipment.stock_location
@variant = variant
@quantity = quantity
end

validates :quantity, numericality: { greater_than: 0 }
validate :current_shipment_not_already_shipped
validate :desired_shipment_different_from_current
validates :desired_stock_location, presence: true
validate :enough_stock_at_desired_location, if: :handle_stock_counts?

# Performs the change of fulfilment
# @return [true, false] Whether the requested fulfilment change was successful
def run!
# Validations here are intended to catch all necessary prerequisites.
# We return early so all checks have happened already.
return false if invalid?
desired_shipment.save! if desired_shipment.new_record?

new_on_hand_quantity = [desired_shipment.stock_location.count_on_hand(variant), quantity].min

ActiveRecord::Base.transaction do
if handle_stock_counts?
# We only run this query if we need it.
current_on_hand_quantity = [current_shipment.inventory_units.on_hand.size, quantity].min

# Restock things we will not fulfil from the current shipment anymore
current_stock_location.restock(variant, current_on_hand_quantity, current_shipment)
# Unstock what we will fulfil with the new shipment
desired_stock_location.unstock(variant, new_on_hand_quantity, desired_shipment)
end

# These two statements are the heart of this class. We change the number
# of inventory units requested from one shipment to the other.
# We order by state, because `'backordered' < 'on_hand'`.
current_shipment.
inventory_units.
where(variant: variant).
order(state: :asc).
limit(new_on_hand_quantity).
update_all(shipment_id: desired_shipment.id, state: :on_hand)

current_shipment.
inventory_units.
where(variant: variant).
order(state: :asc).
limit(quantity - new_on_hand_quantity).
update_all(shipment_id: desired_shipment.id, state: :backordered)
end

# We modified the inventory units at the database level for speed reasons.
# The downside of that is that we need to reload the associations.
current_shipment.inventory_units.reload
desired_shipment.inventory_units.reload

# If the current shipment now has no inventory units left, we won't need it any longer.
if current_shipment.inventory_units.length.zero?
current_shipment.destroy!
else
# The current shipment has changed, so we need to make sure that shipping rates
# have the correct amount.
current_shipment.refresh_rates
end

# The desired shipment has also change, so we need to make sure shipping rates
# are up-to-date, too.
desired_shipment.refresh_rates

true
end

private

# We don't need to handle stock counts for incomplete orders. Also, if
# the new shipment and the desired shipment will ship from the same stock location,
# unstocking and restocking will not be necessary.
def handle_stock_counts?
current_shipment.order.completed? && current_stock_location != desired_stock_location
end

def current_shipment_not_already_shipped
if current_shipment.shipped?
errors.add(:current_shipment, :has_already_been_shipped)
end
end

def enough_stock_at_desired_location
unless Spree::Stock::Quantifier.new(variant, desired_stock_location).can_supply?(quantity)
errors.add(:desired_shipment, :not_enough_stock_at_desired_location)
end
end

def desired_shipment_different_from_current
if desired_shipment.id == current_shipment.id
errors.add(:desired_shipment, :can_not_transfer_within_same_shipment)
end
end
end
end
38 changes: 10 additions & 28 deletions core/app/models/spree/shipment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -334,37 +334,19 @@ def update!(order_or_attrs)
end

def transfer_to_location(variant, quantity, stock_location)
if quantity <= 0
raise ArgumentError
end

transaction do
new_shipment = order.shipments.create!(stock_location: stock_location)

order.contents.remove(variant, quantity, { shipment: self })
order.contents.add(variant, quantity, { shipment: new_shipment })

refresh_rates
new_shipment.refresh_rates
save!
new_shipment.save!
end
Spree::Deprecation.warn("Please use the Spree::FulfilmentChanger class instead of Spree::Shipment#transfer_to_location", caller)
new_shipment = order.shipments.create!(stock_location: stock_location)
transfer_to_shipment(variant, quantity, new_shipment)
end

def transfer_to_shipment(variant, quantity, shipment_to_transfer_to)
if quantity <= 0 || self == shipment_to_transfer_to
raise ArgumentError
end

transaction do
order.contents.remove(variant, quantity, { shipment: self })
order.contents.add(variant, quantity, { shipment: shipment_to_transfer_to })

refresh_rates
save!
shipment_to_transfer_to.refresh_rates
shipment_to_transfer_to.save!
end
Spree::Deprecation.warn("Please use the Spree::FulfilmentChanger class instead of Spree::Shipment#transfer_to_location", caller)
Spree::FulfilmentChanger.new(
current_shipment: self,
desired_shipment: shipment_to_transfer_to,
variant: variant,
quantity: quantity
).run!
end

def requires_shipment?
Expand Down
10 changes: 10 additions & 0 deletions core/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ en:
state: State
shipment: Shipment
cancel: Cancel
errors:
models:
spree/fulfilment_changer:
attributes:
desired_shipment:
can_not_transfer_within_same_shipment: can not be same as current shipment
not_enough_stock_at_desired_location: not enough stock in desired stock location
current_shipment:
has_already_been_shipped: has already been shipped
can_not_have_backordered_inventory_units: has backordered inventory units
activerecord:
attributes:
spree/address:
Expand Down
Loading