diff --git a/app/models/spree/stock_location.rb b/app/models/spree/stock_location.rb new file mode 100644 index 0000000000..cb017cfe68 --- /dev/null +++ b/app/models/spree/stock_location.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Spree + class StockLocation < ActiveRecord::Base + has_many :stock_items, dependent: :delete_all + has_many :stock_movements, through: :stock_items + + belongs_to :state, class_name: 'Spree::State' + belongs_to :country, class_name: 'Spree::Country' + + validates :name, presence: true + + scope :active, -> { where(active: true) } + + after_create :create_stock_items + + # Wrapper for creating a new stock item respecting the backorderable config + def propagate_variant(variant) + stock_items.create!(variant: variant, backorderable: backorderable_default) + end + + def stock_item(variant) + stock_items.where(variant_id: variant).order(:id).first + end + + def stock_item_or_create(variant) + stock_item(variant) || stock_items.create(variant: variant) + end + + def count_on_hand(variant) + stock_item(variant).try(:count_on_hand) + end + + def backorderable?(variant) + stock_item(variant).try(:backorderable?) + end + + def restock(variant, quantity, originator = nil) + move(variant, quantity, originator) + end + + def unstock(variant, quantity, originator = nil) + move(variant, -quantity, originator) + end + + def move(variant, quantity, originator = nil) + variant.move(quantity, originator) + end + + def fill_status(variant, quantity) + variant.fill_status(quantity) + end + + private + + def create_stock_items + Variant.find_each { |variant| propagate_variant(variant) } + end + end +end diff --git a/app/models/spree/stock_location_decorator.rb b/app/models/spree/stock_location_decorator.rb deleted file mode 100644 index 87702616c7..0000000000 --- a/app/models/spree/stock_location_decorator.rb +++ /dev/null @@ -1,9 +0,0 @@ -Spree::StockLocation.class_eval do - def move(variant, quantity, originator = nil) - variant.move(quantity, originator) - end - - def fill_status(variant, quantity) - variant.fill_status(quantity) - end -end diff --git a/app/models/spree/stock_movement.rb b/app/models/spree/stock_movement.rb new file mode 100644 index 0000000000..f435ed1f50 --- /dev/null +++ b/app/models/spree/stock_movement.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Spree + class StockMovement < ActiveRecord::Base + belongs_to :stock_item, class_name: 'Spree::StockItem' + belongs_to :originator, polymorphic: true + + after_create :update_stock_item_quantity + + validates :stock_item, presence: true + validates :quantity, presence: true + + scope :recent, -> { order('created_at DESC') } + + def readonly? + !new_record? + end + + private + + def update_stock_item_quantity + stock_item.adjust_count_on_hand quantity + end + end +end diff --git a/spec/factories/stock_location_factory.rb b/spec/factories/stock_location_factory.rb index 48fc14d772..f348788700 100644 --- a/spec/factories/stock_location_factory.rb +++ b/spec/factories/stock_location_factory.rb @@ -18,5 +18,17 @@ FactoryBot.define do state do |stock_location| stock_location.country.states.first || stock_location.association(:state, country: stock_location.country) end + + factory :stock_location_with_items do + after(:create) do |stock_location, evaluator| + # variant will add itself to all stock_locations in an after_create + # creating a product will automatically create a master variant + product_1 = create(:product) + product_2 = create(:product) + + stock_location.stock_items.where(:variant_id => product_1.master.id).first.adjust_count_on_hand(10) + stock_location.stock_items.where(:variant_id => product_2.master.id).first.adjust_count_on_hand(20) + end + end end end diff --git a/spec/factories/stock_movement_factory.rb b/spec/factories/stock_movement_factory.rb new file mode 100644 index 0000000000..b2eb1cd65e --- /dev/null +++ b/spec/factories/stock_movement_factory.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :stock_movement, class: Spree::StockMovement do + quantity 1 + action 'sold' + end +end diff --git a/spec/models/spree/stock_item_spec.rb b/spec/models/spree/stock_item_spec.rb index 6dccf13665..9c9b233986 100644 --- a/spec/models/spree/stock_item_spec.rb +++ b/spec/models/spree/stock_item_spec.rb @@ -3,15 +3,7 @@ require 'spec_helper' RSpec.describe Spree::StockItem do - let(:stock_location) { create(:stock_location) } - - before do - product_1 = create(:product) - product_2 = create(:product) - - stock_location.stock_items.where(variant_id: product_1.master.id).first.adjust_count_on_hand(10) - stock_location.stock_items.where(variant_id: product_2.master.id).first.adjust_count_on_hand(20) - end + let(:stock_location) { create(:stock_location_with_items) } subject { stock_location.stock_items.order(:id).first } diff --git a/spec/models/spree/stock_location_spec.rb b/spec/models/spree/stock_location_spec.rb new file mode 100644 index 0000000000..86996b40b4 --- /dev/null +++ b/spec/models/spree/stock_location_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Spree + describe StockLocation do + subject { create(:stock_location_with_items, backorderable_default: true) } + let(:stock_item) { subject.stock_items.order(:id).first } + let(:variant) { stock_item.variant } + + it 'creates stock_items for all variants' do + expect(subject.stock_items.count).to eq Variant.count + end + + context "handling stock items" do + let!(:variant) { create(:variant) } + + context "given a variant" do + context "propagate all variants" do + subject { StockLocation.new(name: "testing") } + + specify do + expect(subject).to receive(:propagate_variant).at_least(:once) + subject.save! + end + end + end + end + + it 'finds a stock_item for a variant' do + stock_item = subject.stock_item(variant) + expect(stock_item.count_on_hand).to eq 15 + end + + it 'finds a stock_item for a variant by id' do + stock_item = subject.stock_item(variant.id) + expect(stock_item.variant).to eq variant + end + + it 'returns nil when stock_item is not found for variant' do + stock_item = subject.stock_item(100) + expect(stock_item).to be_nil + end + + it 'finds a count_on_hand for a variant' do + expect(subject.count_on_hand(variant)).to eq 15 + end + + it 'finds determines if you a variant is backorderable' do + expect(subject.backorderable?(variant)).to be_truthy + end + + it 'restocks a variant with a positive stock movement' do + originator = double + expect(subject).to receive(:move).with(variant, 5, originator) + subject.restock(variant, 5, originator) + end + + it 'unstocks a variant with a negative stock movement' do + originator = double + expect(subject).to receive(:move).with(variant, -5, originator) + subject.unstock(variant, 5, originator) + end + + it 'it creates a stock_movement' do + variant.on_demand = false + expect { + subject.move variant, 5 + }.to change { subject.stock_movements.where(stock_item_id: stock_item).count }.by(1) + end + + context 'fill_status' do + before { variant.on_demand = false } + + it 'is all on_hand if variant is on_demand' do + variant.on_demand = true + + on_hand, backordered = subject.fill_status(variant, 25) + expect(on_hand).to eq 25 + expect(backordered).to eq 0 + end + + it 'is all on_hand if on_hand is enough' do + on_hand, backordered = subject.fill_status(variant, 5) + expect(on_hand).to eq 5 + expect(backordered).to eq 0 + end + + it 'is some on_hand if not all available' do + on_hand, backordered = subject.fill_status(variant, 20) + expect(on_hand).to eq 15 + expect(backordered).to eq 0 + end + + it 'is zero on_hand if none available' do + variant.on_hand = 0 + + on_hand, backordered = subject.fill_status(variant, 20) + expect(on_hand).to eq 0 + expect(backordered).to eq 0 + end + end + end +end diff --git a/spec/models/spree/stock_movement_spec.rb b/spec/models/spree/stock_movement_spec.rb new file mode 100644 index 0000000000..2da7976c57 --- /dev/null +++ b/spec/models/spree/stock_movement_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Spree::StockMovement do + let(:stock_location) { create(:stock_location_with_items) } + let(:stock_item) { stock_location.stock_items.order(:id).first } + subject { build(:stock_movement, stock_item: stock_item) } + + it 'should belong to a stock item' do + expect(subject).to respond_to(:stock_item) + end + + it 'is readonly unless new' do + subject.save + expect { + subject.save + }.to raise_error(ActiveRecord::ReadOnlyRecord) + end + + context "when quantity is negative" do + context "after save" do + it "should decrement the stock item count on hand" do + subject.quantity = -1 + subject.save + stock_item.reload + expect(stock_item.count_on_hand).to eq 14 + end + end + end + + context "when quantity is positive" do + context "after save" do + it "should increment the stock item count on hand" do + subject.quantity = 1 + subject.save + stock_item.reload + expect(stock_item.count_on_hand).to eq 16 + end + end + end +end