diff --git a/app/assets/javascripts/admin/utils/directives/variant_autocomplete.js.coffee b/app/assets/javascripts/admin/utils/directives/variant_autocomplete.js.coffee index 2b5fca20a6..69fb5a1163 100644 --- a/app/assets/javascripts/admin/utils/directives/variant_autocomplete.js.coffee +++ b/app/assets/javascripts/admin/utils/directives/variant_autocomplete.js.coffee @@ -19,6 +19,7 @@ angular.module("admin.utils").directive "variantAutocomplete", ($timeout) -> distributor_id: scope.distributor_id order_cycle_id: scope.order_cycle_id eligible_for_subscriptions: scope.eligible_for_subscriptions + include_out_of_stock: scope.include_out_of_stock results: (data, page) -> window.variants = data # this is how spree auto complete JS code picks up variants results: data @@ -27,3 +28,5 @@ angular.module("admin.utils").directive "variantAutocomplete", ($timeout) -> formatSelection: (variant) -> element.parent().children(".options_placeholder").html variant.options_text variant.name + element.on "select2-opening", -> + scope.include_out_of_stock = if $('#include_out_of_stock').is(':checked') then "1" else "" diff --git a/app/controllers/spree/admin/variants_controller.rb b/app/controllers/spree/admin/variants_controller.rb index 7877f7e93b..a06128f921 100644 --- a/app/controllers/spree/admin/variants_controller.rb +++ b/app/controllers/spree/admin/variants_controller.rb @@ -114,7 +114,7 @@ module Spree def variant_search_params params.permit( - :q, :distributor_id, :order_cycle_id, :schedule_id, :eligible_for_subscriptions + :q, :distributor_id, :order_cycle_id, :schedule_id, :eligible_for_subscriptions, :include_out_of_stock ).to_h.with_indifferent_access end end diff --git a/app/views/spree/admin/orders/_add_product.html.haml b/app/views/spree/admin/orders/_add_product.html.haml index 93594bd12f..58c288cfe1 100644 --- a/app/views/spree/admin/orders/_add_product.html.haml +++ b/app/views/spree/admin/orders/_add_product.html.haml @@ -7,8 +7,13 @@ - if @order.canceled? = t(".cannot_add_item_to_canceled_order") - else - .field.twelve.columns.alpha{"data-hook" => "add_product_name"} + .field.nine.columns.alpha{"data-hook" => "add_product_name"} = label_tag :add_variant_id, Spree.t(:name_or_sku) = hidden_field_tag :add_variant_id, "", :class => "variant_autocomplete fullwidth" + .five.columns.omega + .field + = label_tag 'include_out_of_stock', t(".include_out_of_stock_variants") + %br/ + = check_box_tag 'include_out_of_stock', '1' #stock_details diff --git a/config/locales/en.yml b/config/locales/en.yml index 562227021a..3efd95b3ab 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3570,6 +3570,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using orders: add_product: cannot_add_item_to_canceled_order: "Cannot add item to canceled order" + include_out_of_stock_variants: "Include variants with no available stock" index: listing_orders: "Listing Orders" new_order: "New Order" diff --git a/lib/open_food_network/scope_variants_for_search.rb b/lib/open_food_network/scope_variants_for_search.rb index 1ceb52499d..2bfa88a804 100644 --- a/lib/open_food_network/scope_variants_for_search.rb +++ b/lib/open_food_network/scope_variants_for_search.rb @@ -16,6 +16,7 @@ module OpenFoodNetwork def search @variants = query_scope + scope_to_in_stock_only if params[:distributor_id] && params[:include_out_of_stock] != "1" scope_to_schedule if params[:schedule_id] scope_to_order_cycle if params[:order_cycle_id] scope_to_distributor if params[:distributor_id] @@ -68,6 +69,18 @@ module OpenFoodNetwork scope_variants_to_distributor(@variants, distributor) end + def scope_to_in_stock_only + @variants = @variants.joins( + "INNER JOIN spree_stock_items ON spree_stock_items.variant_id = spree_variants.id + LEFT JOIN variant_overrides ON variant_overrides.variant_id = spree_variants.id" + ).where(" + variant_overrides.on_demand IS TRUE OR + variant_overrides.count_on_hand > 0 OR + (variant_overrides.on_demand IS NULL AND spree_stock_items.backorderable IS TRUE) OR + (variant_overrides.count_on_hand IS NULL AND spree_stock_items.count_on_hand > 0) + ") + end + def scope_variants_to_distributor(variants, distributor) scoper = OpenFoodNetwork::ScopeVariantToHub.new(distributor) # Perform scoping after all filtering is done. diff --git a/spec/lib/open_food_network/scope_variants_to_search_spec.rb b/spec/lib/open_food_network/scope_variants_to_search_spec.rb index 3090f5daf9..39ad17bd46 100644 --- a/spec/lib/open_food_network/scope_variants_to_search_spec.rb +++ b/spec/lib/open_food_network/scope_variants_to_search_spec.rb @@ -37,8 +37,7 @@ describe OpenFoodNetwork::ScopeVariantsForSearch do let(:params) { { q: "product", schedule_id: s1.id } } it "returns all products distributed through that schedule" do - lala = result - expect(lala).to include v1, v3 + expect(result).to include v1, v3 expect(result).to_not include v2, v4 end end @@ -59,6 +58,115 @@ describe OpenFoodNetwork::ScopeVariantsForSearch do expect(result).to include v4 expect(result).to_not include v1, v2, v3 end + + context "filtering by stock availability" do + let!(:distributor1_variant_on_hand_but_not_backorderable) do + create_variant_with_stock_item_for(d1, backorderable: false, count_on_hand: 1) + end + let!(:distributor1_variant_backorderable_but_not_on_hand) do + create_variant_with_stock_item_for(d1, backorderable: true, count_on_hand: 0) + end + let!(:distributor1_variant_not_backorderable_and_not_on_hand) do + create_variant_with_stock_item_for(d1, backorderable: false, count_on_hand: 0) + end + let!(:distributor1_variant_with_override_on_hand_but_not_on_demand) do + create_variant_with_variant_override_for(d1, on_demand: false, count_on_hand: 1) + end + let!(:distributor1_variant_with_override_on_demand_but_not_on_hand) do + create_variant_with_variant_override_for(d1, on_demand: true, count_on_hand: nil) + end + let!(:distributor1_variant_with_override_not_on_demand_and_not_on_hand) do + create_variant_with_variant_override_for(d1, on_demand: false, count_on_hand: 0) + end + let!(:distributor1_variant_with_override_not_in_stock_but_producer_in_stock) do + variant = create(:simple_product).variants.first + variant.stock_items.first.update!(backorderable: false, count_on_hand: 1) + create(:simple_order_cycle, distributors: [d1], variants: [variant]) + create(:variant_override, variant: variant, hub: d1, on_demand: false, count_on_hand: 0) + variant + end + let!(:distributor1_variant_with_override_without_stock_level_set_and_producer_not_in_stock) do + variant = create(:simple_product).variants.first + variant.stock_items.first.update!(backorderable: false, count_on_hand: 0) + create(:simple_order_cycle, distributors: [d1], variants: [variant]) + create(:variant_override, variant: variant, hub: d1, on_demand: nil, count_on_hand: nil) + variant + end + let!(:distributor1_variant_with_override_without_stock_level_set_but_producer_in_stock) do + variant = create(:simple_product).variants.first + variant.stock_items.first.update!(backorderable: false, count_on_hand: 1) + create(:simple_order_cycle, distributors: [d1], variants: [variant]) + create(:variant_override, variant: variant, hub: d1, on_demand: nil, count_on_hand: nil) + variant + end + let!(:distributor2_variant_with_override_in_stock) do + create_variant_with_variant_override_for(d2, count_on_hand: 1) + end + + context "when :include_out_of_stock is not specified" do + let(:params) { { distributor_id: d1.id } } + + it "returns variants for the given distributor if they have a variant override which is + in stock, or if they have a variant override with no stock level set but the producer + has stock, or if they don't have a variant override and the producer has stock" do + expect(result).to include( + distributor1_variant_on_hand_but_not_backorderable, + distributor1_variant_backorderable_but_not_on_hand, + distributor1_variant_with_override_on_demand_but_not_on_hand, + distributor1_variant_with_override_on_hand_but_not_on_demand, + distributor1_variant_with_override_without_stock_level_set_but_producer_in_stock + ) + expect(result).to_not include( + distributor1_variant_not_backorderable_and_not_on_hand, + distributor1_variant_with_override_not_on_demand_and_not_on_hand, + distributor1_variant_with_override_not_in_stock_but_producer_in_stock, + distributor1_variant_with_override_without_stock_level_set_and_producer_not_in_stock, + distributor2_variant_with_override_in_stock + ) + end + end + + context "when :include_out_of_stock is specified" do + let(:params) { { distributor_id: d1.id, include_out_of_stock: "1" } } + + it "returns all variants for the given distributor even if they are not in stock" do + expect(result).to include( + distributor1_variant_on_hand_but_not_backorderable, + distributor1_variant_backorderable_but_not_on_hand, + distributor1_variant_with_override_on_demand_but_not_on_hand, + distributor1_variant_with_override_on_hand_but_not_on_demand, + distributor1_variant_with_override_without_stock_level_set_but_producer_in_stock, + distributor1_variant_with_override_without_stock_level_set_and_producer_not_in_stock, + distributor1_variant_not_backorderable_and_not_on_hand, + distributor1_variant_with_override_not_on_demand_and_not_on_hand, + distributor1_variant_with_override_not_in_stock_but_producer_in_stock + ) + expect(result).to_not include( + distributor2_variant_with_override_in_stock + ) + end + end + end end end + + private + + def create_variant_with_stock_item_for(distributor, stock_item_attributes) + variant = create(:simple_product).variants.first + variant.stock_items.first.update!(stock_item_attributes) + create(:simple_order_cycle, distributors: [distributor], variants: [variant]) + variant + end + + def create_variant_with_variant_override_for(distributor, variant_override_attributes) + variant = create(:simple_product).variants.first + variant.stock_items.first.update!(backorderable: false, count_on_hand: 0) + create(:simple_order_cycle, distributors: [distributor], variants: [variant]) + create(:variant_override, { + variant: variant, + hub: distributor + }.merge(variant_override_attributes)) + variant + end end