diff --git a/app/controllers/reports/annual_reports_controller.rb b/app/controllers/reports/annual_reports_controller.rb index 76bc4aff23..e7d9ecfd6e 100644 --- a/app/controllers/reports/annual_reports_controller.rb +++ b/app/controllers/reports/annual_reports_controller.rb @@ -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 @@ -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 diff --git a/app/services/exports/export_report_csv_service.rb b/app/services/exports/export_report_csv_service.rb index ad6c51da99..857feac77a 100644 --- a/app/services/exports/export_report_csv_service.rb +++ b/app/services/exports/export_report_csv_service.rb @@ -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 } @@ -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 + + csv_data + end end end diff --git a/app/views/reports/annual_reports/index.html.erb b/app/views/reports/annual_reports/index.html.erb index fbfffdfdb3..2b5b9453cc 100644 --- a/app/views/reports/annual_reports/index.html.erb +++ b/app/views/reports/annual_reports/index.html.erb @@ -27,6 +27,15 @@
Reports are available at the end of every year.
<%= "#{@actual_year} (available in #{pluralize(@month_remaining_to_report, 'month')})" %> +
+ <%= + 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. +
diff --git a/config/routes.rb b/config/routes.rb index 74bf981a79..b37f316d16 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/docs/user_guide/bank/images/reports/reports-anual-survey-yearly-export.png b/docs/user_guide/bank/images/reports/reports-anual-survey-yearly-export.png new file mode 100644 index 0000000000..86f503f29f Binary files /dev/null and b/docs/user_guide/bank/images/reports/reports-anual-survey-yearly-export.png differ diff --git a/docs/user_guide/bank/reports_annual_survey.md b/docs/user_guide/bank/reports_annual_survey.md index 734de00602..892fb5073d 100644 --- a/docs/user_guide/bank/reports_annual_survey.md +++ b/docs/user_guide/bank/reports_annual_survey.md @@ -1,13 +1,22 @@ # 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) @@ -15,23 +24,23 @@ This brings up the report. ![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) \ No newline at end of file +[Prior: Trends](reports_trends.md) [Next: Distributions by County](reports_distributions_by_county.md) diff --git a/spec/factories/organizations.rb b/spec/factories/organizations.rb index f8c9482fb1..245248219a 100644 --- a/spec/factories/organizations.rb +++ b/spec/factories/organizations.rb @@ -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 diff --git a/spec/requests/reports/annual_reports_requests_spec.rb b/spec/requests/reports/annual_reports_requests_spec.rb index c6229428b5..89daee7177 100644 --- a/spec/requests/reports/annual_reports_requests_spec.rb +++ b/spec/requests/reports/annual_reports_requests_spec.rb @@ -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) } @@ -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