core/app/models/spree/order_inventory.rb
module Spree
class OrderInventory
attr_accessor :order, :line_item, :variant
def initialize(order, line_item)
@order = order
@line_item = line_item
@variant = line_item.variant
end
delegate :inventory_units, to: :line_item
# Only verify inventory for completed orders (as orders in frontend checkout
# have inventory assigned via +order.create_proposed_shipment+) or when
# shipment is explicitly passed
#
# In case shipment is passed the stock location should only unstock or
# restock items if the order is completed. That is so because stock items
# are always unstocked when the order is completed through +shipment.finalize+
def verify(shipment = nil, is_updated: false)
return unless order.completed? || shipment.present?
units_count = inventory_units.reload.sum(&:quantity)
line_item_changed = is_updated ? !line_item.saved_changes? : !line_item.changed?
if units_count < line_item.quantity
quantity = line_item.quantity - units_count
shipment ||= determine_target_shipment
add_to_shipment(shipment, quantity)
elsif (units_count > line_item.quantity) || (units_count == line_item.quantity && line_item_changed)
remove(units_count, shipment)
end
end
private
def remove(units_count, target_shipment = nil)
quantity = set_quantity_to_remove(units_count)
if target_shipment.present?
remove_from_shipment(target_shipment, quantity)
else
order.shipments.each do |shipment|
break if quantity.zero?
quantity -= remove_from_shipment(shipment, quantity)
end
end
end
def set_quantity_to_remove(units_count)
if (units_count - line_item.quantity).zero?
line_item.quantity
else
units_count - line_item.quantity
end
end
# Returns either one of the shipment:
#
# first unshipped that already includes this variant
# first unshipped that's leaving from a stock_location that stocks this variant
def determine_target_shipment
target_shipment = order.shipments.detect do |shipment|
shipment.ready_or_pending? && shipment.include?(variant)
end
target_shipment || order.shipments.detect do |shipment|
shipment.ready_or_pending? && variant.stock_location_ids.include?(shipment.stock_location_id)
end
end
def add_to_shipment(shipment, quantity)
if shipment.nil?
shipment = order.create_proposed_shipments.first
elsif variant.should_track_inventory?
on_hand, back_order = shipment.stock_location.fill_status(variant, quantity)
shipment.set_up_inventory('on_hand', variant, order, line_item, on_hand)
shipment.set_up_inventory('backordered', variant, order, line_item, back_order)
else
shipment.set_up_inventory('on_hand', variant, order, line_item, quantity)
end
# adding to this shipment, and removing from stock_location
if order.completed?
shipment.stock_location.unstock(variant, quantity, shipment)
end
quantity
end
def remove_from_shipment(shipment, quantity)
return 0 if quantity.zero? || shipment.shipped?
shipment_units = shipment.inventory_units_for_item(line_item, variant).reject(&:shipped?).sort_by(&:state)
removed_quantity = 0
removed_backordered = 0
shipment_units.each do |inventory_unit|
inventory_unit.quantity.times do
break if removed_quantity == quantity
if inventory_unit.quantity > 1
inventory_unit.decrement(:quantity)
else
inventory_unit.destroy
end
removed_backordered += 1 if inventory_unit.backordered?
removed_quantity += 1
end
inventory_unit.save! if inventory_unit.persisted?
end
shipment.destroy if shipment.inventory_units.sum(:quantity).zero?
# removing this from shipment, and adding to stock_location
if order.completed?
current_on_hand = shipment.stock_location.count_on_hand(variant)
if current_on_hand&.negative? && current_on_hand.abs < removed_backordered
shipment.stock_location.restock_backordered variant, current_on_hand.abs, shipment
else
shipment.stock_location.restock_backordered variant, removed_backordered, shipment
end
shipment.stock_location.restock variant, removed_quantity - removed_backordered, shipment
end
removed_quantity
end
end
end