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 71e3b807e6..956a7d5e0c 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 f030f6d373..d6b917cb6b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -520,6 +520,7 @@
t.boolean "receive_email_on_requests", default: false, null: false
t.boolean "include_in_kind_values_in_exported_files", default: false, null: false
t.string "reminder_schedule_definition"
+ t.boolean "include_packages_in_distribution_export", default: false, null: false
t.boolean "bank_is_set_up", 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 c719e33101..3dbed2243d 100644
--- a/docs/user_guide/bank/exports.md
+++ b/docs/user_guide/bank/exports.md
@@ -149,6 +149,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 }