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

Make AvailabilityValidator check inventory per StockLocation instead of per Shipment #1693

Merged
merged 8 commits into from
Feb 27, 2017
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
* The "Stores" section of Settings in the admin now supports creating and
editing multiple stores.

* The `AvailabilityValidator` now correctly detects out of stock when there
are multiple shipments from the same stock location.

* Deprecations

* `cache_key_for_taxons` helper has been deprecated in favour of `cache [I18n.locale, @taxons]`
Expand Down
1 change: 1 addition & 0 deletions core/app/models/spree/inventory_unit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class InventoryUnit < Spree::Base

before_destroy :ensure_can_destroy

scope :pending, -> { where pending: true }
scope :backordered, -> { where state: 'backordered' }
scope :on_hand, -> { where state: 'on_hand' }
scope :pre_shipment, -> { where(state: PRE_SHIPMENT_STATES) }
Expand Down
34 changes: 16 additions & 18 deletions core/app/models/spree/stock/availability_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,9 @@ module Spree
module Stock
class AvailabilityValidator < ActiveModel::Validator
def validate(line_item)
units_by_shipment = line_item.inventory_units.group_by(&:shipment)

if units_by_shipment.blank?
ensure_in_stock(line_item, line_item.quantity)
if is_valid?(line_item)
true
else
units_by_shipment.each do |shipment, inventory_units|
inventory_units.select!(&:pending?)
ensure_in_stock(line_item, inventory_units.size, shipment.stock_location)
end
end

line_item.errors[:quantity].empty?
end

private

def ensure_in_stock(line_item, quantity, stock_location = nil)
quantifier = Stock::Quantifier.new(line_item.variant, stock_location)
unless quantifier.can_supply?(quantity)
variant = line_item.variant
display_name = variant.name.to_s
display_name += %{ (#{variant.options_text})} unless variant.options_text.blank?
Expand All @@ -29,6 +13,20 @@ def ensure_in_stock(line_item, quantity, stock_location = nil)
:selected_quantity_not_available,
item: display_name.inspect
)
false
end
end

private

def is_valid?(line_item)
if line_item.inventory_units.empty?
Stock::Quantifier.new(line_item.variant).can_supply?(line_item.quantity)
else
quantity_by_stock_location_id = line_item.inventory_units.pending.joins(:shipment).group(:stock_location_id).count
quantity_by_stock_location_id.all? do |stock_location_id, quantity|
Stock::Quantifier.new(line_item.variant, stock_location_id).can_supply?(quantity)
end
end
end
end
Expand Down
9 changes: 5 additions & 4 deletions core/app/models/spree/stock/quantifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ module Stock
class Quantifier
attr_reader :stock_items

# @param [Variant] variant The variant to check inventory for.
# @param [StockLocation, Integer] stock_location The stock_location to check inventory in. If unspecified it will check inventory in all available StockLocations
def initialize(variant, stock_location = nil)
@variant = variant
where_args = { variant_id: @variant }
@stock_items = Spree::StockItem.where(variant_id: variant)
if stock_location
where_args[:stock_location] = stock_location
@stock_items.where!(stock_location: stock_location)
else
where_args[Spree::StockLocation.table_name] = { active: true }
@stock_items.joins!(:stock_location).merge!(Spree::StockLocation.active)
end
@stock_items = Spree::StockItem.joins(:stock_location).where(where_args)
end

# Returns the total number of inventory units on hand for the variant.
Expand Down
68 changes: 57 additions & 11 deletions core/spec/models/spree/stock/availability_validator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,39 +54,85 @@ module Stock
context "line_item is part of a shipment" do
let!(:order) { create(:order_with_line_items) }

context "has stock in all stock locations" do
context "has stock in one stock location" do
let(:line_item) { order.line_items.first }

before do
variant_ids = order.line_items.map(&:variant_id)
Spree::StockItem.where(variant_id: variant_ids).update_all(count_on_hand: 10, backorderable: false)
line_item.variant.stock_items.update_all(count_on_hand: 10, backorderable: false)
end

include_examples "passes validation"
end

context "doesn't have stock in a particular stock location" do
let(:variant) { create(:variant) }
let(:line_item) { order.line_items.find_by(variant_id: variant.id) }
context "with stock in multiple locations" do
let(:line_item) { order.line_items.first }
let(:variant) { line_item.variant }
let!(:stock_location_1) { create(:stock_location, name: "Test Warehouse", active: false) }

before do
order.contents.add(variant, 1, stock_location_quantities: { stock_location_1.id => 1 })
order.contents.advance
stock_location_1.stock_items.update_all(count_on_hand: 0, backorderable: false)
shipment = order.shipments.create(stock_location: stock_location_1)
order.contents.add(variant, 1, shipment: shipment)
end

include_examples "fails validation"
context "but no stock in either location" do
before do
variant.stock_items.update_all(count_on_hand: 0, backorderable: false)
end
include_examples "fails validation"
end

context "but no stock in one location" do
before do
stock_location_1.stock_items.update_all(count_on_hand: 0, backorderable: false)
end

include_examples "fails validation"
end

context "with enough stock only across locations" do
before do
variant.stock_items.update_all(count_on_hand: 1, backorderable: false)
end
include_examples "passes validation"
end

context "but inventory units are finalized" do
before do
Spree::InventoryUnit.update_all(pending: false)
order.inventory_units.update_all(pending: false)
end

include_examples "passes validation"
end
end
end

context "line_item is split across two shipments" do
let!(:order) { create(:order_with_line_items) }
let(:line_item) { order.line_items.first }
let(:variant) { line_item.variant }
let(:stock_location) { order.shipments.first.stock_location }

before do
shipment2 = order.shipments.create!(stock_location: order.shipments.first.stock_location)
order.contents.add(variant, 1, shipment: shipment2)
variant.stock_items.first.update_columns(count_on_hand: count_on_hand, backorderable: false)
end

context "and there is just enough stock" do
let(:count_on_hand) { 2 }
include_examples "passes validation"
end

context "and there is not enough stock" do
let(:count_on_hand) { 1 }
include_examples "fails validation"
end

context "and there is no available stock" do
let(:count_on_hand) { 0 }
include_examples "fails validation"
end
end
end
end
end