diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index 24b01fd425..137f08daa8 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -39,6 +39,11 @@ def activity_graph @distribution_data = received_distributed_data(helpers.selected_range) end + def itemized_requests + requests = current_organization.requests.during(helpers.selected_range) + @itemized_request_data = RequestItemizedBreakdownService.call(organization: current_organization, request_ids: requests.pluck(:id)) + end + private def total_purchased_unformatted(range = selected_range) diff --git a/app/services/request_itemized_breakdown_service.rb b/app/services/request_itemized_breakdown_service.rb new file mode 100644 index 0000000000..b3b2a33a0e --- /dev/null +++ b/app/services/request_itemized_breakdown_service.rb @@ -0,0 +1,70 @@ +class RequestItemizedBreakdownService + class << self + def call(organization:, request_ids:, format: :hash) + data = fetch(organization: organization, request_ids: request_ids) + (format == :csv) ? convert_to_csv(data) : data + end + + def fetch(organization:, request_ids:) + inventory = View::Inventory.new(organization.id) + current_onhand = current_onhand_quantities(inventory) + current_min_onhand = current_onhand_minimums(inventory) + items_requested = fetch_items_requested(organization: organization, request_ids: request_ids) + + items_requested.each do |item| + id = item[:item_id] + + on_hand = current_onhand[id] + minimum = current_min_onhand[id] + below_onhand_minimum = on_hand && minimum && on_hand < minimum + + item.merge!( + on_hand: on_hand, + onhand_minimum: minimum, + below_onhand_minimum: below_onhand_minimum + ) + end + + items_requested.sort_by { |item| [item[:name], item[:unit].to_s] } + end + + def csv(organization:, request_ids:) + convert_to_csv(fetch(organization: organization, request_ids: request_ids)) + end + + private + + def current_onhand_quantities(inventory) + inventory.all_items.group_by(&:item_id).to_h { |item_id, rows| [item_id, rows.sum { |r| r.quantity.to_i }] } + end + + def current_onhand_minimums(inventory) + inventory.all_items.group_by(&:item_id).to_h { |item_id, rows| [item_id, rows.map(&:on_hand_minimum_quantity).compact.max] } + end + + def fetch_items_requested(organization:, request_ids:) + Partners::ItemRequest + .includes(:item) + .where(partner_request_id: request_ids) + .group_by { |ir| [ir.item_id, ir.request_unit] } + .map do |(item_id, unit), grouped| + item = grouped.first.item + { + item_id: item.id, + name: item.name, + unit: unit, + quantity: grouped.sum { |ri| ri.quantity.to_i } + } + end + end + + def convert_to_csv(items_requested_data) + CSV.generate do |csv| + csv << ["Item", "Total Requested", "Total On Hand"] + items_requested_data.each do |item| + csv << [item[:name], item[:quantity], item[:on_hand]] + end + end + end + end +end diff --git a/app/views/layouts/_lte_sidebar.html.erb b/app/views/layouts/_lte_sidebar.html.erb index d8564aec34..acd724201b 100644 --- a/app/views/layouts/_lte_sidebar.html.erb +++ b/app/views/layouts/_lte_sidebar.html.erb @@ -239,6 +239,11 @@ Purchases - Trends <% end %> + <% if current_user.has_cached_role?(Role::ORG_ADMIN, current_organization) %> diff --git a/app/views/reports/itemized_requests.html.erb b/app/views/reports/itemized_requests.html.erb new file mode 100644 index 0000000000..7561b72239 --- /dev/null +++ b/app/views/reports/itemized_requests.html.erb @@ -0,0 +1,42 @@ +<%= render( + "shared/filtered_card", + id: "purchases", + gradient: "secondary", + title: "Itemized Requests", + subtitle: @selected_date_range, + type: :table, + filter_url: reports_itemized_requests_path +) do %> + + <% if @itemized_request_data.empty? %> + + <% else %> + + + + + + + + + + <% @itemized_request_data.each do |item| %> + + + + + + <% end %> + +
ItemTotal RequestedTotal On Hand
+ <%= item[:name] %> + <% if item[:unit].present? %> + (<%= h(item[:unit]) %>) + <% end %> + <%= item[:quantity] %> + <%= item[:on_hand] || 0 %> +
+ <% end %> +<% end %> diff --git a/config/routes.rb b/config/routes.rb index 0830589f04..d535b64bd5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -117,6 +117,7 @@ def set_up_flipper get :itemized_distributions get :distributions_summary get :activity_graph + get :itemized_requests end resources :transfers, only: %i(index create new show destroy) diff --git a/spec/services/request_itemized_breakdown_service_spec.rb b/spec/services/request_itemized_breakdown_service_spec.rb new file mode 100644 index 0000000000..dd9ead6d93 --- /dev/null +++ b/spec/services/request_itemized_breakdown_service_spec.rb @@ -0,0 +1,53 @@ +RSpec.describe RequestItemizedBreakdownService, type: :service do + let(:organization) { create(:organization) } + + let(:item_a) { create(:item, organization: organization, name: "A Diapers", on_hand_minimum_quantity: 4) } + let(:item_b) { create(:item, organization: organization, name: "B Diapers", on_hand_minimum_quantity: 8) } + + let(:request_1) { create(:request, organization: organization) } + let(:request_2) { create(:request, organization: organization) } + + let(:storage_location) { create(:storage_location, organization: organization) } + + before do + TestInventory.create_inventory(organization, { + storage_location.id => { + item_a.id => 3, + item_b.id => 20 + } + }) + + create(:item_request, request: request_1, partner_request_id: request_1.id, item: item_a, quantity: 5, request_unit: nil) + create(:item_request, request: request_2, partner_request_id: request_2.id, item: item_b, quantity: 10, request_unit: nil) + end + + describe "#fetch" do + subject(:result) do + described_class.call(organization: organization, request_ids: [request_1.id, request_2.id]) + end + + it "should include the break down of requested items" do + expected_output = [ + {name: "A Diapers", item_id: item_a.id, unit: nil, quantity: 5, on_hand: 3, onhand_minimum: 4, below_onhand_minimum: true}, + {name: "B Diapers", item_id: item_b.id, unit: nil, quantity: 10, on_hand: 20, onhand_minimum: 8, below_onhand_minimum: false} + ] + expect(result).to eq(expected_output) + end + end + + describe "#fetch_csv" do + subject(:subject) do + described_class.call(organization: organization, request_ids: [request_1.id, request_2.id], format: :csv) + end + + it "should output the expected output but in CSV format" do + expected_csv = <<~CSV + Item,Total Requested,Total On Hand + A Diapers,5,3 + B Diapers,10,20 + CSV + + expect(subject).to eq(expected_csv) + end + end +end