From 34def31ee70774210188e5b1f87bced4789d365c Mon Sep 17 00:00:00 2001 From: Brody Fischer Date: Fri, 19 Sep 2025 17:41:26 -0600 Subject: [PATCH] [4678] Add option to include package count in distributions export CSV --- app/controllers/organizations_controller.rb | 1 + app/models/organization.rb | 1 + .../export_distributions_csv_service.rb | 44 +++++------------ app/views/organizations/_details.html.erb | 4 ++ app/views/organizations/edit.html.erb | 1 + ...in_distribution_export_to_organizations.rb | 5 ++ db/schema.rb | 3 +- docs/user_guide/bank/exports.md | 7 +++ spec/factories/organizations.rb | 1 + spec/models/organization_spec.rb | 1 + spec/requests/organization_requests_spec.rb | 9 ++++ .../export_distributions_csv_service_spec.rb | 47 ++++++++++++++++--- 12 files changed, 85 insertions(+), 39 deletions(-) create mode 100644 db/migrate/20250913194218_add_include_packages_in_distribution_export_to_organizations.rb diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index 0f1cecd289..ddb213e4f9 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -104,6 +104,7 @@ def organization_params :signature_for_distribution_pdf, :receive_email_on_requests, :bank_is_set_up, :include_in_kind_values_in_exported_files, + :include_packages_in_distribution_export, partner_form_fields: [], request_unit_names: [] ) diff --git a/app/models/organization.rb b/app/models/organization.rb index 87b37c467d..defa258d06 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -15,6 +15,7 @@ # hide_package_column_on_receipt :boolean default(FALSE) # hide_value_columns_on_receipt :boolean default(FALSE) # include_in_kind_values_in_exported_files :boolean default(FALSE), not null +# include_packages_in_distribution_export :boolean default(FALSE), not null # intake_location :integer # invitation_text :text # latitude :float diff --git a/app/services/exports/export_distributions_csv_service.rb b/app/services/exports/export_distributions_csv_service.rb index 5f21053f73..10e831be07 100644 --- a/app/services/exports/export_distributions_csv_service.rb +++ b/app/services/exports/export_distributions_csv_service.rb @@ -27,7 +27,7 @@ def generate_csv def generate_csv_data csv_data = [] - csv_data << headers + csv_data << base_headers + item_headers distributions.each do |distribution| csv_data << build_row_data(distribution) end @@ -39,17 +39,6 @@ def generate_csv_data attr_reader :distributions - def headers - # Build the headers in the correct order - base_headers + item_headers - end - - # Returns a Hash of keys to indexes so that obtaining the index - # doesn't require a linear scan. - def headers_with_indexes - @headers_with_indexes ||= headers.each_with_index.to_h - end - # This method keeps the base headers associated with the lambdas # for extracting the values for the base columns from the given # distribution. @@ -132,35 +121,28 @@ def base_headers base_table.keys end - def item_headers - return @item_headers if @item_headers - - @item_headers = @organization.items.select("DISTINCT ON (LOWER(name)) items.name").order("LOWER(name) ASC").map(&:name) - @item_headers = @item_headers.flat_map { |header| [header, "#{header} In-Kind Value"] } if @organization.include_in_kind_values_in_exported_files + def item_names + @item_names ||= @organization.items.pluck(:name).sort_by(&:downcase) + end - @item_headers + def item_headers + in_kind_value_headers = @organization.include_in_kind_values_in_exported_files ? item_names.map { |item| "#{item} In-Kind Value" } : [] + package_headers = @organization.include_packages_in_distribution_export ? item_names.map { |item| "#{item} Packages" } : [] + item_names.zip(in_kind_value_headers, package_headers).flatten.compact end def build_row_data(distribution) row = base_table.values.map { |closure| closure.call(distribution) } - row += make_item_quantity_and_value_slots - distribution.line_items.each do |line_item| - item_name = line_item.item.name - item_column_idx = headers_with_indexes[item_name] - next unless item_column_idx + item_names.each do |item_name| + line_items = distribution.line_items.where(item: @organization.items.find_by(name: item_name)) - row[item_column_idx] += line_item.quantity - row[item_column_idx + 1] += Money.new(line_item.value_per_line_item) if @organization.include_in_kind_values_in_exported_files + row << line_items.sum(&:quantity) + row << Money.new(line_items.sum(&:value_per_line_item)) if @organization.include_in_kind_values_in_exported_files + row << line_items.map(&:has_packages).compact.sum.round(2) if @organization.include_packages_in_distribution_export end row end - - def make_item_quantity_and_value_slots - slots = Array.new(item_headers.size, 0) - slots = slots.map.with_index { |value, index| index.odd? ? Money.new(0) : value } if @organization.include_in_kind_values_in_exported_files - slots - end end end diff --git a/app/views/organizations/_details.html.erb b/app/views/organizations/_details.html.erb index c6c5729774..c406f604d6 100644 --- a/app/views/organizations/_details.html.erb +++ b/app/views/organizations/_details.html.erb @@ -254,6 +254,10 @@

