diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index fc9a7dccba..84863fa2fa 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -86,10 +86,6 @@ Layout/ExtraSpacing: - 'Guardfile' - 'app/models/barcode_item.rb' - 'app/queries/items_by_storage_collection_query.rb' - - 'app/queries/items_in_query.rb' - - 'app/queries/items_in_total_query.rb' - - 'app/queries/items_out_query.rb' - - 'app/queries/items_out_total_query.rb' - 'config/environments/development.rb' - 'config/environments/test.rb' - 'config/puma.rb' @@ -169,10 +165,6 @@ Layout/MultilineMethodCallIndentation: - 'app/models/partner_distribution.rb' - 'app/models/storage_location.rb' - 'app/queries/items_by_storage_collection_query.rb' - - 'app/queries/items_in_query.rb' - - 'app/queries/items_in_total_query.rb' - - 'app/queries/items_out_query.rb' - - 'app/queries/items_out_total_query.rb' - 'app/services/reports/acquisition_report_service.rb' - 'app/services/reports/adult_incontinence_report_service.rb' - 'app/services/reports/children_served_report_service.rb' diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb index 9b22e22836..db19606952 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -54,15 +54,14 @@ def edit @storage_location = current_organization.storage_locations.find(params[:id]) end - # TODO: Move these queries to Query Object def show + setup_date_range_picker @storage_location = current_organization.storage_locations.find(params[:id]) version_date = params[:version_date].presence&.to_date - # TODO: Find a way to do these with less hard SQL. These queries have to be manually updated because they're not in-sync with the Model - @items_out = ItemsOutQuery.new(organization: current_organization, storage_location: @storage_location).call - @items_out_total = ItemsOutTotalQuery.new(organization: current_organization, storage_location: @storage_location).call - @items_in = ItemsInQuery.new(organization: current_organization, storage_location: @storage_location).call - @items_in_total = ItemsInTotalQuery.new(organization: current_organization, storage_location: @storage_location).call + @items = ItemsFlowQuery.new(storage_location: @storage_location, organization: current_organization, filter_params: date_range).call.to_a + @total_quantity_in = @items.count.positive? ? @items.first["total_quantity_in"].to_i : 0 + @total_quantity_out = @items.count.positive? ? @items.first["total_quantity_out"].to_i : 0 + @total_quantity_change = @total_quantity_in - @total_quantity_out if View::Inventory.within_snapshot?(current_organization.id, version_date) @inventory = View::Inventory.new(current_organization.id, event_time: version_date) else @@ -157,9 +156,18 @@ def include_omitted_items(existing_item_ids = []) end helper_method \ - def filter_params + def filter_params return {} unless params.key?(:filters) - params.require(:filters).permit(:containing) + params.require(:filters).permit(:containing, :date_range, :date_range_label) + end + + def date_range + return if filter_params[:date_range].blank? + + date_range = filter_params[:date_range].split(" - ") + start_date = Date.parse(date_range[0]).beginning_of_day + end_date = Date.parse(date_range[1]).end_of_day + [start_date, end_date] end end diff --git a/app/queries/items_flow_query.rb b/app/queries/items_flow_query.rb new file mode 100644 index 0000000000..2b72d0769a --- /dev/null +++ b/app/queries/items_flow_query.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +class ItemsFlowQuery + attr_reader :organization + attr_reader :filter_params + attr_reader :storage_location + + def initialize(organization:, storage_location:, filter_params: nil) + @organization = organization + @storage_location = storage_location + @filter_params = filter_params + end + + def call + query = <<~SQL + with events_with_flags as ( + select it.id as item_id, + it.name as item_name, + -- in quantity for this row (0 if not matching) + case + when (e.type = 'DonationEvent' and (item->>'to_storage_location')::int = :id) + or (e.type = 'PurchaseEvent' and (item->>'to_storage_location')::int = :id) + or (e.type = 'AdjustmentEvent' and (item->>'to_storage_location')::int = :id) + or (e.type = 'TransferEvent' and (item->>'to_storage_location')::int = :id) + or (e.type = 'AuditEvent' and (item->>'to_storage_location')::int = :id) + and e.organization_id = :organization_id + then (item->>'quantity')::int + else 0 + end as quantity_in, + -- out quantity normalized to positive numbers (0 if not matching) + case + when (e.type = 'DistributionEvent' and (item->>'from_storage_location')::int = :id) + or (e.type = 'AdjustmentEvent' and (item->>'from_storage_location')::int = :id) + or (e.type = 'TransferEvent' and (item->>'from_storage_location')::int = :id) + or (e.type = 'AuditEvent' and (item->>'from_storage_location')::int = :id) + and e.organization_id = :organization_id + then case when (item->>'quantity')::int < 0 then -(item->>'quantity')::int else (item->>'quantity')::int end + else 0 + end as quantity_out, + -- mark rows that are relevant for the overall WHERE in original query + case + when ( (e.type = 'DonationEvent' and (item->>'to_storage_location')::int = :id) + or (e.type = 'PurchaseEvent' and (item->>'to_storage_location')::int = :id) + or (e.type = 'DistributionEvent' and (item->>'from_storage_location')::int = :id) + or (e.type = 'TransferEvent' and ((item->>'from_storage_location')::int = :id or (item->>'to_storage_location')::int = :id)) + or (e.type = 'AdjustmentEvent' and (item->>'from_storage_location')::int = :id or (item->>'to_storage_location')::int = :id) + ) and e.organization_id = :organization_id + then 1 else 0 + end as relevant + from events e + left join lateral jsonb_array_elements(data->'items') as item on true + left join items it on it.id = (item->>'item_id')::int and it.organization_id = :organization_id + where e.created_at >= :start_date and e.created_at <= :end_date + ) + select + item_id, + item_name, + sum(quantity_in) as quantity_in, + sum(quantity_out) as quantity_out, + sum(quantity_in) - sum(quantity_out) as change, + sum(sum(quantity_in)) over () as total_quantity_in, + sum(sum(quantity_out)) over () as total_quantity_out, + sum(sum(quantity_in) - sum(quantity_out)) over () as total_change + from events_with_flags + where relevant = 1 + group by item_id, item_name + order by item_name; + SQL + + ActiveRecord::Base.connection.exec_query( + ActiveRecord::Base.send(:sanitize_sql_array, [query, { + id: @storage_location.id, + organization_id: @organization.id, + start_date: @filter_params ? @filter_params[0] : 20.years.ago, + end_date: @filter_params ? @filter_params[1] : Time.current.end_of_day + }]) + ) + end +end diff --git a/app/queries/items_in_query.rb b/app/queries/items_in_query.rb deleted file mode 100644 index 5792d8b8da..0000000000 --- a/app/queries/items_in_query.rb +++ /dev/null @@ -1,29 +0,0 @@ -# Creates a query object for retrieving the items, grouped by storage location -# We're using query objects for some of these more complicated queries to get -# the raw SQL out of the models and encapsulate it. -class ItemsInQuery - attr_reader :organization - attr_reader :filter_params - attr_reader :storage_location - - def initialize(organization:, storage_location:, filter_params: nil) - @organization = organization - @storage_location = storage_location - @filter_params = filter_params - end - - # rubocop:disable Naming/MemoizedInstanceVariableName - def call - @items ||= LineItem.joins(" -LEFT OUTER JOIN donations ON donations.id = line_items.itemizable_id AND line_items.itemizable_type = 'Donation' -LEFT OUTER JOIN purchases ON purchases.id = line_items.itemizable_id AND line_items.itemizable_type = 'Purchase' -LEFT OUTER JOIN items ON items.id = line_items.item_id -LEFT OUTER JOIN adjustments ON adjustments.id = line_items.itemizable_id AND line_items.itemizable_type = 'Adjustment' -LEFT OUTER JOIN transfers ON transfers.id = line_items.itemizable_id AND line_items.itemizable_type = 'Transfer'") - .where("(donations.storage_location_id = :id or purchases.storage_location_id = :id or (adjustments.storage_location_id = :id and line_items.quantity > 0) or transfers.to_id = :id) and items.organization_id = :organization_id", id: @storage_location.id, organization_id: @organization.id) - .select("sum(line_items.quantity) as quantity, items.id AS item_id, items.name") - .group("items.name, items.id") - .order("items.name") - end - # rubocop:enable Naming/MemoizedInstanceVariableName -end diff --git a/app/queries/items_in_total_query.rb b/app/queries/items_in_total_query.rb deleted file mode 100644 index fa6dc3f23b..0000000000 --- a/app/queries/items_in_total_query.rb +++ /dev/null @@ -1,29 +0,0 @@ -# Creates a query object for retrieving the items, grouped by storage location -# We're using query objects for some of these more complicated queries to get -# the raw SQL out of the models and encapsulate it. -class ItemsInTotalQuery - attr_reader :organization - attr_reader :filter_params - attr_reader :storage_location - - def initialize(organization:, storage_location:, filter_params: nil) - @organization = organization - @storage_location = storage_location - @filter_params = filter_params - end - - # rubocop:disable Naming/MemoizedInstanceVariableName - def call - @items ||= LineItem.joins(" -LEFT OUTER JOIN donations ON donations.id = line_items.itemizable_id AND line_items.itemizable_type = 'Donation' -LEFT OUTER JOIN purchases ON purchases.id = line_items.itemizable_id AND line_items.itemizable_type = 'Purchase' -LEFT OUTER JOIN items ON items.id = line_items.item_id -LEFT OUTER JOIN adjustments ON adjustments.id = line_items.itemizable_id AND line_items.itemizable_type = 'Adjustment' -LEFT OUTER JOIN transfers ON transfers.id = line_items.itemizable_id AND line_items.itemizable_type = 'Transfer'") - .where("(donations.storage_location_id = :id or purchases.storage_location_id = :id or (adjustments.storage_location_id = :id and line_items.quantity > 0) or transfers.to_id = :id) and items.organization_id = :organization_id", id: @storage_location.id, organization_id: @organization.id) - .sum("line_items.quantity") - end - # rubocop:enable Naming/MemoizedInstanceVariableName -end - - diff --git a/app/queries/items_out_query.rb b/app/queries/items_out_query.rb deleted file mode 100644 index 016245c6b2..0000000000 --- a/app/queries/items_out_query.rb +++ /dev/null @@ -1,29 +0,0 @@ -# Creates a query object for retrieving the items, grouped by storage location -# We're using query objects for some of these more complicated queries to get -# the raw SQL out of the models and encapsulate it. -class ItemsOutQuery - attr_reader :organization - attr_reader :filter_params - attr_reader :storage_location - - def initialize(organization:, storage_location:, filter_params: nil) - @organization = organization - @storage_location = storage_location - @filter_params = filter_params - end - - # rubocop:disable Naming/MemoizedInstanceVariableName - def call - @items ||= LineItem.joins(" -LEFT OUTER JOIN distributions ON distributions.id = line_items.itemizable_id AND line_items.itemizable_type = 'Distribution' -LEFT OUTER JOIN items ON items.id = line_items.item_id -LEFT OUTER JOIN adjustments ON adjustments.id = line_items.itemizable_id AND line_items.itemizable_type = 'Adjustment' -LEFT OUTER JOIN transfers ON transfers.id = line_items.itemizable_id AND line_items.itemizable_type = 'Transfer'") - .where("(distributions.storage_location_id = :id or (adjustments.storage_location_id= :id and line_items.quantity < 0) or transfers.from_id = :id) and items.organization_id= :organization_id", - id: @storage_location.id, organization_id: @organization.id) - .select("sum( case when line_items.quantity < 0 then -1*line_items.quantity else line_items.quantity END ) as quantity, items.id AS item_id, items.name") - .group("items.name, items.id") - .order("items.name") - end - # rubocop:enable Naming/MemoizedInstanceVariableName -end diff --git a/app/queries/items_out_total_query.rb b/app/queries/items_out_total_query.rb deleted file mode 100644 index ad0d39e124..0000000000 --- a/app/queries/items_out_total_query.rb +++ /dev/null @@ -1,26 +0,0 @@ -# Creates a query object for retrieving the items, grouped by storage location -# We're using query objects for some of these more complicated queries to get -# the raw SQL out of the models and encapsulate it. -class ItemsOutTotalQuery - attr_reader :organization - attr_reader :filter_params - attr_reader :storage_location - - def initialize(organization:, storage_location:, filter_params: nil) - @organization = organization - @storage_location = storage_location - @filter_params = filter_params - end - - # rubocop:disable Naming/MemoizedInstanceVariableName - def call - @items ||= LineItem.joins(" -LEFT OUTER JOIN distributions ON distributions.id = line_items.itemizable_id AND line_items.itemizable_type = 'Distribution' -LEFT OUTER JOIN items ON items.id = line_items.item_id -LEFT OUTER JOIN adjustments ON adjustments.id = line_items.itemizable_id AND line_items.itemizable_type = 'Adjustment' -LEFT OUTER JOIN transfers ON transfers.id = line_items.itemizable_id AND line_items.itemizable_type = 'Transfer'") - .where("(distributions.storage_location_id = :id or (adjustments.storage_location_id= :id and line_items.quantity < 0) or transfers.from_id = :id) and items.organization_id= :organization_id", id: @storage_location.id, organization_id: @organization.id) - .sum("case when line_items.quantity < 0 then -1*line_items.quantity else line_items.quantity END") - end - # rubocop:enable Naming/MemoizedInstanceVariableName -end diff --git a/app/views/storage_locations/_item_row.html.erb b/app/views/storage_locations/_item_row.html.erb new file mode 100644 index 0000000000..5992eff1fe --- /dev/null +++ b/app/views/storage_locations/_item_row.html.erb @@ -0,0 +1,7 @@ +<% css_class = item_row["change"].negative? ? 'modal-body-warning-text' : '' %> +"> + <%= link_to item_row["item_name"], item_path(item_row["item_id"]) %> + <%= item_row["quantity_in"] %> + <%= item_row["quantity_out"] %> + <%= item_row["change"] %> + diff --git a/app/views/storage_locations/show.html.erb b/app/views/storage_locations/show.html.erb index 066dc3a7ec..3a01ba6771 100644 --- a/app/views/storage_locations/show.html.erb +++ b/app/views/storage_locations/show.html.erb @@ -68,21 +68,18 @@
-
+
<%= form_for @storage_location, method: :get do %> Show Inventory at Date: <%= date_field_tag 'version_date', params[:version_date], min: InventoryItem::EARLIEST_VERSION, autocomplete: "on" %> <%= filter_button(text: 'View') %> @@ -131,41 +128,39 @@
-
- - - - - - - - - <%= render partial: "line_item_row", collection: @items_in %> - - - - - - - -
ItemQuantity
Total<%= @items_in_total %>
-
- -
+
+ <%= form_for @storage_location, method: :get do %> + <%= label_tag "Date Range" %> +
+
+ <%= render partial: "shared/date_range_picker", locals: {css_class: "form-control"} %> +
+
+ <%= filter_button %> + <%= clear_filter_button %> +
+
+ <% end %> +

