Merge branch 'staging' into billing-owner
This commit is contained in:
commit
285021de7c
@ -180,7 +180,7 @@ def do_work(fn, task, csp, **kwargs):
|
|||||||
def do_provision_portfolio(csp: CloudProviderInterface, portfolio_id=None):
|
def do_provision_portfolio(csp: CloudProviderInterface, portfolio_id=None):
|
||||||
portfolio = Portfolios.get_for_update(portfolio_id)
|
portfolio = Portfolios.get_for_update(portfolio_id)
|
||||||
fsm = Portfolios.get_or_create_state_machine(portfolio)
|
fsm = Portfolios.get_or_create_state_machine(portfolio)
|
||||||
fsm.trigger_next_transition()
|
fsm.trigger_next_transition(csp_data=portfolio.to_dictionary())
|
||||||
|
|
||||||
|
|
||||||
@celery.task(bind=True, base=RecordFailure)
|
@celery.task(bind=True, base=RecordFailure)
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
|
import re
|
||||||
|
from string import ascii_lowercase, digits
|
||||||
|
from random import choices
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
from sqlalchemy import Column, String
|
from sqlalchemy import Column, String
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy.types import ARRAY
|
from sqlalchemy.types import ARRAY
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
from atst.models.base import Base
|
from atst.models.base import Base
|
||||||
import atst.models.types as types
|
import atst.models.types as types
|
||||||
import atst.models.mixins as mixins
|
import atst.models.mixins as mixins
|
||||||
|
from atst.models.task_order import TaskOrder
|
||||||
from atst.models.portfolio_role import PortfolioRole, Status as PortfolioRoleStatus
|
from atst.models.portfolio_role import PortfolioRole, Status as PortfolioRoleStatus
|
||||||
from atst.domain.permission_sets import PermissionSets
|
from atst.domain.permission_sets import PermissionSets
|
||||||
from atst.utils import first_or_none
|
from atst.utils import first_or_none
|
||||||
@ -95,6 +100,16 @@ class Portfolio(
|
|||||||
(task_order.total_obligated_funds for task_order in self.active_task_orders)
|
(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
|
@property
|
||||||
def funding_duration(self):
|
def funding_duration(self):
|
||||||
"""
|
"""
|
||||||
@ -159,6 +174,51 @@ class Portfolio(
|
|||||||
def application_id(self):
|
def application_id(self):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def to_dictionary(self):
|
||||||
|
ppoc = self.owner
|
||||||
|
user_id = f"{ppoc.first_name[0]}{ppoc.last_name}".lower()
|
||||||
|
|
||||||
|
domain_name = re.sub("[^0-9a-zA-Z]+", "", self.name).lower() + "".join(
|
||||||
|
choices(ascii_lowercase + digits, k=4)
|
||||||
|
)
|
||||||
|
portfolio_data = {
|
||||||
|
"user_id": user_id,
|
||||||
|
"password": "",
|
||||||
|
"domain_name": domain_name,
|
||||||
|
"first_name": ppoc.first_name,
|
||||||
|
"last_name": ppoc.last_name,
|
||||||
|
"country_code": "US",
|
||||||
|
"password_recovery_email_address": ppoc.email,
|
||||||
|
"address": { # TODO: TBD if we're sourcing this from data or config
|
||||||
|
"company_name": "",
|
||||||
|
"address_line_1": "",
|
||||||
|
"city": "",
|
||||||
|
"region": "",
|
||||||
|
"country": "",
|
||||||
|
"postal_code": "",
|
||||||
|
},
|
||||||
|
"billing_profile_display_name": "ATAT Billing Profile",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
initial_task_order: TaskOrder = self.task_orders[0]
|
||||||
|
initial_clin = initial_task_order.sorted_clins[0]
|
||||||
|
portfolio_data.update(
|
||||||
|
{
|
||||||
|
"initial_clin_amount": initial_clin.obligated_amount,
|
||||||
|
"initial_clin_start_date": initial_clin.start_date.strftime(
|
||||||
|
"%Y/%m/%d"
|
||||||
|
),
|
||||||
|
"initial_clin_end_date": initial_clin.end_date.strftime("%Y/%m/%d"),
|
||||||
|
"initial_clin_type": initial_clin.number,
|
||||||
|
"initial_task_order_id": initial_task_order.number,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return portfolio_data
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Portfolio(name='{}', user_count='{}', id='{}')>".format(
|
return "<Portfolio(name='{}', user_count='{}', id='{}')>".format(
|
||||||
self.name, self.user_count, self.id
|
self.name, self.user_count, self.id
|
||||||
|
@ -99,6 +99,8 @@ class PortfolioStateMachine(
|
|||||||
def trigger_next_transition(self, **kwargs):
|
def trigger_next_transition(self, **kwargs):
|
||||||
state_obj = self.machine.get_state(self.state)
|
state_obj = self.machine.get_state(self.state)
|
||||||
|
|
||||||
|
kwargs["csp_data"] = kwargs.get("csp_data", {})
|
||||||
|
|
||||||
if state_obj.is_system:
|
if state_obj.is_system:
|
||||||
if self.current_state in (FSMStates.UNSTARTED, FSMStates.STARTING):
|
if self.current_state in (FSMStates.UNSTARTED, FSMStates.STARTING):
|
||||||
# call the first trigger availabe for these two system states
|
# call the first trigger availabe for these two system states
|
||||||
|
@ -87,6 +87,10 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
def is_expired(self):
|
def is_expired(self):
|
||||||
return self.status == Status.EXPIRED
|
return self.status == Status.EXPIRED
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_upcoming(self):
|
||||||
|
return self.status == Status.UPCOMING
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def clins_are_completed(self):
|
def clins_are_completed(self):
|
||||||
return all([len(self.clins), (clin.is_completed for clin in self.clins)])
|
return all([len(self.clins), (clin.is_completed for clin in self.clins)])
|
||||||
|
@ -50,8 +50,10 @@ def reports(portfolio_id):
|
|||||||
return render_template(
|
return render_template(
|
||||||
"portfolios/reports/index.html",
|
"portfolios/reports/index.html",
|
||||||
portfolio=portfolio,
|
portfolio=portfolio,
|
||||||
# wrapped in str() because the sum of obligated funds returns a Decimal object
|
# wrapped in str() because this sum returns a Decimal object
|
||||||
total_portfolio_value=str(portfolio.total_obligated_funds),
|
total_portfolio_value=str(
|
||||||
|
portfolio.total_obligated_funds + portfolio.upcoming_obligated_funds
|
||||||
|
),
|
||||||
current_obligated_funds=current_obligated_funds,
|
current_obligated_funds=current_obligated_funds,
|
||||||
expired_task_orders=Reports.expired_task_orders(portfolio),
|
expired_task_orders=Reports.expired_task_orders(portfolio),
|
||||||
retrieved=datetime.now(), # mocked datetime of reporting data retrival
|
retrieved=datetime.now(), # mocked datetime of reporting data retrival
|
||||||
|
@ -195,7 +195,7 @@ def add_task_orders_to_portfolio(portfolio):
|
|||||||
task_order=unsigned_to, start_date=(today - five_days), end_date=today
|
task_order=unsigned_to, start_date=(today - five_days), end_date=today
|
||||||
),
|
),
|
||||||
CLINFactory.build(
|
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(
|
CLINFactory.build(
|
||||||
task_order=expired_to, start_date=(today - five_days), end_date=yesterday
|
task_order=expired_to, start_date=(today - five_days), end_date=yesterday
|
||||||
|
@ -513,10 +513,6 @@
|
|||||||
}
|
}
|
||||||
&__header {
|
&__header {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
&-icon {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&__value {
|
&__value {
|
||||||
font-size: $lead-font-size;
|
font-size: $lead-font-size;
|
||||||
|
@ -95,4 +95,9 @@
|
|||||||
.icon {
|
.icon {
|
||||||
@include icon-size(16);
|
@include icon-size(16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--tight {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
{% from "components/alert.html" import Alert %}
|
{% from "components/alert.html" import Alert %}
|
||||||
{% from "components/checkbox_input.html" import CheckboxInput %}
|
{% from "components/checkbox_input.html" import CheckboxInput %}
|
||||||
{% from "components/text_input.html" import TextInput %}
|
|
||||||
{% from "components/phone_input.html" import PhoneInput %}
|
{% from "components/phone_input.html" import PhoneInput %}
|
||||||
|
{% from "components/text_input.html" import TextInput %}
|
||||||
|
{% from "components/tooltip.html" import Tooltip %}
|
||||||
|
|
||||||
{% macro EnvRoleInput(sub_form, member_role_id=None) %}
|
{% macro EnvRoleInput(sub_form, member_role_id=None) %}
|
||||||
{% set role = sub_form.role.data if not sub_form.disabled.data else "Access Suspended" %}
|
{% set role = sub_form.role.data if not sub_form.disabled.data else "Access Suspended" %}
|
||||||
@ -121,6 +122,6 @@
|
|||||||
{{ TextInput(member_form.email, validation='email', optional=False) }}
|
{{ TextInput(member_form.email, validation='email', optional=False) }}
|
||||||
{{ PhoneInput(member_form.phone_number, member_form.phone_ext)}}
|
{{ PhoneInput(member_form.phone_number, member_form.phone_ext)}}
|
||||||
{{ TextInput(member_form.dod_id, validation='dodId', optional=False) }}
|
{{ TextInput(member_form.dod_id, validation='dodId', optional=False) }}
|
||||||
<a href="#">{{ "forms.new_member.dod_help" | translate }}</a>
|
{{ "forms.new_member.dod_help" | translate }} {{ Tooltip("forms.new_member.dod_text"|translate, title="", classes="icon-tooltip--tight") }}
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% macro Tooltip(message,title='Help', classes="") %}
|
{% macro Tooltip(message,title='Help', classes="") %}
|
||||||
|
|
||||||
<button type="button" tabindex="0" class="icon-tooltip {{classes}}" v-tooltip.top="{content: '{{message}}', container: false}">
|
<button type="button" tabindex="0" class="icon-tooltip {{classes}}" v-tooltip.top='{content: "{{message}}", container: false}'>
|
||||||
{{ Icon('question') }}<span>{{ title }}</span>
|
{{ Icon('question') }}<span>{{ title }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
{% from "components/phone_input.html" import PhoneInput %}
|
{% from "components/phone_input.html" import PhoneInput %}
|
||||||
{% from "components/text_input.html" import TextInput %}
|
{% from "components/text_input.html" import TextInput %}
|
||||||
|
{% from "components/tooltip.html" import Tooltip %}
|
||||||
|
|
||||||
{% macro PermsFields(form, member_role_id=None) %}
|
{% macro PermsFields(form, member_role_id=None) %}
|
||||||
<h2>Set Portfolio Permissions</h2>
|
<h2>Set Portfolio Permissions</h2>
|
||||||
@ -32,6 +33,6 @@
|
|||||||
{{ TextInput(member_form.email, validation='email', optional=False) }}
|
{{ TextInput(member_form.email, validation='email', optional=False) }}
|
||||||
{{ PhoneInput(member_form.phone_number, member_form.phone_ext)}}
|
{{ PhoneInput(member_form.phone_number, member_form.phone_ext)}}
|
||||||
{{ TextInput(member_form.dod_id, validation='dodId', optional=False) }}
|
{{ TextInput(member_form.dod_id, validation='dodId', optional=False) }}
|
||||||
<a href="#">{{ "forms.new_member.dod_help" | translate }}</a>
|
{{ "forms.new_member.dod_help" | translate }} {{ Tooltip("forms.new_member.dod_text"|translate, title="", classes="icon-tooltip--tight") }}
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
@ -6,14 +6,14 @@
|
|||||||
<div class='col col--grow summary-item'>
|
<div class='col col--grow summary-item'>
|
||||||
<h5 class="summary-item__header">
|
<h5 class="summary-item__header">
|
||||||
<span class="summary-item__header-text">{{ "portfolios.reports.total_value.header" | translate }}</span>
|
<span class="summary-item__header-text">{{ "portfolios.reports.total_value.header" | translate }}</span>
|
||||||
{{Tooltip(("portfolios.reports.total_value.tooltip" | translate), title="", classes="summary-item__header-icon")}}
|
{{Tooltip(("portfolios.reports.total_value.tooltip" | translate), title="", classes="icon-tooltip--tight")}}
|
||||||
</h5>
|
</h5>
|
||||||
<p class="summary-item__value">{{ total_portfolio_value | dollars }}</p>
|
<p class="summary-item__value">{{ total_portfolio_value | dollars }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class='col col--grow summary-item'>
|
<div class='col col--grow summary-item'>
|
||||||
<h5 class="summary-item__header">
|
<h5 class="summary-item__header">
|
||||||
<span class="summary-item__header-text">{{ "portfolios.reports.duration.header" | translate }}</span>
|
<span class="summary-item__header-text">{{ "portfolios.reports.duration.header" | translate }}</span>
|
||||||
{{Tooltip(("portfolios.reports.duration.tooltip" | translate), title="", classes="summary-item__header-icon")}}
|
{{Tooltip(("portfolios.reports.duration.tooltip" | translate), title="", classes="icon-tooltip--tight")}}
|
||||||
</h5>
|
</h5>
|
||||||
{% set earliest_pop_start_date, latest_pop_end_date = portfolio.funding_duration %}
|
{% set earliest_pop_start_date, latest_pop_end_date = portfolio.funding_duration %}
|
||||||
{% if earliest_pop_start_date and latest_pop_end_date %}
|
{% if earliest_pop_start_date and latest_pop_end_date %}
|
||||||
@ -29,7 +29,7 @@
|
|||||||
<div class='col col--grow summary-item'>
|
<div class='col col--grow summary-item'>
|
||||||
<h5 class="summary-item__header">
|
<h5 class="summary-item__header">
|
||||||
<span class="summary-item__header-text">{{ "portfolios.reports.days_remaining.header" | translate }}</span>
|
<span class="summary-item__header-text">{{ "portfolios.reports.days_remaining.header" | translate }}</span>
|
||||||
{{Tooltip(("portfolios.reports.days_remaining.toolip" | translate), title="", classes="summary-item__header-icon")}}
|
{{Tooltip(("portfolios.reports.days_remaining.toolip" | translate), title="", classes="icon-tooltip--tight")}}
|
||||||
</h5>
|
</h5>
|
||||||
<p class="summary-item__value">{{ portfolio.days_to_funding_expiration }} days</p>
|
<p class="summary-item__value">{{ portfolio.days_to_funding_expiration }} days</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<div class='col col--grow summary-item'>
|
<div class='col col--grow summary-item'>
|
||||||
<h4 class="summary-item__header">
|
<h4 class="summary-item__header">
|
||||||
<span class="summary-item__header-text">{{ 'task_orders.summary.total' | translate }}</span>
|
<span class="summary-item__header-text">{{ 'task_orders.summary.total' | translate }}</span>
|
||||||
{{ Tooltip(("task_orders.review.tooltip.total_value" | translate), title="", classes="summary-item__header-icon") }}
|
{{ Tooltip(("task_orders.review.tooltip.total_value" | translate), title="", classes="icon-tooltip--tight") }}
|
||||||
</h4>
|
</h4>
|
||||||
<p class="summary-item__value--large">
|
<p class="summary-item__value--large">
|
||||||
{{ contract_amount | dollars }}
|
{{ contract_amount | dollars }}
|
||||||
@ -21,7 +21,7 @@
|
|||||||
<div class='col col--grow summary-item'>
|
<div class='col col--grow summary-item'>
|
||||||
<h4 class="summary-item__header">
|
<h4 class="summary-item__header">
|
||||||
<span class="summary-item__header-text">{{ 'task_orders.summary.obligated' | translate }}</span>
|
<span class="summary-item__header-text">{{ 'task_orders.summary.obligated' | translate }}</span>
|
||||||
{{ Tooltip(("task_orders.review.tooltip.obligated_funds" | translate), title="", classes="summary-item__header-icon") }}
|
{{ Tooltip(("task_orders.review.tooltip.obligated_funds" | translate), title="", classes="icon-tooltip--tight") }}
|
||||||
</h4>
|
</h4>
|
||||||
<p class="summary-item__value--large">
|
<p class="summary-item__value--large">
|
||||||
{{ obligated_funds | dollars }}
|
{{ obligated_funds | dollars }}
|
||||||
@ -30,7 +30,7 @@
|
|||||||
<div class='col col--grow summary-item'>
|
<div class='col col--grow summary-item'>
|
||||||
<h4 class="summary-item__header">
|
<h4 class="summary-item__header">
|
||||||
<span class="summary-item__header-text">{{ 'task_orders.summary.expended' | translate }}</span>
|
<span class="summary-item__header-text">{{ 'task_orders.summary.expended' | translate }}</span>
|
||||||
{{ Tooltip(("task_orders.review.tooltip.expended_funds" | translate), title="", classes="summary-item__header-icon") }}
|
{{ Tooltip(("task_orders.review.tooltip.expended_funds" | translate), title="", classes="icon-tooltip--tight") }}
|
||||||
</h4>
|
</h4>
|
||||||
<p class="summary-item__value--large">
|
<p class="summary-item__value--large">
|
||||||
{{ expended_funds | dollars }}
|
{{ expended_funds | dollars }}
|
||||||
|
@ -7,6 +7,51 @@ from tests.factories import (
|
|||||||
random_past_date,
|
random_past_date,
|
||||||
)
|
)
|
||||||
import datetime
|
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():
|
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()]
|
portfolio=portfolio, signed_at=random_past_date(), clins=[CLINFactory.create()]
|
||||||
)
|
)
|
||||||
assert len(portfolio.active_task_orders) == 1
|
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)
|
||||||
|
@ -246,6 +246,7 @@ forms:
|
|||||||
description: Add, remove and edit applications in this Portfolio.
|
description: Add, remove and edit applications in this Portfolio.
|
||||||
dod_id_label: DoD ID
|
dod_id_label: DoD ID
|
||||||
dod_help: How do I find out the DoD ID?
|
dod_help: How do I find out the DoD ID?
|
||||||
|
dod_text: "An individual's DOD ID (formerly known as EDIPI) number can be found on the back of their CAC. Access to this system requires a valid CAC and DOD ID."
|
||||||
email_label: Email address
|
email_label: Email address
|
||||||
first_name_label: First name
|
first_name_label: First name
|
||||||
funding:
|
funding:
|
||||||
@ -507,7 +508,7 @@ portfolios:
|
|||||||
estimate_warning: Reports displayed in JEDI are estimates and not a system of record.
|
estimate_warning: Reports displayed in JEDI are estimates and not a system of record.
|
||||||
total_value:
|
total_value:
|
||||||
header: Total Portfolio 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:
|
task_orders:
|
||||||
add_new_button: Add New Task Order
|
add_new_button: Add New Task Order
|
||||||
review:
|
review:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user