Skip to content

Commit bb56879

Browse files
author
Olexii Kasianenko
committed
Refactor inventory flow to use ItemsFlowQuery for improved data handling and display
1 parent 53eaa68 commit bb56879

File tree

4 files changed

+135
-10
lines changed

4 files changed

+135
-10
lines changed

app/controllers/storage_locations_controller.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,9 @@ def show
5858
setup_date_range_picker
5959
@storage_location = current_organization.storage_locations.find(params[:id])
6060
version_date = params[:version_date].presence&.to_date
61-
@items = current_organization.items.order(:name)
62-
@items = @items.created_between(*date_range) if filter_params[:date_range].present?
63-
@total_quantity_in = @items.sum { |item| item.quantity_in_storage(@storage_location.id) }
64-
@total_quantity_out = @items.sum { |item| item.quantity_out_storage(@storage_location.id) }
61+
@items = ItemsFlowQuery.new(storage_location: @storage_location, organization: current_organization).call.to_a
62+
@total_quantity_in = @items.first["total_quantity_in"].to_i
63+
@total_quantity_out = @items.first["total_quantity_out"].to_i
6564
@total_quantity_change = @total_quantity_in - @total_quantity_out
6665
if View::Inventory.within_snapshot?(current_organization.id, version_date)
6766
@inventory = View::Inventory.new(current_organization.id, event_time: version_date)

