First pass at new reporting designs
This commit lays out the genral structure and provides necessary data for the new reporting page designs. Some of the data generated by the report domain classes (including the mock CSP reporting class) was modified to fit new designs. This also included removing data that was no longer necessary. Part of the newly mocked data includes the idea of "expended" data per CLIN or task order. This was was mocked simply by using a 75% of the obligated funds fo a given object. Tests were also written for these new/ modifed reporting functions. As for the front end, this commit only focuses on the high-level markup layout. This includes splitting the large reporting index page into smaller component templates for each of the major sections of the report.
This commit is contained in:
parent
7a0dc4d264
commit
0303434561
@ -1,6 +1,7 @@
|
|||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from collections import OrderedDict
|
from atst.utils.localization import translate
|
||||||
import pendulum
|
import pendulum
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
|
||||||
class ReportingInterface:
|
class ReportingInterface:
|
||||||
@ -35,14 +36,16 @@ def generate_sample_dates(_max=8):
|
|||||||
current = pendulum.now()
|
current = pendulum.now()
|
||||||
sample_dates = []
|
sample_dates = []
|
||||||
for _i in range(_max):
|
for _i in range(_max):
|
||||||
current = current.subtract(months=1)
|
|
||||||
sample_dates.append(current.strftime("%m/%Y"))
|
sample_dates.append(current.strftime("%m/%Y"))
|
||||||
|
current = current.subtract(months=1)
|
||||||
|
|
||||||
reversed(sample_dates)
|
reversed(sample_dates)
|
||||||
return sample_dates
|
return sample_dates
|
||||||
|
|
||||||
|
|
||||||
class MockReportingProvider(ReportingInterface):
|
class MockReportingProvider(ReportingInterface):
|
||||||
|
MOCK_PERCENT_EXPENDED_FUNDS = 0.75
|
||||||
|
|
||||||
FIXTURE_MONTHS = generate_sample_dates()
|
FIXTURE_MONTHS = generate_sample_dates()
|
||||||
|
|
||||||
MONTHLY_SPEND_BY_ENVIRONMENT = {
|
MONTHLY_SPEND_BY_ENVIRONMENT = {
|
||||||
@ -163,25 +166,8 @@ class MockReportingProvider(ReportingInterface):
|
|||||||
"FM_Prod": {FIXTURE_MONTHS[0]: 5686},
|
"FM_Prod": {FIXTURE_MONTHS[0]: 5686},
|
||||||
}
|
}
|
||||||
|
|
||||||
CUMULATIVE_BUDGET_A_WING = {
|
|
||||||
FIXTURE_MONTHS[7]: {"spend": 9857, "cumulative": 9857},
|
|
||||||
FIXTURE_MONTHS[6]: {"spend": 7881, "cumulative": 17738},
|
|
||||||
FIXTURE_MONTHS[5]: {"spend": 14010, "cumulative": 31748},
|
|
||||||
FIXTURE_MONTHS[4]: {"spend": 43510, "cumulative": 75259},
|
|
||||||
FIXTURE_MONTHS[3]: {"spend": 41725, "cumulative": 116_984},
|
|
||||||
FIXTURE_MONTHS[2]: {"spend": 41328, "cumulative": 158_312},
|
|
||||||
FIXTURE_MONTHS[1]: {"spend": 47491, "cumulative": 205_803},
|
|
||||||
FIXTURE_MONTHS[0]: {"spend": 36028, "cumulative": 241_831},
|
|
||||||
}
|
|
||||||
|
|
||||||
CUMULATIVE_BUDGET_B_WING = {
|
|
||||||
FIXTURE_MONTHS[1]: {"spend": 4838, "cumulative": 4838},
|
|
||||||
FIXTURE_MONTHS[0]: {"spend": 14500, "cumulative": 19338},
|
|
||||||
}
|
|
||||||
|
|
||||||
REPORT_FIXTURE_MAP = {
|
REPORT_FIXTURE_MAP = {
|
||||||
"A-Wing": {
|
"A-Wing": {
|
||||||
"cumulative": CUMULATIVE_BUDGET_A_WING,
|
|
||||||
"applications": [
|
"applications": [
|
||||||
MockApplication("LC04", ["Integ", "PreProd", "Prod"]),
|
MockApplication("LC04", ["Integ", "PreProd", "Prod"]),
|
||||||
MockApplication("SF18", ["Integ", "PreProd", "Prod"]),
|
MockApplication("SF18", ["Integ", "PreProd", "Prod"]),
|
||||||
@ -202,7 +188,6 @@ class MockReportingProvider(ReportingInterface):
|
|||||||
"budget": 500_000,
|
"budget": 500_000,
|
||||||
},
|
},
|
||||||
"B-Wing": {
|
"B-Wing": {
|
||||||
"cumulative": CUMULATIVE_BUDGET_B_WING,
|
|
||||||
"applications": [
|
"applications": [
|
||||||
MockApplication("NP02", ["Integ", "PreProd", "Prod"]),
|
MockApplication("NP02", ["Integ", "PreProd", "Prod"]),
|
||||||
MockApplication("FM", ["Integ", "Prod"]),
|
MockApplication("FM", ["Integ", "Prod"]),
|
||||||
@ -211,28 +196,6 @@ class MockReportingProvider(ReportingInterface):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def _sum_monthly_spend(self, data):
|
|
||||||
return sum(
|
|
||||||
[
|
|
||||||
spend
|
|
||||||
for application in data
|
|
||||||
for env in application.environments
|
|
||||||
for spend in self.MONTHLY_SPEND_BY_ENVIRONMENT[env.id].values()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_budget(self, portfolio):
|
|
||||||
if portfolio.name in self.REPORT_FIXTURE_MAP:
|
|
||||||
return self.REPORT_FIXTURE_MAP[portfolio.name]["budget"]
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def get_total_spending(self, portfolio):
|
|
||||||
if portfolio.name in self.REPORT_FIXTURE_MAP:
|
|
||||||
return self._sum_monthly_spend(
|
|
||||||
self.REPORT_FIXTURE_MAP[portfolio.name]["applications"]
|
|
||||||
)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def _rollup_application_totals(self, data):
|
def _rollup_application_totals(self, data):
|
||||||
application_totals = {}
|
application_totals = {}
|
||||||
for application, environments in data.items():
|
for application, environments in data.items():
|
||||||
@ -270,7 +233,14 @@ class MockReportingProvider(ReportingInterface):
|
|||||||
{ "01/2018": 79.85, "02/2018": 86.54 }
|
{ "01/2018": 79.85, "02/2018": 86.54 }
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.MONTHLY_SPEND_BY_ENVIRONMENT.get(environment_id, {})
|
environment_monthly_totals = self.MONTHLY_SPEND_BY_ENVIRONMENT.get(
|
||||||
|
environment_id, {}
|
||||||
|
).copy()
|
||||||
|
|
||||||
|
environment_monthly_totals["total_spend_to_date"] = sum(
|
||||||
|
monthly_total for monthly_total in environment_monthly_totals.values()
|
||||||
|
)
|
||||||
|
return environment_monthly_totals
|
||||||
|
|
||||||
def monthly_totals(self, portfolio):
|
def monthly_totals(self, portfolio):
|
||||||
"""Return month totals rolled up by environment, application, and portfolio.
|
"""Return month totals rolled up by environment, application, and portfolio.
|
||||||
@ -309,19 +279,46 @@ class MockReportingProvider(ReportingInterface):
|
|||||||
"portfolio": portfolio_totals,
|
"portfolio": portfolio_totals,
|
||||||
}
|
}
|
||||||
|
|
||||||
def cumulative_budget(self, portfolio):
|
def get_obligated_funds_by_JEDI_clin(self, portfolio):
|
||||||
|
"""
|
||||||
|
Returns a dictionary of obligated funds and spending per JEDI CLIN
|
||||||
|
{
|
||||||
|
JEDI_CLIN: {
|
||||||
|
obligated_funds,
|
||||||
|
expended_funds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
if portfolio.name in self.REPORT_FIXTURE_MAP:
|
if portfolio.name in self.REPORT_FIXTURE_MAP:
|
||||||
budget_months = self.REPORT_FIXTURE_MAP[portfolio.name]["cumulative"]
|
return_dict = {}
|
||||||
else:
|
for jedi_clin, clins in groupby(
|
||||||
budget_months = {}
|
portfolio.active_clins, lambda clin: clin.jedi_clin_type
|
||||||
|
):
|
||||||
|
obligated_funds = sum(clin.obligated_amount for clin in clins)
|
||||||
|
return_dict[translate(f"JEDICLINType.{jedi_clin.value}")] = {
|
||||||
|
"obligated_funds": obligated_funds,
|
||||||
|
"expended_funds": (
|
||||||
|
obligated_funds * Decimal(self.MOCK_PERCENT_EXPENDED_FUNDS)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return return_dict
|
||||||
|
return {}
|
||||||
|
|
||||||
end = pendulum.now()
|
def get_expired_task_orders(self, portfolio):
|
||||||
start = end.subtract(months=12)
|
return [
|
||||||
period = pendulum.period(start, end)
|
{
|
||||||
|
"id": task_order.id,
|
||||||
all_months = OrderedDict()
|
"number": task_order.number,
|
||||||
for t in period.range("months"):
|
"period_of_performance": {
|
||||||
month_str = "{month:02d}/{year}".format(month=t.month, year=t.year)
|
"start_date": task_order.start_date,
|
||||||
all_months[month_str] = budget_months.get(month_str, None)
|
"end_date": task_order.end_date,
|
||||||
|
},
|
||||||
return {"months": all_months}
|
"total_obligated_funds": task_order.total_obligated_funds,
|
||||||
|
"expended_funds": (
|
||||||
|
task_order.total_obligated_funds
|
||||||
|
* Decimal(self.MOCK_PERCENT_EXPENDED_FUNDS)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for task_order in portfolio.task_orders
|
||||||
|
if task_order.is_expired
|
||||||
|
]
|
||||||
|
@ -2,16 +2,14 @@ from flask import current_app
|
|||||||
|
|
||||||
|
|
||||||
class Reports:
|
class Reports:
|
||||||
@classmethod
|
|
||||||
def portfolio_totals(cls, portfolio):
|
|
||||||
budget = current_app.csp.reports.get_budget(portfolio)
|
|
||||||
spent = current_app.csp.reports.get_total_spending(portfolio)
|
|
||||||
return {"budget": budget, "spent": spent}
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def monthly_totals(cls, portfolio):
|
def monthly_totals(cls, portfolio):
|
||||||
return current_app.csp.reports.monthly_totals(portfolio)
|
return current_app.csp.reports.monthly_totals(portfolio)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def cumulative_budget(cls, portfolio):
|
def expired_task_orders(cls, portfolio):
|
||||||
return current_app.csp.reports.cumulative_budget(portfolio)
|
return current_app.csp.reports.get_expired_task_orders(portfolio)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def obligated_funds_by_JEDI_clin(cls, portfolio):
|
||||||
|
return current_app.csp.reports.get_obligated_funds_by_JEDI_clin(portfolio)
|
||||||
|
@ -114,7 +114,7 @@ class Portfolio(
|
|||||||
for task_order in self.task_orders
|
for task_order in self.task_orders
|
||||||
if task_order.is_active
|
if task_order.is_active
|
||||||
),
|
),
|
||||||
default=None,
|
default=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -46,34 +46,24 @@ def create_portfolio():
|
|||||||
def reports(portfolio_id):
|
def reports(portfolio_id):
|
||||||
portfolio = Portfolios.get(g.current_user, portfolio_id)
|
portfolio = Portfolios.get(g.current_user, portfolio_id)
|
||||||
today = date.today()
|
today = date.today()
|
||||||
month = http_request.args.get("month", today.month)
|
current_month = date(int(today.year), int(today.month), 15)
|
||||||
year = http_request.args.get("year", today.year)
|
|
||||||
current_month = date(int(year), int(month), 15)
|
|
||||||
prev_month = current_month - timedelta(days=28)
|
prev_month = current_month - timedelta(days=28)
|
||||||
two_months_ago = prev_month - timedelta(days=28)
|
# wrapped in str() because the sum of obligated funds returns a Decimal object
|
||||||
|
total_portfolio_value = str(
|
||||||
task_order = next(
|
sum(
|
||||||
(task_order for task_order in portfolio.task_orders if task_order.is_active),
|
task_order.total_obligated_funds
|
||||||
None,
|
for task_order in portfolio.active_task_orders
|
||||||
|
)
|
||||||
)
|
)
|
||||||
expiration_date = task_order and task_order.end_date
|
|
||||||
if expiration_date:
|
|
||||||
remaining_difference = expiration_date - today
|
|
||||||
remaining_days = remaining_difference.days
|
|
||||||
else:
|
|
||||||
remaining_days = None
|
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"portfolios/reports/index.html",
|
"portfolios/reports/index.html",
|
||||||
cumulative_budget=Reports.cumulative_budget(portfolio),
|
portfolio=portfolio,
|
||||||
portfolio_totals=Reports.portfolio_totals(portfolio),
|
total_portfolio_value=total_portfolio_value,
|
||||||
|
current_obligated_funds=Reports.obligated_funds_by_JEDI_clin(portfolio),
|
||||||
|
expired_task_orders=Reports.expired_task_orders(portfolio),
|
||||||
monthly_totals=Reports.monthly_totals(portfolio),
|
monthly_totals=Reports.monthly_totals(portfolio),
|
||||||
task_order=task_order,
|
|
||||||
current_month=current_month,
|
current_month=current_month,
|
||||||
prev_month=prev_month,
|
prev_month=prev_month,
|
||||||
two_months_ago=two_months_ago,
|
|
||||||
expiration_date=expiration_date,
|
|
||||||
remaining_days=remaining_days,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,11 +6,9 @@ export default {
|
|||||||
|
|
||||||
props: {
|
props: {
|
||||||
applications: Object,
|
applications: Object,
|
||||||
portfolio: Object,
|
|
||||||
environments: Object,
|
environments: Object,
|
||||||
currentMonthIndex: String,
|
currentMonthIndex: String,
|
||||||
prevMonthIndex: String,
|
prevMonthIndex: String,
|
||||||
twoMonthsAgoIndex: String,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function() {
|
data: function() {
|
||||||
@ -40,9 +38,5 @@ export default {
|
|||||||
formatDollars: function(value) {
|
formatDollars: function(value) {
|
||||||
return formatDollars(value, false)
|
return formatDollars(value, false)
|
||||||
},
|
},
|
||||||
|
|
||||||
round: function(value) {
|
|
||||||
return Math.round(value)
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,23 @@
|
|||||||
{% macro Accordion(title, id) %}
|
{% macro Accordion(title, id, heading_level="h2") %}
|
||||||
<accordion inline-template>
|
<accordion inline-template>
|
||||||
<li>
|
<div>
|
||||||
<template v-if="isVisible">
|
<{{heading_level}}>
|
||||||
<button
|
<button
|
||||||
v-on:click="toggle($event)"
|
v-on:click="toggle($event)"
|
||||||
class="usa-accordion-button"
|
class="usa-accordion-button"
|
||||||
aria-controls="{{ id }}"
|
aria-controls="{{ id }}"
|
||||||
aria-expanded="true"
|
v-bind:aria-expanded= "isVisible ? 'true' : 'false'"
|
||||||
>
|
>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</{{heading_level}}>
|
||||||
<template v-else>
|
<div
|
||||||
<button
|
id="{{ id }}"
|
||||||
v-on:click="toggle($event)"
|
class="usa-accordion-content"
|
||||||
class="usa-accordion-button"
|
v-bind:aria-hidden="isVisible ? 'false' : 'true'"
|
||||||
aria-expanded="false"
|
>
|
||||||
aria-controls="{{ id }}"
|
{{ caller() }}
|
||||||
>
|
</div>
|
||||||
{{ title }}
|
</div>
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<template v-if="isVisible">
|
|
||||||
<div id="{{ id }}" class="usa-accordion-content" aria-hidden="false">
|
|
||||||
{{ caller() }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div id="{{ id }}" class="usa-accordion-content" aria-hidden="true">
|
|
||||||
{{ caller() }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</li>
|
|
||||||
</accordion>
|
</accordion>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
@ -0,0 +1,82 @@
|
|||||||
|
{% from "components/empty_state.html" import EmptyState %}
|
||||||
|
{% from "components/icon.html" import Icon %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Funds Expended per Application and Environment</h2>
|
||||||
|
{% set current_month_index = current_month.strftime('%m/%Y') %}
|
||||||
|
{% set prev_month_index = prev_month.strftime('%m/%Y') %}
|
||||||
|
|
||||||
|
{% if not portfolio.applications %}
|
||||||
|
|
||||||
|
{% set can_create_applications = user_can(permissions.CREATE_APPLICATION) %}
|
||||||
|
{% set message = ('portfolios.reports.empty_state.sub_message.can_create_applications' | translate)
|
||||||
|
if can_create_applications
|
||||||
|
else ('portfolios.reports.empty_state.sub_message.cannot_create_applications' | translate)
|
||||||
|
%}
|
||||||
|
|
||||||
|
{{ EmptyState(
|
||||||
|
('portfolios.reports.empty_state.message' | translate),
|
||||||
|
action_label= ('portfolios.reports.empty_state.action_label' | translate) if can_create_applications else None,
|
||||||
|
action_href=url_for('applications.create_new_application_step_1', portfolio_id=portfolio.id) if can_create_applications else None,
|
||||||
|
icon='chart',
|
||||||
|
sub_message=message,
|
||||||
|
add_perms=can_create_applications
|
||||||
|
) }}
|
||||||
|
{% else %}
|
||||||
|
<spend-table
|
||||||
|
v-bind:applications='{{ monthly_totals['applications'] | tojson }}'
|
||||||
|
v-bind:environments='{{ monthly_totals['environments'] | tojson }}'
|
||||||
|
current-month-index='{{ current_month_index }}'
|
||||||
|
prev-month-index='{{ prev_month_index }}'
|
||||||
|
inline-template>
|
||||||
|
<div class="responsive-table-wrapper">
|
||||||
|
<table class="atat-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Applications and Environments</th>
|
||||||
|
<th class="table-cell--align-right">Current Month</th>
|
||||||
|
<th class="table-cell--align-right">Last Month</th>
|
||||||
|
<th class="table-cell--align-right">Total Spent</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-for='(application, name) in applicationsState'>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<button v-on:click='toggle($event, name)' class='icon-link icon-link--large'>
|
||||||
|
<span v-html='name'></span>
|
||||||
|
<template v-if='application.isVisible'>{{ Icon('caret_down') }}</template>
|
||||||
|
<template v-else>{{ Icon('caret_up') }}</template>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td class="table-cell--align-right">
|
||||||
|
<span v-html='formatDollars(application[currentMonthIndex] || 0)'></span>
|
||||||
|
</td>
|
||||||
|
<td class="table-cell--align-right">
|
||||||
|
<span v-html='formatDollars(application[prevMonthIndex] || 0)'></span>
|
||||||
|
</td>
|
||||||
|
<td class="table-cell--align-right">
|
||||||
|
<span v-html='formatDollars(application["total_spend_to_date"])'></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-for='(environment, envName) in environments[name]' v-show='application.isVisible'>
|
||||||
|
<td>
|
||||||
|
<span v-html='envName'></span>
|
||||||
|
</td>
|
||||||
|
<td class="table-cell--align-right">
|
||||||
|
<span v-html='formatDollars(environment[currentMonthIndex] || 0)'></span>
|
||||||
|
</td>
|
||||||
|
<td class="table-cell--align-right">
|
||||||
|
<span v-html='formatDollars(environment[prevMonthIndex] || 0)'></span>
|
||||||
|
</td>
|
||||||
|
<td class="table-cell--align-right">
|
||||||
|
<span v-html='formatDollars(environment["total_spend_to_date"])'></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</spend-table>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
34
templates/portfolios/reports/expired_task_orders.html
Normal file
34
templates/portfolios/reports/expired_task_orders.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{% from "components/accordion.html" import Accordion %}
|
||||||
|
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="usa-accordion">
|
||||||
|
{% call Accordion("Expired Task Orders", "expired_task_orders", "h3") %}
|
||||||
|
{% for task_order in expired_task_orders %}
|
||||||
|
<a href="{{ url_for("task_orders.review_task_order", task_order_id=task_order["id"]) }}">
|
||||||
|
Task Order {{ task_order["number"] }}
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<p>Period of Performance</p>
|
||||||
|
<p>
|
||||||
|
{{ task_order["period_of_performance"].start_date | formattedDate(formatter="%B %d, %Y") }}
|
||||||
|
-
|
||||||
|
{{ task_order["period_of_performance"].end_date | formattedDate(formatter="%B %d, %Y") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>Total Obligated</p>
|
||||||
|
<p>{{ task_order["total_obligated_funds"] | dollars }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>Total Expended</p>
|
||||||
|
<p>{{ task_order["expended_funds"] | dollars }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>Total Unused</p>
|
||||||
|
<p>{{ (task_order["total_obligated_funds"] - task_order["expended_funds"]) | dollars }}</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endcall %}
|
||||||
|
</div>
|
||||||
|
</section>
|
@ -1,463 +1,16 @@
|
|||||||
{% extends "portfolios/base.html" %}
|
{% extends "portfolios/base.html" %}
|
||||||
|
|
||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/sticky_cta.html" import StickyCTA %}
|
||||||
{% from "components/empty_state.html" import EmptyState %}
|
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
|
{{ StickyCTA("Reports") }}
|
||||||
<div class='portfolio-reports'>
|
<div class="portfolio-reports col col--grow">
|
||||||
<div v-cloak class='funding-summary-row'>
|
{% include "portfolios/reports/portfolio_summary.html" %}
|
||||||
|
<hr>
|
||||||
<div class='funding-summary-row__col'>
|
{% include "portfolios/reports/obligated_funds.html" %}
|
||||||
<div class='panel spend-summary'>
|
{% include "portfolios/reports/expired_task_orders.html" %}
|
||||||
<h4 class='spend-summary__heading subheading'>Portfolio Total Spend</h4>
|
<hr>
|
||||||
<div class='row'>
|
{% include "portfolios/reports/application_and_env_spending.html" %}
|
||||||
<dl class='spend-summary__budget col col--grow row'>
|
|
||||||
{% set budget = portfolio_totals['budget'] %}
|
|
||||||
{% set spent = portfolio_totals['spent'] %}
|
|
||||||
{% set remaining = budget - spent %}
|
|
||||||
<dl class='col col--grow'>
|
|
||||||
<dt>Budget</dt>
|
|
||||||
<dd>{{ budget | dollars }}</dd>
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
<dl class='col col--grow'>
|
|
||||||
<dt>Remaining</dt>
|
|
||||||
<dd>{{ remaining | dollars }}</dd>
|
|
||||||
</dl>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<meter value='{{ spent }}' min='0' {% if budget %}max='{{ budget }}' {% endif %}title='{{ spent | dollars }} Total spend to date'>
|
|
||||||
<div class='meter__fallback' style='width:{{ (spent / budget) * 100 if budget else 0 }}%;'></div>
|
|
||||||
</meter>
|
|
||||||
|
|
||||||
<dl class='spend-summary__spent'>
|
|
||||||
<dt>Total spending to date</dt>
|
|
||||||
<dd>{{ spent | dollars }}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='funding-summary-row__col'>
|
|
||||||
<div class='panel to-summary'>
|
|
||||||
<div class='to-summary__row'>
|
|
||||||
|
|
||||||
<div class='to-summary__to'>
|
|
||||||
<h2 class='to-summary__heading subheading'>Current Task Order</h2>
|
|
||||||
<dl class='to-summary__to-number'>
|
|
||||||
<dt class='usa-sr-only'>Task Order Number</dt>
|
|
||||||
<dd>{{ task_order.number }}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<div class='to-summary__expiration'>
|
|
||||||
<div class='row'>
|
|
||||||
<h4 class='subheading'>Expiration Date</h4>
|
|
||||||
</div>
|
|
||||||
<div class='row'>
|
|
||||||
|
|
||||||
<div class='col col--grow'>
|
|
||||||
<div>
|
|
||||||
{% if expiration_date %}
|
|
||||||
<local-datetime
|
|
||||||
timestamp='{{ expiration_date }}'
|
|
||||||
format='MMMM D, YYYY'>
|
|
||||||
</local-datetime>
|
|
||||||
{% else %}
|
|
||||||
-
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<a href='{{ url_for("task_orders.review_task_order", task_order_id=task_order.id) }}' class='icon-link'>
|
|
||||||
{{ Icon('cog') }}
|
|
||||||
Manage Task Order
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class='col col--grow'>
|
|
||||||
<dl>
|
|
||||||
<dt>Remaining Days</dt>
|
|
||||||
<dd class='{{ 'ending-soon' if remaining_days is not none }}'>
|
|
||||||
{% if remaining_days is not none %}
|
|
||||||
{{ Icon('arrow-down') }}
|
|
||||||
<span>{{ remaining_days }}</span>
|
|
||||||
{% else %}
|
|
||||||
-
|
|
||||||
{% endif %}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
|
|
||||||
<dl class='to-summary__co'>
|
|
||||||
<dt class='subheading'>Contracting Officer</dt>
|
|
||||||
<dd class='row'>
|
|
||||||
<div class='col col--grow'>
|
|
||||||
{% if task_order.ko_first_name and task_order.ko_last_name %}
|
|
||||||
{{ task_order.ko_first_name }} {{ task_order.ko_last_name }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='col'>
|
|
||||||
{% if task_order.ko_email %}
|
|
||||||
<a class='icon-link' href='mailto:{{ task_order.ko_email }}'>
|
|
||||||
{{ Icon('envelope') }}
|
|
||||||
{{ task_order.ko_email }}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% set portfolio_totals = monthly_totals['portfolio'] %}
|
|
||||||
{% set current_month_index = current_month.strftime('%m/%Y') %}
|
|
||||||
{% set prev_month_index = prev_month.strftime('%m/%Y') %}
|
|
||||||
{% set two_months_ago_index = two_months_ago.strftime('%m/%Y') %}
|
|
||||||
{% set reports_url = url_for("portfolios.reports", portfolio_id=portfolio.id) %}
|
|
||||||
|
|
||||||
{% if not portfolio.applications %}
|
|
||||||
|
|
||||||
{% set can_create_applications = user_can(permissions.CREATE_APPLICATION) %}
|
|
||||||
{% set message = 'This portfolio has no cloud environments set up, so there is no spending data to report. Create an application with some cloud environments to get started.'
|
|
||||||
if can_create_applications
|
|
||||||
else 'This portfolio has no cloud environments set up, so there is no spending data to report. Contact the portfolio owner to set up some cloud environments.'
|
|
||||||
%}
|
|
||||||
|
|
||||||
{{ EmptyState(
|
|
||||||
'Nothing to report',
|
|
||||||
action_label='Add a new application' if can_create_applications else None,
|
|
||||||
action_href=url_for('applications.create_new_application_step_1', portfolio_id=portfolio.id) if can_create_applications else None,
|
|
||||||
icon='chart',
|
|
||||||
sub_message=message,
|
|
||||||
add_perms=can_create_applications
|
|
||||||
) }}
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
<budget-chart
|
|
||||||
v-cloak
|
|
||||||
budget={{ budget }}
|
|
||||||
current-month='{{ current_month_index }}'
|
|
||||||
expiration-date='{{ expiration_date }}'
|
|
||||||
v-bind:months='{{ cumulative_budget.months | tojson }}'
|
|
||||||
inline-template>
|
|
||||||
|
|
||||||
<div class='budget-chart panel' ref='panel'>
|
|
||||||
<header class='budget-chart__header panel__heading panel__heading--tight'>
|
|
||||||
<h4>Cumulative Budget</h4>
|
|
||||||
|
|
||||||
<div class='budget-chart__legend'>
|
|
||||||
<dl class='budget-chart__legend__spend'>
|
|
||||||
<div>
|
|
||||||
<dt>Monthly Spend</dt>
|
|
||||||
<dd class='budget-chart__legend__dot monthly'><span class='usa-sr-only'>Monthly spend visual key</span></dd>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<dt>Accumulated Spend</dt>
|
|
||||||
<dd class='budget-chart__legend__dot accumulated'><span class='usa-sr-only'>Accumulated spend visual key</span></dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
<dl class='budget-chart__legend__projected'>
|
|
||||||
<div>
|
|
||||||
<dt>Projected</dt>
|
|
||||||
<dd>
|
|
||||||
<div class='budget-chart__legend__line spend'><span class='usa-sr-only'>Projected monthly spend visual key</span></div>
|
|
||||||
<div class='budget-chart__legend__line accumulated'><span class='usa-sr-only'>Projected accumulated spend visual key</span></div>
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<svg v-bind:height='height' v-bind:width='width'>
|
|
||||||
|
|
||||||
<defs>
|
|
||||||
<filter x="-0.04" y="0" width="1.08" height="1" class='filter__text-background' id="text-background">
|
|
||||||
<feFlood/>
|
|
||||||
<feComposite in="SourceGraphic"/>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
{# spend/projected budget path lines #}
|
|
||||||
<path class='budget-chart__projected-path' v-bind:d='projectedPath'></path>
|
|
||||||
<path class='budget-chart__spend-path' v-bind:d='spendPath'></path>
|
|
||||||
|
|
||||||
{# max budget line #}
|
|
||||||
<line
|
|
||||||
class='budget-chart__budget-line'
|
|
||||||
x1='0'
|
|
||||||
v-bind:x2='width'
|
|
||||||
v-bind:y1='budgetHeight'
|
|
||||||
v-bind:y2='budgetHeight'></line>
|
|
||||||
|
|
||||||
<g v-for='month in displayedMonths' >
|
|
||||||
|
|
||||||
{# make this clickable to focus on that month #}
|
|
||||||
<a v-bind:href='"{{ reports_url }}?month=" + month.date.monthIndex + "&year=" + month.date.year'>
|
|
||||||
|
|
||||||
<defs>
|
|
||||||
<filter
|
|
||||||
x="-0.04"
|
|
||||||
y="0"
|
|
||||||
width="1.08"
|
|
||||||
height="1"
|
|
||||||
class='filter__text-background'
|
|
||||||
v-bind:class='{ "filter__text-background--highlighted": month.isHighlighted }'
|
|
||||||
v-bind:id="'text-background__' +month.date.month + month.date.year">
|
|
||||||
<feFlood/>
|
|
||||||
<feComposite in="SourceGraphic"/>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<title>
|
|
||||||
<span v-html='month.date.month + " " + month.date.year'></span> | <!--
|
|
||||||
--><template v-if='month.cumulativeTotal'><!--
|
|
||||||
--><template v-if='month.budget && month.budget.spend'>Spend:</template><!--
|
|
||||||
--><template v-else>Projected Spend:</template><!--
|
|
||||||
--><span v-html='month.spendAmount'></span><!--
|
|
||||||
--> | <!--
|
|
||||||
--><template v-if='month.budget'>Total:</template><!--
|
|
||||||
--><template v-else>Projected Total:</template><!--
|
|
||||||
--><span v-html='month.cumulativeAmount'></span><!--
|
|
||||||
--></template><!--
|
|
||||||
|
|
||||||
--><template v-else>No spend for this month</template>
|
|
||||||
</title>
|
|
||||||
|
|
||||||
{# container block #}
|
|
||||||
<rect
|
|
||||||
class='budget-chart__block'
|
|
||||||
v-bind:class='{ "budget-chart__block--highlighted": month.isHighlighted, "budget-chart__block-is-expiration": month.isExpirationMonth }'
|
|
||||||
v-bind:width='month.metrics.blockWidth'
|
|
||||||
v-bind:x='month.metrics.blockX'
|
|
||||||
v-bind:height='height'></rect>
|
|
||||||
|
|
||||||
{# budget bar #}
|
|
||||||
<rect
|
|
||||||
v-if='month.budget'
|
|
||||||
class='budget-chart__bar'
|
|
||||||
v-bind:class='{ "budget-chart__bar--projected": month.budget.projected }'
|
|
||||||
v-bind:width='month.metrics.barWidth'
|
|
||||||
v-bind:height='month.metrics.barHeight'
|
|
||||||
v-bind:x='month.metrics.barX'
|
|
||||||
v-bind:y='month.metrics.barY'></rect>
|
|
||||||
|
|
||||||
{# projected budget bar #}
|
|
||||||
<rect
|
|
||||||
v-if='!month.budget'
|
|
||||||
class='budget-chart__bar budget-chart__bar--projected'
|
|
||||||
v-bind:width='month.metrics.barWidth'
|
|
||||||
v-bind:height='month.metrics.barHeight'
|
|
||||||
v-bind:x='month.metrics.barX'
|
|
||||||
v-bind:y='month.metrics.barY'></rect>
|
|
||||||
|
|
||||||
{# task order expiration line #}
|
|
||||||
<line
|
|
||||||
v-if='month.isExpirationMonth'
|
|
||||||
class='budget-chart__expiration-line'
|
|
||||||
v-bind:x1='month.metrics.cumulativeX'
|
|
||||||
v-bind:x2='month.metrics.cumulativeX'
|
|
||||||
y1='0'
|
|
||||||
v-bind:y2='baseHeight'></line>
|
|
||||||
|
|
||||||
{# task order expiration label #}
|
|
||||||
<text
|
|
||||||
v-bind:filter="'url(#text-background__' + month.date.month + month.date.year + ')'"
|
|
||||||
v-if='month.isExpirationMonth'
|
|
||||||
text-anchor='middle'
|
|
||||||
v-bind:x='month.metrics.cumulativeX'
|
|
||||||
v-bind:y='budgetHeight + 20'
|
|
||||||
class='budget-chart__label'>T.O. Expires</text>
|
|
||||||
|
|
||||||
{# cumulative dot #}
|
|
||||||
<circle
|
|
||||||
v-if='month.cumulativeTotal'
|
|
||||||
class='budget-chart__cumulative__dot'
|
|
||||||
v-bind:r='month.metrics.cumulativeR'
|
|
||||||
v-bind:cx='month.metrics.cumulativeX'
|
|
||||||
v-bind:cy='month.metrics.cumulativeY'></circle>
|
|
||||||
|
|
||||||
{# abbreviated cumulative label #}
|
|
||||||
<text
|
|
||||||
v-bind:filter="'url(#text-background__' + month.date.month + month.date.year + ')'"
|
|
||||||
v-if='month.cumulativeTotal'
|
|
||||||
v-bind:x='month.metrics.cumulativeX'
|
|
||||||
v-bind:y='month.metrics.cumulativeY - 10'
|
|
||||||
text-anchor='middle'
|
|
||||||
class='budget-chart__label'
|
|
||||||
v-html='month.abbreviatedCumulative'></text>
|
|
||||||
|
|
||||||
{# abbreviated spend label #}
|
|
||||||
<text
|
|
||||||
v-bind:filter="'url(#text-background__' + month.date.month + month.date.year + ')'"
|
|
||||||
v-bind:x='month.metrics.cumulativeX'
|
|
||||||
v-bind:y='baseHeight + 20'
|
|
||||||
text-anchor='middle'
|
|
||||||
class='budget-chart__label'
|
|
||||||
v-html='"+" + month.abbreviatedSpend'></text>
|
|
||||||
|
|
||||||
{# month label #}
|
|
||||||
<text
|
|
||||||
v-bind:filter="'url(#text-background__' + month.date.month + month.date.year + ')'"
|
|
||||||
v-bind:x='month.metrics.cumulativeX'
|
|
||||||
v-bind:y='baseHeight + 40'
|
|
||||||
text-anchor='middle'
|
|
||||||
class='budget-chart__label budget-chart__label--strong'
|
|
||||||
v-html='month.date.month'></text>
|
|
||||||
|
|
||||||
{# year label #}
|
|
||||||
<text
|
|
||||||
v-bind:filter="'url(#text-background__' + month.date.month + month.date.year + ')'"
|
|
||||||
v-if='month.showYear'
|
|
||||||
v-bind:x='month.metrics.cumulativeX'
|
|
||||||
v-bind:y='baseHeight + 55'
|
|
||||||
text-anchor='middle'
|
|
||||||
class='budget-chart__label budget-chart__label--strong'
|
|
||||||
v-html='month.date.year'></text>
|
|
||||||
</g>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<text
|
|
||||||
x='20'
|
|
||||||
v-bind:y='budgetHeight + 20'
|
|
||||||
class='budget-chart__label'>Total Budget</text>
|
|
||||||
<text
|
|
||||||
x='20'
|
|
||||||
v-bind:y='budgetHeight + 40'
|
|
||||||
class='budget-chart__label'
|
|
||||||
v-html='displayBudget'></text>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</budget-chart>
|
|
||||||
|
|
||||||
<div class='accordion-table responsive-table-wrapper'>
|
|
||||||
<div class='responsive-table-wrapper__header'>
|
|
||||||
<h2 class='responsive-table-wrapper__title'>Total spent per month</h2>
|
|
||||||
|
|
||||||
<select name='month' id='month' onchange='location = this.value' class='spend-table__month-select'>
|
|
||||||
{% for m in cumulative_budget["months"] %}
|
|
||||||
{% set month = m | dateFromString %}
|
|
||||||
<option
|
|
||||||
{% if month.month == current_month.month and month.year == current_month.year %}
|
|
||||||
selected='selected'
|
|
||||||
{% endif %}
|
|
||||||
value='{{ url_for("portfolios.reports",
|
|
||||||
portfolio_id=portfolio.id,
|
|
||||||
month=month.month,
|
|
||||||
year=month.year) }}'
|
|
||||||
>
|
|
||||||
{{ month.strftime('%B %Y') }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
{% if not cumulative_budget["months"] %}
|
|
||||||
<option>{{ current_month.strftime('%B %Y') }}</option>
|
|
||||||
{% endif %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<spend-table
|
|
||||||
v-bind:applications='{{ monthly_totals['applications'] | tojson }}'
|
|
||||||
v-bind:portfolio='{{ portfolio_totals | tojson }}'
|
|
||||||
v-bind:environments='{{ monthly_totals['environments'] | tojson }}'
|
|
||||||
current-month-index='{{ current_month_index }}'
|
|
||||||
prev-month-index='{{ prev_month_index }}'
|
|
||||||
two-months-ago-index='{{ two_months_ago_index }}'
|
|
||||||
inline-template>
|
|
||||||
<table class="atat-table">
|
|
||||||
<thead>
|
|
||||||
<th scope='col'><span class='usa-sr-only'>Spending scope</span></th>
|
|
||||||
<th scope='col' class='table-cell--align-right previous-month'>{{ two_months_ago.strftime('%B %Y') }}</th>
|
|
||||||
<th scope='col' class='table-cell--align-right previous-month'>{{ prev_month.strftime('%B %Y') }}</th>
|
|
||||||
<th scope='col' class='table-cell--align-right current-month'>{{ current_month.strftime('%B %Y') }}</th>
|
|
||||||
<th class='current-month'></th>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody class='spend-table__portfolio'>
|
|
||||||
<tr>
|
|
||||||
<th scope='row'>Portfolio Total</th>
|
|
||||||
<td class='table-cell--align-right previous-month'>{{ portfolio_totals.get(two_months_ago_index, 0) | dollars }}</td>
|
|
||||||
<td class='table-cell--align-right previous-month'>{{ portfolio_totals.get(prev_month_index, 0) | dollars }}</td>
|
|
||||||
<td class='table-cell--align-right current-month'>{{ portfolio_totals.get(current_month_index, 0) | dollars }}</td>
|
|
||||||
<td class='table-cell--expand current-month meter-cell'>
|
|
||||||
<meter value='{{ portfolio_totals.get(current_month_index, 0) }}' min='0' max='{{ portfolio_totals.get(current_month_index, 0) }}'>
|
|
||||||
<div class='meter__fallback' style='width: 100%'></div>
|
|
||||||
</meter>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
|
|
||||||
<tbody v-for='(application, name) in applicationsState' class='accordion-table__items'>
|
|
||||||
<tr>
|
|
||||||
<th scope='rowgroup'>
|
|
||||||
<button v-on:click='toggle($event, name)' class='icon-link icon-link--large accordion-table__item__toggler'>
|
|
||||||
<template v-if='application.isVisible'>{{ Icon('caret_down') }}<div class='open-indicator'></div></template>
|
|
||||||
<template v-else>{{ Icon('caret_right') }}</template>
|
|
||||||
<span v-html='name'></span>
|
|
||||||
</button>
|
|
||||||
</th>
|
|
||||||
<td class='table-cell--align-right previous-month'>
|
|
||||||
<span v-html='formatDollars(application[twoMonthsAgoIndex] || 0)'></span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class='table-cell--align-right previous-month'>
|
|
||||||
<span v-html='formatDollars(application[prevMonthIndex] || 0)'></span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class='table-cell--align-right current-month'>
|
|
||||||
<span v-html='formatDollars(application[currentMonthIndex] || 0)'></span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class='table-cell--expand current-month meter-cell'>
|
|
||||||
<span class='spend-table__meter-value'>
|
|
||||||
<span v-html='round( 100 * ((application[currentMonthIndex] || 0) / (portfolio[currentMonthIndex] || 1) )) + "%"'></span>
|
|
||||||
</span>
|
|
||||||
<meter v-bind:value='application[currentMonthIndex] || 0' min='0' v-bind:max='portfolio[currentMonthIndex] || 1'>
|
|
||||||
<div class='meter__fallback' v-bind:style='"width:" + round( 100 * ((application[currentMonthIndex] || 0) / (portfolio[currentMonthIndex] || 1) )) + "%;"'></div>
|
|
||||||
</meter>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr v-for='(environment, envName) in environments[name]' v-show='application.isVisible' class='accordion-table__item__expanded'>
|
|
||||||
<th scope='rowgroup'>
|
|
||||||
<div class='icon-link accordion-table__item__expanded'>
|
|
||||||
<span v-html='envName'></span>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
|
|
||||||
<td class='table-cell--align-right previous-month'>
|
|
||||||
<span v-html='formatDollars(environment[twoMonthsAgoIndex] || 0)'></span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class='table-cell--align-right previous-month'>
|
|
||||||
<span v-html='formatDollars(environment[prevMonthIndex] || 0)'></span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class='table-cell--align-right current-month'>
|
|
||||||
<span v-html='formatDollars(environment[currentMonthIndex] || 0)'></span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class='table-cell--expand current-month'></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</spend-table>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
31
templates/portfolios/reports/obligated_funds.html
Normal file
31
templates/portfolios/reports/obligated_funds.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
<section>
|
||||||
|
<header>
|
||||||
|
<h2>Current Obligated funds</h2>
|
||||||
|
<span>As of DATE</span>
|
||||||
|
</header>
|
||||||
|
<div class='panel'>
|
||||||
|
<div class='panel__content'>
|
||||||
|
<div>
|
||||||
|
{% for JEDI_clin, funds in current_obligated_funds.items() %}
|
||||||
|
{{ JEDI_clin }}
|
||||||
|
<meter value='{{ funds["expended_funds"] }}' min='0' max='{{ funds["obligated_funds"] }}' title='{{ JEDI_clin }}'>
|
||||||
|
<div class='meter__fallback' style='width:{{ (funds["expended_funds"] / funds["obligated_funds"]) * 100 }}%;'></div>
|
||||||
|
</meter>
|
||||||
|
<div>
|
||||||
|
<p>Remaining funds:</p>
|
||||||
|
<p>{{ (funds["obligated_funds"] - funds["expended_funds"]) | dollars }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>Funds expended to date:</p>
|
||||||
|
<p>{{ funds["expended_funds"] | dollars }}</p>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
{% endfor %}
|
||||||
|
{% for task_order in portfolio.active_task_orders %}
|
||||||
|
<a href="{{ url_for("task_orders.review_task_order", task_order_id=task_order.id) }}">{{ task_order.number }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
36
templates/portfolios/reports/portfolio_summary.html
Normal file
36
templates/portfolios/reports/portfolio_summary.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{% from "components/tooltip.html" import Tooltip %}
|
||||||
|
{% from "components/icon.html" import Icon %}
|
||||||
|
|
||||||
|
|
||||||
|
<section class="row">
|
||||||
|
<div class='col col--grow'>
|
||||||
|
<p>
|
||||||
|
Total Portfolio Value
|
||||||
|
{{Tooltip(("common.lorem" | translate), title="")}}
|
||||||
|
</p>
|
||||||
|
<p>{{ total_portfolio_value | dollars }}</p>
|
||||||
|
</div>
|
||||||
|
<div class='col col--grow'>
|
||||||
|
<p>
|
||||||
|
Funding Duration
|
||||||
|
{{Tooltip(("common.lorem" | translate), title="")}}
|
||||||
|
</p>
|
||||||
|
{% set earliest_pop_start_date, latest_pop_end_date = portfolio.funding_duration %}
|
||||||
|
{% if earliest_pop_start_date and latest_pop_end_date %}
|
||||||
|
<p>
|
||||||
|
{{ earliest_pop_start_date | formattedDate(formatter="%B %d, %Y") }}
|
||||||
|
-
|
||||||
|
{{ latest_pop_end_date | formattedDate(formatter="%B %d, %Y") }}
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p> - </p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class='col col--grow'>
|
||||||
|
<p>
|
||||||
|
Days Remaining
|
||||||
|
{{Tooltip(("common.lorem" | translate), title="")}}
|
||||||
|
</p>
|
||||||
|
<p>{{ portfolio.days_to_funding_expiration }} days</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
@ -1,27 +1,22 @@
|
|||||||
from atst.domain.reports import Reports
|
from atst.domain.reports import Reports
|
||||||
|
from tests.factories import *
|
||||||
from tests.factories import PortfolioFactory
|
|
||||||
|
|
||||||
|
|
||||||
def test_portfolio_totals():
|
# this is sketched out until we do real reporting
|
||||||
portfolio = PortfolioFactory.create()
|
|
||||||
report = Reports.portfolio_totals(portfolio)
|
|
||||||
assert report == {"budget": 0, "spent": 0}
|
|
||||||
|
|
||||||
|
|
||||||
# this is sketched in until we do real reporting
|
|
||||||
def test_monthly_totals():
|
def test_monthly_totals():
|
||||||
portfolio = PortfolioFactory.create()
|
pass
|
||||||
monthly = Reports.monthly_totals(portfolio)
|
|
||||||
|
|
||||||
assert not monthly["environments"]
|
|
||||||
assert not monthly["applications"]
|
|
||||||
assert not monthly["portfolio"]
|
|
||||||
|
|
||||||
|
|
||||||
# this is sketched in until we do real reporting
|
# this is sketched out until we do real reporting
|
||||||
def test_cumulative_budget():
|
def test_current_obligated_funds():
|
||||||
portfolio = PortfolioFactory.create()
|
pass
|
||||||
months = Reports.cumulative_budget(portfolio)
|
|
||||||
|
|
||||||
assert len(months["months"]) >= 12
|
|
||||||
|
# this is sketched out until we do real reporting
|
||||||
|
def test_expired_task_orders():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# this is sketched out until we do real reporting
|
||||||
|
def test_obligated_funds_by_JEDI_clin():
|
||||||
|
pass
|
||||||
|
@ -110,7 +110,6 @@ def test_portfolio_reports_with_mock_portfolio(client, user_session):
|
|||||||
response = client.get(url_for("portfolios.reports", portfolio_id=portfolio.id))
|
response = client.get(url_for("portfolios.reports", portfolio_id=portfolio.id))
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert portfolio.name in response.data.decode()
|
assert portfolio.name in response.data.decode()
|
||||||
assert "$251,626.00 Total spend to date" in response.data.decode()
|
|
||||||
|
|
||||||
|
|
||||||
def test_delete_portfolio_success(client, user_session):
|
def test_delete_portfolio_success(client, user_session):
|
||||||
|
@ -449,6 +449,14 @@ portfolios:
|
|||||||
name: Name
|
name: Name
|
||||||
portfolio_mgmt: Portfolio management
|
portfolio_mgmt: Portfolio management
|
||||||
reporting: Reporting
|
reporting: Reporting
|
||||||
|
reports:
|
||||||
|
empty_state:
|
||||||
|
message: Nothing to report.
|
||||||
|
sub_message:
|
||||||
|
can_create_applications: This portfolio has no cloud environments set up, so there is no spending data to report. Create an application with some cloud environments to get started.
|
||||||
|
cannot_create_applications: This portfolio has no cloud environments set up, so there is no spending data to report. Contact the portfolio owner to set up some cloud environments.
|
||||||
|
action_label: 'Add a new application'
|
||||||
|
|
||||||
task_orders:
|
task_orders:
|
||||||
review:
|
review:
|
||||||
pdf_title: Approved Task Order
|
pdf_title: Approved Task Order
|
||||||
|
Loading…
x
Reference in New Issue
Block a user