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
49 changes: 45 additions & 4 deletions app/controllers/reports/annual_reports_controller.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
class Reports::AnnualReportsController < ApplicationController
before_action :validate_show_params, only: [:show, :recalculate]
before_action :validate_range_params, only: [:range]

def index
# 2813_update_annual_report -- changed to earliest_reporting_year
# so that we can do system tests and staging
foundation_year = current_organization.earliest_reporting_year
@foundation_year = current_organization.earliest_reporting_year
@current_year = Time.current.year

@actual_year = Time.current.year

@years = (foundation_year...@actual_year).to_a
@years = (@foundation_year...@current_year).to_a

@month_remaining_to_report = 12 - Time.current.month
end
Expand All @@ -32,13 +32,54 @@ def recalculate
redirect_to reports_annual_report_path(year), notice: "Recalculated annual report!"
end

def range
# Set range to be within valid reporting bounds
# Start year cannot be before org founding year
# End year cannot be after current year
year_start = [range_params[:year_start].to_i, current_organization.earliest_reporting_year].max
year_end = [range_params[:year_end].to_i, Time.current.year].min

# Sort years if out of order
year_start, year_end = [year_start, year_end].minmax

reports = get_range_report(year_start, year_end)

respond_to do |format|
format.csv do
send_data Exports::ExportReportCSVService.new(reports:).generate_csv(range: true),
filename: "NdbnAnnuals-#{year_start}-#{year_end}.csv"
end
end
end

private

def get_range_report(year_start, year_end)
(year_start..year_end).map do |year|
Reports.retrieve_report(organization: current_organization, year: year, recalculate: true)
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error("Failed to retrieve annual report for year #{year}: #{e.message}")
nil
end.compact
end

def year_param
params.require(:year)
end

def range_params
params.permit(:year_start, :year_end)
end

def validate_show_params
not_found! unless year_param.to_i.positive?
end

def validate_range_params
not_found! unless range_params[:year_start] =~ year_regex && range_params[:year_end] =~ year_regex
end

def year_regex
/^\d{4}$/
end
end
21 changes: 19 additions & 2 deletions app/services/exports/export_report_csv_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ def initialize(reports:)
@reports = reports
end

def generate_csv
csv_data = generate_csv_data
def generate_csv(range: false)
csv_data = range ? generate_range_csv_data : generate_csv_data

::CSV.generate(headers: true) do |csv|
csv_data.each { |row| csv << row }
Expand All @@ -31,5 +31,22 @@ def generate_csv_data

csv_data
end

def generate_range_csv_data
csv_data = []
return csv_data if @reports.empty?

headers = @reports.first.all_reports.flat_map { |r| r['entries'].keys }
csv_data << ['Year'] + headers

@reports.each do |report|
report_data = report.all_reports
year = report['year']
values = report_data.flat_map { |r| r['entries'].values }
csv_data << [year] + values
end
Comment on lines +42 to +47
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hi! Different reports end up having different columns. Here, instead of plucking out the values in the specific order, can you instead do a pass through all the reports and get all the headers and then build out a hash or something so that we can handle blank values and then go through and output the CSV based on that?

Something like this (note this is COMPLETELY UNTESTED SUGGESTION) :)

Suggested change
@reports.each do |report|
report_data = report.all_reports
year = report['year']
values = report_data.flat_map { |r| r['entries'].values }
csv_data << [year] + values
end
@reports.each do |report|
report_data = report.all_reports
year = report['year']
values = report_data.flat_map { |r|
headers.map { |field| r['entries'][field] }
}
csv_data << [year] + values
end

That way if year 1 has some headers that year 2 doesn't have (or vice-versa) then we will get all of the entries.


csv_data
end
end
end
9 changes: 9 additions & 0 deletions app/views/reports/annual_reports/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@
<h5 class="mb-1">Reports are available at the end of every year.</h5>
<span class="text-muted"> <%= "#{@actual_year} (available in #{pluralize(@month_remaining_to_report, 'month')})" %> </span>
</div>
<div class="col-md-12 mb-2">
<%=
download_button_to(
range_reports_annual_reports_path(year_start: @foundation_year, year_end: (@current_year - 1), format: :csv),
text: "Export Yearly Reports"
)
%>
This will recalculate all the reports, and may take some time.
</div>
<div class="col-md-12">
<div class="card card-primary">
<div class="card-header">
Expand Down
5 changes: 5 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ def set_up_flipper
namespace :reports do
resources :annual_reports, only: [:index, :show], param: :year do
post :recalculate, on: :member
get 'range/:year_start/:year_end',
to: 'annual_reports#range',
on: :collection,
as: 'range',
constraints: { year_start: /\d{4}/, year_end: /\d{4}/ }
end
get :donations_summary
get :manufacturer_donations_summary
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 18 additions & 9 deletions docs/user_guide/bank/reports_annual_survey.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,46 @@
# Annual Survey

