diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb
index 3af1c0714a..ac3adec7f8 100644
--- a/app/controllers/admin/reports_controller.rb
+++ b/app/controllers/admin/reports_controller.rb
@@ -55,6 +55,15 @@ module Admin
@report_title = report_title
@rendering_options = rendering_options
@data = Reporting::FrontendData.new(spree_current_user)
+
+ variant_id_in = params[:variant_id_in]&.compact_blank
+ load_selected_variant if variant_id_in.present?
+ end
+
+ # Orders and Fulfillment Reports include a per product filter, load any selected product
+ def load_selected_variant
+ variant = Spree::Variant.find(params[:variant_id_in][0])
+ @variant_serialized = Api::Admin::VariantSerializer.new(variant)
end
def render_data?
diff --git a/app/views/admin/reports/filters/_orders_and_fulfillment.html.haml b/app/views/admin/reports/filters/_orders_and_fulfillment.html.haml
index 0a9963544f..aa6097c6ba 100755
--- a/app/views/admin/reports/filters/_orders_and_fulfillment.html.haml
+++ b/app/views/admin/reports/filters/_orders_and_fulfillment.html.haml
@@ -12,3 +12,9 @@
.alpha.two.columns= label_tag nil, t(:report_customers_cycle)
.omega.fourteen.columns
= f.select(:order_cycle_id_in, report_order_cycle_options(@data.order_cycles), {selected: params.dig(:q, :order_cycle_id_in)}, {class: "select2 fullwidth", multiple: true})
+.row
+ .alpha.two.columns= label_tag :add_variant_id, Spree.t(:name_or_sku)
+ .omega.fourteen.columns
+ - variant_json = @variant_serialized.present? ? @variant_serialized.to_json() : {}
+ = select_tag(:variant_id_in, params[:variant_id_in], { class: "fullwidth", multiple: true , data: { controller: "select-variant", "select-variant-selected-value": "#{variant_json}" } })
+
diff --git a/app/webpacker/controllers/select_variant_controller.js b/app/webpacker/controllers/select_variant_controller.js
new file mode 100644
index 0000000000..75d9a9ebcf
--- /dev/null
+++ b/app/webpacker/controllers/select_variant_controller.js
@@ -0,0 +1,90 @@
+import TomSelectController from "./tom_select_controller";
+
+// This is simalar to the "variantAutocomplete" directive that uses "select2", but it doesn't
+// have all the same feature
+//
+export default class extends TomSelectController {
+ static values = { options: Object, distributor: Number, selected: Object };
+
+ connect() {
+ const options = {
+ valueField: "id",
+ searchField: ["name", "sku"],
+ load: this.#load.bind(this),
+ shouldLoad: (query) => query.length > 2,
+ render: {
+ option: this.#renderOption.bind(this),
+ item: this.#renderItem.bind(this),
+ },
+ };
+ super.connect(options);
+ // Add the selected value if any and select it.
+ // It will need to include data used in the templates below:
+ // - id
+ // - image
+ // - name
+ // - producer_name
+ // - sku
+ // - on_demand
+ // - on_hand
+ // - options_text
+ //
+ if (this.hasSelectedValue && Object.keys(this.selectedValue).length > 0) {
+ this.control.addOption(this.selectedValue);
+ this.control.addItem(this.selectedValue.id);
+ }
+ }
+
+ // private
+
+ #load(query, callback) {
+ const url = "/admin/variants/search.json?q=" + encodeURIComponent(query);
+ fetch(url)
+ .then((response) => response.json())
+ .then((json) => {
+ callback(json);
+ })
+ .catch((error) => {
+ console.log(error);
+ callback();
+ });
+ }
+
+ #renderOption(variant, escape) {
+ return `
+
+ ${variant.image ? `
` : "
"}
+
+
+
${escape(variant.name)}
+
+ -
+ ${I18n.t("spree.admin.variants.autocomplete.producer_name")}:
+ ${escape(variant.producer_name)}
+
+
+
+ -
+ ${I18n.t("admin.sku")}:
+ ${escape(variant.sku)}
+
+ ${
+ variant.on_demand
+ ? `- ${I18n.t("on_demand")}
`
+ : `-
+ ${I18n.t("on_hand")}: ${escape(variant.on_hand)}
+
`
+ }
+ -
+ ${I18n.t("spree.admin.variants.autocomplete.unit")}:
+ ${escape(variant.options_text)}
+
+
+
+
`;
+ }
+
+ #renderItem(variant, escape) {
+ return `${escape(variant.name)}`;
+ }
+}
diff --git a/lib/reporting/line_items.rb b/lib/reporting/line_items.rb
index 714d071405..a949e02dce 100644
--- a/lib/reporting/line_items.rb
+++ b/lib/reporting/line_items.rb
@@ -23,6 +23,12 @@ module Reporting
line_items = line_items.supplied_by_any(@params[:supplier_id_in])
end
+ # Filter by product
+ variant_id_in = @params[:variant_id_in]&.compact_blank
+ if variant_id_in.present?
+ line_items = line_items.where('spree_line_items.variant_id': variant_id_in)
+ end
+
if line_item_includes.present?
line_items = line_items.includes(*line_item_includes).references(:line_items)
end
diff --git a/spec/lib/reports/line_items_spec.rb b/spec/lib/reports/line_items_spec.rb
index 3a0bd9beb8..0814778837 100644
--- a/spec/lib/reports/line_items_spec.rb
+++ b/spec/lib/reports/line_items_spec.rb
@@ -9,13 +9,13 @@ describe Reporting::LineItems do
# under test and the various objects it depends on. Other more common moking strategies where very
# hard.
class FakeOrderPermissions
- def initialize(line_item, orders_relation)
- @relation = Spree::LineItem.where(id: line_item.id)
+ def initialize(line_items, orders_relation)
+ @relations = Spree::LineItem.where(id: line_items.map(&:id))
@orders_relation = orders_relation
end
def visible_line_items
- relation
+ relations
end
def editable_line_items
@@ -29,7 +29,7 @@ describe Reporting::LineItems do
private
- attr_reader :relation, :orders_relation
+ attr_reader :relations, :orders_relation
end
describe '#list' do
@@ -41,15 +41,39 @@ describe Reporting::LineItems do
shipments: [build(:shipment)]
)
end
- let!(:line_item) { create(:line_item, order: order) }
+ let!(:line_item1) { create(:line_item, order: order) }
let(:orders_relation) { Spree::Order.where(id: order.id) }
- let(:order_permissions) { FakeOrderPermissions.new(line_item, orders_relation) }
+ let(:order_permissions) { FakeOrderPermissions.new([line_item1], orders_relation) }
let(:params) { {} }
it 'returns masked data' do
line_items = reports_line_items.list
expect(line_items.first.order.email).to eq('HIDDEN')
end
+
+ context "when filtering by product" do
+ subject(:line_items) { reports_line_items.list }
+
+ let!(:line_item2) { create(:line_item, order: order) }
+ let!(:line_item3) { create(:line_item, order: order) }
+ let(:order_permissions) do
+ FakeOrderPermissions.new([line_item1, line_item2, line_item3], orders_relation)
+ end
+ let(:params) { { variant_id_in: [line_item3.variant.id, line_item1.variant.id] } }
+
+ context "with an empty array" do
+ let(:params) { { variant_id_in: [""] } }
+
+ it "does not filter" do
+ expect(line_items).to include(line_item1, line_item2, line_item3)
+ end
+ end
+
+ it "includes selected products" do
+ expect(line_items).to include(line_item1, line_item3)
+ expect(line_items).not_to include(line_item2)
+ end
+ end
end
end
diff --git a/spec/system/admin/reports/orders_and_fulfillment_spec.rb b/spec/system/admin/reports/orders_and_fulfillment_spec.rb
index 9a6642405a..97dcc5cc11 100644
--- a/spec/system/admin/reports/orders_and_fulfillment_spec.rb
+++ b/spec/system/admin/reports/orders_and_fulfillment_spec.rb
@@ -176,6 +176,36 @@ describe "Orders And Fulfillment" do
expect(rows[3]).to have_content "Ave Zebu"
end
end
+
+ context "When filtering by product" do
+ let(:variant1) { create(:variant, product: product, unit_description: "Big") }
+ let(:variant3) { create(:variant) }
+
+ before do
+ create(:line_item_with_shipment, variant: variant1, quantity: 1, order: order1)
+ create(:line_item_with_shipment, variant: variant2, quantity: 3, order: order1)
+ create(:line_item_with_shipment, variant: variant1, quantity: 2, order: order2)
+ create(:line_item_with_shipment, variant: variant3, quantity: 1, order: order2)
+ end
+
+ it "includes only selected product" do
+ tomselect_search_and_select(variant3.sku, from: "variant_id_in[]")
+ click_button 'Go'
+
+ rows = find("table.report__table").all("tbody tr")
+ table = rows.map { |r| r.all("td").map { |c| c.text.strip } }
+ expect(table).to have_content(variant3.product.name)
+ expect(table).not_to have_content(product.name)
+
+ # Check the product dropdown still show the selected product
+ selected_product = page
+ .find("[name='variant_id_in[]']")
+ .sibling(".ts-wrapper")
+ .first(".ts-control")
+ .first(".item")
+ expect(selected_product.text).to have_content(variant3.product.name)
+ end
+ end
end
describe "Order Cycle Supplier" do
@@ -508,6 +538,7 @@ describe "Orders And Fulfillment" do
expect(page).to have_checked_field('Summary Row')
end
end
+
context "Columns to show" do
it "should store columns to show for every report separately" do
# Step 1: Update report rendering options on two reports
@@ -542,6 +573,7 @@ describe "Orders And Fulfillment" do
end
end
end
+
context "Revisiting a report after logout" do
context "Display options" do
it "should store display options" do