app/queries/items_flow_query.rb

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# frozen_string_literal: true
2+
3+
class ItemsFlowQuery
4+
attr_reader :organization
5+
attr_reader :filter_params
6+
attr_reader :storage_location
7+
8+
def initialize(organization:, storage_location:, filter_params: nil)
9+
@organization = organization
10+
@storage_location = storage_location
11+
@filter_params = filter_params
12+
end
13+
14+
def call
15+
query = <<~SQL
16+
WITH line_items_with_flags AS (
17+
SELECT
18+
li.item_id,
19+
it.name AS item_name,
20+
-- in quantity for this row (0 if not matching)
21+
CASE
22+
WHEN (donations.storage_location_id = :id
23+
OR purchases.storage_location_id = :id
24+
OR (adjustments.storage_location_id = :id AND li.quantity > 0)
25+
OR transfers.to_id = :id)
26+
AND it.organization_id = :organization_id
27+
THEN li.quantity
28+
ELSE 0
29+
END AS quantity_in,
30+
-- out quantity normalized to positive numbers (0 if not matching)
31+
CASE
32+
WHEN (distributions.storage_location_id = :id
33+
OR (adjustments.storage_location_id = :id AND li.quantity < 0)
34+
OR transfers.from_id = :id)
35+
AND it.organization_id = :organization_id
36+
THEN CASE WHEN li.quantity < 0 THEN -li.quantity ELSE li.quantity END
37+
ELSE 0
38+
END AS quantity_out,
39+
-- mark rows that are relevant for the overall WHERE in original query
40+
CASE
41+
WHEN (donations.storage_location_id = :id
42+
OR purchases.storage_location_id = :id
43+
OR distributions.storage_location_id = :id
44+
OR transfers.from_id = :id
45+
OR transfers.to_id = :id
46+
OR adjustments.storage_location_id = :id)
47+
AND it.organization_id = :organization_id
48+
THEN 1 ELSE 0
49+
END AS relevant
50+
FROM line_items li
51+
LEFT JOIN donations ON donations.id = li.itemizable_id AND li.itemizable_type = 'Donation'
52+
LEFT JOIN purchases ON purchases.id = li.itemizable_id AND li.itemizable_type = 'Purchase'
53+
LEFT JOIN distributions ON distributions.id = li.itemizable_id AND li.itemizable_type = 'Distribution'
54+
LEFT JOIN adjustments ON adjustments.id = li.itemizable_id AND li.itemizable_type = 'Adjustment'
55+
LEFT JOIN transfers ON transfers.id = li.itemizable_id AND li.itemizable_type = 'Transfer'
56+
LEFT JOIN items it ON it.id = li.item_id
57+
)
58+
SELECT
59+
item_id,
60+
item_name,
61+
SUM(quantity_in) AS quantity_in,
62+
SUM(quantity_out) AS quantity_out,
63+
SUM(quantity_in) - SUM(quantity_out) AS change,
64+
SUM(SUM(quantity_in)) OVER () AS total_quantity_in,
65+
SUM(SUM(quantity_out)) OVER () AS total_quantity_out,
66+
SUM(SUM(quantity_in) - SUM(quantity_out)) OVER () AS total_change
67+
FROM line_items_with_flags
68+
WHERE relevant = 1
69+
GROUP BY item_id, item_name
70+
ORDER BY item_name;
71+
SQL
72+
73+
ActiveRecord::Base.connection.exec_query(
74+
ActiveRecord::Base.send(:sanitize_sql_array, [query, {id: @storage_location.id, organization_id: @organization.id}])
75+
)
76+
end
77+
end
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
<% css_class = item_row.quantity_change(@storage_location.id).negative? ? 'modal-body-warning-text' : '' %>
2-
<tr id="<%= item_row.id %>">
3-
<td><%= link_to item_row.name, item_path(item_row.id) %></td>
4-
<td><%= item_row.quantity_in_storage(@storage_location.id) %></td>
5-
<td><%= item_row.quantity_out_storage(@storage_location.id) %></td>
6-
<td class="<%= css_class %>"><%= item_row.quantity_change(@storage_location.id) %></td>
1+
<% css_class = item_row["change"].negative? ? 'modal-body-warning-text' : '' %>
2+
<tr id="<%= item_row["item_id"] %>">
3+
<td><%= link_to item_row["item_name"], item_path(item_row["item_id"]) %></td>
4+
<td><%= item_row["quantity_in"] %></td>
5+
<td><%= item_row["quantity_out"] %></td>
6+
<td class="<%= css_class %>"><%= item_row["change"] %></td>
77
</tr>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# frozen_string_literal: true
2+
3+
require "rspec"
4+
5+
RSpec.describe ItemsFlowQuery do
6+
let(:items) { create_list(:item, 2) }
7+
let!(:storage_location) { create(:storage_location, name: "here") }
8+
let(:organization) { storage_location.organization }
9+
let!(:result) do
10+
[
11+
{
12+
item_id: items[0].id,
13+
item_name: items[0].name,
14+
quantity_in: 10,
15+
quantity_out: 5,
16+
change: 5,
17+
total_quantity_in: 16,
18+
total_quantity_out: 7,
19+
total_change: 9
20+
},
21+
{
22+
item_id: items[1].id,
23+
item_name: items[1].name,
24+
quantity_in: 6,
25+
quantity_out: 2,
26+
change: 4,
27+
total_quantity_in: 16,
28+
total_quantity_out: 7,
29+
total_change: 9
30+
}
31+
].map(&:with_indifferent_access)
32+
end
33+
34+
before do
35+
create(:donation, :with_items, item: items[0], item_quantity: 10, storage_location: storage_location)
36+
create(:distribution, :with_items, item: items[0], item_quantity: 5, storage_location: storage_location)
37+
create(:donation, :with_items, item: items[1], item_quantity: 3, storage_location: storage_location)
38+
create(:adjustment, :with_items, item: items[1], item_quantity: 3, storage_location: storage_location)
39+
create(:transfer, :with_items, item: items[1], item_quantity: 2, from: storage_location, to: create(:storage_location))
40+
end
41+
42+
subject { described_class.new(organization: organization, storage_location: storage_location).call }
43+
44+
context "without filter params" do
45+
it "returns array of hashes" do
46+
expect(subject.to_a).to match_array(result)
47+
end
48+
end
49+
end

0 commit comments

Comments
 (0)