Skip to content

Commit

Permalink
Merge pull request #5216 from akvo/5212-adjust-reports-for-cumulative…
Browse files Browse the repository at this point in the history
…-carried-over-values

[#5212] Hide carried-over values for future periods on the reports

Closes #5212
  • Loading branch information
MichaelAkvo authored Feb 8, 2023
2 parents 58f94e5 + 0f7c7ef commit a0d2a60
Show file tree
Hide file tree
Showing 8 changed files with 81 additions and 37 deletions.
37 changes: 31 additions & 6 deletions akvo/rsr/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ def make(cls, data, prefix=''):


class ReportingPeriodMixin(ABC):
period_start: Optional[date] = None
target_value: Optional[Decimal] = None
indicator_type: int = QUANTITATIVE
indicator_measure: str = ''
Expand All @@ -139,6 +140,10 @@ def is_percentage(self):
def is_cumulative(self):
return self.indicator_cumulative and not self.is_percentage

@property
def is_cumulative_future(self):
return self.is_cumulative and self.period_start and self.period_start > date.today()

@cached_property
def approved_updates(self):
return [u for u in self.updates if u.is_approved]
Expand Down Expand Up @@ -174,7 +179,7 @@ def updates_denominator(self):
def aggregated_value(self):
if self.is_percentage or self.is_qualitative:
return None
value = self.updates_value
value = ensure_decimal(self.updates_value)
for contributor in self.contributors:
value += ensure_decimal(contributor.aggregated_value)
return value
Expand All @@ -183,7 +188,7 @@ def aggregated_value(self):
def aggregated_numerator(self):
if not self.is_percentage:
return None
value = self.updates_numerator
value = ensure_decimal(self.updates_numerator)
for contributor in self.contributors:
value += ensure_decimal(contributor.aggregated_numerator)
return value
Expand All @@ -192,7 +197,7 @@ def aggregated_numerator(self):
def aggregated_denominator(self):
if not self.is_percentage:
return None
value = self.updates_denominator
value = ensure_decimal(self.updates_denominator)
for contributor in self.contributors:
value += ensure_decimal(contributor.aggregated_denominator)
return value
Expand Down Expand Up @@ -272,6 +277,8 @@ def get_aggregated_disaggregation_target_value(self, category, type):
return item.value if item else None

def get_disaggregation_value(self, category, type):
if self.is_cumulative_future:
return None
item = self._select_disaggregation(self.period_disaggregations if self.is_cumulative else self.disaggregations, category, type)
if not item:
return None
Expand All @@ -280,6 +287,8 @@ def get_disaggregation_value(self, category, type):
return item.value

def get_aggregated_disaggregation_value(self, category, type):
if self.is_cumulative_future:
return None
item = self._select_disaggregation(self.period_disaggregations if self.is_cumulative else self.aggregated_disaggregations, category, type)
if not item:
return None
Expand All @@ -305,6 +314,7 @@ def _get_disaggregations(self, updates):
@dataclass(frozen=True)
class ContributorData(ReportingPeriodMixin):
id: int
period_start: Optional[date] = None
parent: Optional[int] = None
indicator_type: int = QUANTITATIVE
indicator_measure: str = ''
Expand All @@ -323,6 +333,7 @@ class ContributorData(ReportingPeriodMixin):
def make(cls, data, prefix=''):
return cls(
id=data[f"{prefix}id"],
period_start=data.get(f"{prefix}period_start", None),
parent=data.get(f"{prefix}parent_period", None),
indicator_type=data.get(f"{prefix}indicator__type", QUANTITATIVE),
indicator_measure=data.get(f"{prefix}indicator__measure", ''),
Expand All @@ -347,6 +358,8 @@ def has_contributions(self):
def actual_value(self):
if self.is_qualitative:
return None
if self.is_cumulative_future:
return 0
if self.is_cumulative:
return self.period_actual_value
if self.is_percentage:
Expand All @@ -361,7 +374,7 @@ class PeriodData(ReportingPeriodMixin):
period_end: Optional[date] = None
target_value: Optional[Decimal] = None
target_comment: str = ''
actual_value: Optional[Decimal] = None
period_actual_value: Optional[Decimal] = None
actual_comment: str = ''
narrative: str = ''
indicator_type: int = QUANTITATIVE
Expand All @@ -380,7 +393,7 @@ def make(cls, data, prefix=''):
period_end=data.get(f"{prefix}period_end", None),
target_value=maybe_decimal(data.get(f"{prefix}target_value", None)),
target_comment=data.get(f"{prefix}target_comment", ''),
actual_value=maybe_decimal(data.get(f"{prefix}actual_value", None)),
period_actual_value=ensure_decimal(data.get(f"{prefix}actual_value", None)),
actual_comment=data.get(f"{prefix}actual_comment", ''),
narrative=data.get(f"{prefix}narrative", ''),
indicator_type=data.get(f"{prefix}indicator__type", QUANTITATIVE),
Expand All @@ -392,15 +405,27 @@ def make(cls, data, prefix=''):
def aggregated_value(self):
if self.is_qualitative:
return None
if self.is_cumulative_future:
return 0
if self.is_cumulative:
return self.actual_value
if self.is_percentage:
return calculate_percentage(self.aggregated_numerator, self.aggregated_denominator)
value = self.updates_value
value = ensure_decimal(self.updates_value)
for contributor in self.contributors:
value += ensure_decimal(contributor.aggregated_value)
return value

@cached_property
def actual_value(self):
if self.is_qualitative:
return None
if self.is_percentage:
return calculate_percentage(self.updates_numerator, self.updates_denominator)
if self.is_cumulative_future:
return 0
return self.period_actual_value

@cached_property
def pending_updates(self):
return [u for u in self.updates if u.is_pending]
Expand Down
40 changes: 24 additions & 16 deletions akvo/rsr/project_overview.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

import copy
from collections import OrderedDict
from datetime import date
from functools import cached_property
from django.conf import settings
from akvo.rsr.models import IndicatorPeriod, IndicatorPeriodData
from akvo.rsr.models.result.utils import QUALITATIVE, PERCENTAGE_MEASURE, calculate_percentage
Expand Down Expand Up @@ -127,7 +129,6 @@ def __init__(self, period, children=[], aggregate_targets=False, project_disaggr
self._project = None
self._updates = None
self._actual_comment = None
self._actual_value = None
self._actual_numerator = None
self._actual_denominator = None
self._target_value = None
Expand Down Expand Up @@ -192,6 +193,10 @@ def indicator_target_value(self):
self._indicator_target_value = ensure_decimal(self.indicator.target_value)
return self._indicator_target_value

@cached_property
def is_cumulative(self):
return self.indicator.is_cumulative()

@property
def actual_comment(self):
if self._actual_comment is None:
Expand All @@ -200,13 +205,13 @@ def actual_comment(self):
else False
return self._actual_comment or None

@property
@cached_property
def actual_value(self):
if self._actual_value is None:
self._actual_value = calculate_percentage(self.actual_numerator, self.actual_denominator) \
if self.type == IndicatorType.PERCENTAGE \
else ensure_decimal(self._real.actual_value)
return self._actual_value
if self.type == IndicatorType.PERCENTAGE:
return calculate_percentage(self.actual_numerator, self._actual_denominator)
if self.is_cumulative and self.period_start and self.period_start > date.today():
return 0
return self._real.actual_value

@property
def actual_numerator(self):
Expand Down Expand Up @@ -351,7 +356,7 @@ def _build(self):
contributor = Contributor(node['item'], node['children'], self.type, self._project_disaggregations)

if not contributor.project.aggregate_to_parent or (
contributor.actual_value < 1 and len(contributor.updates) < 1
ensure_decimal(contributor.actual_value) < 1 and len(contributor.updates) < 1
):
continue

Expand All @@ -364,7 +369,7 @@ def _build(self):
self._total_numerator += contributor.actual_numerator
self._total_denominator += contributor.actual_denominator
else:
self._total_value += contributor.actual_value
self._total_value += ensure_decimal(contributor.actual_value)

# calculate disaggregations
for key in contributor.contributors.disaggregations:
Expand All @@ -387,7 +392,6 @@ def __init__(self, period, children=[], type=IndicatorType.UNIT, project_disaggr
self._project_disaggregations = project_disaggregations
self._project = None
self._country = None
self._actual_value = None
self._actual_numerator = None
self._actual_denominator = None
self._location = None
Expand Down Expand Up @@ -419,13 +423,17 @@ def updates(self):
self._updates = UpdateCollection(self.period, self.type)
return self._updates

@property
@cached_property
def is_cumulative(self):
return self.period.indicator.is_cumulative()

@cached_property
def actual_value(self):
if self._actual_value is None:
self._actual_value = calculate_percentage(self.actual_numerator, self.actual_denominator) \
if self.type == IndicatorType.PERCENTAGE \
else ensure_decimal(self.period.actual_value)
return self._actual_value
if self.type == IndicatorType.PERCENTAGE:
return calculate_percentage(self.actual_numerator, self._actual_denominator)
if self.is_cumulative and self.period.period_start and self.period.period_start > date.today():
return 0
return ensure_decimal(self.period.actual_value)

@property
def actual_numerator(self):
Expand Down
12 changes: 6 additions & 6 deletions akvo/rsr/views/py_reports/program_overview_excel_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def fetch_contributors(root_period_ids):
)\
.filter(id__in=contributor_ids)\
.values(
'id', 'parent_period', 'target_value', 'actual_value', 'indicator__id',
'id', 'period_start', 'parent_period', 'target_value', 'actual_value', 'indicator__id',
'indicator__type', 'indicator__measure', 'indicator__cumulative', 'indicator__target_value',
'indicator__baseline_value', 'indicator__result__project__id',
'indicator__result__project__title', 'indicator__result__project__subtitle',
Expand Down Expand Up @@ -240,8 +240,8 @@ def add_email_report_job(request, program_id):

def handle_email_report(params, recipient):
program = Project.objects.prefetch_related('results').get(pk=params['program_id'])
start_date = utils.parse_date(params.get('period_start', ''))
end_date = utils.parse_date(params.get('period_end', ''))
start_date = utils.parse_date(params.get('period_start', ''), datetime(1900, 1, 1))
end_date = utils.parse_date(params.get('period_end', ''), datetime(2999, 12, 31))
wb = generate_workbok(program, start_date, end_date)
filename = '{}-{}-program-overview-report.xlsx'.format(datetime.today().strftime('%Y%b%d'), program.id)
utils.send_excel_report(wb, recipient, filename)
Expand Down Expand Up @@ -381,7 +381,7 @@ def render_period(ws, row, result, indicator, period, aggregate_targets=False, u
ws.set_cell_value(row, col, indicator.target_value if use_indicator_target else ensure_decimal(period.target_value))
col += 1
ws.set_cell_value(row, col, period.aggregated_value)
if period.is_quantitative:
if period.is_quantitative and not period.is_cumulative_future:
col += 3
for category, types in disaggregations.items():
for type in [t for t in types.keys()]:
Expand Down Expand Up @@ -423,8 +423,8 @@ def render_contributor(ws, row, result, indicator, period, contributor, aggregat
col += 2
ws.set_cell_value(row, col, contributor.actual_value)
col += 1
if period.is_quantitative:
contribution = calculate_percentage(ensure_decimal(contributor.updates_value), ensure_decimal(period.aggregated_value))
if period.is_quantitative and not period.is_cumulative_future:
contribution = calculate_percentage(ensure_decimal(contributor.actual_value), ensure_decimal(period.aggregated_value))
ws.set_cell_style(row, col, Style(alignment=Alignment(horizontal='right')))
ws.set_cell_value(row, col, f"{contribution}%")
col += 1
Expand Down
4 changes: 2 additions & 2 deletions akvo/rsr/views/py_reports/program_overview_pdf_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ def add_email_report_job(request, program_id):
def handle_email_report(params, recipient):
now = datetime.today()
program = Project.objects.prefetch_related('results').get(pk=params['program_id'])
start_date = utils.parse_date(params.get('period_start', ''))
end_date = utils.parse_date(params.get('period_end', ''))
start_date = utils.parse_date(params.get('period_start', ''), datetime(1900, 1, 1))
end_date = utils.parse_date(params.get('period_end', ''), datetime(2999, 12, 31))
program_view = build_view_object(program, start_date or datetime(1900, 1, 1), end_date or (datetime.today() + relativedelta(years=10)))
coordinates = [
Coordinate(loc.latitude, loc.longitude)
Expand Down
12 changes: 8 additions & 4 deletions akvo/rsr/views/py_reports/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,9 +409,9 @@ def _get_non_cumulative_period_values(self):
return value

def _get_cumulative_period_values(self):
periods = [period for period in self.periods if period.has_approved_updates]
periods = [period for period in self.periods if period.period_start < date.today()]
latest_period = sorted(periods, key=lambda p: p.period_start)[-1] if periods else None
return ensure_decimal(latest_period.actual_value)
return ensure_decimal(latest_period.actual_value) if latest_period else 0

@property
def periods(self):
Expand Down Expand Up @@ -492,9 +492,13 @@ def period_start(self):
def period_end(self):
return get_period_end(self._real, self.indicator.result.project.in_eutf_hierarchy)

@property
@cached_property
def actual_value(self):
return self.approved_updates.total_value if not self.is_cumulative else ensure_decimal(self._real.actual_value)
if self.is_cumulative and self.period_start and self.period_start > date.today():
return 0
if self.is_cumulative:
return ensure_decimal(self._real.actual_value)
return self.approved_updates.total_value

@property
def actual_comment(self):
Expand Down
4 changes: 2 additions & 2 deletions akvo/templates/reports/program-overview.html
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ <h3>{{ indicator.title }}</h3>
<tr class="level level-one">
<td>{{ contrib.project.title }}</td>
<td class="text-center">{{ contrib.country.name }}</td>
<td class="text-center">{{ contrib.updates.total_value|floatformat|intcomma }}</td>
<td class="text-center">{{ contrib.updates.total_value|percent_of:period.actual_value|floatformat }}%</td>
<td class="text-center">{{ contrib.actual_value|floatformat|intcomma }}</td>
<td class="text-center">{{ contrib.actual_value|percent_of:period.actual_value|floatformat }}%</td>
</tr>

{% if contrib.updates.disaggregations %}
Expand Down
5 changes: 5 additions & 0 deletions docker-compose.override.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,8 @@ services:
rsrdbhost:
ports:
- "5432:5432"

mailhog:
image: mailhog/mailhog:v1.0.0
ports:
- 8025:8025
4 changes: 3 additions & 1 deletion scripts/docker/dev/50-docker-local-dev.conf
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,14 @@ RSR_DOMAIN = os.getenv('RSR_DOMAIN', 'localhost')
AKVOAPP_DOMAIN = 'localakvoapp.org'


EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# EMAIL_HOST = 'fakesmtp'
# EMAIL_PORT = 25
# EMAIL_HOST_USER = ''
# EMAIL_HOST_PASSWORD = ''
# EMAIL_USE_TLS = True
EMAIL_HOST = "mailhog"
EMAIL_PORT = 1025


WEBPACK_LOADER['DEFAULT']['STATS_FILE'] = os.path.join(BASE_DIR, 'rsr/front-end/static/webpack-stats.json')
Expand Down

0 comments on commit a0d2a60

Please sign in to comment.