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.new(organization: current_organization, request_ids: requests.pluck(:id)).fetch
end

private

def total_purchased_unformatted(range = selected_range)
Expand Down
101 changes: 101 additions & 0 deletions app/services/request_itemized_breakdown_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
class RequestItemizedBreakdownService
#
# Initializes the RequestItemizedBreakdownService whose
# purpose is to construct an itemized breakdown of requested items
#
# @param organization [Organization]
# @param request_ids [Array<Integer>]
# @return [RequestItemizedBreakdownService]
def initialize(organization:, request_ids:)
@organization = organization
@request_ids = request_ids
end

#
# Returns a hash containing the itemized breakdown of
# requested items.
#
# @return [Array]
def fetch
inventory = View::Inventory.new(@organization.id)
current_onhand = current_onhand_quantities(inventory)
current_min_onhand = current_onhand_minimums(inventory)
items_requested = fetch_items_requested

items_requested.map! do |item|
item_id = item[:item_id]

on_hand = current_onhand[item_id]
minimum = current_min_onhand[item_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

#
# Returns a CSV string representation of the itemized breakdown of
# what was requested
#
# @return [String]
def fetch_csv
convert_to_csv(fetch)
end

private

attr_reader :organization, :request_ids

def current_onhand_quantities(inventory)
inventory.all_items.group_by(&:id).to_h do |id, items|
[id, items.sum(&:quantity)]
end
end

def current_onhand_minimums(inventory)
inventory.all_items.group_by(&:id).to_h do |id, items|
[id, items.map(&:on_hand_minimum_quantity).compact.max]
end
end

def fetch_items_requested
Request
.includes(:partner, :organization, :item_requests)
.where(id: @request_ids)
.flat_map do |request|
request.request_items.map do |json_item|
RequestItem.from_json(json_item, request)
end
end
.group_by { |ri| [ri.item.id, ri.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

def inventory
@inventory ||= View::Inventory.new(@organization.id)
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
62 changes: 62 additions & 0 deletions spec/services/request_itemized_breakdown_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
RSpec.describe RequestItemizedBreakdownService, type: :service do
let(:organization) { create(:organization) }

let(:item_a) do
create(:item, organization: organization, on_hand_minimum_quantity: 4, name: "A Diapers")
end
let(:item_b) do
create(:item, organization: organization, on_hand_minimum_quantity: 8, name: "B Diapers")
end

let(:request_1) do
create(:request, organization: organization, request_items: [
{"item_id" => item_a.id, "quantity" => 5}
])
end

let(:request_2) do
create(:request, organization: organization, request_items: [
{"item_id" => item_b.id, "quantity" => 10}
])
end

let(:expected_output) do
[
{name: item_a.name, item_id: item_a.id, unit: nil, quantity: 5, on_hand: 3, onhand_minimum: 4, below_onhand_minimum: true},
{name: item_b.name, item_id: item_b.id, unit: nil, quantity: 10, on_hand: 20, onhand_minimum: 8, below_onhand_minimum: false}
]
end

before do
allow_any_instance_of(View::Inventory).to receive(:quantity_for).with(item_id: item_a.id).and_return(3)
allow_any_instance_of(View::Inventory).to receive(:quantity_for).with(item_id: item_b.id).and_return(20)
allow_any_instance_of(View::Inventory).to receive(:all_items).and_return([
OpenStruct.new(id: item_a.id, quantity: 3, on_hand_minimum_quantity: 4),
OpenStruct.new(id: item_b.id, quantity: 20, on_hand_minimum_quantity: 8)
])
end

describe "#fetch" do
subject { service.fetch }
let(:service) { described_class.new(organization: organization, request_ids: [request_1.id, request_2.id]) }

it "should include the break down of requested items" do
expect(subject).to eq(expected_output)
end
end

describe "#fetch_csv" do
subject { service.fetch_csv }
let(:service) { described_class.new(organization: organization, request_ids: [request_1.id, request_2.id]) }

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