Merge pull request #1236 from dod-ccpo/obligated-funds-graph-and-reporting-refactor
Obligated funds graph and reporting refactor
This commit is contained in:
commit
4ab27eb625
@ -1,344 +1,121 @@
|
||||
from itertools import groupby
|
||||
import pendulum
|
||||
from collections import defaultdict
|
||||
import json
|
||||
from decimal import Decimal
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
class ReportingInterface:
|
||||
def monthly_totals_for_environment(environment):
|
||||
"""Return the monthly totals for the specified environment.
|
||||
def load_fixture_data():
|
||||
with open("fixtures/fixture_spend_data.json") as json_file:
|
||||
return json.load(json_file)
|
||||
|
||||
Data should be in the format of a dictionary with the month as the key
|
||||
and the spend in that month as the value. For example:
|
||||
|
||||
{ "01/2018": 79.85, "02/2018": 86.54 }
|
||||
class MockReportingProvider:
|
||||
FIXTURE_SPEND_DATA = load_fixture_data()
|
||||
|
||||
@classmethod
|
||||
def get_portfolio_monthly_spending(cls, portfolio):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class MockEnvironment:
|
||||
def __init__(self, id_, env_name):
|
||||
self.id = id_
|
||||
self.name = env_name
|
||||
|
||||
|
||||
class MockApplication:
|
||||
def __init__(self, application_name, envs):
|
||||
def make_env(name):
|
||||
return MockEnvironment("{}_{}".format(application_name, name), name)
|
||||
|
||||
self.name = application_name
|
||||
self.environments = [make_env(env_name) for env_name in envs]
|
||||
|
||||
|
||||
def generate_sample_dates(_max=8):
|
||||
current = pendulum.now()
|
||||
sample_dates = []
|
||||
for _i in range(_max):
|
||||
sample_dates.append(current.strftime("%m/%Y"))
|
||||
current = current.subtract(months=1)
|
||||
|
||||
reversed(sample_dates)
|
||||
return sample_dates
|
||||
|
||||
|
||||
class MockReportingProvider(ReportingInterface):
|
||||
MOCK_PERCENT_EXPENDED_FUNDS = 0.75
|
||||
|
||||
FIXTURE_MONTHS = generate_sample_dates()
|
||||
|
||||
MONTHLY_SPEND_BY_ENVIRONMENT = {
|
||||
"LC04_Integ": {
|
||||
FIXTURE_MONTHS[7]: 284,
|
||||
FIXTURE_MONTHS[6]: 1210,
|
||||
FIXTURE_MONTHS[5]: 1430,
|
||||
FIXTURE_MONTHS[4]: 1366,
|
||||
FIXTURE_MONTHS[3]: 1169,
|
||||
FIXTURE_MONTHS[2]: 991,
|
||||
FIXTURE_MONTHS[1]: 978,
|
||||
FIXTURE_MONTHS[0]: 737,
|
||||
},
|
||||
"LC04_PreProd": {
|
||||
FIXTURE_MONTHS[7]: 812,
|
||||
FIXTURE_MONTHS[6]: 1389,
|
||||
FIXTURE_MONTHS[5]: 1425,
|
||||
FIXTURE_MONTHS[4]: 1306,
|
||||
FIXTURE_MONTHS[3]: 1112,
|
||||
FIXTURE_MONTHS[2]: 936,
|
||||
FIXTURE_MONTHS[1]: 921,
|
||||
FIXTURE_MONTHS[0]: 694,
|
||||
},
|
||||
"LC04_Prod": {
|
||||
FIXTURE_MONTHS[7]: 1742,
|
||||
FIXTURE_MONTHS[6]: 1716,
|
||||
FIXTURE_MONTHS[5]: 1866,
|
||||
FIXTURE_MONTHS[4]: 1809,
|
||||
FIXTURE_MONTHS[3]: 1839,
|
||||
FIXTURE_MONTHS[2]: 1633,
|
||||
FIXTURE_MONTHS[1]: 1654,
|
||||
FIXTURE_MONTHS[0]: 1103,
|
||||
},
|
||||
"SF18_Integ": {
|
||||
FIXTURE_MONTHS[5]: 1498,
|
||||
FIXTURE_MONTHS[4]: 1400,
|
||||
FIXTURE_MONTHS[3]: 1394,
|
||||
FIXTURE_MONTHS[2]: 1171,
|
||||
FIXTURE_MONTHS[1]: 1200,
|
||||
FIXTURE_MONTHS[0]: 963,
|
||||
},
|
||||
"SF18_PreProd": {
|
||||
FIXTURE_MONTHS[5]: 1780,
|
||||
FIXTURE_MONTHS[4]: 1667,
|
||||
FIXTURE_MONTHS[3]: 1703,
|
||||
FIXTURE_MONTHS[2]: 1474,
|
||||
FIXTURE_MONTHS[1]: 1441,
|
||||
FIXTURE_MONTHS[0]: 933,
|
||||
},
|
||||
"SF18_Prod": {
|
||||
FIXTURE_MONTHS[5]: 1686,
|
||||
FIXTURE_MONTHS[4]: 1779,
|
||||
FIXTURE_MONTHS[3]: 1792,
|
||||
FIXTURE_MONTHS[2]: 1570,
|
||||
FIXTURE_MONTHS[1]: 1539,
|
||||
FIXTURE_MONTHS[0]: 986,
|
||||
},
|
||||
"Canton_Prod": {
|
||||
FIXTURE_MONTHS[4]: 28699,
|
||||
FIXTURE_MONTHS[3]: 26766,
|
||||
FIXTURE_MONTHS[2]: 22619,
|
||||
FIXTURE_MONTHS[1]: 24090,
|
||||
FIXTURE_MONTHS[0]: 16719,
|
||||
},
|
||||
"BD04_Integ": {},
|
||||
"BD04_PreProd": {
|
||||
FIXTURE_MONTHS[7]: 7019,
|
||||
FIXTURE_MONTHS[6]: 3004,
|
||||
FIXTURE_MONTHS[5]: 2691,
|
||||
FIXTURE_MONTHS[4]: 2901,
|
||||
FIXTURE_MONTHS[3]: 3463,
|
||||
FIXTURE_MONTHS[2]: 3314,
|
||||
FIXTURE_MONTHS[1]: 3432,
|
||||
FIXTURE_MONTHS[0]: 723,
|
||||
},
|
||||
"SCV18_Dev": {FIXTURE_MONTHS[1]: 9797},
|
||||
"Crown_CR Portal Dev": {
|
||||
FIXTURE_MONTHS[6]: 208,
|
||||
FIXTURE_MONTHS[5]: 457,
|
||||
FIXTURE_MONTHS[4]: 671,
|
||||
FIXTURE_MONTHS[3]: 136,
|
||||
FIXTURE_MONTHS[2]: 1524,
|
||||
FIXTURE_MONTHS[1]: 2077,
|
||||
FIXTURE_MONTHS[0]: 1858,
|
||||
},
|
||||
"Crown_CR Staging": {
|
||||
FIXTURE_MONTHS[6]: 208,
|
||||
FIXTURE_MONTHS[5]: 457,
|
||||
FIXTURE_MONTHS[4]: 671,
|
||||
FIXTURE_MONTHS[3]: 136,
|
||||
FIXTURE_MONTHS[2]: 1524,
|
||||
FIXTURE_MONTHS[1]: 2077,
|
||||
FIXTURE_MONTHS[0]: 1858,
|
||||
},
|
||||
"Crown_CR Portal Test 1": {
|
||||
FIXTURE_MONTHS[2]: 806,
|
||||
FIXTURE_MONTHS[1]: 1966,
|
||||
FIXTURE_MONTHS[0]: 2597,
|
||||
},
|
||||
"Crown_Jewels Prod": {
|
||||
FIXTURE_MONTHS[2]: 806,
|
||||
FIXTURE_MONTHS[1]: 1966,
|
||||
FIXTURE_MONTHS[0]: 2597,
|
||||
},
|
||||
"Crown_Jewels Dev": {
|
||||
FIXTURE_MONTHS[6]: 145,
|
||||
FIXTURE_MONTHS[5]: 719,
|
||||
FIXTURE_MONTHS[4]: 1243,
|
||||
FIXTURE_MONTHS[3]: 2214,
|
||||
FIXTURE_MONTHS[2]: 2959,
|
||||
FIXTURE_MONTHS[1]: 4151,
|
||||
FIXTURE_MONTHS[0]: 4260,
|
||||
},
|
||||
"NP02_Integ": {FIXTURE_MONTHS[1]: 284, FIXTURE_MONTHS[0]: 1210},
|
||||
"NP02_PreProd": {FIXTURE_MONTHS[1]: 812, FIXTURE_MONTHS[0]: 1389},
|
||||
"NP02_Prod": {FIXTURE_MONTHS[1]: 3742, FIXTURE_MONTHS[0]: 4716},
|
||||
"FM_Integ": {FIXTURE_MONTHS[1]: 1498},
|
||||
"FM_Prod": {FIXTURE_MONTHS[0]: 5686},
|
||||
}
|
||||
|
||||
REPORT_FIXTURE_MAP = {
|
||||
"A-Wing": {
|
||||
"applications": [
|
||||
MockApplication("LC04", ["Integ", "PreProd", "Prod"]),
|
||||
MockApplication("SF18", ["Integ", "PreProd", "Prod"]),
|
||||
MockApplication("Canton", ["Prod"]),
|
||||
MockApplication("BD04", ["Integ", "PreProd"]),
|
||||
MockApplication("SCV18", ["Dev"]),
|
||||
MockApplication(
|
||||
"Crown",
|
||||
[
|
||||
"CR Portal Dev",
|
||||
"CR Staging",
|
||||
"CR Portal Test 1",
|
||||
"Jewels Prod",
|
||||
"Jewels Dev",
|
||||
],
|
||||
),
|
||||
],
|
||||
"budget": 500_000,
|
||||
},
|
||||
"B-Wing": {
|
||||
"applications": [
|
||||
MockApplication("NP02", ["Integ", "PreProd", "Prod"]),
|
||||
MockApplication("FM", ["Integ", "Prod"]),
|
||||
],
|
||||
"budget": 70000,
|
||||
},
|
||||
}
|
||||
|
||||
def _rollup_application_totals(self, data):
|
||||
application_totals = {}
|
||||
for application, environments in data.items():
|
||||
application_spend = [
|
||||
(month, spend)
|
||||
for env in environments.values()
|
||||
if env
|
||||
for month, spend in env.items()
|
||||
]
|
||||
application_totals[application] = {
|
||||
month: sum([spend[1] for spend in spends])
|
||||
for month, spends in groupby(sorted(application_spend), lambda x: x[0])
|
||||
}
|
||||
|
||||
return application_totals
|
||||
|
||||
def _rollup_portfolio_totals(self, application_totals):
|
||||
monthly_spend = [
|
||||
(month, spend)
|
||||
for application in application_totals.values()
|
||||
for month, spend in application.items()
|
||||
]
|
||||
portfolio_totals = {}
|
||||
for month, spends in groupby(sorted(monthly_spend), lambda m: m[0]):
|
||||
portfolio_totals[month] = sum([spend[1] for spend in spends])
|
||||
|
||||
return portfolio_totals
|
||||
|
||||
def monthly_totals_for_environment(self, environment_id):
|
||||
"""Return the monthly totals for the specified environment.
|
||||
|
||||
Data should be in the format of a dictionary with the month as the key
|
||||
and the spend in that month as the value. For example:
|
||||
|
||||
{ "01/2018": 79.85, "02/2018": 86.54 }
|
||||
|
||||
"""
|
||||
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):
|
||||
"""Return month totals rolled up by environment, application, and portfolio.
|
||||
|
||||
Data should returned with three top level keys, "portfolio", "applications",
|
||||
and "environments".
|
||||
The "applications" key will have budget data per month for each application,
|
||||
The "environments" key will have budget data for each environment.
|
||||
The "portfolio" key will be total monthly spending for the portfolio.
|
||||
For example:
|
||||
|
||||
returns an array of application and environment spending for the
|
||||
portfolio. Applications and their nested environments are sorted in
|
||||
alphabetical order by name.
|
||||
[
|
||||
{
|
||||
"environments": { "X-Wing": { "Prod": { "01/2018": 75.42 } } },
|
||||
"applications": { "X-Wing": { "01/2018": 75.42 } },
|
||||
"portfolio": { "01/2018": 75.42 },
|
||||
name
|
||||
this_month
|
||||
last_month
|
||||
total
|
||||
environments [
|
||||
{
|
||||
name
|
||||
this_month
|
||||
last_month
|
||||
total
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
]
|
||||
"""
|
||||
applications = portfolio.applications
|
||||
if portfolio.name in self.REPORT_FIXTURE_MAP:
|
||||
applications = self.REPORT_FIXTURE_MAP[portfolio.name]["applications"]
|
||||
environments = {
|
||||
application.name: {
|
||||
env.name: self.monthly_totals_for_environment(env.id)
|
||||
for env in application.environments
|
||||
}
|
||||
for application in applications
|
||||
}
|
||||
|
||||
application_totals = self._rollup_application_totals(environments)
|
||||
portfolio_totals = self._rollup_portfolio_totals(application_totals)
|
||||
|
||||
return {
|
||||
"environments": environments,
|
||||
"applications": application_totals,
|
||||
"portfolio": portfolio_totals,
|
||||
}
|
||||
|
||||
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:
|
||||
return_dict = {}
|
||||
for jedi_clin, clins in groupby(
|
||||
portfolio.active_clins, lambda clin: clin.jedi_clin_type
|
||||
):
|
||||
obligated_funds = sum(clin.obligated_amount for clin in clins)
|
||||
return_dict[jedi_clin.value] = {
|
||||
"obligated_funds": obligated_funds,
|
||||
"expended_funds": (
|
||||
obligated_funds * Decimal(self.MOCK_PERCENT_EXPENDED_FUNDS)
|
||||
),
|
||||
}
|
||||
return OrderedDict(
|
||||
# 0 index for dict item, -1 for last digit of 4 digit CLIN, e.g. 0001
|
||||
sorted(return_dict.items(), key=lambda clin: clin[0][-1])
|
||||
if portfolio.name in cls.FIXTURE_SPEND_DATA:
|
||||
applications = cls.FIXTURE_SPEND_DATA[portfolio.name]["applications"]
|
||||
return sorted(
|
||||
[
|
||||
cls._get_application_monthly_totals(application)
|
||||
for application in applications
|
||||
],
|
||||
key=lambda app: app["name"],
|
||||
)
|
||||
return {}
|
||||
return []
|
||||
|
||||
def get_expired_task_orders(self, portfolio):
|
||||
def sorted_task_orders(to_list):
|
||||
return sorted(to_list, key=lambda to: to["number"])
|
||||
@classmethod
|
||||
def _get_environment_monthly_totals(cls, environment):
|
||||
"""
|
||||
returns a dictionary that represents spending totals for an environment e.g.
|
||||
{
|
||||
name
|
||||
this_month
|
||||
last_month
|
||||
total
|
||||
}
|
||||
"""
|
||||
return {
|
||||
"name": environment["name"],
|
||||
"this_month": sum(environment["spending"]["this_month"].values()),
|
||||
"last_month": sum(environment["spending"]["last_month"].values()),
|
||||
"total": sum(environment["spending"]["total"].values()),
|
||||
}
|
||||
|
||||
def sorted_clins(clin_list):
|
||||
return sorted(clin_list, key=lambda clin: clin["number"])
|
||||
|
||||
def serialize_clin(clin):
|
||||
return {
|
||||
"number": clin.number,
|
||||
"jedi_clin_type": clin.jedi_clin_type,
|
||||
"period_of_performance": {
|
||||
"start_date": clin.start_date,
|
||||
"end_date": clin.end_date,
|
||||
},
|
||||
"total_value": clin.total_amount,
|
||||
"total_obligated_funds": clin.obligated_amount,
|
||||
"expended_funds": (
|
||||
clin.obligated_amount * Decimal(self.MOCK_PERCENT_EXPENDED_FUNDS)
|
||||
),
|
||||
@classmethod
|
||||
def _get_application_monthly_totals(cls, application):
|
||||
"""
|
||||
returns a dictionary that represents spending totals for an application
|
||||
and its environments e.g.
|
||||
{
|
||||
name
|
||||
this_month
|
||||
last_month
|
||||
total
|
||||
environments: [
|
||||
{
|
||||
name
|
||||
this_month
|
||||
last_month
|
||||
total
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return sorted_task_orders(
|
||||
"""
|
||||
environments = sorted(
|
||||
[
|
||||
{
|
||||
"id": task_order.id,
|
||||
"number": task_order.number,
|
||||
"clins": sorted_clins(
|
||||
[serialize_clin(clin) for clin in task_order.clins]
|
||||
),
|
||||
}
|
||||
for task_order in portfolio.task_orders
|
||||
if task_order.is_expired
|
||||
]
|
||||
cls._get_environment_monthly_totals(env)
|
||||
for env in application["environments"]
|
||||
],
|
||||
key=lambda env: env["name"],
|
||||
)
|
||||
return {
|
||||
"name": application["name"],
|
||||
"this_month": sum(env["this_month"] for env in environments),
|
||||
"last_month": sum(env["last_month"] for env in environments),
|
||||
"total": sum(env["total"] for env in environments),
|
||||
"environments": environments,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_spending_by_JEDI_clin(cls, portfolio):
|
||||
"""
|
||||
returns an dictionary of spending per JEDI CLIN for a portfolio
|
||||
{
|
||||
jedi_clin: {
|
||||
invoiced
|
||||
estimated
|
||||
},
|
||||
}
|
||||
"""
|
||||
if portfolio.name in cls.FIXTURE_SPEND_DATA:
|
||||
CLIN_spend_dict = defaultdict(lambda: defaultdict(Decimal))
|
||||
for application in cls.FIXTURE_SPEND_DATA[portfolio.name]["applications"]:
|
||||
for environment in application["environments"]:
|
||||
for clin, spend in environment["spending"]["this_month"].items():
|
||||
CLIN_spend_dict[clin]["estimated"] += Decimal(spend)
|
||||
for clin, spend in environment["spending"]["total"].items():
|
||||
CLIN_spend_dict[clin]["invoiced"] += Decimal(spend)
|
||||
return CLIN_spend_dict
|
||||
return {}
|
||||
|
@ -1,15 +1,44 @@
|
||||
from flask import current_app
|
||||
from itertools import groupby
|
||||
|
||||
|
||||
class Reports:
|
||||
@classmethod
|
||||
def monthly_totals(cls, portfolio):
|
||||
return current_app.csp.reports.monthly_totals(portfolio)
|
||||
def monthly_spending(cls, portfolio):
|
||||
return current_app.csp.reports.get_portfolio_monthly_spending(portfolio)
|
||||
|
||||
@classmethod
|
||||
def expired_task_orders(cls, portfolio):
|
||||
return current_app.csp.reports.get_expired_task_orders(portfolio)
|
||||
return [
|
||||
task_order for task_order in portfolio.task_orders if task_order.is_expired
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def obligated_funds_by_JEDI_clin(cls, portfolio):
|
||||
return current_app.csp.reports.get_obligated_funds_by_JEDI_clin(portfolio)
|
||||
clin_spending = current_app.csp.reports.get_spending_by_JEDI_clin(portfolio)
|
||||
active_clins = portfolio.active_clins
|
||||
for jedi_clin, clins in groupby(
|
||||
active_clins, key=lambda clin: clin.jedi_clin_type
|
||||
):
|
||||
if not clin_spending.get(jedi_clin.name):
|
||||
clin_spending[jedi_clin.name] = {}
|
||||
clin_spending[jedi_clin.name]["obligated"] = sum(
|
||||
clin.obligated_amount for clin in clins
|
||||
)
|
||||
|
||||
output = []
|
||||
for clin in clin_spending.keys():
|
||||
invoiced = clin_spending[clin].get("invoiced", 0)
|
||||
estimated = clin_spending[clin].get("estimated", 0)
|
||||
obligated = clin_spending[clin].get("obligated", 0)
|
||||
remaining = obligated - (invoiced + estimated)
|
||||
output.append(
|
||||
{
|
||||
"name": clin,
|
||||
"invoiced": invoiced,
|
||||
"estimated": estimated,
|
||||
"obligated": obligated,
|
||||
"remaining": remaining,
|
||||
}
|
||||
)
|
||||
return output
|
||||
|
@ -5,6 +5,7 @@ from flask import render_template
|
||||
from jinja2 import contextfilter
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
|
||||
from decimal import DivisionByZero as DivisionByZeroException
|
||||
|
||||
|
||||
def iconSvg(name):
|
||||
@ -38,6 +39,14 @@ def usPhone(number):
|
||||
return "+1 ({}) {} - {}".format(phone[0:3], phone[3:6], phone[6:])
|
||||
|
||||
|
||||
def obligatedFundingGraphWidth(values):
|
||||
numerator, denominator = values
|
||||
try:
|
||||
return (numerator / denominator) * 100
|
||||
except DivisionByZeroException:
|
||||
return 0
|
||||
|
||||
|
||||
def formattedDate(value, formatter="%m/%d/%Y"):
|
||||
if value:
|
||||
return value.strftime(formatter)
|
||||
@ -76,6 +85,7 @@ def register_filters(app):
|
||||
app.jinja_env.filters["pageWindow"] = pageWindow
|
||||
app.jinja_env.filters["renderAuditEvent"] = renderAuditEvent
|
||||
app.jinja_env.filters["withExtraParams"] = with_extra_params
|
||||
app.jinja_env.filters["obligatedFundingGraphWidth"] = obligatedFundingGraphWidth
|
||||
|
||||
@contextfilter
|
||||
def translateWithoutCache(context, *kwargs):
|
||||
|
@ -65,4 +65,6 @@ class CLIN(Base, mixins.TimestampsMixin):
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return self.start_date <= date.today() <= self.end_date
|
||||
return (
|
||||
self.start_date <= date.today() <= self.end_date
|
||||
) and self.task_order.signed_at
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import date, datetime, timedelta
|
||||
from datetime import datetime
|
||||
|
||||
from flask import redirect, render_template, url_for, request as http_request, g
|
||||
|
||||
@ -35,9 +35,12 @@ def create_portfolio():
|
||||
@user_can(Permissions.VIEW_PORTFOLIO_REPORTS, message="view portfolio reports")
|
||||
def reports(portfolio_id):
|
||||
portfolio = Portfolios.get(g.current_user, portfolio_id)
|
||||
today = date.today()
|
||||
current_month = date(int(today.year), int(today.month), 15)
|
||||
prev_month = current_month - timedelta(days=28)
|
||||
|
||||
current_obligated_funds = Reports.obligated_funds_by_JEDI_clin(portfolio)
|
||||
|
||||
if any(map(lambda clin: clin["remaining"] < 0, current_obligated_funds)):
|
||||
flash("insufficient_funds")
|
||||
|
||||
# wrapped in str() because the sum of obligated funds returns a Decimal object
|
||||
total_portfolio_value = str(
|
||||
sum(
|
||||
@ -49,12 +52,10 @@ def reports(portfolio_id):
|
||||
"portfolios/reports/index.html",
|
||||
portfolio=portfolio,
|
||||
total_portfolio_value=total_portfolio_value,
|
||||
current_obligated_funds=Reports.obligated_funds_by_JEDI_clin(portfolio),
|
||||
current_obligated_funds=current_obligated_funds,
|
||||
expired_task_orders=Reports.expired_task_orders(portfolio),
|
||||
monthly_totals=Reports.monthly_totals(portfolio),
|
||||
current_month=current_month,
|
||||
prev_month=prev_month,
|
||||
now=datetime.now(), # mocked datetime of reporting data retrival
|
||||
monthly_spending=Reports.monthly_spending(portfolio),
|
||||
retrieved=datetime.now(), # mocked datetime of reporting data retrival
|
||||
)
|
||||
|
||||
|
||||
|
@ -96,6 +96,11 @@ MESSAGES = {
|
||||
"message_template": "<p>Please see below.</p>",
|
||||
"category": "error",
|
||||
},
|
||||
"insufficient_funds": {
|
||||
"title_template": "Insufficient Funds",
|
||||
"message_template": "",
|
||||
"category": "warning",
|
||||
},
|
||||
"logged_out": {
|
||||
"title_template": translate("flash.logged_out"),
|
||||
"message_template": """
|
||||
|
390
fixtures/fixture_spend_data.json
Normal file
390
fixtures/fixture_spend_data.json
Normal file
@ -0,0 +1,390 @@
|
||||
{
|
||||
"A-Wing": {
|
||||
"applications": [
|
||||
{
|
||||
"name": "LC04",
|
||||
"environments": [
|
||||
{
|
||||
"name": "Integ",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 663,
|
||||
"JEDI_CLIN_2": 397
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 590,
|
||||
"JEDI_CLIN_2": 829
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 42467,
|
||||
"JEDI_CLIN_2": 33873
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "PreProd",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 1000,
|
||||
"JEDI_CLIN_2": 626
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 685,
|
||||
"JEDI_CLIN_2": 331
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 21874,
|
||||
"JEDI_CLIN_2": 25506
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Prod",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 856,
|
||||
"JEDI_CLIN_2": 627
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 921,
|
||||
"JEDI_CLIN_2": 473
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 35566,
|
||||
"JEDI_CLIN_2": 42514
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SF18",
|
||||
"environments": [
|
||||
{
|
||||
"name": "Integ",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 777,
|
||||
"JEDI_CLIN_2": 850
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 584,
|
||||
"JEDI_CLIN_2": 362
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 44505,
|
||||
"JEDI_CLIN_2": 21378
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "PreProd",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 487,
|
||||
"JEDI_CLIN_2": 733
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 542,
|
||||
"JEDI_CLIN_2": 999
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 8713,
|
||||
"JEDI_CLIN_2": 10586
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Prod",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 420,
|
||||
"JEDI_CLIN_2": 503
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 756,
|
||||
"JEDI_CLIN_2": 941
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 43003,
|
||||
"JEDI_CLIN_2": 20601
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Canton",
|
||||
"environments": [
|
||||
{
|
||||
"name": "Prod",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 661,
|
||||
"JEDI_CLIN_2": 599
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 962,
|
||||
"JEDI_CLIN_2": 383
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 24501,
|
||||
"JEDI_CLIN_2": 7551
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "BD04",
|
||||
"environments": [
|
||||
{
|
||||
"name": "Integ",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 790,
|
||||
"JEDI_CLIN_2": 513
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 886,
|
||||
"JEDI_CLIN_2": 991
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 43684,
|
||||
"JEDI_CLIN_2": 40196
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "PreProd",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 513,
|
||||
"JEDI_CLIN_2": 706
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 945,
|
||||
"JEDI_CLIN_2": 380
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 28189,
|
||||
"JEDI_CLIN_2": 9759
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SCV18",
|
||||
"environments": [
|
||||
{
|
||||
"name": "Dev",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 933,
|
||||
"JEDI_CLIN_2": 993
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 319,
|
||||
"JEDI_CLIN_2": 619
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 40585,
|
||||
"JEDI_CLIN_2": 28872
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Crown",
|
||||
"environments": [
|
||||
{
|
||||
"name": "CR Portal Dev",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 711,
|
||||
"JEDI_CLIN_2": 413
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 908,
|
||||
"JEDI_CLIN_2": 632
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 18753,
|
||||
"JEDI_CLIN_2": 4004
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "CR Staging",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 440,
|
||||
"JEDI_CLIN_2": 918
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 370,
|
||||
"JEDI_CLIN_2": 472
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 40602,
|
||||
"JEDI_CLIN_2": 6834
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "CR Portal Test 1",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 928,
|
||||
"JEDI_CLIN_2": 796
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 680,
|
||||
"JEDI_CLIN_2": 312
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 36058,
|
||||
"JEDI_CLIN_2": 42375
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Jewels Prod",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 304,
|
||||
"JEDI_CLIN_2": 428
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 898,
|
||||
"JEDI_CLIN_2": 729
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 3162,
|
||||
"JEDI_CLIN_2": 49836
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Jewels Dev",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 498,
|
||||
"JEDI_CLIN_2": 890
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 506,
|
||||
"JEDI_CLIN_2": 659
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 6248,
|
||||
"JEDI_CLIN_2": 3866
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"B-Wing": {
|
||||
"applications": [
|
||||
{
|
||||
"name": "NP02",
|
||||
"environments": [
|
||||
{
|
||||
"name": "Integ",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 455,
|
||||
"JEDI_CLIN_2": 746
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 973,
|
||||
"JEDI_CLIN_2": 504
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 11493,
|
||||
"JEDI_CLIN_2": 17751
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "PreProd",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 582,
|
||||
"JEDI_CLIN_2": 339
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 392,
|
||||
"JEDI_CLIN_2": 885
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 41856,
|
||||
"JEDI_CLIN_2": 46399
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Prod",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 446,
|
||||
"JEDI_CLIN_2": 670
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 368,
|
||||
"JEDI_CLIN_2": 963
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 10030,
|
||||
"JEDI_CLIN_2": 29253
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FM",
|
||||
"environments": [
|
||||
{
|
||||
"name": "Integ",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 994,
|
||||
"JEDI_CLIN_2": 573
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 699,
|
||||
"JEDI_CLIN_2": 418
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 27881,
|
||||
"JEDI_CLIN_2": 37092
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Prod",
|
||||
"spending": {
|
||||
"this_month": {
|
||||
"JEDI_CLIN_1": 838,
|
||||
"JEDI_CLIN_2": 839
|
||||
},
|
||||
"last_month": {
|
||||
"JEDI_CLIN_1": 775,
|
||||
"JEDI_CLIN_2": 946
|
||||
},
|
||||
"total": {
|
||||
"JEDI_CLIN_1": 45007,
|
||||
"JEDI_CLIN_2": 16197
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -1,14 +1,12 @@
|
||||
import { set } from 'vue/dist/vue'
|
||||
import { formatDollars } from '../../lib/dollars'
|
||||
import { set as _set } from 'lodash'
|
||||
|
||||
export default {
|
||||
name: 'spend-table',
|
||||
|
||||
props: {
|
||||
applications: Object,
|
||||
environments: Object,
|
||||
currentMonthIndex: String,
|
||||
prevMonthIndex: String,
|
||||
applications: Array,
|
||||
},
|
||||
|
||||
data: function() {
|
||||
@ -18,20 +16,16 @@ export default {
|
||||
},
|
||||
|
||||
created: function() {
|
||||
Object.keys(this.applications).forEach(application => {
|
||||
set(this.applicationsState[application], 'isVisible', false)
|
||||
this.applicationsState.forEach(application => {
|
||||
application.isVisible = false
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggle: function(e, applicationName) {
|
||||
this.applicationsState = Object.assign(this.applicationsState, {
|
||||
[applicationName]: Object.assign(
|
||||
this.applicationsState[applicationName],
|
||||
{
|
||||
isVisible: !this.applicationsState[applicationName].isVisible,
|
||||
}
|
||||
),
|
||||
toggle: function(e, applicationIndex) {
|
||||
set(this.applicationsState, applicationIndex, {
|
||||
...this.applicationsState[applicationIndex],
|
||||
isVisible: !this.applicationsState[applicationIndex].isVisible,
|
||||
})
|
||||
},
|
||||
|
||||
|
@ -14,6 +14,7 @@ from atst.app import make_config, make_app
|
||||
from atst.database import db
|
||||
|
||||
from atst.models.application import Application
|
||||
from atst.models.clin import JEDICLINType
|
||||
from atst.models.environment_role import CSPRole
|
||||
|
||||
from atst.domain.application_roles import ApplicationRoles
|
||||
@ -199,7 +200,22 @@ def add_task_orders_to_portfolio(portfolio):
|
||||
CLINFactory.build(
|
||||
task_order=expired_to, start_date=(today - five_days), end_date=yesterday
|
||||
),
|
||||
CLINFactory.build(task_order=active_to, start_date=yesterday, end_date=future),
|
||||
CLINFactory.build(
|
||||
task_order=active_to,
|
||||
start_date=yesterday,
|
||||
end_date=future,
|
||||
total_amount=1_000_000,
|
||||
obligated_amount=500_000,
|
||||
jedi_clin_type=JEDICLINType.JEDI_CLIN_1,
|
||||
),
|
||||
CLINFactory.build(
|
||||
task_order=active_to,
|
||||
start_date=yesterday,
|
||||
end_date=future,
|
||||
total_amount=500_000,
|
||||
obligated_amount=200_000,
|
||||
jedi_clin_type=JEDICLINType.JEDI_CLIN_2,
|
||||
),
|
||||
]
|
||||
|
||||
task_orders = [draft_to, unsigned_to, upcoming_to, expired_to, active_to]
|
||||
@ -300,9 +316,9 @@ def create_demo_portfolio(name, data):
|
||||
|
||||
for mock_application in data["applications"]:
|
||||
application = Application(
|
||||
portfolio=portfolio, name=mock_application.name, description=""
|
||||
portfolio=portfolio, name=mock_application["name"], description=""
|
||||
)
|
||||
env_names = [env.name for env in mock_application.environments]
|
||||
env_names = [env["name"] for env in mock_application["environments"]]
|
||||
envs = Environments.create_many(portfolio.owner, application, env_names)
|
||||
db.session.add(application)
|
||||
db.session.commit()
|
||||
@ -313,8 +329,8 @@ def seed_db():
|
||||
amanda = Users.get_by_dod_id("2345678901")
|
||||
|
||||
# Create Portfolios for Amanda with mocked reporting data
|
||||
create_demo_portfolio("A-Wing", MockReportingProvider.REPORT_FIXTURE_MAP["A-Wing"])
|
||||
create_demo_portfolio("B-Wing", MockReportingProvider.REPORT_FIXTURE_MAP["B-Wing"])
|
||||
create_demo_portfolio("A-Wing", MockReportingProvider.FIXTURE_SPEND_DATA["A-Wing"])
|
||||
create_demo_portfolio("B-Wing", MockReportingProvider.FIXTURE_SPEND_DATA["B-Wing"])
|
||||
|
||||
tie_interceptor = Portfolios.create(
|
||||
user=amanda,
|
||||
|
@ -73,6 +73,10 @@
|
||||
color: $color-green;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: $color-secondary;
|
||||
}
|
||||
|
||||
.user-permission {
|
||||
font-weight: $font-normal;
|
||||
}
|
||||
|
@ -16,6 +16,16 @@
|
||||
}
|
||||
|
||||
.jedi-clin-funding {
|
||||
$insufficient-gradient: repeating-linear-gradient(
|
||||
45deg,
|
||||
$color-secondary-dark,
|
||||
$color-secondary-dark 10px,
|
||||
$color-secondary-darkest 11px,
|
||||
$color-secondary-darkest 14px
|
||||
);
|
||||
|
||||
$graph-bar-height: 2rem;
|
||||
|
||||
padding-top: $gap * 3;
|
||||
padding-bottom: $gap * 3;
|
||||
|
||||
@ -37,14 +47,36 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__meter {
|
||||
margin: 10px 0;
|
||||
-moz-transform: scale(-1, 1);
|
||||
-webkit-transform: scale(-1, 1);
|
||||
-o-transform: scale(-1, 1);
|
||||
-ms-transform: scale(-1, 1);
|
||||
transform: scale(-1, 1);
|
||||
&__graph {
|
||||
width: 100%;
|
||||
height: $graph-bar-height;
|
||||
margin-top: $gap * 2;
|
||||
margin-bottom: $gap * 2;
|
||||
display: flex;
|
||||
|
||||
&-bar {
|
||||
height: 100%;
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: $gap / 2;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&--invoiced {
|
||||
background: $color-green;
|
||||
}
|
||||
&--estimated {
|
||||
background: $color-green-lighter;
|
||||
}
|
||||
&--remaining {
|
||||
background: $color-primary-darkest;
|
||||
}
|
||||
&--insufficient {
|
||||
background: $insufficient-gradient;
|
||||
}
|
||||
}
|
||||
|
||||
&-values {
|
||||
display: flex;
|
||||
@ -52,13 +84,32 @@
|
||||
}
|
||||
|
||||
&__meta {
|
||||
&--remaining {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
margin-right: $gap * 5;
|
||||
|
||||
&-header {
|
||||
@include small-copy;
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-key {
|
||||
height: $graph-bar-height;
|
||||
width: $graph-bar-height;
|
||||
margin-right: $gap / 2;
|
||||
|
||||
&--invoiced {
|
||||
background: $color-green;
|
||||
}
|
||||
&--estimated {
|
||||
background: $color-green-lighter;
|
||||
}
|
||||
&--remaining {
|
||||
background: $color-primary-darkest;
|
||||
}
|
||||
&--insufficient {
|
||||
background: $insufficient-gradient;
|
||||
}
|
||||
}
|
||||
&-value {
|
||||
margin-bottom: 0;
|
||||
|
@ -3,9 +3,6 @@
|
||||
|
||||
<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) %}
|
||||
@ -24,12 +21,7 @@
|
||||
) }}
|
||||
|
||||
{% 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>
|
||||
<spend-table v-bind:applications='{{ monthly_spending | tojson }}' inline-template>
|
||||
<div class="responsive-table-wrapper">
|
||||
<table class="atat-table">
|
||||
<thead>
|
||||
@ -41,41 +33,41 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for='(application, name) in applicationsState'>
|
||||
<template v-for='(application, applicationIndex) in applicationsState'>
|
||||
<tr>
|
||||
<td>
|
||||
<button v-on:click='toggle($event, name)' class='icon-link icon-link--large'>
|
||||
<span v-html='name'></span>
|
||||
<button v-on:click='toggle($event, applicationIndex)' class='icon-link icon-link--large'>
|
||||
<span v-html='application.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>
|
||||
<span v-html='formatDollars(application.this_month || 0)'></span>
|
||||
</td>
|
||||
<td class="table-cell--align-right">
|
||||
<span v-html='formatDollars(application[prevMonthIndex] || 0)'></span>
|
||||
<span v-html='formatDollars(application.last_month || 0)'></span>
|
||||
</td>
|
||||
<td class="table-cell--align-right">
|
||||
<span v-html='formatDollars(application["total_spend_to_date"])'></span>
|
||||
<span v-html='formatDollars(application.total)'></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for='(environment, envName, index) in environments[name]'
|
||||
v-show='application.isVisible'
|
||||
v-bind:class="[ index == Object.keys(environments[name]).length -1 ? 'reporting-spend-table__env-row--last' : '']"
|
||||
v-for='(environment, index) in application.environments'
|
||||
v-bind:class="[ index == application.environments.length -1 ? 'reporting-spend-table__env-row--last' : '']"
|
||||
>
|
||||
<td>
|
||||
<span class="reporting-spend-table__env-row-label" v-html='envName'></span>
|
||||
<span class="reporting-spend-table__env-row-label" v-html='environment.name'></span>
|
||||
</td>
|
||||
<td class="table-cell--align-right">
|
||||
<span v-html='formatDollars(environment[currentMonthIndex] || 0)'></span>
|
||||
<span v-html='formatDollars(environment.this_month || 0)'></span>
|
||||
</td>
|
||||
<td class="table-cell--align-right">
|
||||
<span v-html='formatDollars(environment[prevMonthIndex] || 0)'></span>
|
||||
<span v-html='formatDollars(environment.last_month || 0)'></span>
|
||||
</td>
|
||||
<td class="table-cell--align-right">
|
||||
<span v-html='formatDollars(environment["total_spend_to_date"])'></span>
|
||||
<span v-html='formatDollars(environment.total)'></span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
@ -35,13 +35,13 @@
|
||||
<div>{{ ("{}".format(clin.jedi_clin_type) | translate)[15:] }}</div>
|
||||
</td>
|
||||
<td>
|
||||
{{ clin.period_of_performance.start_date | formattedDate(formatter="%b %d, %Y") }}
|
||||
{{ clin.start_date | formattedDate(formatter="%b %d, %Y") }}
|
||||
-
|
||||
{{ clin.period_of_performance.end_date | formattedDate(formatter="%b %d, %Y") }}
|
||||
{{ clin.end_date | formattedDate(formatter="%b %d, %Y") }}
|
||||
</td>
|
||||
<td>{{ clin.total_value | dollars }}</td>
|
||||
<td>{{ clin.total_obligated_funds | dollars }}</td>
|
||||
<td>{{ (clin.total_obligated_funds - clin.expended_funds) | dollars }}</td>
|
||||
<td>{{ clin.total_amount | dollars }}</td>
|
||||
<td>{{ clin.obligated_amount | dollars }}</td>
|
||||
<td>{{ 0 | dollars }}</td>
|
||||
<tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
@ -5,7 +5,9 @@
|
||||
|
||||
{% block portfolio_content %}
|
||||
{{ StickyCTA("Reports") }}
|
||||
|
||||
<div class="portfolio-reports col col--grow">
|
||||
{% include "fragments/flash.html" %}
|
||||
<p class="row estimate-warning">{{ "portfolios.reports.estimate_warning" | translate }}</p>
|
||||
{% include "portfolios/reports/portfolio_summary.html" %}
|
||||
<hr>
|
||||
|
@ -3,28 +3,61 @@
|
||||
<section>
|
||||
<header class="reporting-section-header">
|
||||
<h2 class="reporting-section-header__header">Current Obligated funds</h2>
|
||||
<span class="reporting-section-header__subheader">As of {{ now | formattedDate(formatter="%B %d, %Y at %H:%M") }}</span>
|
||||
<span class="reporting-section-header__subheader">As of {{ retrieved | formattedDate(formatter="%B %d, %Y at %H:%M") }}</span>
|
||||
</header>
|
||||
<div class='panel'>
|
||||
<div class='panel__content jedi-clin-funding'>
|
||||
{% for JEDI_clin, funds in current_obligated_funds.items() %}
|
||||
{% set remaining_funds = (funds["obligated_funds"] - funds["expended_funds"]) %}
|
||||
{% for JEDI_clin in current_obligated_funds | sort(attribute='name')%}
|
||||
<div class="jedi-clin-funding__clin-wrapper">
|
||||
<h3 class="h5 jedi-clin-funding__header">
|
||||
{{ "JEDICLINType.{}".format(JEDI_clin) | translate }}
|
||||
{{ "JEDICLINType.{}".format(JEDI_clin.name) | translate }}
|
||||
</h3>
|
||||
<p class="jedi-clin-funding__subheader">Total obligated amount: {{ funds["obligated_funds"] | dollars }}</p>
|
||||
<meter class="jedi-clin-funding__meter" value='{{remaining_funds}}' min='0' max='{{ funds["obligated_funds"] }}' title='{{ JEDI_clin }}'>
|
||||
<div class='jedi-clin-funding__meter-fallback' style='width:{{ (funds["expended_funds"] / funds["obligated_funds"]) * 100 }}%;'></div>
|
||||
</meter>
|
||||
<div class="jedi-clin-funding__meter-values">
|
||||
<p class="jedi-clin-funding__subheader">Total obligated amount: {{ JEDI_clin.obligated | dollars }}</p>
|
||||
<div class="jedi-clin-funding__graph">
|
||||
{% if JEDI_clin.remaining < 0 %}
|
||||
<span style="width:100%" class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--insufficient"></span>
|
||||
{% else %}
|
||||
{% set invoiced_width = (JEDI_clin.invoiced, JEDI_clin.obligated) | obligatedFundingGraphWidth %}
|
||||
{% if invoiced_width %}
|
||||
<span style="width:{{ invoiced_width }}%"
|
||||
class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--invoiced">
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% set estimated_width = (JEDI_clin.estimated, JEDI_clin.obligated) | obligatedFundingGraphWidth %}
|
||||
{% if estimated_width %}
|
||||
<span style="width:{{ (JEDI_clin.estimated, JEDI_clin.obligated) | obligatedFundingGraphWidth }}%"
|
||||
class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--estimated">
|
||||
</span>
|
||||
{% endif %}
|
||||
<span style="width:{{ (JEDI_clin.remaining, JEDI_clin.obligated) | obligatedFundingGraphWidth }}%"
|
||||
class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--remaining">
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
<div class="jedi-clin-funding__graph-values">
|
||||
<div class="jedi-clin-funding__meta">
|
||||
<p class="jedi-clin-funding__meta-header">Funds expended:</p>
|
||||
<p class="h3 jedi-clin-funding__meta-value">{{ funds["expended_funds"] | dollars }}</p>
|
||||
<p class="jedi-clin-funding__meta-header">
|
||||
<span class="jedi-clin-funding__meta-key jedi-clin-funding__meta-key--invoiced"></span>
|
||||
Invoiced expended funds:
|
||||
</p>
|
||||
<p class="h3 jedi-clin-funding__meta-value">{{ JEDI_clin.invoiced | dollars }}</p>
|
||||
</div>
|
||||
<div class="jedi-clin-funding__meta jedi-clin-funding__meta--remaining">
|
||||
<p class="jedi-clin-funding__meta-header">Remaining funds:</p>
|
||||
<p class="h3 jedi-clin-funding__meta-value">{{ remaining_funds | dollars }}</p>
|
||||
<div class="jedi-clin-funding__meta">
|
||||
<p class="jedi-clin-funding__meta-header">
|
||||
<span class="jedi-clin-funding__meta-key jedi-clin-funding__meta-key--estimated"></span>
|
||||
Estimated expended funds:
|
||||
</p>
|
||||
<p class="h3 jedi-clin-funding__meta-value">{{ JEDI_clin.estimated | dollars }}</p>
|
||||
</div>
|
||||
<div class="jedi-clin-funding__meta">
|
||||
<p class="jedi-clin-funding__meta-header">
|
||||
<span class="jedi-clin-funding__meta-key jedi-clin-funding__meta-key--{{"remaining" if JEDI_clin.remaining > 0 else "insufficient"}}"></span>
|
||||
Remaining funds:
|
||||
</p>
|
||||
<p class="h3 jedi-clin-funding__meta-value {% if JEDI_clin.remaining < 0 %}text-danger{% endif %}">{{ JEDI_clin.remaining | dollars }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
0
tests/domain/cloud/reports/__init__.py
Normal file
0
tests/domain/cloud/reports/__init__.py
Normal file
50
tests/domain/cloud/reports/test_reports.py
Normal file
50
tests/domain/cloud/reports/test_reports.py
Normal file
@ -0,0 +1,50 @@
|
||||
from atst.domain.csp.reports import MockReportingProvider
|
||||
|
||||
|
||||
def test_get_environment_monthly_totals():
|
||||
environment = {
|
||||
"name": "Test Environment",
|
||||
"spending": {
|
||||
"this_month": {"JEDI_CLIN_1": 100, "JEDI_CLIN_2": 100},
|
||||
"last_month": {"JEDI_CLIN_1": 200, "JEDI_CLIN_2": 200},
|
||||
"total": {"JEDI_CLIN_1": 1000, "JEDI_CLIN_2": 1000},
|
||||
},
|
||||
}
|
||||
totals = MockReportingProvider._get_environment_monthly_totals(environment)
|
||||
assert totals == {
|
||||
"name": "Test Environment",
|
||||
"this_month": 200,
|
||||
"last_month": 400,
|
||||
"total": 2000,
|
||||
}
|
||||
|
||||
|
||||
def test_get_application_monthly_totals():
|
||||
application = {
|
||||
"name": "Test Application",
|
||||
"environments": [
|
||||
{
|
||||
"name": "Z",
|
||||
"spending": {
|
||||
"this_month": {"JEDI_CLIN_1": 50, "JEDI_CLIN_2": 50},
|
||||
"last_month": {"JEDI_CLIN_1": 150, "JEDI_CLIN_2": 150},
|
||||
"total": {"JEDI_CLIN_1": 250, "JEDI_CLIN_2": 250},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "A",
|
||||
"spending": {
|
||||
"this_month": {"JEDI_CLIN_1": 100, "JEDI_CLIN_2": 100},
|
||||
"last_month": {"JEDI_CLIN_1": 200, "JEDI_CLIN_2": 200},
|
||||
"total": {"JEDI_CLIN_1": 1000, "JEDI_CLIN_2": 1000},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
totals = MockReportingProvider._get_application_monthly_totals(application)
|
||||
assert totals["name"] == "Test Application"
|
||||
assert totals["this_month"] == 300
|
||||
assert totals["last_month"] == 700
|
||||
assert totals["total"] == 2500
|
||||
assert [env["name"] for env in totals["environments"]] == ["A", "Z"]
|
@ -1,22 +1,8 @@
|
||||
from atst.domain.reports import Reports
|
||||
from tests.factories import *
|
||||
|
||||
|
||||
# this is sketched out until we do real reporting
|
||||
def test_monthly_totals():
|
||||
pass
|
||||
|
||||
|
||||
# this is sketched out until we do real reporting
|
||||
def test_current_obligated_funds():
|
||||
pass
|
||||
|
||||
|
||||
# this is sketched out until we do real reporting
|
||||
# TODO: Implement when we get real reporting data
|
||||
def test_expired_task_orders():
|
||||
pass
|
||||
|
||||
|
||||
# this is sketched out until we do real reporting
|
||||
# TODO: Implement when we get real reporting data
|
||||
def test_obligated_funds_by_JEDI_clin():
|
||||
pass
|
||||
|
Loading…
x
Reference in New Issue
Block a user