Skip to content
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
9 changes: 2 additions & 7 deletions app/controllers/distributions_by_county_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,8 @@ class DistributionsByCountyController < ApplicationController

def report
setup_date_range_picker
start_date = helpers.selected_range.first.utc.iso8601
end_date = helpers.selected_range.last.utc.iso8601

@breakdown = DistributionSummaryByCountyQuery.call(
organization_id: current_organization.id,
start_date: start_date,
end_date: end_date
)
@dbc_info = View::DistributionsByCounty.from_params(params: params,
organization: current_organization, helpers: helpers)
end
end
2 changes: 2 additions & 0 deletions app/models/item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ class Item < ApplicationRecord

scope :visible, -> { where(visible_to_partners: true) }
scope :alphabetized, -> { order(:name) }
scope :loose, -> { where(type: "ConcreteItem") }
scope :by_base_item, ->(base_item) { where(base_item: base_item) }
scope :by_reporting_category, ->(reporting_category) { where(reporting_category: reporting_category) }
scope :by_name, ->(name) { where(name: name) }
scope :by_partner_key, ->(partner_key) { where(partner_key: partner_key) }

scope :period_supplies, -> {
Expand Down
44 changes: 44 additions & 0 deletions app/models/view/distributions_by_county.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module View
DistributionsByCounty = Data.define(
:breakdown,
:filters,
:items,
:reporting_categories
) do
class << self
def filter_params(params)
return {} unless params.key?(:filters)
params
.require(:filters)
.permit(:by_item_id, :by_reporting_category, :date_range)
end

def from_params(params:, organization:, helpers:)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to pass helpers into this if you're including DateRangeHelpers?

All you need is a params method on the object. So you could do something like

def params = @params
def from_params(params:, organization:)
  @params = params
   filters = filter_params(params)
  start_data = selected_range.first.utc.iso8601
...

@cielf cielf May 14, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dorner Hrm -- I wonder how this case is different from the donations view object where you are passing helpers and has included DateRangeHelper?

I tried your suggestion and it broke most of the DistributionByCounty tests -- looking at the rspec log it doesn't know about selected_range at that point.

However, removing the DateRangeHelper include is harmless (the relevant tests still pass), so I've done that.

filters = filter_params(params)
start_date = helpers.selected_range.first.utc.iso8601
end_date = helpers.selected_range.last.utc.iso8601
breakdown = DistributionSummaryByCountyQuery.call(
organization_id: organization.id,
start_date: start_date,
end_date: end_date,
reporting_category: filters[:by_reporting_category].presence,
item_id: filters[:by_item_id].presence
)

new(
breakdown: breakdown,
filters: filters,
reporting_categories: Item.reporting_categories_for_select,
items: organization.items.loose.alphabetized.select(:id, :name)
)
end
end
def selected_reporting_category
filters[:by_reporting_category].presence
end

def selected_item
filters[:by_item_id].presence
end
end
end
51 changes: 43 additions & 8 deletions app/queries/distribution_summary_by_county_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ class DistributionSummaryByCountyQuery
SQL_MULTILINE_COMMENTS = /\/\*.*?\*\//

DISTRIBUTION_BY_COUNTY_SQL = <<~SQL.squish.gsub(SQL_MULTILINE_COMMENTS, "").freeze
/* Calculate total item quantity and value per distribution */
WITH distribution_totals AS
/* Calculate total item quantity and value per distribution of "loose" items */

WITH loose_distribution_totals AS
(
SELECT DISTINCT d.id,
d.partner_id,
Expand All @@ -17,9 +18,39 @@ class DistributionSummaryByCountyQuery
JOIN items i ON i.id = li.item_id
WHERE d.issued_at BETWEEN :start_date AND :end_date
AND d.organization_id = :organization_id
AND i.reporting_category LIKE CONCAT('%', :reporting_category , '%')
AND i.id = CASE WHEN :item_id <> 0 THEN :item_id ELSE i.id END
GROUP BY d.id, li.id, i.id
),
/* Match distribution totals with client share and counties.
/* Calculate total item and value per distribution of items that happen to be in kits */
kitted_distribution_totals AS (
SELECT DISTINCT d.id,
d.partner_id,
COALESCE(SUM(li.quantity * kli.quantity) OVER (PARTITION BY d.id), 0) AS quantity,
COALESCE(SUM(COALESCE(ki.value_in_cents, 0) * li.quantity * kli.quantity) OVER (PARTITION BY d.id), 0) AS value
FROM distributions d
INNER JOIN line_items li ON li.itemizable_id = d.id AND li.itemizable_type = 'Distribution'
INNER JOIN items i ON i.id = li.item_id
INNER JOIN line_items AS kli ON i.id = kli.itemizable_id AND kli.itemizable_type = 'Item'
INNER JOIN items AS ki ON ki.id = kli.item_id
WHERE d.issued_at BETWEEN :start_date AND :end_date
AND d.organization_id = :organization_id
AND ki.reporting_category LIKE CONCAT('%', :reporting_category , '%')
AND ki.id = CASE WHEN :item_id <> 0 THEN :item_id ELSE ki.id END
GROUP BY d.id, li.id, i.id, kli.id, ki.id
),

/* Combine the loose and kitted */
full_distribution_totals as (
SELECT distinct COALESCE(ld.id,kd.id) as id,
COALESCE(ld.partner_id, kd.partner_id) AS partner_id,
COALESCE(ld.quantity,0) + COALESCE(kd.quantity, 0) as quantity,
COALESCE(ld.value,0) + COALESCE(kd.value, 0) as value
FROM loose_distribution_totals ld
FULL OUTER JOIN kitted_distribution_totals kd ON ld.id = kd.id
),

/* Match full distribution totals with client share and counties.
If distribution has no associated county, set county name to "Unspecified"
and set region to ZZZ so it will be last when sorted */
totals_by_county AS
Expand All @@ -30,7 +61,7 @@ class DistributionSummaryByCountyQuery
COALESCE(psa.client_share::float / 100, 1) AS percentage,
COALESCE(c.name, 'Unspecified') county_name,
COALESCE(c.region, 'ZZZ') county_region
FROM distribution_totals dt
FROM full_distribution_totals dt
LEFT JOIN partners p ON p.id = dt.partner_id
LEFT JOIN partner_profiles pp ON pp.partner_id = p.id
LEFT JOIN partner_served_areas psa ON psa.partner_profile_id = pp.id
Expand Down Expand Up @@ -58,11 +89,13 @@ class DistributionSummaryByCountyQuery
class << self
# Timestamps are stored in Postgres without timezones so
# start_date and end_date must be strings in UTC.
def call(organization_id:, start_date: nil, end_date: nil)
def call(organization_id:, start_date: nil, end_date: nil, reporting_category: nil, item_id: nil)
params = {
organization_id: organization_id,
start_date: start_date || "1000-01-01",
end_date: end_date || "3000-01-01"
end_date: end_date || "3000-01-01",
reporting_category: reporting_category,
item_id: item_id
}

execute(to_sql(DISTRIBUTION_BY_COUNTY_SQL, **params)).to_a.map(&to_county_summary)
Expand All @@ -74,13 +107,15 @@ def execute(sql)
ActiveRecord::Base.connection.execute(sql)
end

def to_sql(query, organization_id:, start_date:, end_date:)
def to_sql(query, organization_id:, start_date:, end_date:, reporting_category:, item_id:)
ActiveRecord::Base.sanitize_sql_array(
[
query,
organization_id: organization_id,
start_date: start_date,
end_date: end_date
end_date: end_date,
reporting_category: reporting_category,
item_id: item_id
]
)
end
Expand Down
38 changes: 29 additions & 9 deletions app/views/distributions_by_county/report.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
<h1>
Estimated Distributions by County for <%= current_organization.name %>
</h1>
<i>
Please note that these estimates include any items that are within kits, but do not include the kits themselves. These are estimates only -- the totals may differ slightly from other distribution reports due to rounding when dividing them between the counties.
</i>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
Expand All @@ -25,17 +28,33 @@
<div class="row">
<div class="col-12">
<!-- Default box -->
<div class="card">
<div class="row">
<div class="col-sm-3 col-6">
<div class="card card-primary">
<div class="card-body">

<div class="description-block border-right">
<%= simple_form_for :filters, url: distributions_by_county_report_path(current_organization), method: :get do |f| %>
<%= render partial: "shared/date_range_picker", locals: {css_class: "form-control"} %><br>
<%= filter_button %>
<%= form_tag(distributions_by_county_report_path(current_organization), method: :get) do |f| %>
<div class="row">
<% if @dbc_info.items.present? %>
<div class="form-group col-lg-2 col-md-2 col-sm-6 col-xs-12">
<%= filter_select(label: "Item", scope: :by_item_id, collection: @dbc_info.items, selected: @dbc_info.selected_item) %>
</div>
<% end %>
<div class="form-group col-lg-2 col-md-2 col-sm-6 col-xs-12">
<%= filter_select(label: "Reporting Category", scope: :by_reporting_category, collection: @dbc_info.reporting_categories, selected: @dbc_info.selected_reporting_category) %>
</div>

<div class="form-group col-lg-3 col-md-3 col-sm-6 col-xs-12">
<%= label_tag "Date Range", "Date Range" %>
<%= render partial: "shared/date_range_picker", locals: {css_class: "form-control"} %>
</div>
</div>
<div class="card-footer">
<%= filter_button %>
<%= clear_filter_button %>
</div>
<% end %>
</div>
<!-- /.description-block -->
</div>

</div>

<div class="card-body table-responsive p-0">
Expand All @@ -48,7 +67,7 @@
</tr>
</thead>
<tbody>
<% @breakdown.each do |bd| %>
<% @dbc_info.breakdown.each do |bd| %>
<tr>
<td><%= bd.name %></td>
<td class="numeric"><%= number_with_delimiter(bd.quantity) %></td>
Expand All @@ -65,4 +84,5 @@
</div>
</div>
</div>
</div>
</section>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 10 additions & 3 deletions docs/user_guide/bank/reports_distributions_by_county.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,24 @@

![distributions_by_county_default_view](images/reports/reports_distributions_by_county_1.png)

This report shows the total items and estimated total market value of the distributed Items for the time period.
This report initially shows the total items and estimated total market value of the distributed Items for the time period.

We use the area served information entered in each partner's [profile](pm_partner_profiles.md) to allocate the items that have been distributed to them. If that information has not been provided, the items you've distributed are put into the 'Unspecified' category.


**N.B.** If you are using Kits, please note that this treats each Kit as an item, rather than counting the items within the Kits separately.
**N.B. If you are using Kits, please note that this counts the items within the kits, rather than the kits. You can not use this report to determine the number of kits that have been distributed to each county.**

If you want to see a time period other than the default of 60 days prior to 30 days forward from today, change the date range and click "Filter". We recommend you use the little pop-up gizmo to enter your date range, as the format of the date range is very fussy.

![distributions_by_county_with_date_range_gizmo](images/reports/reports_distributions_by_county_2.png)


Similarly, you can also filter by a single item, or by a reporting category, if, for example, you wanted to see distribution of disposable diapers, you would select "Disposalbe Diapers" from the Reporting Category menu, and then click "Filter"

![distributions_by_county_with_reporting_category_dropdown](images/reports/reports_distributions_by_county_3.png)


To go back to the default values, click "Clear filters".

[Back to Annual Survey](reports_annual_survey.md)

[Next: Manufacturer Donations Report](reports_manufacturers_donations.md)
122 changes: 121 additions & 1 deletion spec/queries/distribution_summary_by_county_query_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
let(:organization_id) { organization.id }
let(:start_date) { nil }
let(:end_date) { nil }
let(:params) { {organization_id:, start_date:, end_date:} }
let(:reporting_category) { nil }
let(:item_id) { nil }
let(:params) { {organization_id:, start_date:, end_date:, reporting_category:} }

include_examples "distribution_by_county"

Expand Down Expand Up @@ -64,4 +66,122 @@
expect(num_with_0).to eq 0
end
end

describe "handling reporting categories" do
context "with loose items only" do
let(:reporting_category) { :cloth_diapers }
let(:params) { {organization_id:, start_date:, end_date:, reporting_category:} }

it "divides the item numbers and values according to the partner profile" do
create(:distribution, :with_items, item: item_1, organization: user.organization, partner: partner_1)
create(:distribution, :with_items, item: item_2, organization: user.organization, partner: partner_1)

breakdown = DistributionSummaryByCountyQuery.call(**params)
expect(breakdown.size).to eq(5)
expect(breakdown[4].quantity).to eq(0)
expect(breakdown[4].value).to be_within(0.01).of(0)
4.times do |i|
expect(breakdown[i].quantity).to eq(25)
expect(breakdown[i].value).to be_within(0.01).of(26250.0)
end
end
end

context "with kits only" do
let(:reporting_category) { :pads }
let(:params) { {organization_id:, start_date:, end_date:, reporting_category:} }

it "divides the item numbers and values according to the partner profile" do
create(:distribution, :with_items, item: kit_a, organization: user.organization, partner: partner_1)
create(:distribution, :with_items, item: item_2, organization: user.organization, partner: partner_1)

breakdown = DistributionSummaryByCountyQuery.call(**params)
expect(breakdown.size).to eq(5)
expect(breakdown[-1].quantity).to eq(0)
expect(breakdown[-1].value).to be_within(0.01).of(0)
4.times do |i|
expect(breakdown[i].quantity).to eq(500)
expect(breakdown[i].value).to be_within(0.01).of(37500.0)
end
end
end

context "with an item that is in a kit and loose, and another item that is in the reporting category" do
let(:reporting_category) { :pads }
let(:params) { {organization_id:, start_date:, end_date:, reporting_category:} }

it "divides the item numbers and values according to the partner profile" do
create(:distribution, :with_items, item: kit_a, organization: user.organization, partner: partner_1)
create(:distribution, :with_items, item: item_3, organization: user.organization, partner: partner_1)
create(:distribution, :with_items, item: item_4, organization: user.organization, partner: partner_1)
breakdown = DistributionSummaryByCountyQuery.call(**params)
expect(breakdown.size).to eq(5)
expect(breakdown[-1].quantity).to eq(0)
expect(breakdown[-1].value).to be_within(0.01).of(0)
4.times do |i|
expect(breakdown[i].quantity).to eq(550)
expect(breakdown[i].value).to be_within(0.01).of(40625.0)
end
end
end
end

describe "handling filtering by item" do
context "with loose items only" do
let(:item_id) { item_1.id }
let(:params) { {organization_id:, start_date:, end_date:, reporting_category:, item_id:} }

it "divides the item numbers and values according to the partner profile" do
create(:distribution, :with_items, item: item_1, organization: user.organization, partner: partner_1)
create(:distribution, :with_items, item: item_2, organization: user.organization, partner: partner_1)

breakdown = DistributionSummaryByCountyQuery.call(**params)
expect(breakdown.size).to eq(5)
expect(breakdown[4].quantity).to eq(0)
expect(breakdown[4].value).to be_within(0.01).of(0)
4.times do |i|
expect(breakdown[i].quantity).to eq(25)
expect(breakdown[i].value).to be_within(0.01).of(26250.0)
end
end
end

context "with kits only" do
let(:item_id) { item_3.id }
let(:params) { {organization_id:, start_date:, end_date:, reporting_category:, item_id:} }

it "divides the item numbers and values according to the partner profile" do
create(:distribution, :with_items, item: kit_a, organization: user.organization, partner: partner_1)
create(:distribution, :with_items, item: item_2, organization: user.organization, partner: partner_1)

breakdown = DistributionSummaryByCountyQuery.call(**params)
expect(breakdown.size).to eq(5)
expect(breakdown[-1].quantity).to eq(0)
expect(breakdown[-1].value).to be_within(0.01).of(0)
4.times do |i|
expect(breakdown[i].quantity).to eq(500)
expect(breakdown[i].value).to be_within(0.01).of(37500.0)
end
end
end

context "with an item that is in a kit and loose, and another item that is in the reporting category" do
let(:item_id) { item_3.id }
let(:params) { {organization_id:, start_date:, end_date:, reporting_category:, item_id:} }

it "divides the item numbers and values according to the partner profile" do
create(:distribution, :with_items, item: kit_a, organization: user.organization, partner: partner_1)
create(:distribution, :with_items, item: item_3, organization: user.organization, partner: partner_1)
create(:distribution, :with_items, item: item_4, organization: user.organization, partner: partner_1)
breakdown = DistributionSummaryByCountyQuery.call(**params)
expect(breakdown.size).to eq(5)
expect(breakdown[-1].quantity).to eq(0)
expect(breakdown[-1].value).to be_within(0.01).of(0)
4.times do |i|
expect(breakdown[i].quantity).to eq(525)
expect(breakdown[i].value).to be_within(0.01).of(39375.0)
end
end
end
end
end
Loading
Loading