- + + + - <%= render partial: "line_item_row", collection: @items_out %> + <%= render partial: "item_row", collection: @items %> - + + + <% css_class = @total_quantity_change.negative? ? 'modal-body-warning-text' : '' %> +
ItemQuantityQuantity InQuantity OutChange
Total<%= @items_out_total %><%= @total_quantity_in %><%= @total_quantity_out %><%= @total_quantity_change %>
diff --git a/spec/queries/items_flow_query_spec.rb b/spec/queries/items_flow_query_spec.rb new file mode 100644 index 0000000000..6634be8bff --- /dev/null +++ b/spec/queries/items_flow_query_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require "rspec" + +RSpec.describe ItemsFlowQuery do + let(:items) { create_list(:item, 2) } + let!(:storage_location) { create(:storage_location, name: "here") } + let(:organization) { storage_location.organization } + let!(:result) do + [ + { + item_id: items[0].id, + item_name: items[0].name, + quantity_in: 10, + quantity_out: 5, + change: 5, + total_quantity_in: 19, + total_quantity_out: 7, + total_change: 12 + }, + { + item_id: items[1].id, + item_name: items[1].name, + quantity_in: 9, + quantity_out: 2, + change: 7, + total_quantity_in: 19, + total_quantity_out: 7, + total_change: 12 + } + ].map(&:with_indifferent_access) + end + + before do + create(:donation, :with_items, item: items[0], item_quantity: 10, storage_location: storage_location) + distribution = create(:distribution, :with_items, item: items[0], item_quantity: 5, storage_location: storage_location) + DistributionEvent.publish(distribution) + create(:donation, :with_items, item: items[1], item_quantity: 3, storage_location: storage_location) + adjustment = create(:adjustment, :with_items, item: items[1], item_quantity: 3, storage_location: storage_location) + AdjustmentEvent.publish(adjustment) + transfer = create(:transfer, :with_items, item: items[1], item_quantity: 2, from: storage_location, to: create(:storage_location)) + TransferEvent.publish(transfer) + audit = create(:audit, :with_items, item: items[1], item_quantity: 3, adjustment: adjustment, storage_location: storage_location) + AuditEvent.publish(audit) + end + + subject { described_class.new(organization: organization, storage_location: storage_location).call } + + context "without filter params" do + it "returns array of hashes" do + expect(subject.to_a).to match_array(result) + end + end + + context "with filter params" do + let(:filter_params) { [11.days.ago, 9.days.ago] } + let!(:old_items) { create_list(:item, 2) } + let(:other_location) { create(:storage_location, organization: organization) } + + subject { described_class.new(organization: organization, storage_location: storage_location, filter_params: filter_params).call } + + before do + create(:donation, :with_items, item: old_items[0], item_quantity: 10, storage_location: storage_location) + Event.last.update(created_at: 10.days.ago) + create(:donation, :with_items, item: old_items[1], item_quantity: 8, storage_location: storage_location) + distribution = create(:distribution, :with_items, item: old_items[1], item_quantity: 5, storage_location: storage_location) + DistributionEvent.publish(distribution) + Event.last.update(created_at: 10.days.ago) + end + + let(:filtered_result) do + [ + { + item_id: old_items[0].id, + item_name: old_items[0].name, + quantity_in: 10, + quantity_out: 0, + change: 10, + total_quantity_in: 10, + total_quantity_out: 5, + total_change: 5 + }, + { + item_id: old_items[1].id, + item_name: old_items[1].name, + quantity_in: 0, + quantity_out: 5, + change: -5, + total_quantity_in: 10, + total_quantity_out: 5, + total_change: 5 + } + ].map(&:with_indifferent_access) + end + + it "returns array of hashes" do + expect(subject.to_a).to match_array(filtered_result) + end + end +end diff --git a/spec/queries/items_in_query_spec.rb b/spec/queries/items_in_query_spec.rb deleted file mode 100644 index 25632c92e7..0000000000 --- a/spec/queries/items_in_query_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# Spec for /app/queries/items_in_query.rb - -RSpec.describe ItemsInQuery do - let(:organization) { create(:organization) } - let!(:storage_location) { create(:storage_location, organization: organization) } - let(:other_item) { create(:item, organization: organization) } - let!(:other_storage_location) { create(:storage_location, :with_items, item: other_item, item_quantity: 10, organization: organization) } - - subject { ItemsInQuery.new(storage_location: storage_location, organization: organization).call } - - describe "items_in" do - before do - create_list(:donation, 2, :with_items, item: create(:item, organization: organization), item_quantity: 5, storage_location: storage_location, organization: organization) - create_list(:purchase, 2, :with_items, item: create(:item, organization: organization), item_quantity: 5, storage_location: storage_location, organization: organization) - create_list(:adjustment, 2, :with_items, item: create(:item, organization: organization), item_quantity: 5, storage_location: storage_location, organization: organization) - create_list(:transfer, 2, :with_items, item_quantity: 5, item: other_item, from: other_storage_location, to: storage_location, organization: organization) - end - - it "returns a collection with the fields name, item_id, quantity" do - expect(subject.first).to be_respond_to(:name) - expect(subject.first).to be_respond_to(:quantity) - expect(subject.first).to be_respond_to(:item_id) - end - - it "includes donations, purchases, adjustments, transfers among sources" do - expect(subject.to_a.size).to eq(4) - end - - it "does not count negative adjustments towards in-flow" do - shared_item = create(:item, organization: organization) - create(:donation, :with_items, item: shared_item, item_quantity: 10, storage_location: storage_location, organization: organization) - create(:adjustment, :with_items, item: shared_item, item_quantity: -10, storage_location: storage_location, organization: organization) - expect(subject.to_a.size).to eq(5) - end - - it "does not count transfers going in the negative direction" do - shared_item = create(:item, organization: organization) - create(:donation, :with_items, item: shared_item, item_quantity: 10, storage_location: storage_location, organization: organization) - create(:transfer, :with_items, item_quantity: 10, item: shared_item, from: storage_location, to: other_storage_location, organization: organization) - expect(subject.to_a.size).to eq(5) - end - end -end diff --git a/spec/queries/items_in_total_query_spec.rb b/spec/queries/items_in_total_query_spec.rb deleted file mode 100644 index 00c18b9bf7..0000000000 --- a/spec/queries/items_in_total_query_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -# Spec for /app/queries/items_in_total_query.rb - -RSpec.describe ItemsInTotalQuery do - let(:organization) { create(:organization) } - let!(:storage_location) { create(:storage_location, organization: organization) } - let(:other_item) { create(:item, organization: organization) } - let!(:other_storage_location) { create(:storage_location, :with_items, item: other_item, item_quantity: 10, organization: organization) } - let!(:shared_item) { create(:item, organization: organization) } - - subject { ItemsInTotalQuery.new(storage_location: storage_location, organization: organization).call } - - describe "items_in_total_query" do - before do - create(:donation, :with_items, item: create(:item, organization: organization), item_quantity: 10, storage_location: storage_location, organization: organization) - create(:purchase, :with_items, item: create(:item, organization: organization), item_quantity: 10, storage_location: storage_location, organization: organization) - create(:adjustment, :with_items, item: create(:item, organization: organization), item_quantity: 10, storage_location: storage_location, organization: organization) - create(:transfer, :with_items, item_quantity: 10, item: other_item, from: other_storage_location, to: storage_location, organization: organization) - end - - it "returns a sum total of all in-flows" do - expect(subject).to eq(40) - end - - it "counts shared items together" do - create(:donation, :with_items, item: shared_item, item_quantity: 50, storage_location: storage_location, organization: organization) - create(:purchase, :with_items, item: shared_item, item_quantity: 100, storage_location: storage_location, organization: organization) - create(:adjustment, :with_items, item: shared_item, item_quantity: 50, storage_location: storage_location, organization: organization) - create(:donation, :with_items, item: shared_item, item_quantity: 100, storage_location: storage_location, organization: organization) - create(:purchase, :with_items, item: shared_item, item_quantity: 50, storage_location: storage_location, organization: organization) - create(:adjustment, :with_items, item: shared_item, item_quantity: 100, storage_location: storage_location, organization: organization) - expect(subject).to eq(490) - end - - it "does not count negative adjustments towards in-flow" do - create(:donation, :with_items, item: shared_item, item_quantity: 10, storage_location: storage_location, organization: organization) - create(:adjustment, :with_items, item: shared_item, item_quantity: -10, storage_location: storage_location, organization: organization) - expect(subject).to eq(50) - end - - it "does not count transfers going in the negative direction" do - create(:donation, :with_items, item: shared_item, item_quantity: 10, storage_location: storage_location, organization: organization) - create(:transfer, :with_items, item_quantity: 10, item: shared_item, from: storage_location, to: other_storage_location, organization: organization) - expect(subject).to eq(50) - end - - it "does not count donations, purchases, adjustments to other storage locations" do - create(:donation, :with_items, item: create(:item, organization: organization), item_quantity: 100, storage_location: other_storage_location, organization: organization) - create(:purchase, :with_items, item: create(:item, organization: organization), item_quantity: 10, storage_location: other_storage_location, organization: organization) - create(:adjustment, :with_items, item: create(:item, organization: organization), item_quantity: 10, storage_location: other_storage_location, organization: organization) - expect(subject).to eq(40) - end - end -end diff --git a/spec/queries/items_out_query_spec.rb b/spec/queries/items_out_query_spec.rb deleted file mode 100644 index 9bbe735ac7..0000000000 --- a/spec/queries/items_out_query_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -# Spec for /app/queries/items_out_query.rb - -RSpec.describe ItemsOutQuery do - let(:organization) { create(:organization) } - let!(:storage_location) { create(:storage_location, organization: organization) } - subject { ItemsOutQuery.new(storage_location: storage_location, organization: organization).call } - - describe "items_out_query" do - before do - items = create_list(:item, 3, organization: organization) - TestInventory.create_inventory(storage_location.organization, { - storage_location.id => items.to_h { |i| [i.id, 10] } - }) - other_storage_location = create(:storage_location, organization: organization) - create(:transfer, :with_items, item_quantity: 8, item: items[0], to: other_storage_location, from: storage_location, organization: organization) - create(:distribution, :with_items, item: items[1], item_quantity: 9, storage_location: storage_location, organization: organization) - create(:adjustment, :with_items, item: items[2], item_quantity: -10, storage_location: storage_location, organization: organization) - end - - it "returns a collection with the fields name, item_id, quantity" do - expect(subject.first).to be_respond_to(:name) - expect(subject.first).to be_respond_to(:quantity) - expect(subject.first).to be_respond_to(:item_id) - end - - it "includes distributions, adjustments, transfers among sources" do - expect(subject.to_a.size).to eq(3) - end - end -end diff --git a/spec/queries/items_out_total_query_spec.rb b/spec/queries/items_out_total_query_spec.rb deleted file mode 100644 index deab96d3ed..0000000000 --- a/spec/queries/items_out_total_query_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# Spec for /app/queries/items_out_total_query.rb - -RSpec.describe ItemsOutTotalQuery do - let(:organization) { create(:organization) } - let!(:storage_location) { create(:storage_location, organization: organization) } - subject { ItemsOutTotalQuery.new(storage_location: storage_location, organization: organization).call } - - describe "items_out_total_query" do - before do - items = create_list(:item, 3, organization: organization) - TestInventory.create_inventory(storage_location.organization, { - storage_location.id => items.to_h { |i| [i.id, 10] } - }) - other_storage_location = create(:storage_location, organization: organization) - create(:transfer, :with_items, item_quantity: 10, item: items[0], to: other_storage_location, from: storage_location, organization: organization) - create(:distribution, :with_items, item: items[1], item_quantity: 10, storage_location: storage_location, organization: organization) - create(:adjustment, :with_items, item: items[2], item_quantity: -10, storage_location: storage_location, organization: organization) - end - - it "returns a sum total of all out-flows" do - expect(subject).to eq(30) - end - end -end diff --git a/spec/system/storage_location_system_spec.rb b/spec/system/storage_location_system_spec.rb index 662af5f3a3..e2d8b3b6b6 100644 --- a/spec/system/storage_location_system_spec.rb +++ b/spec/system/storage_location_system_spec.rb @@ -99,13 +99,6 @@ click_on "View", match: :first - find("#custom-tabs-inventory-in-tab").click - - within "#custom-tabs-inventory-in" do - expect(page).to have_content("Needle") - expect(page).to have_content(100) - end - find("#custom-tabs-inventory-tab").click within "#custom-tabs-inventory" do @@ -189,24 +182,104 @@ end context "when viewing an existing storage location" do - let(:item) { create(:item, name: "AAA Diapers") } - let!(:storage_location) { create(:storage_location, :with_items, item: item, name: "here") } - let!(:adjustment) { create(:adjustment, :with_items, storage_location: storage_location) } + let(:items) { create_list(:item, 2) } + let!(:storage_location) { create(:storage_location, name: "here") } + let(:result) do + [ + { + item_id: items[0].id, + item_name: items[0].name, + quantity_in: 10, + quantity_out: 5, + change: 5, + total_quantity_in: 16, + total_quantity_out: 7, + total_change: 9 + }, + { + item_id: items[1].id, + item_name: items[1].name, + quantity_in: 6, + quantity_out: 2, + change: 4, + total_quantity_in: 16, + total_quantity_out: 7, + total_change: 9 + } + ].map(&:with_indifferent_access) + end subject { storage_location_path(storage_location.id) } - it "Items in (adjustments)" do - visit subject - find("#custom-tabs-inventory-in-tab").click - - expect(page.find("#custom-tabs-inventory-in", visible: true)).to have_content "100" - end + context "Inventory Flow Tab" do + before do + create(:donation, :with_items, item: items[0], item_quantity: 10, storage_location: storage_location) + distribution = create(:distribution, :with_items, item: items[0], item_quantity: 5, storage_location: storage_location) + DistributionEvent.publish(distribution) + create(:donation, :with_items, item: items[1], item_quantity: 3, storage_location: storage_location) + adjustment = create(:adjustment, :with_items, item: items[1], item_quantity: 3, storage_location: storage_location) + AdjustmentEvent.publish(adjustment) + transfer = create(:transfer, :with_items, item: items[1], item_quantity: 2, from: storage_location, to: create(:storage_location)) + TransferEvent.publish(transfer) + visit subject + find("#custom-tabs-inventory-flow-tab").click + end - it "Items out (distributions)" do - create(:distribution, :with_items, storage_location: storage_location) - visit subject - find("#custom-tabs-inventory-out-tab").click + it "shows the inventory flow for the storage location" do + within("#custom-tabs-inventory-flow table tbody") do + result.each do |item| + row = find(:css, "tr[id='#{item[:item_id]}']") + change_column_css = item[:change].negative? ? "td.modal-body-warning-text" : "td" + expect(row).to have_link(item[:name], href: item_path(item[:item_id])) + expect(row).to have_css("td", text: item[:quantity_in]) + expect(row).to have_css("td", text: item[:quantity_out]) + expect(row).to have_css(change_column_css, text: item[:change]) + end + end + within("#custom-tabs-inventory-flow table tfoot") do + expect(page).to have_css("td", text: "Total") + expect(page).to have_css("td", text: result.first[:total_quantity_in]) + expect(page).to have_css("td", text: result.first[:total_quantity_out]) + expect(page).to have_css("td", text: result.first[:total_change]) + end + end - expect(page.find("#custom-tabs-inventory-out", visible: true)).to have_content "100" + context "date range filter" do + let!(:start_date) { 2.days.ago } + let!(:end_date) { 1.day.ago } + let!(:item) { create(:item, name: "Filtered Item") } + let(:result) do + [ + { + item_id: item.id, + item_name: item.name, + quantity_in: 10, + quantity_out: 0, + change: 10, + total_quantity_in: 10, + total_quantity_out: 0, + total_change: 10 + } + ].map(&:with_indifferent_access) + end + before do + create(:donation, :with_items, item: item, item_quantity: 10, storage_location: storage_location) + Event.last.update(created_at: start_date) + fill_in "filters[date_range]", with: "#{start_date} - #{end_date}" + click_button "Filter" + find("#custom-tabs-inventory-flow-tab").click + end + + it "filters the inventory flow by date range" do + within("#custom-tabs-inventory-flow table tbody") do + expect(page).to have_css("tr", count: 1) + row = find(:css, "tr[id='#{result.first[:item_id]}']") + expect(row).to have_link(result.first[:name], href: item_path(result.first[:item_id])) + expect(row).to have_css("td", text: result.first[:quantity_in]) + expect(row).to have_css("td", text: result.first[:quantity_out]) + expect(row).to have_css("td", text: result.first[:change]) + end + end + end end end end