module Spree # Tracks the state of line items' fulfillment. # class InventoryUnit < Spree::Base PRE_SHIPMENT_STATES = %w(backordered on_hand) POST_SHIPMENT_STATES = %w(returned) CANCELABLE_STATES = ['on_hand', 'backordered', 'shipped'] belongs_to :variant, -> { with_deleted }, class_name: "Spree::Variant", inverse_of: :inventory_units belongs_to :order, class_name: "Spree::Order", inverse_of: :inventory_units belongs_to :shipment, class_name: "Spree::Shipment", touch: true, inverse_of: :inventory_units belongs_to :return_authorization, class_name: "Spree::ReturnAuthorization", inverse_of: :inventory_units belongs_to :carton, class_name: "Spree::Carton", inverse_of: :inventory_units belongs_to :line_item, class_name: "Spree::LineItem", inverse_of: :inventory_units has_many :return_items, inverse_of: :inventory_unit, dependent: :destroy has_one :original_return_item, class_name: "Spree::ReturnItem", foreign_key: :exchange_inventory_unit_id, dependent: :destroy has_one :unit_cancel, class_name: "Spree::UnitCancel" validates_presence_of :order, :shipment, :line_item, :variant before_destroy :ensure_can_destroy scope :backordered, -> { where state: 'backordered' } scope :on_hand, -> { where state: 'on_hand' } scope :pre_shipment, -> { where(state: PRE_SHIPMENT_STATES) } scope :shipped, -> { where state: 'shipped' } scope :post_shipment, -> { where(state: POST_SHIPMENT_STATES) } scope :returned, -> { where state: 'returned' } scope :canceled, -> { where(state: 'canceled') } scope :not_canceled, -> { where.not(state: 'canceled') } scope :cancelable, -> { where(state: Spree::InventoryUnit::CANCELABLE_STATES) } scope :backordered_per_variant, ->(stock_item) do includes(:shipment, :order) .where("spree_shipments.state != 'canceled'").references(:shipment) .where(variant_id: stock_item.variant_id) .where('spree_orders.completed_at is not null') .backordered.order(Spree::Order.arel_table[:completed_at].asc) end # @param stock_item [Spree::StockItem] the stock item of the desired # inventory units # @return [ActiveRecord::Relation] backordered # inventory units for the given stock item scope :backordered_for_stock_item, ->(stock_item) do backordered_per_variant(stock_item) .where(spree_shipments: { stock_location_id: stock_item.stock_location_id }) end scope :shippable, -> { on_hand } # state machine (see http://github.com/pluginaweek/state_machine/tree/master for details) state_machine initial: :on_hand do event :fill_backorder do transition to: :on_hand, from: :backordered end after_transition on: :fill_backorder, do: :fulfill_order event :ship do transition to: :shipped, if: :allow_ship? end event :return do transition to: :returned, from: :shipped end event :cancel do transition to: :canceled, from: CANCELABLE_STATES.map(&:to_sym) end end # Updates the given inventory units to not be pending. # # @param inventory_units [] the inventory to be # finalized def self.finalize_units!(inventory_units) inventory_units.map do |iu| iu.update_columns( pending: false, updated_at: Time.current ) end end # @return [Spree::StockItem] the first stock item from this shipment's # stock location that is associated with this inventory unit's variant def find_stock_item Spree::StockItem.where(stock_location_id: shipment.stock_location_id, variant_id: variant_id).first end # @return [Spree::ReturnItem] a valid return item for this inventory unit # if one exists, or a new one if one does not def current_or_new_return_item Spree::ReturnItem.from_inventory_unit(self) end # @return [BigDecimal] the portion of the additional tax on the line item # this inventory unit belongs to that is associated with this individual # inventory unit def additional_tax_total line_item.additional_tax_total * percentage_of_line_item end # @return [BigDecimal] the portion of the included tax on the line item # this inventory unit belongs to that is associated with this # individual inventory unit def included_tax_total line_item.included_tax_total * percentage_of_line_item end # @return [Boolean] true if this inventory unit has any return items # which have requested exchanges def exchange_requested? return_items.not_expired.any?(&:exchange_requested?) end def allow_ship? on_hand? end private def fulfill_order reload order.fulfill! end def percentage_of_line_item 1 / BigDecimal.new(line_item.quantity) end def current_return_item return_items.not_cancelled.first end def ensure_can_destroy if !backordered? && !on_hand? errors.add(:state, :cannot_destroy, state: state) throw :abort end unless shipment.pending? errors.add(:base, :cannot_destroy_shipment_state, state: shipment.state) throw :abort end end end end