Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce distribution value totals by item category filter on distribution index page #4924

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions app/controllers/distributions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ def index
@storage_locations = current_organization.storage_locations.active_locations.alphabetized.select(:id, :name)
@partners = current_organization.partners.active.alphabetized.select(:id, :name)
@selected_item = filter_params[:by_item_id].presence
@distribution_totals = DistributionTotalsService.new(current_organization.distributions, scope_filters)
@total_value_all_distributions = @distribution_totals.total_value
@total_items_all_distributions = @distribution_totals.total_quantity
@distribution_totals = DistributionTotalsService.call(current_organization.distributions.class_filter(scope_filters))
@total_value_all_distributions = @distribution_totals.values.sum(&:value)
@total_items_all_distributions = @distribution_totals.values.sum(&:quantity)
paginated_ids = @paginated_distributions.ids
@total_value_paginated_distributions = @distribution_totals.total_value(paginated_ids)
@total_items_paginated_distributions = @distribution_totals.total_quantity(paginated_ids)
@selected_item_category = filter_params[:by_item_category_id]
@total_value_paginated_distributions = @distribution_totals.slice(*paginated_ids).values.sum(&:value)
@total_items_paginated_distributions = @distribution_totals.slice(*paginated_ids).values.sum(&:quantity)
@selected_item_category = filter_params[:by_item_category_id].presence
@selected_partner = filter_params[:by_partner]
@selected_status = filter_params[:by_state]
@selected_location = filter_params[:by_location]
Expand All @@ -69,7 +69,11 @@ def index
respond_to do |format|
format.html
format.csv do
send_data Exports::ExportDistributionsCSVService.new(distributions: @distributions, organization: current_organization, filters: scope_filters).generate_csv, filename: "Distributions-#{Time.zone.today}.csv"
send_data Exports::ExportDistributionsCSVService.new(
distributions: @distributions,
organization: current_organization,
filters: scope_filters
).generate_csv, filename: "Distributions-#{Time.zone.today}.csv"
end
end
end
Expand Down
88 changes: 27 additions & 61 deletions app/services/distribution_totals_service.rb
Original file line number Diff line number Diff line change
@@ -1,65 +1,31 @@
class DistributionTotalsService
def initialize(distributions, filter_params)
@filter_params = filter_params
@distribution_quantities = calculate_quantities(distributions)
@distribution_values = calculate_values(distributions)
end

def total_quantity(filter_ids = [])
totals = filter_ids.present? ? @distribution_quantities.slice(*filter_ids) : @distribution_quantities
totals.sum { |_, quantity| quantity }
end

def total_value(filter_ids = [])
totals = filter_ids.present? ? @distribution_values.slice(*filter_ids) : @distribution_values
totals.sum { |_, value| value }
end

def fetch_value(id)
@distribution_values.fetch(id)
end

def fetch_quantity(id)
@distribution_quantities.fetch(id)
end

private

attr_reader :filter_params

# Returns hash of total quantity of items per distribution
# Quantity of items after item filtering (id/category)
#
# @return [Hash<Integer, Integer>]
def calculate_quantities(distributions)
distributions
.class_filter(filter_params)
.left_joins(line_items: [:item])
.group("distributions.id, line_items.id, items.id")
.pluck(
Arel.sql(
"distributions.id,
COALESCE(SUM(line_items.quantity) OVER (PARTITION BY distributions.id), 0) AS quantity"
)
)
.to_h
end

