Skip to content

Commit 2b4d3e5

Browse files
authored
Merge pull request #5300 from seabeeberry/5225-itemized-request-report
Resolves #5225: Implemented "Itemized Request Report"
2 parents 83508dc + d88243a commit 2b4d3e5

File tree

6 files changed

+176
-0
lines changed

6 files changed

+176
-0
lines changed

app/controllers/reports_controller.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ def activity_graph
3939
@distribution_data = received_distributed_data(helpers.selected_range)
4040
end
4141

42+
def itemized_requests
43+
requests = current_organization.requests.during(helpers.selected_range)
44+
@itemized_request_data = RequestItemizedBreakdownService.call(organization: current_organization, request_ids: requests.pluck(:id))
45+
end
46+
4247
private
4348

4449
def total_purchased_unformatted(range = selected_range)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
class RequestItemizedBreakdownService
2+
class << self
3+
def call(organization:, request_ids:, format: :hash)
4+
data = fetch(organization: organization, request_ids: request_ids)
5+
(format == :csv) ? convert_to_csv(data) : data
6+
end
7+
8+
def fetch(organization:, request_ids:)
9+
inventory = View::Inventory.new(organization.id)
10+
current_onhand = current_onhand_quantities(inventory)
11+
current_min_onhand = current_onhand_minimums(inventory)
12+
items_requested = fetch_items_requested(organization: organization, request_ids: request_ids)
13+
14+
items_requested.each do |item|
15+
id = item[:item_id]
16+
17+
on_hand = current_onhand[id]
18+
minimum = current_min_onhand[id]
19+
below_onhand_minimum = on_hand && minimum && on_hand < minimum
20+
21+
item.merge!(
22+
on_hand: on_hand,
23+
onhand_minimum: minimum,
24+
below_onhand_minimum: below_onhand_minimum
25+
)
26+
end
27+
28+
items_requested.sort_by { |item| [item[:name], item[:unit].to_s] }
29+
end
30+
31+
def csv(organization:, request_ids:)
32+
convert_to_csv(fetch(organization: organization, request_ids: request_ids))
33+
end
34+
35+
private
36+
37+
def current_onhand_quantities(inventory)
38+
inventory.all_items.group_by(&:item_id).to_h { |item_id, rows| [item_id, rows.sum { |r| r.quantity.to_i }] }
39+
end
40+
41+
def current_onhand_minimums(inventory)
42+
inventory.all_items.group_by(&:item_id).to_h { |item_id, rows| [item_id, rows.map(&:on_hand_minimum_quantity).compact.max] }
43+
end
44+
45+
def fetch_items_requested(organization:, request_ids:)
46+
Partners::ItemRequest
47+
.includes(:item)
48+
.where(partner_request_id: request_ids)
49+
.group_by { |ir| [ir.item_id, ir.request_unit] }
50+
.map do |(item_id, unit), grouped|
51+
item = grouped.first.item
52+
{
53+
item_id: item.id,
54+
name: item.name,
55+
unit: unit,
56+
quantity: grouped.sum { |ri| ri.quantity.to_i }
57+
}
58+
end
59+
end
60+
61+
def convert_to_csv(items_requested_data)
62+
CSV.generate do |csv|
63+
csv << ["Item", "Total Requested", "Total On Hand"]
64+
items_requested_data.each do |item|
65+
csv << [item[:name], item[:quantity], item[:on_hand]]
66+
end
67+
end
68+
end
69+
end
70+
end

