Refactor mock reporting data and class methods

All mock reporting data was moved to a JSON file. The concept of
what JEDI CLIN a particular environment drew money from was added to the
data.

This change had a cascade effect to the reporting class methods,
templates, and Vue components that ingested that reporting data. Many of
these files were modified to adapt to these changes. This also included
modifying the obligated funding bar graphs to reflect new design
changes.
This commit is contained in:
graham-dds 2019-12-09 09:20:31 -05:00
parent d9c79b9b58
commit dc9a21a501
11 changed files with 676 additions and 400 deletions

View 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
}
}
}
]
}
]
}
}

View File

@ -1,344 +1,124 @@
from itertools import groupby
import pendulum
from collections import defaultdict
import json
import os
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(
os.path.join(os.path.dirname(__file__), "fixture_spend_data.json"), "r"
) 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",
returns an array of application and environment spending for the
portfolio. Applications and their nested environments are sorted in
alphabetical order by name.
[
"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:
{
"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
}
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
}
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])
],
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"])
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)
),
}
return sorted_task_orders(
[
@classmethod
def _get_environment_monthly_totals(cls, environment):
"""
returns a dictionary that represents spending totals for an environment e.g.
{
"id": task_order.id,
"number": task_order.number,
"clins": sorted_clins(
[serialize_clin(clin) for clin in task_order.clins]
),
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()),
}
@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
}
for task_order in portfolio.task_orders
if task_order.is_expired
]
}
"""
environments = sorted(
[
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 {}

View File

@ -1,15 +1,36 @@
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
)
return [
{
"name": clin,
"invoiced": clin_spending[clin].get("invoiced", 0),
"estimated": clin_spending[clin].get("estimated", 0),
"obligated": clin_spending[clin].get("obligated", 0),
}
for clin in sorted(clin_spending.keys())
]

View File

@ -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):

View File

@ -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,6 @@ 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)
# wrapped in str() because the sum of obligated funds returns a Decimal object
total_portfolio_value = str(
sum(
@ -51,10 +48,10 @@ def reports(portfolio_id):
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_spending=Reports.monthly_spending(portfolio),
current_month=current_month,
prev_month=prev_month,
now=datetime.now(), # mocked datetime of reporting data retrival
retrieved=datetime.now(), # mocked datetime of reporting data retrival
)

View File

@ -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,
})
},

View File

@ -73,6 +73,10 @@
color: $color-green;
}
.text-danger {
color: $color-secondary;
}
.user-permission {
font-weight: $font-normal;
}

View File

@ -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;

View File

@ -24,12 +24,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 +36,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>

View File

@ -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 %}

View File

@ -3,28 +3,62 @@
<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 %}
{% set remaining_funds = JEDI_clin.obligated - (JEDI_clin.invoiced + JEDI_clin.estimated) %}
<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">
<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__subheader">Total obligated amount: {{ JEDI_clin.obligated | dollars }}</p>
<div class="jedi-clin-funding__graph">
{% if remaining_funds < 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:{{ (remaining_funds, JEDI_clin.obligated) | obligatedFundingGraphWidth }}%"
class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--remaining">
</span>
{% endif %}
</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__graph-values">
<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--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">
<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 remaining_funds > 0 else "insufficient"}}"></span>
Remaining funds:
</p>
<p class="h3 jedi-clin-funding__meta-value {% if remaining_funds < 0 %}text-danger{% endif %}">{{ remaining_funds | dollars }}</p>
</div>
</div>
</div>