diff --git a/atst/domain/csp/reports.py b/atst/domain/csp/reports.py
index 88c30482..683c1139 100644
--- a/atst/domain/csp/reports.py
+++ b/atst/domain/csp/reports.py
@@ -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
+ ]
diff --git a/atst/domain/reports.py b/atst/domain/reports.py
index 96085afb..94f6c54e 100644
--- a/atst/domain/reports.py
+++ b/atst/domain/reports.py
@@ -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)
diff --git a/atst/models/clin.py b/atst/models/clin.py
index e376da9c..2802e292 100644
--- a/atst/models/clin.py
+++ b/atst/models/clin.py
@@ -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
diff --git a/atst/models/portfolio.py b/atst/models/portfolio.py
index d749e470..08a65f1c 100644
--- a/atst/models/portfolio.py
+++ b/atst/models/portfolio.py
@@ -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 (
diff --git a/atst/routes/portfolios/index.py b/atst/routes/portfolios/index.py
index 9c2ec1a0..336cd2e6 100644
--- a/atst/routes/portfolios/index.py
+++ b/atst/routes/portfolios/index.py
@@ -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,
)
diff --git a/js/components/accordion.js b/js/components/accordion.js
new file mode 100644
index 00000000..d281a9e7
--- /dev/null
+++ b/js/components/accordion.js
@@ -0,0 +1,14 @@
+import ToggleMixin from '../mixins/toggle'
+
+export default {
+ name: 'accordion',
+
+ mixins: [ToggleMixin],
+
+ props: {
+ defaultVisible: {
+ type: Boolean,
+ default: false,
+ },
+ },
+}
diff --git a/js/components/charts/budget_chart.js b/js/components/charts/budget_chart.js
deleted file mode 100644
index 11fee9b0..00000000
--- a/js/components/charts/budget_chart.js
+++ /dev/null
@@ -1,189 +0,0 @@
-import {
- format,
- isWithinRange,
- addMonths,
- isSameMonth,
- getMonth,
-} from 'date-fns'
-import { abbreviateDollars, formatDollars } from '../../lib/dollars'
-
-const TOP_OFFSET = 20
-const BOTTOM_OFFSET = 70
-const CHART_HEIGHT = 360
-
-export default {
- name: 'budget-chart',
- props: {
- currentMonth: String,
- expirationDate: String,
- months: Object,
- budget: String,
- },
-
- data: function() {
- const heightScale =
- this.budget / (CHART_HEIGHT - TOP_OFFSET - BOTTOM_OFFSET)
- return {
- numMonths: 10,
- focusedMonthPosition: 4,
- height: CHART_HEIGHT,
- heightScale,
- budgetHeight: CHART_HEIGHT - BOTTOM_OFFSET - this.budget / heightScale,
- baseHeight: CHART_HEIGHT - BOTTOM_OFFSET,
- width: 0,
- displayedMonths: [],
- spendPath: '',
- projectedPath: '',
- displayBudget: formatDollars(parseFloat(this.budget)),
- }
- },
-
- mounted: function() {
- this._setDisplayedMonths()
- this._setMetrics()
- addEventListener('load', this._setMetrics)
- addEventListener('resize', this._setMetrics)
- },
-
- methods: {
- _setMetrics: function() {
- this.width = this.$refs.panel.clientWidth
- this.spendPath = ''
- this.projectedPath = ''
-
- let lastSpend = 0
- let lastSpendPoint = ''
-
- for (let i = 0; i < this.numMonths; i++) {
- const {
- metrics,
- budget,
- rollingAverage,
- cumulativeTotal,
- } = this.displayedMonths[i]
- const blockWidth = this.width / this.numMonths
- const blockX = blockWidth * i
- const spend = budget && budget.spend ? budget.spend : rollingAverage
- const barHeight = spend / this.heightScale
- lastSpend = spend
- const cumulativeY =
- this.height - cumulativeTotal / this.heightScale - BOTTOM_OFFSET
- const cumulativeX = blockX + blockWidth / 2
- const cumulativePoint = `${cumulativeX} ${cumulativeY}`
-
- this.displayedMonths[i].metrics = Object.assign(metrics, {
- blockWidth,
- blockX,
- barHeight,
- barWidth: 30,
- barX: blockX + (blockWidth / 2 - 15),
- barY: this.height - barHeight - BOTTOM_OFFSET,
- cumulativeR: 2.5,
- cumulativeY,
- cumulativeX,
- })
-
- if (budget && budget.spend) {
- this.spendPath += this.spendPath === '' ? 'M' : ' L'
- this.spendPath += cumulativePoint
- lastSpendPoint = cumulativePoint
- } else if (lastSpendPoint !== '') {
- this.projectedPath +=
- this.projectedPath === '' ? `M${lastSpendPoint} L` : ' L'
- this.projectedPath += cumulativePoint
- }
- }
- },
-
- _setDisplayedMonths: function() {
- const [month, year] = this.currentMonth.split('/')
- const [expYear, expMonth, expDate] = this.expirationDate.split('-') // assumes format 'YYYY-MM-DD'
- const monthsRange = []
- const monthsBack = this.focusedMonthPosition + 1
- const monthsForward = this.numMonths - this.focusedMonthPosition - 1
-
- // currently focused date
- const current = new Date(year, month)
-
- // starting date of the chart
- const start = addMonths(current, -monthsBack)
-
- // ending date of the chart
- const end = addMonths(start, this.numMonths + 1)
-
- // expiration date
- const expires = new Date(expYear, expMonth - 1, expDate)
-
- // is the expiration date within the displayed date range?
- const expirationWithinRange = isWithinRange(expires, start, end)
-
- let rollingAverage = 0
- let cumulativeTotal = 0
-
- for (let i = 0; i < this.numMonths; i++) {
- const date = addMonths(start, i)
- const dateMinusOne = addMonths(date, -1)
- const dateMinusTwo = addMonths(date, -2)
- const dateMinusThree = addMonths(date, -3)
-
- const index = format(date, 'MM/YYYY')
- const indexMinusOne = format(dateMinusOne, 'MM/YYYY')
- const indexMinusTwo = format(dateMinusTwo, 'MM/YYYY')
- const indexMinusThree = format(dateMinusThree, 'MM/YYYY')
-
- const budget = this.months[index] || null
- const spendAmount = budget ? budget.spend : rollingAverage
- const spendMinusOne = this.months[indexMinusOne]
- ? this.months[indexMinusOne].spend
- : rollingAverage
- const spendMinusTwo = this.months[indexMinusTwo]
- ? this.months[indexMinusTwo].spend
- : rollingAverage
- const spendMinusThree = this.months[indexMinusThree]
- ? this.months[indexMinusThree].spend
- : rollingAverage
-
- const isExpirationMonth = isSameMonth(date, expires)
-
- if (budget && budget.cumulative) {
- cumulativeTotal = budget.cumulative
- } else {
- cumulativeTotal += spendAmount
- }
-
- rollingAverage =
- (spendAmount + spendMinusOne + spendMinusTwo + spendMinusThree) / 4
-
- monthsRange.push({
- budget,
- rollingAverage,
- cumulativeTotal,
- isExpirationMonth,
- spendAmount: formatDollars(spendAmount),
- abbreviatedSpend: abbreviateDollars(spendAmount),
- cumulativeAmount: formatDollars(cumulativeTotal),
- abbreviatedCumulative: abbreviateDollars(cumulativeTotal),
- date: {
- monthIndex: format(date, 'M'),
- month: format(date, 'MMM'),
- year: format(date, 'YYYY'),
- },
- showYear: isExpirationMonth || i === 0 || getMonth(date) === 0,
- isHighlighted: this.currentMonth === index,
- metrics: {
- blockWidth: 0,
- blockX: 0,
- barHeight: 0,
- barWidth: 0,
- barX: 0,
- barY: 0,
- cumulativeY: 0,
- cumulativeX: 0,
- cumulativeR: 0,
- },
- })
- }
- this.displayedMonths = monthsRange
- },
- },
-}
diff --git a/js/components/tables/spend_table.js b/js/components/tables/spend_table.js
index f8d663a3..c3a1c90f 100644
--- a/js/components/tables/spend_table.js
+++ b/js/components/tables/spend_table.js
@@ -6,11 +6,9 @@ export default {
props: {
applications: Object,
- portfolio: Object,
environments: Object,
currentMonthIndex: String,
prevMonthIndex: String,
- twoMonthsAgoIndex: String,
},
data: function() {
@@ -40,9 +38,5 @@ export default {
formatDollars: function(value) {
return formatDollars(value, false)
},
-
- round: function(value) {
- return Math.round(value)
- },
},
}
diff --git a/js/index.js b/js/index.js
index 7381c828..dd5ef06a 100644
--- a/js/index.js
+++ b/js/index.js
@@ -17,7 +17,6 @@ import ApplicationEnvironments from './components/forms/new_application/environm
import MultiStepModalForm from './components/forms/multi_step_modal_form'
import uploadinput from './components/upload_input'
import Modal from './mixins/modal'
-import BudgetChart from './components/charts/budget_chart'
import SpendTable from './components/tables/spend_table'
import LocalDatetime from './components/local_datetime'
import { isNotInVerticalViewport } from './lib/viewport'
@@ -30,6 +29,7 @@ import SemiCollapsibleText from './components/semi_collapsible_text'
import ToForm from './components/forms/to_form'
import ClinFields from './components/clin_fields'
import PopDateRange from './components/pop_date_range'
+import Accordion from './components/accordion'
Vue.config.productionTip = false
@@ -40,6 +40,7 @@ Vue.mixin(Modal)
const app = new Vue({
el: '#app-root',
components: {
+ Accordion,
dodlogin,
toggler,
optionsinput,
@@ -47,7 +48,6 @@ const app = new Vue({
textinput,
checkboxinput,
ApplicationEnvironments,
- BudgetChart,
SpendTable,
LocalDatetime,
MultiStepModalForm,
diff --git a/styles/atat.scss b/styles/atat.scss
index 346b5d44..c3fd1a55 100644
--- a/styles/atat.scss
+++ b/styles/atat.scss
@@ -33,7 +33,6 @@
@import "components/progress_menu.scss";
@import "components/forms";
@import "components/selector";
-@import "components/budget_chart";
@import "components/audit_log";
@import "components/usa_banner";
@import "components/dod_login_notice.scss";
diff --git a/styles/components/_budget_chart.scss b/styles/components/_budget_chart.scss
deleted file mode 100644
index d5930236..00000000
--- a/styles/components/_budget_chart.scss
+++ /dev/null
@@ -1,169 +0,0 @@
-.budget-chart {
- svg {
- display: block;
-
- .filter__text-background {
- feFlood {
- flood-color: $color-white;
- flood-opacity: 1;
- }
-
- &--highlighted {
- feFlood {
- flood-color: $color-aqua-lightest;
- flood-opacity: 1;
- }
- }
- }
-
- a {
- text-decoration: none;
-
- &:focus {
- outline: none;
- stroke: $color-gray-light;
- stroke-dasharray: 2px;
- }
-
- &:hover {
- .filter__text-background {
- feFlood {
- flood-color: $color-aqua-lightest;
- flood-opacity: 1;
- }
- }
- }
- }
- }
-
- &__header {
- border-bottom: 1px solid $color-gray-light;
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
- }
-
- &__legend {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
-
- dl {
- margin: 0 0 0 ($gap * 2);
-
- > div {
- margin: 0;
- display: flex;
- flex-direction: row-reverse;
- align-items: center;
-
- dt {
- @include small-label;
- }
- }
- }
-
- &__dot {
- width: $gap;
- height: $gap;
- border-radius: $gap / 2;
- margin: 0 $gap;
-
- &.accumulated {
- background-color: $color-gold;
- }
-
- &.monthly {
- background-color: $color-blue;
- }
- }
-
- &__line {
- height: 2px;
- width: $gap * 3;
- border-top-width: 2px;
- border-top-style: dashed;
- margin: $gap;
-
- &.spend {
- border-color: $color-blue;
- }
-
- &.accumulated {
- border-color: $color-gold;
- }
- }
- }
-
- &__block {
- fill: transparent;
- cursor: pointer;
-
- &--highlighted {
- fill: rgba($color-aqua, 0.15);
- }
-
- &--is-expiration {
- border-left: 2px dotted $color-gray;
- }
-
- &:hover {
- fill: rgba($color-aqua, 0.15);
- }
- }
-
- &__bar {
- fill: $color-blue;
-
- &--projected {
- fill: transparent;
- stroke-width: 2px;
- stroke: $color-blue;
- stroke-dasharray: 4px;
- }
- }
-
- &__expiration-line {
- stroke-width: 2px;
- stroke: $color-gray-light;
- stroke-dasharray: 4px;
- }
-
- &__cumulative {
- &__dot {
- fill: $color-gold;
- }
- }
-
- &__projected-path {
- stroke-width: 1px;
- stroke: $color-gold;
- stroke-dasharray: 4px;
- fill: none;
- }
-
- &__spend-path {
- stroke-width: 1px;
- stroke: $color-gold;
- fill: none;
- }
-
- &__budget-line {
- stroke-width: 2px;
- stroke: $color-gray-light;
- stroke-dasharray: 4px;
- }
-
- &__label {
- @include small-label;
-
- fill: $color-gray;
- pointer-events: none;
-
- &--strong {
- fill: $color-black;
- }
- }
-}
diff --git a/templates/components/accordion.html b/templates/components/accordion.html
new file mode 100644
index 00000000..8e508321
--- /dev/null
+++ b/templates/components/accordion.html
@@ -0,0 +1,23 @@
+{% macro Accordion(title, id, heading_level="h2") %}
+
Applications and Environments | +Current Month | +Last Month | +Total Spent | +
---|---|---|---|
+ + | ++ + | ++ + | ++ + | +
+ + | ++ + | ++ + | ++ + | +
Period of Performance
++ {{ task_order["period_of_performance"].start_date | formattedDate(formatter="%B %d, %Y") }} + - + {{ task_order["period_of_performance"].end_date | formattedDate(formatter="%B %d, %Y") }} +
+Total Obligated
+{{ task_order["total_obligated_funds"] | dollars }}
+Total Expended
+{{ task_order["expended_funds"] | dollars }}
+Total Unused
+{{ (task_order["total_obligated_funds"] - task_order["expended_funds"]) | dollars }}
+Spending scope | -{{ two_months_ago.strftime('%B %Y') }} | -{{ prev_month.strftime('%B %Y') }} | -{{ current_month.strftime('%B %Y') }} | -- - - - |
---|---|---|---|---|
Portfolio Total | -{{ portfolio_totals.get(two_months_ago_index, 0) | dollars }} | -{{ portfolio_totals.get(prev_month_index, 0) | dollars }} | -{{ portfolio_totals.get(current_month_index, 0) | dollars }} | -
- |
-
- - | -- - | - -- - | - -- - | - -
-
-
-
- |
-
- | - - -- - | - -- - | - -- - | - -- |
Remaining funds:
+{{ (funds["obligated_funds"] - funds["expended_funds"]) | dollars }}
+Funds expended to date:
+{{ funds["expended_funds"] | dollars }}
++ Total Portfolio Value + {{Tooltip(("common.lorem" | translate), title="")}} +
+{{ total_portfolio_value | dollars }}
++ Funding Duration + {{Tooltip(("common.lorem" | translate), title="")}} +
+ {% set earliest_pop_start_date, latest_pop_end_date = portfolio.funding_duration %} + {% if earliest_pop_start_date and latest_pop_end_date %} ++ {{ earliest_pop_start_date | formattedDate(formatter="%B %d, %Y") }} + - + {{ latest_pop_end_date | formattedDate(formatter="%B %d, %Y") }} +
+ {% else %} +-
+ {% endif %} ++ Days Remaining + {{Tooltip(("common.lorem" | translate), title="")}} +
+{{ portfolio.days_to_funding_expiration }} days
+