app/views/layouts/_lte_sidebar.html.erb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,11 @@
239239
<i class="nav-icon fa fa-circle-o"></i> Purchases - Trends
240240
<% end %>
241241
</li>
242+
<li class="nav-item <%= active_class(['reports/itemized_requests']) %>">
243+
<%= link_to(reports_itemized_requests_path, class: "nav-link #{active_class(['reports/itemized_requests'])}") do %>
244+
<i class="nav-icon fa fa-circle-o"></i> Requests - Itemized
245+
<% end %>
246+
</li>
242247
</ul>
243248
</li>
244249
<% if current_user.has_cached_role?(Role::ORG_ADMIN, current_organization) %>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<%= render(
2+
"shared/filtered_card",
3+
id: "purchases",
4+
gradient: "secondary",
5+
title: "Itemized Requests",
6+
subtitle: @selected_date_range,
7+
type: :table,
8+
filter_url: reports_itemized_requests_path
9+
) do %>
10+
11+
<% if @itemized_request_data.empty? %>
12+
<div class="alert alert-warning" role="alert">
13+
No itemized requests found for the selected date range.
14+
</div>
15+
<% else %>
16+
<table class="table table-hover striped text-left">
17+
<thead>
18+
<tr>
19+
<th>Item</th>
20+
<th class="text-right">Total Requested</th>
21+
<th class="text-right">Total On Hand</th>
22+
</tr>
23+
</thead>
24+
<tbody>
25+
<% @itemized_request_data.each do |item| %>
26+
<tr>
27+
<td>
28+
<%= item[:name] %>
29+
<% if item[:unit].present? %>
30+
(<%= h(item[:unit]) %>)
31+
<% end %>
32+
</td>
33+
<td class="text-right"><%= item[:quantity] %></td>
34+
<td class="text-right <%= 'table-danger' if item[:below_onhand_minimum] %>">
35+
<%= item[:on_hand] || 0 %>
36+
</td>
37+
</tr>
38+
<% end %>
39+
</tbody>
40+
</table>
41+
<% end %>
42+
<% end %>

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ def set_up_flipper
117117
get :itemized_distributions
118118
get :distributions_summary
119119
get :activity_graph
120+
get :itemized_requests
120121
end
121122

122123
resources :transfers, only: %i(index create new show destroy) do
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
RSpec.describe RequestItemizedBreakdownService, type: :service do
2+
let(:organization) { create(:organization) }
3+
4+
let(:item_a) { create(:item, organization: organization, name: "A Diapers", on_hand_minimum_quantity: 4) }
5+
let(:item_b) { create(:item, organization: organization, name: "B Diapers", on_hand_minimum_quantity: 8) }
6+
7+
let(:request_1) { create(:request, organization: organization) }
8+
let(:request_2) { create(:request, organization: organization) }
9+
10+
let(:storage_location) { create(:storage_location, organization: organization) }
11+
12+
before do
13+
TestInventory.create_inventory(organization, {
14+
storage_location.id => {
15+
item_a.id => 3,
16+
item_b.id => 20
17+
}
18+
})
19+
20+
create(:item_request, request: request_1, partner_request_id: request_1.id, item: item_a, quantity: 5, request_unit: nil)
21+
create(:item_request, request: request_2, partner_request_id: request_2.id, item: item_b, quantity: 10, request_unit: nil)
22+
end
23+
24+
describe "#fetch" do
25+
subject(:result) do
26+
described_class.call(organization: organization, request_ids: [request_1.id, request_2.id])
27+
end
28+
29+
it "should include the break down of requested items" do
30+
expected_output = [
31+
{name: "A Diapers", item_id: item_a.id, unit: nil, quantity: 5, on_hand: 3, onhand_minimum: 4, below_onhand_minimum: true},
32+
{name: "B Diapers", item_id: item_b.id, unit: nil, quantity: 10, on_hand: 20, onhand_minimum: 8, below_onhand_minimum: false}
33+
]
34+
expect(result).to eq(expected_output)
35+
end
36+
end
37+
38+
describe "#fetch_csv" do
39+
subject(:subject) do
40+
described_class.call(organization: organization, request_ids: [request_1.id, request_2.id], format: :csv)
41+
end
42+
43+
it "should output the expected output but in CSV format" do
44+
expected_csv = <<~CSV
45+
Item,Total Requested,Total On Hand
46+
A Diapers,5,3
47+
B Diapers,10,20
48+
CSV
49+
50+
expect(subject).to eq(expected_csv)
51+
end
52+
end
53+
end

0 commit comments

Comments
 (0)