From 6a1c4a9d609ed2405d7dae5d4cee5721e5a2766f Mon Sep 17 00:00:00 2001 From: seabeeberry Date: Fri, 1 Aug 2025 14:47:46 +0200 Subject: [PATCH 1/9] Implemented Itemized Requests Report --- app/controllers/reports_controller.rb | 5 ++ .../request_itemized_breakdown_service.rb | 81 +++++++++++++++++++ app/views/layouts/_lte_sidebar.html.erb | 5 ++ app/views/reports/itemized_requests.html.erb | 42 ++++++++++ config/routes.rb | 1 + 5 files changed, 134 insertions(+) create mode 100644 app/services/request_itemized_breakdown_service.rb create mode 100644 app/views/reports/itemized_requests.html.erb diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index 24b01fd425..44ff38699f 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.new(organization: current_organization, request_ids: requests.pluck(:id)).fetch + 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..6ad5422055 --- /dev/null +++ b/app/services/request_itemized_breakdown_service.rb @@ -0,0 +1,81 @@ +class RequestItemizedBreakdownService + # + # Initializes the RequestItemizedBreakdownService whose + # purpose is to construct an itemized breakdown of requested items + # + # @param organization [Organization] + # @param request_ids [Array] + # @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 + items_requested = fetch_items_requested + + items_requested.map! do |item| + on_hand = inventory.quantity_for(item_id: item[:item_id]) + below_requested = on_hand && item[:quantity] && on_hand < item[:quantity] + + item.merge( + on_hand: on_hand, + below_requested: below_requested + ) + 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 distributed + # + # @return [String] + def fetch_csv + convert_to_csv(fetch) + end + + private + + attr_reader :organization, :request_ids + + 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 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..486e34da4d --- /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].nil? ? "Unknown" : item[:on_hand] %> +
+ <% 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) From 8b94612afe2a10af60e5179fbc1d8659b7ac58d1 Mon Sep 17 00:00:00 2001 From: seabeeberry Date: Fri, 1 Aug 2025 15:58:13 +0200 Subject: [PATCH 2/9] Added test for itemized breakdown of requested items --- ...request_itemized_breakdown_service_spec.rb | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 spec/services/request_itemized_breakdown_service_spec.rb 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..6d56737896 --- /dev/null +++ b/spec/services/request_itemized_breakdown_service_spec.rb @@ -0,0 +1,54 @@ +RSpec.describe RequestItemizedBreakdownService, type: :service do + let(:organization) { create(:organization) } + + let(:item_a) { create(:item, organization: organization, name: "A Diapers") } + let(:item_b) { create(:item, organization: organization, name: "B Diapers") } + + 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, below_requested: true}, + {name: item_b.name, item_id: item_b.id, unit: nil, quantity: 10, on_hand: 20, below_requested: 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) + 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 match_array(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 From 759cd3070dedc1d040bf4c623556db98c1584caa Mon Sep 17 00:00:00 2001 From: seabeeberry Date: Sun, 3 Aug 2025 16:03:20 +0200 Subject: [PATCH 3/9] Applied requested changes for displaying red items in the overview with the same criteria for distributions. Modelled files more closely after itemized distributions --- .../request_itemized_breakdown_service.rb | 34 +++++++++++++++---- app/views/reports/itemized_requests.html.erb | 2 +- ...request_itemized_breakdown_service_spec.rb | 18 +++++++--- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/app/services/request_itemized_breakdown_service.rb b/app/services/request_itemized_breakdown_service.rb index 6ad5422055..9067d165fa 100644 --- a/app/services/request_itemized_breakdown_service.rb +++ b/app/services/request_itemized_breakdown_service.rb @@ -17,15 +17,23 @@ def initialize(organization:, request_ids:) # # @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| - on_hand = inventory.quantity_for(item_id: item[:item_id]) - below_requested = on_hand && item[:quantity] && on_hand < item[:quantity] + 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, - below_requested: below_requested + onhand_minimum: minimum, + below_onhand_minimum: below_onhand_minimum ) end @@ -34,7 +42,7 @@ def fetch # # Returns a CSV string representation of the itemized breakdown of - # what was distributed + # what was requested # # @return [String] def fetch_csv @@ -45,15 +53,27 @@ def fetch_csv 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) + request.request_items.map do |json_item| + RequestItem.from_json(json_item, request) + end end - end .group_by { |ri| [ri.item.id, ri.unit] } .map do |(item_id, unit), grouped| item = grouped.first.item diff --git a/app/views/reports/itemized_requests.html.erb b/app/views/reports/itemized_requests.html.erb index 486e34da4d..442eb95dd2 100644 --- a/app/views/reports/itemized_requests.html.erb +++ b/app/views/reports/itemized_requests.html.erb @@ -31,7 +31,7 @@ <% end %> <%= item[:quantity] %> - + <%= item[:on_hand].nil? ? "Unknown" : item[:on_hand] %> diff --git a/spec/services/request_itemized_breakdown_service_spec.rb b/spec/services/request_itemized_breakdown_service_spec.rb index 6d56737896..6a4370e4c3 100644 --- a/spec/services/request_itemized_breakdown_service_spec.rb +++ b/spec/services/request_itemized_breakdown_service_spec.rb @@ -1,8 +1,12 @@ RSpec.describe RequestItemizedBreakdownService, type: :service do let(:organization) { create(:organization) } - let(:item_a) { create(:item, organization: organization, name: "A Diapers") } - let(:item_b) { create(:item, organization: organization, name: "B Diapers") } + 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: [ @@ -18,14 +22,18 @@ let(:expected_output) do [ - {name: item_a.name, item_id: item_a.id, unit: nil, quantity: 5, on_hand: 3, below_requested: true}, - {name: item_b.name, item_id: item_b.id, unit: nil, quantity: 10, on_hand: 20, below_requested: false} + {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 @@ -33,7 +41,7 @@ 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 match_array(expected_output) + expect(subject).to eq(expected_output) end end From 6aa181075bb1712c987dbbfe56a4259724896fe3 Mon Sep 17 00:00:00 2001 From: seabeeberry Date: Mon, 4 Aug 2025 21:30:21 +0200 Subject: [PATCH 4/9] Fixed display of Unknown when a quantity of an item is zero to display 0 instead --- app/services/request_itemized_breakdown_service.rb | 4 ++-- app/views/reports/itemized_requests.html.erb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/services/request_itemized_breakdown_service.rb b/app/services/request_itemized_breakdown_service.rb index 9067d165fa..7971e2612e 100644 --- a/app/services/request_itemized_breakdown_service.rb +++ b/app/services/request_itemized_breakdown_service.rb @@ -25,8 +25,8 @@ def fetch items_requested.map! do |item| item_id = item[:item_id] - on_hand = current_onhand[item_id] - minimum = current_min_onhand[item_id] + on_hand = current_onhand[item_id] || 0 + minimum = current_min_onhand[item_id] || 0 below_onhand_minimum = on_hand && minimum && on_hand < minimum diff --git a/app/views/reports/itemized_requests.html.erb b/app/views/reports/itemized_requests.html.erb index 442eb95dd2..7ae01283ea 100644 --- a/app/views/reports/itemized_requests.html.erb +++ b/app/views/reports/itemized_requests.html.erb @@ -32,7 +32,7 @@ <%= item[:quantity] %> - <%= item[:on_hand].nil? ? "Unknown" : item[:on_hand] %> + <%= item[:on_hand] %> <% end %> From f2bb2dfbd4975578f9d9195f84c00c4184685d20 Mon Sep 17 00:00:00 2001 From: seabeeberry Date: Mon, 4 Aug 2025 21:50:32 +0200 Subject: [PATCH 5/9] Deleted fallback in fetch method and added fallback to the view instead to fix error thrown in automated test --- app/services/request_itemized_breakdown_service.rb | 4 ++-- app/views/reports/itemized_requests.html.erb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/services/request_itemized_breakdown_service.rb b/app/services/request_itemized_breakdown_service.rb index 7971e2612e..9067d165fa 100644 --- a/app/services/request_itemized_breakdown_service.rb +++ b/app/services/request_itemized_breakdown_service.rb @@ -25,8 +25,8 @@ def fetch items_requested.map! do |item| item_id = item[:item_id] - on_hand = current_onhand[item_id] || 0 - minimum = current_min_onhand[item_id] || 0 + on_hand = current_onhand[item_id] + minimum = current_min_onhand[item_id] below_onhand_minimum = on_hand && minimum && on_hand < minimum diff --git a/app/views/reports/itemized_requests.html.erb b/app/views/reports/itemized_requests.html.erb index 7ae01283ea..7561b72239 100644 --- a/app/views/reports/itemized_requests.html.erb +++ b/app/views/reports/itemized_requests.html.erb @@ -32,7 +32,7 @@ <%= item[:quantity] %> - <%= item[:on_hand] %> + <%= item[:on_hand] || 0 %> <% end %> From 0a3cd08b400d551e1513395ce52797913234dafc Mon Sep 17 00:00:00 2001 From: seabeeberry Date: Fri, 15 Aug 2025 03:08:15 +0200 Subject: [PATCH 6/9] Implemented Itemized Request Report correctly now using Partners::ItemRequest, changed tests appropriately --- .../request_itemized_breakdown_service.rb | 32 ++++------- ...request_itemized_breakdown_service_spec.rb | 54 ++++++++----------- 2 files changed, 34 insertions(+), 52 deletions(-) diff --git a/app/services/request_itemized_breakdown_service.rb b/app/services/request_itemized_breakdown_service.rb index 9067d165fa..8e6922babf 100644 --- a/app/services/request_itemized_breakdown_service.rb +++ b/app/services/request_itemized_breakdown_service.rb @@ -22,15 +22,14 @@ def fetch 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] + items_requested.each do |item| + name = item[:name] + on_hand = current_onhand[name] + minimum = current_min_onhand[name] below_onhand_minimum = on_hand && minimum && on_hand < minimum - item.merge( + item.merge!( on_hand: on_hand, onhand_minimum: minimum, below_onhand_minimum: below_onhand_minimum @@ -54,27 +53,18 @@ def fetch_csv 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 + inventory.all_items.group_by(&:name).to_h { |k, v| [k, v.sum { |r| r.quantity.to_i }] } 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 + inventory.all_items.group_by(&:name).to_h { |k, v| [k, v.map(&:on_hand_minimum_quantity).compact.max] } 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] } + 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 { diff --git a/spec/services/request_itemized_breakdown_service_spec.rb b/spec/services/request_itemized_breakdown_service_spec.rb index 6a4370e4c3..3da4f6121d 100644 --- a/spec/services/request_itemized_breakdown_service_spec.rb +++ b/spec/services/request_itemized_breakdown_service_spec.rb @@ -1,47 +1,39 @@ 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(: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_2) do - create(:request, organization: organization, request_items: [ - {"item_id" => item_b.id, "quantity" => 10} - ]) - end + let(:request_1) { create(:request, organization: organization) } + let(:request_2) { create(:request, organization: organization) } - 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 + let(:storage_location) { create(:storage_location, organization: organization) } 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) - ]) + create(:inventory_item, storage_location: storage_location, item: item_a, quantity: 3) + create(:inventory_item, storage_location: storage_location, item: item_b, quantity: 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) + + allow_any_instance_of(RequestItemizedBreakdownService) + .to receive(:current_onhand_quantities) + .and_return({item_a.name => 3, item_b.name => 20, item_a.id => 3, item_b.id => 20}) + allow_any_instance_of(RequestItemizedBreakdownService) + .to receive(:current_onhand_minimums) + .and_return({item_a.name => 4, item_b.name => 8, item_a.id => 4, item_b.id => 8}) end describe "#fetch" do - subject { service.fetch } + subject(:result) { 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) + 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 From 87b581f3c1c1d3ab0d13e700250c90493a8b2344 Mon Sep 17 00:00:00 2001 From: seabeeberry Date: Mon, 18 Aug 2025 00:09:31 +0200 Subject: [PATCH 7/9] Implemented tests using TestInventory.create_item --- .../request_itemized_breakdown_service_spec.rb | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/spec/services/request_itemized_breakdown_service_spec.rb b/spec/services/request_itemized_breakdown_service_spec.rb index 3da4f6121d..40fe69d38a 100644 --- a/spec/services/request_itemized_breakdown_service_spec.rb +++ b/spec/services/request_itemized_breakdown_service_spec.rb @@ -10,18 +10,15 @@ let(:storage_location) { create(:storage_location, organization: organization) } before do - create(:inventory_item, storage_location: storage_location, item: item_a, quantity: 3) - create(:inventory_item, storage_location: storage_location, item: item_b, quantity: 20) + 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) - - allow_any_instance_of(RequestItemizedBreakdownService) - .to receive(:current_onhand_quantities) - .and_return({item_a.name => 3, item_b.name => 20, item_a.id => 3, item_b.id => 20}) - allow_any_instance_of(RequestItemizedBreakdownService) - .to receive(:current_onhand_minimums) - .and_return({item_a.name => 4, item_b.name => 8, item_a.id => 4, item_b.id => 8}) end describe "#fetch" do @@ -38,7 +35,7 @@ end describe "#fetch_csv" do - subject { service.fetch_csv } + subject(: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 From cf686c191fb60c52e20a58437c7ed751d309fdfb Mon Sep 17 00:00:00 2001 From: seabeeberry Date: Mon, 18 Aug 2025 00:42:06 +0200 Subject: [PATCH 8/9] Implemented ItemizedRequestBreakdownService using class-level methods --- app/controllers/reports_controller.rb | 2 +- .../request_itemized_breakdown_service.rb | 129 ++++++++---------- ...request_itemized_breakdown_service_spec.rb | 10 +- 3 files changed, 61 insertions(+), 80 deletions(-) diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index 44ff38699f..137f08daa8 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -41,7 +41,7 @@ def activity_graph 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 + @itemized_request_data = RequestItemizedBreakdownService.call(organization: current_organization, request_ids: requests.pluck(:id)) end private diff --git a/app/services/request_itemized_breakdown_service.rb b/app/services/request_itemized_breakdown_service.rb index 8e6922babf..fed33ff7f9 100644 --- a/app/services/request_itemized_breakdown_service.rb +++ b/app/services/request_itemized_breakdown_service.rb @@ -1,91 +1,70 @@ class RequestItemizedBreakdownService - # - # Initializes the RequestItemizedBreakdownService whose - # purpose is to construct an itemized breakdown of requested items - # - # @param organization [Organization] - # @param request_ids [Array] - # @return [RequestItemizedBreakdownService] - def initialize(organization:, request_ids:) - @organization = organization - @request_ids = request_ids - end + 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 - # - # 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 + 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| - name = item[:name] + items_requested.each do |item| + name = item[:name] - on_hand = current_onhand[name] - minimum = current_min_onhand[name] - below_onhand_minimum = on_hand && minimum && on_hand < minimum + on_hand = current_onhand[name] + minimum = current_min_onhand[name] + 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 + item.merge!( + on_hand: on_hand, + onhand_minimum: minimum, + below_onhand_minimum: below_onhand_minimum + ) + end - # - # Returns a CSV string representation of the itemized breakdown of - # what was requested - # - # @return [String] - def fetch_csv - convert_to_csv(fetch) - end + items_requested.sort_by { |item| [item[:name], item[:unit].to_s] } + end - private + def csv(organization:, request_ids:) + convert_to_csv(fetch(organization: organization, request_ids: request_ids)) + end - attr_reader :organization, :request_ids + private - def current_onhand_quantities(inventory) - inventory.all_items.group_by(&:name).to_h { |k, v| [k, v.sum { |r| r.quantity.to_i }] } - end + def current_onhand_quantities(inventory) + inventory.all_items.group_by(&:name).to_h { |k, v| [k, v.sum { |r| r.quantity.to_i }] } + end - def current_onhand_minimums(inventory) - inventory.all_items.group_by(&:name).to_h { |k, v| [k, v.map(&:on_hand_minimum_quantity).compact.max] } - end + def current_onhand_minimums(inventory) + inventory.all_items.group_by(&:name).to_h { |k, v| [k, v.map(&:on_hand_minimum_quantity).compact.max] } + end - def fetch_items_requested - 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 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]] + 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 - - def inventory - @inventory ||= View::Inventory.new(@organization.id) - end end diff --git a/spec/services/request_itemized_breakdown_service_spec.rb b/spec/services/request_itemized_breakdown_service_spec.rb index 40fe69d38a..dd9ead6d93 100644 --- a/spec/services/request_itemized_breakdown_service_spec.rb +++ b/spec/services/request_itemized_breakdown_service_spec.rb @@ -22,8 +22,9 @@ end describe "#fetch" do - subject(:result) { service.fetch } - let(:service) { described_class.new(organization: organization, request_ids: [request_1.id, request_2.id]) } + 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 = [ @@ -35,8 +36,9 @@ end describe "#fetch_csv" do - subject(:subject) { service.fetch_csv } - let(:service) { described_class.new(organization: organization, request_ids: [request_1.id, request_2.id]) } + 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 From d88243af9fdee83f14470a2b911b7a906c47981a Mon Sep 17 00:00:00 2001 From: seabeeberry Date: Mon, 18 Aug 2025 15:07:56 +0200 Subject: [PATCH 9/9] Fetching items by their IDs instead of their names --- app/services/request_itemized_breakdown_service.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/services/request_itemized_breakdown_service.rb b/app/services/request_itemized_breakdown_service.rb index fed33ff7f9..b3b2a33a0e 100644 --- a/app/services/request_itemized_breakdown_service.rb +++ b/app/services/request_itemized_breakdown_service.rb @@ -12,10 +12,10 @@ def fetch(organization:, request_ids:) items_requested = fetch_items_requested(organization: organization, request_ids: request_ids) items_requested.each do |item| - name = item[:name] + id = item[:item_id] - on_hand = current_onhand[name] - minimum = current_min_onhand[name] + on_hand = current_onhand[id] + minimum = current_min_onhand[id] below_onhand_minimum = on_hand && minimum && on_hand < minimum item.merge!( @@ -35,11 +35,11 @@ def csv(organization:, request_ids:) private def current_onhand_quantities(inventory) - inventory.all_items.group_by(&:name).to_h { |k, v| [k, v.sum { |r| r.quantity.to_i }] } + 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(&:name).to_h { |k, v| [k, v.map(&:on_hand_minimum_quantity).compact.max] } + 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:)