<%= humanize_boolean(@organization.include_in_kind_values_in_exported_files) %>

+
Include packages in distribution export:
+

+ <%= humanize_boolean(@organization.include_packages_in_distribution_export) %> +

Annual Survey

diff --git a/app/views/organizations/edit.html.erb b/app/views/organizations/edit.html.erb index eb9ed752d6..b3618e4e34 100644 --- a/app/views/organizations/edit.html.erb +++ b/app/views/organizations/edit.html.erb @@ -171,6 +171,7 @@

Exports

<%= f.input :include_in_kind_values_in_exported_files, label: 'Include in-kind value in donation and distribution exports?', as: :radio_buttons, collection: [[true, 'Yes'], [false, 'No']], label_method: :second, value_method: :first %> + <%= f.input :include_packages_in_distribution_export, label: 'Include packages in distribution export?', as: :radio_buttons, collection: [[true, 'Yes'], [false, 'No']], label_method: :second, value_method: :first %>

Annual Survey

<%= f.input :repackage_essentials, label: 'Does your Bank repackage essentials?', as: :radio_buttons, collection: [[true, 'Yes'], [false, 'No']], label_method: :second, value_method: :first %> diff --git a/db/migrate/20250913194218_add_include_packages_in_distribution_export_to_organizations.rb b/db/migrate/20250913194218_add_include_packages_in_distribution_export_to_organizations.rb new file mode 100644 index 0000000000..5da8e8e3a7 --- /dev/null +++ b/db/migrate/20250913194218_add_include_packages_in_distribution_export_to_organizations.rb @@ -0,0 +1,5 @@ +class AddIncludePackagesInDistributionExportToOrganizations < ActiveRecord::Migration[8.0] + def change + add_column :organizations, :include_packages_in_distribution_export, :boolean, default: false, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 2d0ae2276e..6426e2ca5d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_09_13_173217) do +ActiveRecord::Schema[8.0].define(version: 2025_09_13_194218) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -496,6 +496,7 @@ t.boolean "include_in_kind_values_in_exported_files", default: false, null: false t.integer "reminder_day" t.string "reminder_schedule_definition" + t.boolean "include_packages_in_distribution_export", default: false, null: false t.index ["latitude", "longitude"], name: "index_organizations_on_latitude_and_longitude" end diff --git a/docs/user_guide/bank/exports.md b/docs/user_guide/bank/exports.md index 2b4aedb3c1..24c9a033b3 100644 --- a/docs/user_guide/bank/exports.md +++ b/docs/user_guide/bank/exports.md @@ -135,6 +135,13 @@ Click "My Organization" in the left hand menu. Click "Edit" button. Set the "Inc [!NOTE] Setting this affects both the donation and distribution exports. +### Add Package Counts for each item +If you want to also have the export include the package count for each item in the distributions, you can set that option. +- Click "My Organization" in the left hand menu. +- Click "Edit" button. +- Set the "Include packages in distribution export" to "yes" +- Click "Save" + ## Donations ### Navigating to export Donations diff --git a/spec/factories/organizations.rb b/spec/factories/organizations.rb index f8c9482fb1..fa35788030 100644 --- a/spec/factories/organizations.rb +++ b/spec/factories/organizations.rb @@ -15,6 +15,7 @@ # hide_package_column_on_receipt :boolean default(FALSE) # hide_value_columns_on_receipt :boolean default(FALSE) # include_in_kind_values_in_exported_files :boolean default(FALSE), not null +# include_packages_in_distribution_export :boolean default(FALSE), not null # intake_location :integer # invitation_text :text # latitude :float diff --git a/spec/models/organization_spec.rb b/spec/models/organization_spec.rb index 22dda1c842..a7c85c0c33 100644 --- a/spec/models/organization_spec.rb +++ b/spec/models/organization_spec.rb @@ -15,6 +15,7 @@ # hide_package_column_on_receipt :boolean default(FALSE) # hide_value_columns_on_receipt :boolean default(FALSE) # include_in_kind_values_in_exported_files :boolean default(FALSE), not null +# include_packages_in_distribution_export :boolean default(FALSE), not null # intake_location :integer # invitation_text :text # latitude :float diff --git a/spec/requests/organization_requests_spec.rb b/spec/requests/organization_requests_spec.rb index c025c88d92..1c31a53f1e 100644 --- a/spec/requests/organization_requests_spec.rb +++ b/spec/requests/organization_requests_spec.rb @@ -228,6 +228,7 @@ expect(html.text).to include("Logo") expect(html.text).to include("Use one-step Partner invite and approve process?") expect(html.text).to include("Receive email when Partner makes a Request?") + expect(html.text).to include("Include packages in distribution export:") end context "when enable_packs flipper is on" do @@ -446,6 +447,14 @@ expect(response.body).to include("Yes") end end + + context "can enable if package count is included in distribution exports" do + let(:update_param) { { organization: { include_packages_in_distribution_export: true } } } + + it "can update include_packages_in_distribution_export" do + expect { subject }.to change { organization.reload.include_packages_in_distribution_export }.to true + end + end end describe "POST #promote_to_org_admin" do diff --git a/spec/services/exports/export_distributions_csv_service_spec.rb b/spec/services/exports/export_distributions_csv_service_spec.rb index 956604dc0a..bb3538e5d5 100644 --- a/spec/services/exports/export_distributions_csv_service_spec.rb +++ b/spec/services/exports/export_distributions_csv_service_spec.rb @@ -1,18 +1,20 @@ RSpec.describe Exports::ExportDistributionsCSVService do - let(:organization) { create(:organization) } + let(:organization) { create(:organization, include_in_kind_values_in_exported_files: include_in_kind_values, include_packages_in_distribution_export: include_packages) } let(:storage_location) { create(:storage_location, organization: organization) } let(:partner) { create(:partner, name: "first partner", email: "firstpartner@gmail.com", notes: "just a note.", organization_id: organization.id) } + let(:include_in_kind_values) { false } + let(:include_packages) { false } describe '#generate_csv' do subject { described_class.new(distributions: distributions, organization: organization, filters: filters).generate_csv } - let(:duplicate_item) { create(:item, name: "Dupe Item", value_in_cents: 300, organization: organization) } + let(:duplicate_item) { create(:item, name: "Dupe Item", value_in_cents: 300, organization: organization, package_size: 2) } let(:distribution_items_and_quantities) { [ [ [duplicate_item, 5], - [create(:item, name: "A Item", value_in_cents: 1000, organization: organization), 7], + [create(:item, name: "A Item", value_in_cents: 1000, organization: organization, package_size: 6), 7], [duplicate_item, 3] ], [[create(:item, name: "B Item", value_in_cents: 2000, organization: organization), 1]], @@ -49,8 +51,8 @@ let(:item_name) { duplicate_item.name } let(:filters) { {by_item_id: item_id} } - context 'while "Include in-kind value in donation and distribution exports?" is set to no' do - it 'should match the expected content without in-kind value of each item for the csv' do + context 'while both in-kind values and package count are disabled for export' do + it 'should match the expected content without in-kind value or package count for each item for the csv' do csv = <<~CSV Partner,Initial Allocation,Scheduled for,Source Inventory,Total Number of #{item_name},Total Value of #{item_name},Delivery Method,Shipping Cost,Status,Agency Representative,Comments,A Item,B Item,C Item,Dupe Item,E Item #{partner.name},04/04/2025,04/04/2025,#{storage_location.name},8,24.0,shipped,$15.01,scheduled,"",comment 0,7,0,0,8,0 @@ -63,9 +65,9 @@ end context 'while "Include in-kind value in donation and distribution exports?" is set to yes' do - it 'should match the expected content with in-kind value of each item for the csv' do - allow(organization).to receive(:include_in_kind_values_in_exported_files).and_return(true) + let(:include_in_kind_values) { true } + it 'should match the expected content with in-kind value of each item for the csv' do csv = <<~CSV Partner,Initial Allocation,Scheduled for,Source Inventory,Total Number of #{item_name},Total Value of #{item_name},Delivery Method,Shipping Cost,Status,Agency Representative,Comments,A Item,A Item In-Kind Value,B Item,B Item In-Kind Value,C Item,C Item In-Kind Value,Dupe Item,Dupe Item In-Kind Value,E Item,E Item In-Kind Value #{partner.name},04/04/2025,04/04/2025,#{storage_location.name},8,24.0,shipped,$15.01,scheduled,"",comment 0,7,70.00,0,0.00,0,0.00,8,24.00,0,0.00 @@ -77,6 +79,37 @@ end end + context 'while "Include packages in distribution export" is set to yes' do + let(:include_packages) { true } + + it 'should match the expected content with package count of each item for the csv' do + csv = <<~CSV + Partner,Initial Allocation,Scheduled for,Source Inventory,Total Number of #{item_name},Total Value of #{item_name},Delivery Method,Shipping Cost,Status,Agency Representative,Comments,A Item,A Item Packages,B Item,B Item Packages,C Item,C Item Packages,Dupe Item,Dupe Item Packages,E Item,E Item Packages + #{partner.name},04/04/2025,04/04/2025,#{storage_location.name},8,24.0,shipped,$15.01,scheduled,"",comment 0,7,1.17,0,0,0,0,8,4.0,0,0 + #{partner.name},04/04/2025,04/04/2025,#{storage_location.name},0,0.0,shipped,$15.01,scheduled,"",comment 1,0,0,1,0,0,0,0,0,0,0 + #{partner.name},04/04/2025,04/04/2025,#{storage_location.name},0,0.0,shipped,$15.01,scheduled,"",comment 2,0,0,0,0,2,0,0,0,0,0 + #{partner.name},04/04/2025,04/04/2025,#{storage_location.name},0,0.0,shipped,$15.01,scheduled,"",comment 3,0,0,0,0,0,0,0,0,3,0 + CSV + expect(subject).to eq(csv) + end + end + + context 'while both in-kind values and package count are enabled for export' do + let(:include_in_kind_values) { true } + let(:include_packages) { true } + + it 'should match the expected content with in-kind value and package count of each item for the csv' do + csv = <<~CSV + Partner,Initial Allocation,Scheduled for,Source Inventory,Total Number of #{item_name},Total Value of #{item_name},Delivery Method,Shipping Cost,Status,Agency Representative,Comments,A Item,A Item In-Kind Value,A Item Packages,B Item,B Item In-Kind Value,B Item Packages,C Item,C Item In-Kind Value,C Item Packages,Dupe Item,Dupe Item In-Kind Value,Dupe Item Packages,E Item,E Item In-Kind Value,E Item Packages + #{partner.name},04/04/2025,04/04/2025,#{storage_location.name},8,24.0,shipped,$15.01,scheduled,"",comment 0,7,70.00,1.17,0,0.00,0,0,0.00,0,8,24.00,4.0,0,0.00,0 + #{partner.name},04/04/2025,04/04/2025,#{storage_location.name},0,0.0,shipped,$15.01,scheduled,"",comment 1,0,0.00,0,1,20.00,0,0,0.00,0,0,0.00,0,0,0.00,0 + #{partner.name},04/04/2025,04/04/2025,#{storage_location.name},0,0.0,shipped,$15.01,scheduled,"",comment 2,0,0.00,0,0,0.00,0,2,60.00,0,0,0.00,0,0,0.00,0 + #{partner.name},04/04/2025,04/04/2025,#{storage_location.name},0,0.0,shipped,$15.01,scheduled,"",comment 3,0,0.00,0,0,0.00,0,0,0.00,0,0,0.00,0,3,120.00,0 + CSV + expect(subject).to eq(csv) + end + end + context 'when a new item is added' do let(:new_item_name) { "New Item" } let(:original_columns_count) { 15 }