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 %>
+
+ <%= link_to(reports_itemized_requests_path, class: "nav-link #{active_class(['reports/itemized_requests'])}") do %>
+ Requests - Itemized
+ <% 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? %>
+
+ No itemized requests found for the selected date range.
+
+ <% else %>
+
+
+
+ | Item |
+ Total Requested |
+ Total On Hand |
+
+
+
+ <% @itemized_request_data.each do |item| %>
+
+ |
+ <%= item[:name] %>
+ <% if item[:unit].present? %>
+ (<%= h(item[:unit]) %>)
+ <% end %>
+ |
+ <%= item[:quantity] %> |
+
+ <%= item[:on_hand] || 0 %>
+ |
+
+ <% end %>
+
+
+ <% 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