The annual survey contains information useful for completing the NDBN or Alliance for Period Supplies annual survey, but also for grant writing.
Each year's annual survey becomes available January 1 of the following year.

## How to get the report
## How to get all year report

1. Click on “Reports”
2. Click on “Annual Survey”
3. Click on "Export Yearly Reports

![Annual Report Yearly](images/reports/reports-anual-survey-yearly-export.png)

## How to get single year report

Click on "Reports", then "Annual Survey" in the left-hand menu. Then click on the year of the report you wish to view.
Click on "Reports", then "Annual Survey" in the left-hand menu. Then click on the year of the report you wish to view.
![Navigation to annual report](images/reports/reports_annual_survey_1.png)

This brings up the report.
This brings up the report.

![Annual Report_screen_1](images/reports/reports_annual_survey_2.png)
![Annual Report_screen_2](images/reports/reports_annual_survey_3.png)
![Annual Report_screen_3](images/reports/reports_annual_survey_4.png)
![Annual Report_screen_4](images/reports/reports_annual_survey_5.png)

### Recalculation

If you have added information for that year since the last time you ran the report, you should recalculate the report by clicking "Recalculate Report" (A)
You may want to recalculate older reports as they will not reflect any changes we made since they were generated.

### Exporting

You can also extract the report to a csv file by clicking "Export Report" (B)

### Calculation notes

1/ We are in the process of changing the calculations so that Items in Kits appear in the totals for each value.
At time of writing, the # of disposable and cloth diapers distributed include any diapers in Kits, and period supplies also include period supplies in Kits, but adult incontinence and other do not.
At time of writing, the # of disposable and cloth diapers distributed include any diapers in Kits, and period supplies also include period supplies in Kits, but adult incontinence and other do not.
At time of writing, purchased/donated supplies do not include any Kit Purchases/Donations. ( We only know of one bank that currently has kits donated.)

2/ % donated / purchased is based on the number of Items acquired, not the number of Items distributed.
2/ % donated / purchased is based on the number of Items acquired, not the number of Items distributed.

3/ How we calculate per person values:
If you have entered a non-zero value for an [Item](inventory_items.md#editing-an-item) in the quantity per individual field, we use that value. Otherwise, we assume 50 of the Item per individual.


If you have entered a non-zero value for an [Item](inventory_items.md#editing-an-item) in the quantity per individual field, we use that value. Otherwise, we assume 50 of the Item per individual.

[Prior: Trends](reports_trends.md) [Next: Distributions by County](reports_distributions_by_county.md)
[Prior: Trends](reports_trends.md) [Next: Distributions by County](reports_distributions_by_county.md)
6 changes: 6 additions & 0 deletions spec/factories/organizations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,11 @@
Organization.seed_items(instance) # creates 1 item for each base item
end
end

trait :created_at_2006 do
after(:create) do |instance|
instance.update(created_at: Time.zone.local(2006, 1, 1))
end
end
end
end
33 changes: 32 additions & 1 deletion spec/requests/reports/annual_reports_requests_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
RSpec.describe "Annual Reports", type: :request do
let(:organization) { create(:organization) }
let(:organization) { create(:organization, :created_at_2006) }
let(:user) { create(:user, organization: organization) }
let(:organization_admin) { create(:organization_admin, organization: organization) }

Expand Down Expand Up @@ -78,5 +78,36 @@
expect(report.reload.updated_at).not_to be_within(1.second).of(old_time)
end
end

describe "GET /range" do
it "returns AnnualReports within given range" do
get range_reports_annual_reports_path(year_start: 2016, year_end: 2018, format: :csv)
expect(response.body).to include("2016")
expect(response.body).to include("2017")
expect(response.body).to include("2018")
end

it "returns URL error if years are not valid format" do
expect { get range_reports_annual_reports_path(year_start: 'test', year_end: 'test', format: :csv) }
.to raise_error(ActionController::UrlGenerationError)
end

it "uses the earliest(smallest) year between year_start and organization's earliest_reporting_year" do
get range_reports_annual_reports_path(year_start: 2004, year_end: 2008, format: :csv)
# the organization was created in 2006 (created_at_2006)
# so the below years should not be in the output
expect(response.body).not_to include("2004")
expect(response.body).not_to include("2005")
response.body.split("\n")
end
it "orders the years in ascending order" do
get range_reports_annual_reports_path(year_start: 2018, year_end: 2016, format: :csv)
csv_array = response.body.split("\n")
# csv_array[0] is the header row
expect(csv_array[1]).to include("2016")
expect(csv_array[2]).to include("2017")
expect(csv_array[3]).to include("2018")
end
end
end
end