Merge pull request #1199 from dod-ccpo/reporting-refactor-part-1

Reporting refactor part 1
This commit is contained in:
graham-dds
2019-11-25 16:37:49 -05:00
committed by GitHub
22 changed files with 463 additions and 932 deletions

View File

@@ -1,6 +1,7 @@
from itertools import groupby
from collections import OrderedDict
from atst.utils.localization import translate
import pendulum
from decimal import Decimal
class ReportingInterface:
@@ -35,14 +36,16 @@ def generate_sample_dates(_max=8):
current = pendulum.now()
sample_dates = []
for _i in range(_max):
current = current.subtract(months=1)
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 = {
@@ -163,25 +166,8 @@ class MockReportingProvider(ReportingInterface):
"FM_Prod": {FIXTURE_MONTHS[0]: 5686},
}
CUMULATIVE_BUDGET_A_WING = {
FIXTURE_MONTHS[7]: {"spend": 9857, "cumulative": 9857},
FIXTURE_MONTHS[6]: {"spend": 7881, "cumulative": 17738},
FIXTURE_MONTHS[5]: {"spend": 14010, "cumulative": 31748},
FIXTURE_MONTHS[4]: {"spend": 43510, "cumulative": 75259},
FIXTURE_MONTHS[3]: {"spend": 41725, "cumulative": 116_984},
FIXTURE_MONTHS[2]: {"spend": 41328, "cumulative": 158_312},
FIXTURE_MONTHS[1]: {"spend": 47491, "cumulative": 205_803},
FIXTURE_MONTHS[0]: {"spend": 36028, "cumulative": 241_831},
}
CUMULATIVE_BUDGET_B_WING = {
FIXTURE_MONTHS[1]: {"spend": 4838, "cumulative": 4838},
FIXTURE_MONTHS[0]: {"spend": 14500, "cumulative": 19338},
}
REPORT_FIXTURE_MAP = {
"A-Wing": {
"cumulative": CUMULATIVE_BUDGET_A_WING,
"applications": [
MockApplication("LC04", ["Integ", "PreProd", "Prod"]),
MockApplication("SF18", ["Integ", "PreProd", "Prod"]),
@@ -202,7 +188,6 @@ class MockReportingProvider(ReportingInterface):
"budget": 500_000,
},
"B-Wing": {
"cumulative": CUMULATIVE_BUDGET_B_WING,
"applications": [
MockApplication("NP02", ["Integ", "PreProd", "Prod"]),
MockApplication("FM", ["Integ", "Prod"]),
@@ -211,28 +196,6 @@ class MockReportingProvider(ReportingInterface):
},
}
def _sum_monthly_spend(self, data):
return sum(
[
spend
for application in data
for env in application.environments
for spend in self.MONTHLY_SPEND_BY_ENVIRONMENT[env.id].values()
]
)
def get_budget(self, portfolio):
if portfolio.name in self.REPORT_FIXTURE_MAP:
return self.REPORT_FIXTURE_MAP[portfolio.name]["budget"]
return 0
def get_total_spending(self, portfolio):
if portfolio.name in self.REPORT_FIXTURE_MAP:
return self._sum_monthly_spend(
self.REPORT_FIXTURE_MAP[portfolio.name]["applications"]
)
return 0
def _rollup_application_totals(self, data):
application_totals = {}
for application, environments in data.items():
@@ -270,7 +233,14 @@ class MockReportingProvider(ReportingInterface):
{ "01/2018": 79.85, "02/2018": 86.54 }
"""
return self.MONTHLY_SPEND_BY_ENVIRONMENT.get(environment_id, {})
environment_monthly_totals = self.MONTHLY_SPEND_BY_ENVIRONMENT.get(
environment_id, {}
).copy()
environment_monthly_totals["total_spend_to_date"] = sum(
monthly_total for monthly_total in environment_monthly_totals.values()
)
return environment_monthly_totals
def monthly_totals(self, portfolio):
"""Return month totals rolled up by environment, application, and portfolio.
@@ -309,19 +279,46 @@ class MockReportingProvider(ReportingInterface):
"portfolio": portfolio_totals,
}
def cumulative_budget(self, portfolio):
def get_obligated_funds_by_JEDI_clin(self, portfolio):
"""
Returns a dictionary of obligated funds and spending per JEDI CLIN
{
JEDI_CLIN: {
obligated_funds,
expended_funds
}
}
"""
if portfolio.name in self.REPORT_FIXTURE_MAP:
budget_months = self.REPORT_FIXTURE_MAP[portfolio.name]["cumulative"]
else:
budget_months = {}
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[translate(f"JEDICLINType.{jedi_clin.value}")] = {
"obligated_funds": obligated_funds,
"expended_funds": (
obligated_funds * Decimal(self.MOCK_PERCENT_EXPENDED_FUNDS)
),
}
return return_dict
return {}
end = pendulum.now()
start = end.subtract(months=12)
period = pendulum.period(start, end)
all_months = OrderedDict()
for t in period.range("months"):
month_str = "{month:02d}/{year}".format(month=t.month, year=t.year)
all_months[month_str] = budget_months.get(month_str, None)
return {"months": all_months}
def get_expired_task_orders(self, portfolio):
return [
{
"id": task_order.id,
"number": task_order.number,
"period_of_performance": {
"start_date": task_order.start_date,
"end_date": task_order.end_date,
},
"total_obligated_funds": task_order.total_obligated_funds,
"expended_funds": (
task_order.total_obligated_funds
* Decimal(self.MOCK_PERCENT_EXPENDED_FUNDS)
),
}
for task_order in portfolio.task_orders
if task_order.is_expired
]

View File

@@ -2,16 +2,14 @@ from flask import current_app
class Reports:
@classmethod
def portfolio_totals(cls, portfolio):
budget = current_app.csp.reports.get_budget(portfolio)
spent = current_app.csp.reports.get_total_spending(portfolio)
return {"budget": budget, "spent": spent}
@classmethod
def monthly_totals(cls, portfolio):
return current_app.csp.reports.monthly_totals(portfolio)
@classmethod
def cumulative_budget(cls, portfolio):
return current_app.csp.reports.cumulative_budget(portfolio)
def expired_task_orders(cls, portfolio):
return current_app.csp.reports.get_expired_task_orders(portfolio)
@classmethod
def obligated_funds_by_JEDI_clin(cls, portfolio):
return current_app.csp.reports.get_obligated_funds_by_JEDI_clin(portfolio)

View File

@@ -1,6 +1,7 @@
from enum import Enum
from sqlalchemy import Column, Date, Enum as SQLAEnum, ForeignKey, Numeric, String
from sqlalchemy.orm import relationship
from datetime import date
from atst.models.base import Base
import atst.models.mixins as mixins
@@ -61,3 +62,7 @@ class CLIN(Base, mixins.TimestampsMixin):
for c in self.__table__.columns
if c.name not in ["id"]
}
@property
def is_active(self):
return self.start_date <= date.today() <= self.end_date

