diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb new file mode 100644 index 0000000000..9436daa48a --- /dev/null +++ b/app/helpers/events_helper.rb @@ -0,0 +1,19 @@ +module EventsHelper + # KitAllocateEvent/KitDeallocateEvent mix directions within a single event: the + # kit's own line item moves opposite the component items, so we can't rely on + # picking any one item's from/to. Every item shares the same storage location + # (just recorded in the opposite field for the kit vs. its components), so pull + # that location from whichever entry has it and place it by event type instead. + def event_direction_locations(event) + if event.is_a?(KitAllocateEvent) || event.is_a?(KitDeallocateEvent) + first_item = event.data.items.first + kit_storage_loc = first_item&.from_storage_location || first_item&.to_storage_location + from_loc = event.is_a?(KitDeallocateEvent) ? kit_storage_loc : nil + to_loc = event.is_a?(KitAllocateEvent) ? kit_storage_loc : nil + else + from_loc = event.data.items.first&.from_storage_location + to_loc = event.data.items.first&.to_storage_location + end + [from_loc, to_loc] + end +end diff --git a/app/views/events/_event_row.html.erb b/app/views/events/_event_row.html.erb index 3ef1e11386..4ad02991ec 100644 --- a/app/views/events/_event_row.html.erb +++ b/app/views/events/_event_row.html.erb @@ -1,8 +1,7 @@ <% if event.is_a?(SnapshotEvent) %> <%= render partial: "snapshot_event_row", object: event, as: :event, locals: { items: items, storage_locs: storage_locs } %> <% else %> - <% from_loc = event.data.items.first&.from_storage_location %> - <% to_loc = event.data.items.first&.to_storage_location %> + <% from_loc, to_loc = event_direction_locations(event) %> <%= event.user&.name || "No Name Provided" %> @@ -34,12 +33,12 @@ end %> <% sorted_entries.each do |entry| %> <% item = items.find { |i| i.id == entry.item_id } %> + <% quantity = entry.from_storage_location == from_loc ? entry.quantity : -entry.quantity %> <% if item %> - <%= link_to item.name, item_path(item.id) %>: + <%= link_to item.name, item_path(item.id) %>: <%= quantity %>
<% else %> - Item <%= entry.item_id %> (deleted) + Item <%= entry.item_id %> (deleted): <%= quantity %>
<% end %> - <%= entry.from_storage_location == from_loc ? entry.quantity : -entry.quantity %>
<% end %> diff --git a/spec/requests/events_requests_spec.rb b/spec/requests/events_requests_spec.rb index 35500753b6..ccaed24c5c 100644 --- a/spec/requests/events_requests_spec.rb +++ b/spec/requests/events_requests_spec.rb @@ -6,6 +6,21 @@ let(:item) { create(:item, organization: organization, name: "Item1") } let(:item2) { create(:item, organization: organization, name: "Item2") } + # Returns the table row for the given event type as a CSV-like mapping of + # header names to their cells, e.g. row["From Location"]. + def row_cells(event_type) + doc = Nokogiri::HTML(response.body) + headers = doc.css("thead th").map(&:text) + row = doc.css("tbody tr").find do |r| + r.css("td")[1]&.text&.strip == event_type + end + return nil unless row + + row.css("td").each_with_index.each_with_object({}) do |(cell, index), cells| + cells[headers[index]] = cell + end + end + context "When signed in" do before { sign_in(user) } @@ -274,6 +289,96 @@ expect(response.body).not_to include("99
") end end + + context "with kit allocation and deallocation events" do + let(:component_item) { create(:item, organization: organization, name: "Widget") } + let(:kit) do + create_kit(organization: organization, line_items_attributes: [ + {item_id: component_item.id, quantity: 5} + ]) + end + + before do + TestInventory.create_inventory(organization, { + storage_location.id => {component_item.id => 100} + }) + KitAllocateEvent.publish(kit, storage_location.id, 3) + KitDeallocateEvent.publish(kit, storage_location.id, 1) + end + + it "shows a positive kit quantity to the storage location and a negative item quantity for KitAllocate" do + subject + row = row_cells("KitAllocate") + expect(row).not_to be_nil + expect(row["From Location"].text).not_to include(storage_location.name) + expect(row["To Location"].text).to include(storage_location.name) + expect(row["Items"].text).to include("#{kit.name}: 3") + expect(row["Items"].text).to include("Widget: -15") + end + + it "shows a positive kit quantity from the storage location and a negative item quantity for KitDeallocate" do + subject + row = row_cells("KitDeallocate") + expect(row).not_to be_nil + expect(row["From Location"].text).to include(storage_location.name) + expect(row["To Location"].text).not_to include(storage_location.name) + expect(row["Items"].text).to include("#{kit.name}: 1") + expect(row["Items"].text).to include("Widget: -5") + end + end + + context "with kit events whose line items are not in the usual order" do + # KitAllocateEvent/KitDeallocateEvent normally append the kit's own line item + # last, but the view must not depend on that ordering to work out direction. + let(:component_item) { create(:item, organization: organization, name: "Widget") } + let(:kit) do + create_kit(organization: organization, line_items_attributes: [ + {item_id: component_item.id, quantity: 5} + ]) + end + + before do + TestInventory.create_inventory(organization, { + storage_location.id => {component_item.id => 100, kit.id => 100} + }) + end + + it "shows the correct direction for KitAllocate with the kit's line item first" do + KitAllocateEvent.create!( + eventable: kit, + organization_id: organization.id, + event_time: Time.zone.now, + data: EventTypes::InventoryPayload.new( + items: KitAllocateEvent.event_line_items(kit, storage_location.id, 2).reverse + ) + ) + subject + row = row_cells("KitAllocate") + expect(row).not_to be_nil + expect(row["From Location"].text).not_to include(storage_location.name) + expect(row["To Location"].text).to include(storage_location.name) + expect(row["Items"].text).to include("#{kit.name}: 2") + expect(row["Items"].text).to include("Widget: -10") + end + + it "shows the correct direction for KitDeallocate with the kit's line item first" do + KitDeallocateEvent.create!( + eventable: kit, + organization_id: organization.id, + event_time: Time.zone.now, + data: EventTypes::InventoryPayload.new( + items: KitDeallocateEvent.event_line_items(kit, storage_location.id, 4).reverse + ) + ) + subject + row = row_cells("KitDeallocate") + expect(row).not_to be_nil + expect(row["From Location"].text).to include(storage_location.name) + expect(row["To Location"].text).not_to include(storage_location.name) + expect(row["Items"].text).to include("#{kit.name}: 4") + expect(row["Items"].text).to include("Widget: -20") + end + end end end end