diff --git a/atst/domain/csp/fixture_spend_data.json b/atst/domain/csp/fixture_spend_data.json new file mode 100644 index 00000000..2a59eadf --- /dev/null +++ b/atst/domain/csp/fixture_spend_data.json @@ -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 + } + } + } + ] + } + ] + } +} \ No newline at end of file diff --git a/atst/domain/csp/reports.py b/atst/domain/csp/reports.py index 4b69e904..9949050e 100644 --- a/atst/domain/csp/reports.py +++ b/atst/domain/csp/reports.py @@ -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", - [ - "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 {} diff --git a/atst/domain/reports.py b/atst/domain/reports.py index 94f6c54e..22760ec9 100644 --- a/atst/domain/reports.py +++ b/atst/domain/reports.py @@ -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()) + ] diff --git a/atst/filters.py b/atst/filters.py index 48b8166c..3508f1e9 100644 --- a/atst/filters.py +++ b/atst/filters.py @@ -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): diff --git a/atst/routes/portfolios/index.py b/atst/routes/portfolios/index.py index a34d9057..8787c4f6 100644 --- a/atst/routes/portfolios/index.py +++ b/atst/routes/portfolios/index.py @@ -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 ) diff --git a/js/components/tables/spend_table.js b/js/components/tables/spend_table.js index c3a1c90f..bd5f5a01 100644 --- a/js/components/tables/spend_table.js +++ b/js/components/tables/spend_table.js @@ -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, }) }, diff --git a/styles/core/_util.scss b/styles/core/_util.scss index b719d855..ff6e8e3f 100644 --- a/styles/core/_util.scss +++ b/styles/core/_util.scss @@ -73,6 +73,10 @@ color: $color-green; } +.text-danger { + color: $color-secondary; +} + .user-permission { font-weight: $font-normal; } diff --git a/styles/sections/_reports.scss b/styles/sections/_reports.scss index 0cd737c8..ee43ffa2 100644 --- a/styles/sections/_reports.scss +++ b/styles/sections/_reports.scss @@ -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; diff --git a/templates/portfolios/reports/application_and_env_spending.html b/templates/portfolios/reports/application_and_env_spending.html index 0af72112..c7d94f55 100644 --- a/templates/portfolios/reports/application_and_env_spending.html +++ b/templates/portfolios/reports/application_and_env_spending.html @@ -24,12 +24,7 @@ ) }} {% else %} - +
@@ -41,41 +36,41 @@ -