Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -833,4 +833,4 @@ DEPENDENCIES
webmock (~> 3.24)

BUNDLED WITH
2.6.6
2.6.7
2 changes: 1 addition & 1 deletion app/controllers/donations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def index
respond_to do |format|
format.html
format.csv do
send_data Exports::ExportDonationsCSVService.new(donation_ids: @donations.map(&:id)).generate_csv, filename: "Donations-#{Time.zone.today}.csv"
send_data Exports::ExportDonationsCSVService.new(donation_ids: @donations.map(&:id), organization: current_organization).generate_csv, filename: "Donations-#{Time.zone.today}.csv"
end
end
end
Expand Down
1 change: 1 addition & 0 deletions app/controllers/organizations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def organization_params
:ytd_on_distribution_printout, :one_step_partner_invite,
:hide_value_columns_on_receipt, :hide_package_column_on_receipt,
:signature_for_distribution_pdf, :receive_email_on_requests,
:include_in_kind_values_in_exported_files,
partner_form_fields: [],
request_unit_names: []
)
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/items_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def dollar_value(value)
end

def cents_to_dollar(value_in_cents)
value_in_cents.to_f / 100
Money.new(value_in_cents).to_f
end

def selected_item_request_units(item)
Expand Down
2 changes: 1 addition & 1 deletion app/models/donation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def storage_view
end

def in_kind_value_money
Money.new(value_per_itemizable)
Money.new(value_per_itemizable).to_f
end

private
Expand Down
65 changes: 33 additions & 32 deletions app/models/organization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,39 @@
#
# Table name: organizations
#
# id :integer not null, primary key
# city :string
# deadline_day :integer
# default_storage_location :integer
# distribute_monthly :boolean default(FALSE), not null
# email :string
# enable_child_based_requests :boolean default(TRUE), not null
# enable_individual_requests :boolean default(TRUE), not null
# enable_quantity_based_requests :boolean default(TRUE), not null
# hide_package_column_on_receipt :boolean default(FALSE)
# hide_value_columns_on_receipt :boolean default(FALSE)
# intake_location :integer
# invitation_text :text
# latitude :float
# longitude :float
# name :string
# one_step_partner_invite :boolean default(FALSE), not null
# partner_form_fields :text default([]), is an Array
# receive_email_on_requests :boolean default(FALSE), not null
# reminder_day :integer
# repackage_essentials :boolean default(FALSE), not null
# short_name :string
# signature_for_distribution_pdf :boolean default(FALSE)
# state :string
# street :string
# url :string
# ytd_on_distribution_printout :boolean default(TRUE), not null
# zipcode :string
# created_at :datetime not null
# updated_at :datetime not null
# account_request_id :integer
# ndbn_member_id :bigint
# id :integer not null, primary key
# city :string
# deadline_day :integer
# default_storage_location :integer
# distribute_monthly :boolean default(FALSE), not null
# email :string
# enable_child_based_requests :boolean default(TRUE), not null
# enable_individual_requests :boolean default(TRUE), not null
# enable_quantity_based_requests :boolean default(TRUE), not null
# 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
# intake_location :integer
# invitation_text :text
# latitude :float
# longitude :float
# name :string
# one_step_partner_invite :boolean default(FALSE), not null
# partner_form_fields :text default([]), is an Array
# receive_email_on_requests :boolean default(FALSE), not null
# reminder_day :integer
# repackage_essentials :boolean default(FALSE), not null
# short_name :string
# signature_for_distribution_pdf :boolean default(FALSE)
# state :string
# street :string
# url :string
# ytd_on_distribution_printout :boolean default(TRUE), not null
# zipcode :string
# created_at :datetime not null
# updated_at :datetime not null
# account_request_id :integer
# ndbn_member_id :bigint
#

class Organization < ApplicationRecord
Expand Down
16 changes: 7 additions & 9 deletions app/services/exports/export_distributions_csv_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ def initialize(distributions:, organization:, filters: [])
@filters = filters
@organization = organization
@distribution_totals = DistributionTotalsService.call(Distribution.where(organization:).class_filter(filters))

@export_line_item_adder = Exports::ExportLineItemColumnsAdder.new
@export_line_item_adder.register(Exports::ExportInKindTotalValue) if @organization.include_in_kind_values_in_exported_files
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.

This seems like an overly complex way of adding a column to an export. You're basically turning a single if statement into a factory pattern. Can you explain why you did it this way?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If you have other attributes that need to be added to the exported CSV, just create another class and register it here. There's no need to modify any other code. Since both distribution and donation share the same export functionality, I extracted it to avoid duplication.

Copy link
Copy Markdown
Collaborator

@dorner dorner Apr 16, 2025

Choose a reason for hiding this comment

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

This is a case of unnecessary DRY along with a big helping of YAGNI. As far as we know there are no additional attributes that will need this, and in general the idea that "attributes in a CSV" is a generic feature is probably not a helpful construct. I'd much prefer a simpler approach even if it means a little bit of duplicated code.

end

