Merge pull request #1199 from dod-ccpo/reporting-refactor-part-1
Reporting refactor part 1
This commit is contained in:
@@ -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
|
||||
]
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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 (
|
||||
|
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user