diff --git a/atst/models/portfolio.py b/atst/models/portfolio.py index ffa674c3..c006dd37 100644 --- a/atst/models/portfolio.py +++ b/atst/models/portfolio.py @@ -100,6 +100,16 @@ class Portfolio( (task_order.total_obligated_funds for task_order in self.active_task_orders) ) + @property + def upcoming_obligated_funds(self): + return sum( + ( + task_order.total_obligated_funds + for task_order in self.task_orders + if task_order.is_upcoming + ) + ) + @property def funding_duration(self): """ diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 3ab493a9..c6dda237 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -87,6 +87,10 @@ class TaskOrder(Base, mixins.TimestampsMixin): def is_expired(self): return self.status == Status.EXPIRED + @property + def is_upcoming(self): + return self.status == Status.UPCOMING + @property def clins_are_completed(self): return all([len(self.clins), (clin.is_completed for clin in self.clins)]) diff --git a/atst/routes/portfolios/index.py b/atst/routes/portfolios/index.py index 795d4b70..44cac768 100644 --- a/atst/routes/portfolios/index.py +++ b/atst/routes/portfolios/index.py @@ -50,8 +50,10 @@ def reports(portfolio_id): return render_template( "portfolios/reports/index.html", portfolio=portfolio, - # wrapped in str() because the sum of obligated funds returns a Decimal object - total_portfolio_value=str(portfolio.total_obligated_funds), + # wrapped in str() because this sum returns a Decimal object + total_portfolio_value=str( + portfolio.total_obligated_funds + portfolio.upcoming_obligated_funds + ), current_obligated_funds=current_obligated_funds, expired_task_orders=Reports.expired_task_orders(portfolio), retrieved=datetime.now(), # mocked datetime of reporting data retrival diff --git a/script/seed_sample.py b/script/seed_sample.py index 72c16c6c..d1cf1c9e 100644 --- a/script/seed_sample.py +++ b/script/seed_sample.py @@ -195,7 +195,7 @@ def add_task_orders_to_portfolio(portfolio): task_order=unsigned_to, start_date=(today - five_days), end_date=today ), CLINFactory.build( - task_order=upcoming_to, start_date=future, end_date=(today + five_days) + task_order=upcoming_to, start_date=(today + five_days), end_date=future ), CLINFactory.build( task_order=expired_to, start_date=(today - five_days), end_date=yesterday diff --git a/tests/models/test_portfolio.py b/tests/models/test_portfolio.py index af082876..71e11bb3 100644 --- a/tests/models/test_portfolio.py +++ b/tests/models/test_portfolio.py @@ -7,6 +7,51 @@ from tests.factories import ( random_past_date, ) import datetime +import pendulum +from decimal import Decimal +import pytest + + +@pytest.fixture(scope="function") +def upcoming_task_order(): + return dict( + signed_at=pendulum.today().subtract(days=3), + create_clins=[ + dict( + start_date=pendulum.today().add(days=2), + end_date=pendulum.today().add(days=3), + obligated_amount=Decimal(700.0), + ) + ], + ) + + +@pytest.fixture(scope="function") +def current_task_order(): + return dict( + signed_at=pendulum.today().subtract(days=3), + create_clins=[ + dict( + start_date=pendulum.today().subtract(days=1), + end_date=pendulum.today().add(days=1), + obligated_amount=Decimal(1000.0), + ) + ], + ) + + +@pytest.fixture(scope="function") +def past_task_order(): + return dict( + signed_at=pendulum.today().subtract(days=3), + create_clins=[ + dict( + start_date=pendulum.today().subtract(days=3), + end_date=pendulum.today().subtract(days=2), + obligated_amount=Decimal(500.0), + ) + ], + ) def test_portfolio_applications_excludes_deleted(): @@ -85,3 +130,53 @@ def test_active_task_orders(session): portfolio=portfolio, signed_at=random_past_date(), clins=[CLINFactory.create()] ) assert len(portfolio.active_task_orders) == 1 + + +class TestCurrentObligatedFunds: + """ + Tests the current_obligated_funds property + """ + + def test_no_task_orders(self): + portfolio = PortfolioFactory() + assert portfolio.total_obligated_funds == Decimal(0) + + def test_with_current(self, current_task_order): + portfolio = PortfolioFactory( + task_orders=[current_task_order, current_task_order] + ) + assert portfolio.total_obligated_funds == Decimal(2000.0) + + def test_with_others( + self, past_task_order, current_task_order, upcoming_task_order + ): + portfolio = PortfolioFactory( + task_orders=[past_task_order, current_task_order, upcoming_task_order,] + ) + # Only sums the current task order + assert portfolio.total_obligated_funds == Decimal(1000.0) + + +class TestUpcomingObligatedFunds: + """ + Tests the upcoming_obligated_funds property + """ + + def test_no_task_orders(self): + portfolio = PortfolioFactory() + assert portfolio.upcoming_obligated_funds == Decimal(0) + + def test_with_upcoming(self, upcoming_task_order): + portfolio = PortfolioFactory( + task_orders=[upcoming_task_order, upcoming_task_order] + ) + assert portfolio.upcoming_obligated_funds == Decimal(1400.0) + + def test_with_others( + self, past_task_order, current_task_order, upcoming_task_order + ): + portfolio = PortfolioFactory( + task_orders=[past_task_order, current_task_order, upcoming_task_order] + ) + # Only sums the upcoming task order + assert portfolio.upcoming_obligated_funds == Decimal(700.0) diff --git a/translations.yaml b/translations.yaml index c3d73c3a..6fe84865 100644 --- a/translations.yaml +++ b/translations.yaml @@ -508,7 +508,7 @@ portfolios: estimate_warning: Reports displayed in JEDI are estimates and not a system of record. total_value: header: Total Portfolio Value - tooltip: Total portfolio value is all obligated and projected funds for all task orders in this portfolio. + tooltip: Total portfolio value is all obligated funds for current and upcoming task orders in this portfolio. task_orders: add_new_button: Add New Task Order review: