From 47e303b8c353e4ea645b916d1989ee3ce3e48949 Mon Sep 17 00:00:00 2001 From: Pea Tyczynska Date: Fri, 12 Nov 2021 18:32:35 +0000 Subject: [PATCH] Add downloadable report for org usage This is so org level users can use this data easier for things like determining spending per service. We do not include sms fragments sent column and remove other sms columns consistency. Do not add sms fragments sent column for now until we agree on an unambiguous name for it. The data in this column is sms billing units multiplied by international sms weighing. My favourite for a clear name would be 'text message credits used', but we need a naming strategy for this. --- app/main/views/organisations.py | 40 +++++++++++-- .../organisations/organisation/index.html | 2 +- .../views/organisations/test_organisations.py | 58 ++++++++++++++++++- tests/app/test_navigation.py | 1 + 4 files changed, 95 insertions(+), 6 deletions(-) diff --git a/app/main/views/organisations.py b/app/main/views/organisations.py index c1d247b5b..92d56fd10 100644 --- a/app/main/views/organisations.py +++ b/app/main/views/organisations.py @@ -1,4 +1,5 @@ from collections import OrderedDict +from datetime import datetime from functools import partial from flask import flash, redirect, render_template, request, session, url_for @@ -43,6 +44,7 @@ from app.main.views.dashboard import ( from app.main.views.service_settings import get_branding_as_value_and_label from app.models.organisation import Organisation, Organisations from app.models.user import InvitedOrgUser, User +from app.utils.csv import Spreadsheet from app.utils.user import user_has_permissions, user_is_platform_admin @@ -158,16 +160,46 @@ def organisation_dashboard(org_id): for key in ('emails_sent', 'sms_cost', 'letter_cost') }, download_link=url_for( - '.download_services_report_for_org', + '.download_organisation_usage_report', org_id=org_id, + selected_year=year ) ) -@main.route("/organisations/", methods=['GET']) +@main.route("/organisations//download-usage-report.csv", methods=['GET']) @user_has_permissions() -def download_services_report_for_org(org_id): - pass +def download_organisation_usage_report(org_id): + selected_year = request.args.get('selected_year') + services_usage = current_organisation.services_and_usage( + financial_year=selected_year + )['services'] + + column_names = OrderedDict([ + ('service_id', 'Service ID'), + ('service_name', 'Service Name'), + ('emails_sent', 'Emails sent'), + ('sms_remainder', 'Free text message allowance remaining'), + ('sms_cost', 'Spent on text messages (£)'), + ('letter_cost', 'Spent on letters (£)') + ]) + + org_usage_data = [[x for x in column_names.values()]] + + for service in services_usage: + org_usage_data.append([service[attribute] for attribute in column_names.keys()]) + + return Spreadsheet.from_rows(org_usage_data).as_csv_data, 200, { + 'Content-Type': 'text/csv; charset=utf-8', + 'Content-Disposition': ( + 'inline;' + 'filename="{} organisation usage report for year {}' + ' - generated on {}.csv"'.format( + current_organisation.name, + selected_year, + datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ") + )) + } @main.route("/organisations//trial-services", methods=['GET']) diff --git a/app/templates/views/organisations/organisation/index.html b/app/templates/views/organisations/organisation/index.html index 5309ac813..ae9c27b48 100644 --- a/app/templates/views/organisations/organisation/index.html +++ b/app/templates/views/organisations/organisation/index.html @@ -113,7 +113,7 @@ {% endif %} diff --git a/tests/app/main/views/organisations/test_organisations.py b/tests/app/main/views/organisations/test_organisations.py index 6cd22e0b9..56e94e2b4 100644 --- a/tests/app/main/views/organisations/test_organisations.py +++ b/tests/app/main/views/organisations/test_organisations.py @@ -624,6 +624,7 @@ def test_organisation_services_hides_search_bar_for_7_or_fewer_services( assert not page.select_one('.live-search') +@freeze_time("2021-11-12 11:09:00.061258") def test_organisation_services_links_to_downloadable_report( client_request, mock_get_organisation, @@ -651,7 +652,62 @@ def test_organisation_services_links_to_downloadable_report( page = client_request.get('.organisation_dashboard', org_id=ORGANISATION_ID) link_to_report = page.find('a', text="Download this report") - assert link_to_report.attrs["href"] == url_for('.download_services_report_for_org', org_id=ORGANISATION_ID) + assert link_to_report.attrs["href"] == url_for( + '.download_organisation_usage_report', + org_id=ORGANISATION_ID, + selected_year=2021 + ) + + +@freeze_time("2021-11-12 11:09:00.061258") +def test_download_organisation_usage_report( + client_request, + mock_get_organisation, + mocker, + active_user_with_permissions, + fake_uuid, +): + mocker.patch( + 'app.organisations_client.get_services_and_usage', + return_value={"services": [ + { + 'service_id': SERVICE_ONE_ID, + 'service_name': 'Service 1', + 'chargeable_billable_sms': 22, + 'emails_sent': 13000, + 'free_sms_limit': 100, + 'letter_cost': 30.50, + 'sms_billable_units': 122, + 'sms_cost': 1.93, + 'sms_remainder': None + }, + { + 'service_id': SERVICE_TWO_ID, + 'service_name': 'Service 1', + 'chargeable_billable_sms': 222, + 'emails_sent': 23000, + 'free_sms_limit': 250000, + 'letter_cost': 60.50, + 'sms_billable_units': 322, + 'sms_cost': 3.93, + 'sms_remainder': None + }, + ]} + ) + client_request.login(active_user_with_permissions) + csv_report = client_request.get( + '.download_organisation_usage_report', + org_id=ORGANISATION_ID, + selected_year=2021, + _test_page_title=False + ) + + assert csv_report.string == ( + "Service ID,Service Name,Emails sent,Free text message allowance remaining," + "Spent on text messages (£),Spent on letters (£)" + "\r\n596364a0-858e-42c8-9062-a8fe822260eb,Service 1,13000,,1.93,30.5" + "\r\n147ad62a-2951-4fa1-9ca0-093cd1a52c52,Service 1,23000,,3.93,60.5\r\n" + ) def test_organisation_trial_mode_services_shows_all_non_live_services( diff --git a/tests/app/test_navigation.py b/tests/app/test_navigation.py index 0f6e23116..fad19a5db 100644 --- a/tests/app/test_navigation.py +++ b/tests/app/test_navigation.py @@ -89,6 +89,7 @@ EXCLUDED_ENDPOINTS = tuple(map(Navigation.get_endpoint_with_blueprint, { 'documentation', 'download_contact_list', 'download_notifications_csv', + 'download_organisation_usage_report', 'edit_and_format_messages', 'edit_data_retention', 'edit_organisation_agreement',