diff --git a/Gemfile.lock b/Gemfile.lock index d558cfb14c..5c4ec4093b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -858,4 +858,4 @@ DEPENDENCIES webmock (~> 3.26) BUNDLED WITH - 2.7.1 + 4.0.11 diff --git a/app/controllers/distributions_by_county_controller.rb b/app/controllers/distributions_by_county_controller.rb index ef829846e0..13b6915043 100644 --- a/app/controllers/distributions_by_county_controller.rb +++ b/app/controllers/distributions_by_county_controller.rb @@ -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 diff --git a/app/models/item.rb b/app/models/item.rb index 12adb66f18..b1567935ec 100644 --- a/app/models/item.rb +++ b/app/models/item.rb @@ -66,6 +66,7 @@ class Item < ApplicationRecord scope :alphabetized, -> { order(:name) } 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, -> { diff --git a/app/models/view/distributions_by_county.rb b/app/models/view/distributions_by_county.rb new file mode 100644 index 0000000000..3bf566e540 --- /dev/null +++ b/app/models/view/distributions_by_county.rb @@ -0,0 +1,46 @@ +module View + DistributionsByCounty = Data.define( + :breakdown, + :filters, + :items, + :reporting_categories + ) do + include DateRangeHelper + + 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:) + 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 diff --git a/app/queries/distribution_summary_by_county_query.rb b/app/queries/distribution_summary_by_county_query.rb index 1275da7a3a..d20f28ada6 100644 --- a/app/queries/distribution_summary_by_county_query.rb +++ b/app/queries/distribution_summary_by_county_query.rb @@ -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, @@ -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 @@ -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 @@ -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) @@ -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 diff --git a/app/views/distributions_by_county/report.html.erb b/app/views/distributions_by_county/report.html.erb index ea8f760327..24399ee155 100644 --- a/app/views/distributions_by_county/report.html.erb +++ b/app/views/distributions_by_county/report.html.erb @@ -6,6 +6,8 @@

Estimated Distributions by County for <%= current_organization.name %>

+
Please note that any items within kits are included in these estimates +