core/app/models/spree/stock_item.rb

Summary

Maintainability
A
55 mins
Test Coverage
A
98%
module Spree
  class StockItem < Spree::Base
    acts_as_paranoid

    include Spree::Metadata
    if defined?(Spree::Webhooks::HasWebhooks)
      include Spree::Webhooks::HasWebhooks
    end

    with_options inverse_of: :stock_items do
      belongs_to :stock_location, class_name: 'Spree::StockLocation'
      belongs_to :variant, -> { with_deleted }, class_name: 'Spree::Variant'
    end
    has_many :stock_movements, inverse_of: :stock_item

    validates :stock_location, :variant, presence: true
    validates :variant_id, uniqueness: { scope: :stock_location_id }, unless: :deleted_at

    validates :count_on_hand, numericality: {
      greater_than_or_equal_to: 0,
      less_than_or_equal_to: 2**31 - 1,
      only_integer: true
    }, if: :verify_count_on_hand?

    delegate :weight, :should_track_inventory?, to: :variant
    delegate :name, to: :variant, prefix: true
    delegate :product, to: :variant

    after_save :conditional_variant_touch, if: :saved_changes?
    after_touch { variant.touch }
    after_destroy { variant.touch }

    self.whitelisted_ransackable_attributes = %w[count_on_hand stock_location_id variant_id]
    self.whitelisted_ransackable_associations = %w[variant stock_location]

    scope :with_active_stock_location, -> { joins(:stock_location).merge(Spree::StockLocation.active) }

    def backordered_inventory_units
      Spree::InventoryUnit.backordered_for_stock_item(self)
    end

    def adjust_count_on_hand(value)
      with_lock do
        set_count_on_hand(count_on_hand + value)
      end
    end

    def set_count_on_hand(value)
      self.count_on_hand = value
      process_backorders(count_on_hand - count_on_hand_was)

      save!
    end

    def in_stock?
      count_on_hand > 0
    end

    # Tells whether it's available to be included in a shipment
    def available?
      in_stock? || backorderable?
    end

    def reduce_count_on_hand_to_zero
      set_count_on_hand(0) if count_on_hand > 0
    end

    private

    def verify_count_on_hand?
      count_on_hand_changed? && !backorderable? && (count_on_hand < count_on_hand_was) && (count_on_hand < 0)
    end

    # Process backorders based on amount of stock received
    # If stock was -20 and is now -15 (increase of 5 units), then we can process atmost 5 inventory orders.
    # If stock was -20 but then was -25 (decrease of 5 units), do nothing.
    def process_backorders(number)
      return unless number.positive?

      units = backordered_inventory_units.first(number) # We can process atmost n backorders

      units.each do |unit|
        break unless number.positive?

        if unit.quantity > number
          # if required quantity is greater than available
          # split off and fulfill that
          split = unit.split_inventory!(number)
          split.fill_backorder
        else
          unit.fill_backorder
        end
        number -= unit.quantity
      end
    end

    def conditional_variant_touch
      # the variant_id changes from nil when a new stock location is added
      stock_changed = (saved_change_to_count_on_hand? &&
                        saved_change_to_count_on_hand.any?(&:zero?)) ||
        saved_change_to_variant_id?

      variant.touch if !Spree::Config.binary_inventory_cache || stock_changed
    end
  end
end