def generate_csv
Expand Down Expand Up @@ -136,20 +139,15 @@ 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 = @export_line_item_adder.headers(@item_headers)

@item_headers
end

def build_row_data(distribution)
row = base_table.values.map { |closure| closure.call(distribution) }

row += Array.new(item_headers.size, 0)

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

row[item_column_idx] += line_item.quantity
end
row += @export_line_item_adder.get_row(distribution.line_items)

row
end
Expand Down
19 changes: 10 additions & 9 deletions app/services/exports/export_donations_csv_service.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Exports
class ExportDonationsCSVService
def initialize(donation_ids:)
def initialize(donation_ids:, organization:)
# Use a where lookup so that I can eager load all the resources
# needed rather than depending on external code to do it for me.
# This makes this code more self contained and efficient!
Expand All @@ -13,6 +13,11 @@ def initialize(donation_ids:)
).where(
id: donation_ids,
).order(created_at: :asc)

@organization = organization

@export_line_item_adder = Exports::ExportLineItemColumnsAdder.new
@export_line_item_adder.register(Exports::ExportInKindTotalValue) if @organization.include_in_kind_values_in_exported_files
end

def generate_csv
Expand Down Expand Up @@ -80,7 +85,7 @@ def base_table
"Variety of Items" => ->(donation) {
donation.line_items.map(&:name).uniq.size
},
"In-Kind Value" => ->(donation) {
"In-Kind Total" => ->(donation) {
donation.in_kind_value_money
},
"Comments" => ->(donation) {
Expand All @@ -105,18 +110,14 @@ def item_headers
end

@item_headers = item_names.sort

@item_headers = @export_line_item_adder.headers(@item_headers)
end

def build_row_data(donation)
row = base_table.values.map { |closure| closure.call(donation) }

row += Array.new(item_headers.size, 0)

donation.line_items.each do |line_item|
item_name = line_item.item.name
item_column_idx = headers_with_indexes[item_name]
row[item_column_idx] += line_item.quantity
end
row += @export_line_item_adder.get_row(donation.line_items)

row
end
Expand Down
73 changes: 73 additions & 0 deletions app/services/exports/export_line_item_columns_adder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
module Exports
class ExportLineItemColumnsAdder
def initialize
@registry = []
end

def headers(item_headers)
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.

In general this code is written in an unusual manner... I'd need to see a bunch more comments here since I can't really understand what some of it is supposed to be doing. What does "registry" contain? What kind of "row" does get_row return? What additional_columns do we need and how does that relate to the registry...?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The registry contains classes that implement Exports::ExportColumnsBase. Each element in the registry adds a column after each "line_item" column.
get_row returns a row like this:
|item A|col1|col2|item B|col1|col2|...
These col1, col2, etc., are the additional_columns.

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.

The explanation has to be in comments, not in the PR.

But in general, if you have to explain what the code is doing via comments, it might be doing things in a way that isn't straightforward.

@item_headers ||= item_headers.each_with_index.to_h
@item_headers.keys.flat_map do |header|
@registry.inject([header]) do |arr, klass|
arr.append(klass.convert_header(header))
end
end
end

def register(column)
@registry.append(column)
end

def get_row(line_items)
chunk_size = @registry.size + 1
row = Array.new(@item_headers.size * chunk_size, 0)
@registry.each_with_index do |klass, idx|
row.each_with_index { |_, i| row[i] = klass.init_value if i % chunk_size == idx + 1 }
end
line_items.each do |line_item|
idx = @item_headers[line_item.item.name]
next unless idx

process_additional_columns(row, idx, line_item)
end
row
end

private

def process_additional_columns(row, idx, line_item)
idx *= (@registry.size + 1)
row[idx] += line_item.quantity
@registry.each_with_index do |klass, index|
row[idx + index + 1] += klass.value(line_item)
end
end
end

class ExportColumnsBase
def self.convert_header(header)
raise NotImplementedError, "Subclass must implement convert_header."
end

def self.init_value
raise NotImplementedError, "Subclass must implement init_value."
end

def self.value(raw_value)
raise NotImplementedError, "Subclass must implement value."
end
end

class ExportInKindTotalValue < Exports::ExportColumnsBase
def self.convert_header(header)
"#{header} In-Kind Value"
end

def self.init_value
Money.new(0)
end

def self.value(line_item)
Money.new(line_item.value_per_line_item)
end
end
end
8 changes: 8 additions & 0 deletions app/views/organizations/_details.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,14 @@
</div>
<hr>

<h4>Exports</h4>
<div>
<h6 class='font-weight-bold'>Include in-kind value in donation and distribution exports:</h6>
<p>
<%= humanize_boolean(@organization.include_in_kind_values_in_exported_files) %>
</p>
</div>

<h4>Annual Survey</h4>
<div>
<h6 class="font-weight-bold">Does your Bank repackage essentials?</h6>
Expand Down
3 changes: 3 additions & 0 deletions app/views/organizations/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@
<%= f.input :hide_value_columns_on_receipt, label: 'Hide value columns on Distribution and Donation printout?', as: :radio_buttons, collection: [[true, 'Yes'], [false, 'No']], label_method: :second, value_method: :first %>
<%= f.input :hide_package_column_on_receipt, label: 'Hide package column on Distribution printout?', as: :radio_buttons, collection: [[true, 'Yes'], [false, 'No']], label_method: :second, value_method: :first %>

<h4>Exports</h4>
<%= 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 %>

<h4>Annual Survey</h4>
<%= f.input :repackage_essentials, label: 'Does your Bank repackage essentials?', as: :radio_buttons, collection: [[true, 'Yes'], [false, 'No']], label_method: :second, value_method: :first %>
<%= f.input :distribute_monthly, label: 'Does your Bank distribute monthly?', as: :radio_buttons, collection: [[true, 'Yes'], [false, 'No']], label_method: :second, value_method: :first %>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AddIncludeInKindValuesInExportedFilesToOrganizations < ActiveRecord::Migration[8.0]
def change
add_column :organizations, :include_in_kind_values_in_exported_files, :boolean
add_check_constraint :organizations, "include_in_kind_values_in_exported_files IS NOT NULL", name: "include_in_kind_values_in_exported_files_not_null", validate: false
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.

This seems a very awkward way of just saying null: false...?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Adding the NOT NULL constraint directly will lock the table. This method helps avoid that. I don't know how big the table will be, so I think it's better to avoid it.

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.

Organizations is a tiny table. You can lock it.

end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class BackfillFalseToIncludeInKindValuesInExportedFilesInOrganizations < ActiveRecord::Migration[8.0]
def up
Organization.unscoped.in_batches do |relation|
relation.update_all include_in_kind_values_in_exported_files: false
sleep(0.01)
end

validate_check_constraint :organizations, name: "include_in_kind_values_in_exported_files_not_null"
end

def down

end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class EnforceNotNullOnIncludeInKindValuesInExportedFilesInOrganizations < ActiveRecord::Migration[8.0]
def up
change_column_null :organizations, :include_in_kind_values_in_exported_files, false
change_column_default :organizations, :include_in_kind_values_in_exported_files, false
remove_check_constraint :organizations, name: "include_in_kind_values_in_exported_files_not_null"
end

def down
add_check_constraint :organizations, "include_in_kind_values_in_exported_files IS NOT NULL", name: "include_in_kind_values_in_exported_files_not_null", validate: false
change_column_default :organizations, :include_in_kind_values_in_exported_files, nil
change_column_null :organizations, :include_in_kind_values_in_exported_files, true
end
end
3 changes: 2 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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_03_02_154355) do
ActiveRecord::Schema[8.0].define(version: 2025_03_23_075801) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"

Expand Down Expand Up @@ -495,6 +495,7 @@
t.boolean "hide_package_column_on_receipt", default: false
t.boolean "signature_for_distribution_pdf", default: false
t.boolean "receive_email_on_requests", default: false, null: false
t.boolean "include_in_kind_values_in_exported_files", default: false, null: false
t.index ["latitude", "longitude"], name: "index_organizations_on_latitude_and_longitude"
t.index ["short_name"], name: "index_organizations_on_short_name"
end
Expand Down
12 changes: 12 additions & 0 deletions docs/user_guide/bank/exports.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ For each of the distributions in the filtered list:

[!NOTE] This includes inactive Items as well as active ones.

### Add In-Kind Value for each item
The default export includes the number of items exported, but not the values. If you want to also have the export include the in-kind value 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 in-kind value in donation and distribution exports?" to "yes", then click "Save".

[!NOTE] Setting this affects both the donation and distribution exports.

## Donations

### Navigating to export Donations
Expand Down Expand Up @@ -156,6 +162,12 @@ For each of the Donations in the filtered list:
- Comments,
- and the quantity of each of your organization's Items in the Donations.

### Add In-Kind Value for each item
The default export includes the number of items exported, but not the values. If you want to also have the export include the in-kind value 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 in-kind value in donation and distribution exports?" to "yes", then click "Save".

[!NOTE] Setting this affects both the donation and distribution exports.

## Donation Sites
### Navigating to export Donation Sites
Click "Community", then "Donation Sites" in the left hand menu. Then click "Export Donation Sites"
Expand Down
9 changes: 9 additions & 0 deletions docs/user_guide/bank/getting_started_customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,15 @@ column is useful mainly for banks that repackage their essentials into uniform p

![Distribution printout marked up with customizable sections](images/getting_started/customization/gs_customization_distribution_printout_customizable_sections.png)

## Exports

### Include in-kind value in donation and distribution exports?

You can configure whether the exports for donations and distributions include the "In-Kind Value" column for each item. By default, these values are excluded. To include them, set the "Include in-kind value in donation and distribution exports?" option to "Yes".

Click "My Organization" in the left hand menu. Click "Edit" button. Set the "Include in-kind value in donation and distribution exports?" to "yes", then click "Save".

[!NOTE] Setting this affects both the donation and distribution exports.

## Annual Survey
These two fields are only here to be reported on the Annual Survey.
Expand Down
Loading