View File

@@ -65,6 +65,58 @@ class Portfolio(
def num_task_orders(self):
return len(self.task_orders)
@property
def active_clins(self):
return [
clin
for task_order in self.task_orders
for clin in task_order.clins
if clin.is_active
]
@property
def active_task_orders(self):
return [task_order for task_order in self.task_orders if task_order.is_active]
@property
def funding_duration(self):
"""
Return the earliest period of performance start date and latest period
of performance end date for all active task orders in a portfolio.
@return: (datetime.date or None, datetime.date or None)
"""
start_dates = (
task_order.start_date
for task_order in self.task_orders
if task_order.is_active
)
end_dates = (
task_order.end_date
for task_order in self.task_orders
if task_order.is_active
)
earliest_pop_start_date = min(start_dates, default=None)
latest_pop_end_date = max(end_dates, default=None)
return (earliest_pop_start_date, latest_pop_end_date)
@property
def days_to_funding_expiration(self):
"""
Returns the number of days between today and the lastest period performance
end date of all active Task Orders
"""
return max(
(
task_order.days_to_expiration
for task_order in self.task_orders
if task_order.is_active
),
default=0,
)
@property
def members(self):
return (

View File

@@ -46,34 +46,24 @@ def create_portfolio():
def reports(portfolio_id):
portfolio = Portfolios.get(g.current_user, portfolio_id)
today = date.today()
month = http_request.args.get("month", today.month)
year = http_request.args.get("year", today.year)
current_month = date(int(year), int(month), 15)
current_month = date(int(today.year), int(today.month), 15)
prev_month = current_month - timedelta(days=28)
two_months_ago = prev_month - timedelta(days=28)
task_order = next(
(task_order for task_order in portfolio.task_orders if task_order.is_active),
None,
# wrapped in str() because the sum of obligated funds returns a Decimal object
total_portfolio_value = str(
sum(
task_order.total_obligated_funds
for task_order in portfolio.active_task_orders
)
)
expiration_date = task_order and task_order.end_date
if expiration_date:
remaining_difference = expiration_date - today
remaining_days = remaining_difference.days
else:
remaining_days = None
return render_template(
"portfolios/reports/index.html",
cumulative_budget=Reports.cumulative_budget(portfolio),
portfolio_totals=Reports.portfolio_totals(portfolio),
portfolio=portfolio,
total_portfolio_value=total_portfolio_value,
current_obligated_funds=Reports.obligated_funds_by_JEDI_clin(portfolio),
expired_task_orders=Reports.expired_task_orders(portfolio),
monthly_totals=Reports.monthly_totals(portfolio),
task_order=task_order,
current_month=current_month,
prev_month=prev_month,
two_months_ago=two_months_ago,
expiration_date=expiration_date,
remaining_days=remaining_days,
)