Skip to content
5 changes: 5 additions & 0 deletions app/controllers/reports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
70 changes: 70 additions & 0 deletions app/services/request_itemized_breakdown_service.rb
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions app/views/layouts/_lte_sidebar.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@
<i class="nav-icon fa fa-circle-o"></i> Purchases - Trends
<% end %>
</li>
<li class="nav-item <%= active_class(['reports/itemized_requests']) %>">
<%= link_to(reports_itemized_requests_path, class: "nav-link #{active_class(['reports/itemized_requests'])}") do %>
<i class="nav-icon fa fa-circle-o"></i> Requests - Itemized
<% end %>
</li>
</ul>
</li>
<% if current_user.has_cached_role?(Role::ORG_ADMIN, current_organization) %>
Expand Down
42 changes: 42 additions & 0 deletions app/views/reports/itemized_requests.html.erb
Original file line number Diff line number Diff line change
@@ -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? %>
<div class="alert alert-warning" role="alert">
No itemized requests found for the selected date range.
</div>
<% else %>
<table class="table table-hover striped text-left">
<thead>
<tr>
<th>Item</th>
<th class="text-right">Total Requested</th>
<th class="text-right">Total On Hand</th>
</tr>
</thead>
<tbody>
<% @itemized_request_data.each do |item| %>
<tr>
<td>
<%= item[:name] %>
<% if item[:unit].present? %>
(<%= h(item[:unit]) %>)
<% end %>
</td>
<td class="text-right"><%= item[:quantity] %></td>
<td class="text-right <%= 'table-danger' if item[:below_onhand_minimum] %>">
<%= item[:on_hand] || 0 %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
<% end %>
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
53 changes: 53 additions & 0 deletions spec/services/request_itemized_breakdown_service_spec.rb
Original file line number Diff line number Diff line change
@@ -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