# Returns hash of total value of items per distribution WITHOUT item id/category filter
# Value of entire distribution (not reduced by filtered items)
#
# @return [Hash<Integer, Integer>]
def calculate_values(distributions)
Distribution
.where(id: distributions.class_filter(filter_params))
.left_joins(line_items: [:item])
.group("distributions.id, line_items.id, items.id")
.pluck(
Arel.sql(
"distributions.id,
COALESCE(SUM(COALESCE(items.value_in_cents, 0) * line_items.quantity) OVER (PARTITION BY distributions.id), 0) AS value"
DistributionTotal = Data.define(:quantity, :value)

class << self
def call(distributions)
calculate_totals(distributions)
end

private

# Returns hash with quantity/value totals for each distribution.
# Quantity and value of items are reduced if item filtering present (id/category)
#
# @return [Hash<Integer, DistributionTotal>]
def calculate_totals(distributions)
distributions
.left_joins(line_items: [:item])
.group("distributions.id, line_items.id, items.id")
.pluck(
Arel.sql(
"distributions.id,
COALESCE(SUM(line_items.quantity) OVER (PARTITION BY distributions.id), 0) AS quantity,
COALESCE(SUM(COALESCE(items.value_in_cents, 0) * line_items.quantity) OVER (PARTITION BY distributions.id), 0) AS value"
)
)
)
.to_h
.to_h do |(id, quantity, value)|
[id, DistributionTotal.new(quantity:, value:)]
end
end
end
end
45 changes: 31 additions & 14 deletions app/services/exports/export_distributions_csv_service.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module Exports
class ExportDistributionsCSVService
include DistributionHelper
include ItemsHelper

def initialize(distributions:, organization:, filters: [])
# Currently, the @distributions are already loaded by the controllers that are delegating exporting
# to this service object; this is happening within the same request/response cycle, so it's already
Expand All @@ -11,6 +13,7 @@ def initialize(distributions:, organization:, filters: [])
@distributions = distributions
@filters = filters
@organization = organization
@distribution_totals = DistributionTotalsService.call(Distribution.where(organization:).class_filter(filters))
end

def generate_csv
Expand Down Expand Up @@ -72,17 +75,11 @@ def base_table
"Source Inventory" => ->(distribution) {
distribution.storage_location.name
},
base_item_header_col_name => ->(distribution) {
# filter the line items by item id (for selected item filter) to
# get the number of items
if @filters[:by_item_id].present?
distribution.line_items.where(item_id: @filters[:by_item_id].to_i).total
else
distribution.line_items.total
end
item_quantity_header_col_name => ->(distribution) {
@distribution_totals[distribution.id]&.quantity || 0
},
"Total Value" => ->(distribution) {
distribution.cents_to_dollar(distribution.line_items.total_value)
item_value_header_col_name => ->(distribution) {
cents_to_dollar(@distribution_totals[distribution.id]&.value || 0)
},
"Delivery Method" => ->(distribution) {
distribution.delivery_method
Expand All @@ -103,12 +100,32 @@ def base_table
end

# if filtered based on an item, change the column accordingly
def base_item_header_col_name
@filters[:by_item_id].present? ? "Total Number of #{filtered_item.name}" : "Total Items"
def item_quantity_header_col_name
if @filters[:by_item_id].present?
"Total Number of #{filtered_item_name}"
elsif @filters[:by_item_category_id].present?
"Total Number of #{filtered_item_category_name}"
else
"Total Items"
end
end

def item_value_header_col_name
if @filters[:by_item_id].present?
"Total Value of #{filtered_item_name}"
elsif @filters[:by_item_category_id].present?
"Total Value of #{filtered_item_category_name}"
else
"Total Value"
end
end

def filtered_item_name
@filtered_item ||= Item.find(@filters[:by_item_id].to_i).name
end

def filtered_item
@filtered_item ||= Item.find(@filters[:by_item_id].to_i)
def filtered_item_category_name
@filtered_item_category ||= ItemCategory.find(@filters[:by_item_category_id].to_i).name
end

def base_headers
Expand Down
4 changes: 2 additions & 2 deletions app/views/distributions/_distribution_row.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<td class="date"><%= (distribution_row.issued_at.presence || distribution_row.created_at).strftime("%m/%d/%Y") %></td>
<td><%= distribution_row.storage_location.name %></td>

<td class="numeric"><%= @distribution_totals.fetch_quantity(distribution_row.id) %></td>
<td class="numeric"><%= dollar_value(@distribution_totals.fetch_value(distribution_row.id)) %></td>
<td class="numeric"><%= @distribution_totals[distribution_row.id].quantity %></td>
<td class="numeric"><%= dollar_value(@distribution_totals[distribution_row.id].value) %></td>
<td><%= distribution_row.delivery_method.humanize %></td>
<td><%= distribution_shipping_cost(distribution_row.shipping_cost) %></td>
<td><%= distribution_row.comment %></td>
Expand Down
21 changes: 16 additions & 5 deletions app/views/distributions/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,28 @@
<th class="date">Date of Distribution</th>
<th>Source Inventory</th>

<% filtered_item_name = @selected_item ? @items.find { |i| i.id == filter_params[:by_item_id].to_i }&.name : nil %>
<% filtered_category_name = @selected_item_category ? @item_categories.find { |ic| ic.id == filter_params[:by_item_category_id].to_i }&.name : nil %>

<!-- Quantity -->
<% if filter_params[:by_item_id].present? %>
<th class="numeric">Total <%= @items.find { |i| i.id == filter_params[:by_item_id].to_i }&.name %></th>
<% elsif filter_params[:by_item_category_id].present? %>
<th class="numeric">Total in <%= @item_categories.find { |ic| ic.id == filter_params[:by_item_category_id].to_i }&.name %></th>
<% if filtered_item_name %>
<th class="numeric">Total <%= filtered_item_name %></th>
<% elsif filtered_category_name %>
<th class="numeric">Total in <%= filtered_category_name %></th>
<% else %>
<th class="numeric">Total Items</th>
<% end %>
<!-- End Quantity -->

<th class="numeric">Total Value</th>
<!-- Value -->
<% if filtered_item_name %>
<th class="numeric">Value of <%= filtered_item_name %></th>
<% elsif filtered_category_name %>
<th class="numeric">Value of <%= filtered_category_name %></th>
<% else %>
<th class="numeric">Total Value</th>
<% end %>
<!-- End Value -->

<th>Delivery Method</th>
<th>Shipping Cost</th>
Expand Down
18 changes: 8 additions & 10 deletions spec/requests/distributions_requests_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,20 +140,19 @@
expect(distribution.total_quantity).to eq(20)
expect(distribution.value_per_itemizable).to eq(2000)

# displays quantity of filtered item in distribution
# displays total value of distribution
# displays quantity and value of filtered item in distribution
expect(item_quantity.text).to eq("10")
expect(item_value.text).to eq("$20.00")
expect(item_value.text).to eq("$10.00")
end

it "changes the total quantity header" do
it "changes the total quantity and value headers" do
get distributions_path, params: params

page = Nokogiri::HTML(response.body)
item_total_header, item_value_header = page.css("table thead tr th.numeric")

expect(item_total_header.text).to eq("Total #{item.name}")
expect(item_value_header.text).to eq("Total Value")
expect(item_value_header.text).to eq("Value of #{item.name}")
end
end

Expand All @@ -178,20 +177,19 @@
expect(distribution.total_quantity).to eq(20)
expect(distribution.value_per_itemizable).to eq(2000)

# displays quantity of filtered item in distribution
# displays total value of distribution
# displays quantity and value of filtered item in distribution
expect(item_quantity.text).to eq("10")
expect(item_value.text).to eq("$20.00")
expect(item_value.text).to eq("$10.00")
end

it "changes the total quantity header" do
it "changes the total quantity and value headers" do
get distributions_path, params: params

page = Nokogiri::HTML(response.body)
item_total_header, item_value_header = page.css("table thead tr th.numeric")

expect(item_total_header.text).to eq("Total in #{item_category.name}")
expect(item_value_header.text).to eq("Total Value")
expect(item_value_header.text).to eq("Value of #{item_category.name}")
end

it "doesn't show duplicate distributions" do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
items_lists.each_with_index.map do |items, i|
distribution = create(
:distribution,
issued_at: start_time - i.days, delivery_method: "shipped", shipping_cost: "12.09"
issued_at: start_time - i.days, delivery_method: "shipped", shipping_cost: "12.09",
organization: organization
)

items.each do |(item, quantity)|
Expand Down Expand Up @@ -65,7 +66,7 @@
"Scheduled for",
"Source Inventory",
"Total Number of #{item_name}",
"Total Value",
"Total Value of #{item_name}",
"Delivery Method",
"Shipping Cost",
"Status",
Expand Down
Loading