Repository: akvo/akvo-rsr

Issue:
Show disaggregations on the program overview excel report.



Discussion:

Resolving patch:
diff --git a/akvo/rsr/project_overview.py b/akvo/rsr/project_overview.py
--- a/akvo/rsr/project_overview.py
+++ b/akvo/rsr/project_overview.py
@@ -8,6 +8,7 @@
 """
 
 import copy
+from collections import OrderedDict
 from akvo.rsr.models import IndicatorPeriod, IndicatorPeriodData
 from akvo.rsr.models.result.utils import QUALITATIVE, PERCENTAGE_MEASURE, calculate_percentage
 from akvo.utils import ensure_decimal, ObjectReaderProxy
@@ -39,9 +40,9 @@ def get_periods_with_contributors(root_periods, aggregate_targets=False):
 
 
 def get_disaggregations(project):
-    disaggregations = {}
+    disaggregations = OrderedDict()
     for n in project.dimension_names.all():
-        disaggregations[n.name] = {}
+        disaggregations[n.name] = OrderedDict()
         for v in n.dimension_values.all():
             disaggregations[n.name][v.value] = None
     return disaggregations
@@ -213,23 +214,30 @@ def locations(self):
     @property
     def disaggregation_contributions(self):
         if self._disaggregation_contributions is None:
-            self._disaggregation_contributions = self.contributors.disaggregations.values()
+            self._disaggregation_contributions = self.contributors.disaggregations
         return self._disaggregation_contributions
 
     @property
     def disaggregation_targets(self):
         if self._disaggregation_targets is None:
-            self._disaggregation_targets = [
+            disaggregations = [
                 DisaggregationTarget(t)
                 for t in self._real.disaggregation_targets.all()
             ]
+            self._disaggregation_targets = {(d.category, d.type): d for d in disaggregations}
         return self._disaggregation_targets
 
+    def get_disaggregation_target_of(self, category, type):
+        key = (category, type)
+        if key not in self.disaggregation_targets:
+            return None
+        return ensure_decimal(self.disaggregation_targets[key].value)
+
     @property
     def disaggregation_contributions_view(self):
         if self._disaggregation_contributions_view is None:
             self._disaggregation_contributions_view = copy.deepcopy(self._project_disaggregations)
-            for d in self.disaggregation_contributions:
+            for d in self.disaggregation_contributions.values():
                 category = d['category']
                 type = d['type']
                 value = d['value']
@@ -245,6 +253,12 @@ def disaggregation_contributions_view(self):
 
         return self._disaggregation_contributions_view
 
+    def get_disaggregation_contribution_of(self, category, type):
+        key = (category, type)
+        if key not in self.disaggregation_contributions:
+            return None
+        return self.disaggregation_contributions[key]['value']
+
 
 class ContributorCollection(object):
     def __init__(self, nodes, type=IndicatorType.UNIT, project_disaggregations=None):
@@ -415,19 +429,26 @@ def actual_comment(self):
     def target_value(self):
         if self._target_value is None:
             self._target_value = ensure_decimal(self.period.target_value) \
-                if self.type == IndicatorType.NARRATIVE \
+                if self.type != IndicatorType.NARRATIVE \
                 else self.period.target_value
         return self._target_value
 
     @property
     def disaggregation_targets(self):
         if self._disaggregation_targets is None:
-            self._disaggregation_targets = [
+            disaggregations = [
                 DisaggregationTarget(t)
                 for t in self.period.disaggregation_targets.all()
             ]
+            self._disaggregation_targets = {(d.category, d.type): d for d in disaggregations}
         return self._disaggregation_targets
 
+    def get_disaggregation_target_of(self, category, type):
+        key = (category, type)
+        if key not in self.disaggregation_targets:
+            return None
+        return ensure_decimal(self.disaggregation_targets[key].value)
+
     @property
     def country(self):
         if self._country is None:
@@ -472,6 +493,12 @@ def disaggregations_view(self):
 
         return self._disaggregations_view
 
+    def get_disaggregation_of(self, category, type):
+        key = (category, type)
+        if key not in self.updates.disaggregations:
+            return None
+        return self.updates.disaggregations[key]['value']
+
 
 class UpdateCollection(object):
     def __init__(self, period, type):
diff --git a/akvo/rsr/views/py_reports/program_overview_excel_report.py b/akvo/rsr/views/py_reports/program_overview_excel_report.py
--- a/akvo/rsr/views/py_reports/program_overview_excel_report.py
+++ b/akvo/rsr/views/py_reports/program_overview_excel_report.py
@@ -9,7 +9,7 @@
 
 from akvo.rsr.models import Project, IndicatorPeriod
 from akvo.rsr.models.result.utils import calculate_percentage
-from akvo.rsr.project_overview import get_periods_with_contributors, is_aggregating_targets
+from akvo.rsr.project_overview import get_periods_with_contributors, is_aggregating_targets, get_disaggregations
 from akvo.rsr.decorators import with_download_indicator
 from dateutil.relativedelta import relativedelta
 from django.contrib.auth.decorators import login_required
@@ -66,6 +66,12 @@ def render_report(request, program_id):
         else ''
     )
 
+    disaggregations = get_disaggregations(program)
+    disaggregation_types_length = 0
+    for category, types in disaggregations.items():
+        disaggregation_types_length += len(types.keys())
+    disaggregations_last_colnum = 7 + (disaggregation_types_length * 2)
+
     wb = Workbook()
     for type, results in results_by_types.items():
         ws = wb.new_sheet(type)
@@ -76,6 +82,7 @@ def render_report(request, program_id):
         ws.set_col_style(4, Style(size=25))
         ws.set_col_style(5, Style(size=25))
         ws.set_col_style(6, Style(size=25))
+        ws.set_col_style(7, Style(size=25))
 
         # r1
         ws.set_row_style(1, Style(size=41))
@@ -108,10 +115,17 @@ def render_report(request, program_id):
             result_header1_style = Style(
                 font=Font(bold=True, size=12, color=Color(255, 255, 255)),
                 fill=Fill(background=Color(89, 89, 89)))
-            for i in range(1, 7):
+            for i in range(1, 8):
                 ws.set_cell_style(row, i, result_header1_style)
             ws.set_cell_value(row, 1, 'Result title:')
             ws.set_cell_value(row, 4, 'Result description:')
+            if disaggregation_types_length:
+                ws.set_cell_style(row, 8, Style(
+                    font=Font(bold=True, size=12, color=Color(255, 255, 255)),
+                    alignment=Alignment(horizontal='center'),
+                    fill=Fill(background=Color(89, 89, 89))))
+                ws.set_cell_value(row, 8, 'Disaggregations')
+                ws.range('H' + str(row), utils.xl_column_name(disaggregations_last_colnum) + str(row)).merge()
             row += 1
 
             # r6
@@ -120,12 +134,25 @@ def render_report(request, program_id):
                 font=Font(size=12, color=Color(255, 255, 255)),
                 alignment=Alignment(wrap_text=True),
                 fill=Fill(background=Color(89, 89, 89)))
+            result_header_disaggregation_style = Style(
+                font=Font(size=12, color=Color(255, 255, 255)),
+                alignment=Alignment(wrap_text=True, horizontal='center'),
+                fill=Fill(background=Color(89, 89, 89)))
             ws.range('A' + str(row), 'C' + str(row)).merge()
             ws.set_cell_style(row, 1, result_header2_style)
             ws.set_cell_value(row, 1, result.title)
-            ws.range('D' + str(row), 'F' + str(row)).merge()
+            ws.range('D' + str(row), 'G' + str(row)).merge()
             ws.set_cell_style(row, 4, result_header2_style)
             ws.set_cell_value(row, 4, result.description)
+            if disaggregation_types_length:
+                col = 8
+                for category, types in disaggregations.items():
+                    ws.set_cell_style(row, col, result_header_disaggregation_style)
+                    ws.set_cell_value(row, col, category.upper())
+                    type_length = len(types.keys()) * 2
+                    next_col = col + type_length
+                    ws.range(utils.xl_column_name(col) + str(row), utils.xl_column_name(next_col - 1) + str(row)).merge()
+                    col = next_col
             row += 1
 
             for indicator in result.indicators:
@@ -134,23 +161,38 @@ def render_report(request, program_id):
                 row7_style = Style(
                     font=Font(bold=True, size=12),
                     fill=Fill(background=Color(211, 211, 211)))
-                for i in range(1, 7):
+                for i in range(1, disaggregations_last_colnum + 1):
                     ws.set_cell_style(row, i, row7_style)
                 ws.range('B' + str(row), 'C' + str(row)).merge()
                 ws.set_cell_value(row, 1, 'Indicator title')
                 ws.set_cell_value(row, 2, 'Indicator description')
                 ws.set_cell_value(row, 4, 'Indicator type:')
+                if disaggregation_types_length:
+                    col = 8
+                    types = [t for ts in disaggregations.values() for t in ts.keys()]
+                    for type in types:
+                        ws.set_cell_value(row, col, type)
+                        next_col = col + 2
+                        ws.range(utils.xl_column_name(col) + str(row), utils.xl_column_name(next_col - 1) + str(row)).merge()
+                        col = next_col
                 row += 1
 
                 # r8
                 row8_style = Style(
                     fill=Fill(background=Color(211, 211, 211)), alignment=Alignment(wrap_text=True))
-                for i in range(1, 7):
+                for i in range(1, disaggregations_last_colnum + 1):
                     ws.set_cell_style(row, i, row8_style)
                 ws.range('B' + str(row), 'C' + str(row)).merge()
                 ws.set_cell_value(row, 1, indicator.title)
                 ws.set_cell_value(row, 2, indicator.description)
                 ws.set_cell_value(row, 4, 'Qualitative' if indicator.is_qualitative else 'Quantitative')
+                if disaggregation_types_length:
+                    col = 8
+                    while col <= disaggregations_last_colnum:
+                        ws.set_cell_value(row, col, 'value')
+                        col += 1
+                        ws.set_cell_value(row, col, 'target')
+                        col += 1
                 row += 1
 
                 for period in indicator.periods:
@@ -159,14 +201,15 @@ def render_report(request, program_id):
                     row9_style = Style(
                         font=Font(bold=True, size=12),
                         fill=Fill(background=Color(220, 230, 242)))
-                    for i in range(1, 7):
+                    for i in range(1, disaggregations_last_colnum + 1):
                         ws.set_cell_style(row, i, row9_style)
                     ws.range('B' + str(row), 'C' + str(row)).merge()
                     ws.set_cell_value(row, 1, 'Reporting Period:')
                     ws.set_cell_value(row, 2, 'Number of contrributors')
                     ws.set_cell_value(row, 4, 'Countries')
                     ws.set_cell_value(row, 5, 'Aggregated Actual Value')
-                    ws.set_cell_value(row, 6, '% of Contribution')
+                    ws.set_cell_value(row, 6, 'Target value')
+                    ws.set_cell_value(row, 7, '% of Contribution')
                     row += 1
 
                     # r10
@@ -174,18 +217,29 @@ def render_report(request, program_id):
                     row10_style = Style(
                         font=Font(size=12),
                         fill=Fill(background=Color(220, 230, 242)))
-                    for i in range(1, 6):
+                    for i in range(1, 7):
                         ws.set_cell_style(row, i, row10_style)
                     ws.range('B' + str(row), 'C' + str(row)).merge()
                     ws.set_cell_value(row, 1, '{} - {}'.format(period.period_start, period.period_end))
                     ws.set_cell_value(row, 2, number_of_contributors)
                     ws.set_cell_value(row, 4, len(period.countries))
                     ws.set_cell_value(row, 5, period.actual_value)
-                    ws.set_cell_style(row, 6, Style(
+                    ws.set_cell_value(row, 6, period.target_value)
+                    ws.set_cell_style(row, 7, Style(
                         alignment=Alignment(horizontal='right'),
                         font=Font(size=12),
                         fill=Fill(background=Color(220, 230, 242))))
-                    ws.set_cell_value(row, 6, '100%')
+                    ws.set_cell_value(row, 7, '100%')
+                    if disaggregation_types_length:
+                        for i in range(8, disaggregations_last_colnum + 1):
+                            ws.set_cell_style(row, i, row10_style)
+                        col = 8
+                        for category, types in disaggregations.items():
+                            for type in [t for t in types.keys()]:
+                                ws.set_cell_value(row, col, period.get_disaggregation_contribution_of(category, type) or '')
+                                col += 1
+                                ws.set_cell_value(row, col, period.get_disaggregation_target_of(category, type) or '')
+                                col += 1
                     row += 1
 
                     if not number_of_contributors:
@@ -195,7 +249,7 @@ def render_report(request, program_id):
                         # r11
                         ws.range('B' + str(row), 'C' + str(row)).merge()
                         ws.set_cell_style(row, 2, Style(font=Font(bold=True)))
-                        ws.set_cell_value(row, 2, 'Level 1 contributors:')
+                        ws.set_cell_value(row, 2, 'Level 1 contributor:')
                         row += 1
 
                         # r12
@@ -206,8 +260,17 @@ def render_report(request, program_id):
                         ws.set_cell_style(row, 4, Style(alignment=Alignment(horizontal='right')))
                         ws.set_cell_value(row, 4, getattr(contrib.country, 'name', ' '))
                         ws.set_cell_value(row, 5, contrib.updates.total_value)
-                        ws.set_cell_style(row, 6, Style(alignment=Alignment(horizontal='right')))
-                        ws.set_cell_value(row, 6, '{}%'.format(calculate_percentage(contrib.updates.total_value, period.actual_value)))
+                        ws.set_cell_value(row, 6, contrib.target_value)
+                        ws.set_cell_style(row, 7, Style(alignment=Alignment(horizontal='right')))
+                        ws.set_cell_value(row, 7, '{}%'.format(calculate_percentage(contrib.updates.total_value, period.actual_value)))
+                        if disaggregation_types_length:
+                            col = 8
+                            for category, types in disaggregations.items():
+                                for type in [t for t in types.keys()]:
+                                    ws.set_cell_value(row, col, contrib.get_disaggregation_of(category, type) or '')
+                                    col += 1
+                                    ws.set_cell_value(row, col, contrib.get_disaggregation_target_of(category, type) or '')
+                                    col += 1
                         row += 1
 
                         if len(contrib.contributors) < 1:
@@ -225,8 +288,17 @@ def render_report(request, program_id):
                             ws.set_cell_style(row, 4, Style(alignment=Alignment(horizontal='right')))
                             ws.set_cell_value(row, 4, getattr(subcontrib.country, 'name', ' '))
                             ws.set_cell_value(row, 5, subcontrib.actual_value)
-                            ws.set_cell_style(row, 6, Style(alignment=Alignment(horizontal='right')))
-                            ws.set_cell_value(row, 6, '{}%'.format(calculate_percentage(subcontrib.actual_value, period.actual_value)))
+                            ws.set_cell_value(row, 6, subcontrib.target_value)
+                            ws.set_cell_style(row, 7, Style(alignment=Alignment(horizontal='right')))
+                            ws.set_cell_value(row, 7, '{}%'.format(calculate_percentage(subcontrib.actual_value, period.actual_value)))
+                            if disaggregation_types_length:
+                                col = 8
+                                for category, types in disaggregations.items():
+                                    for type in [t for t in types.keys()]:
+                                        ws.set_cell_value(row, col, subcontrib.get_disaggregation_of(category, type) or '')
+                                        col += 1
+                                        ws.set_cell_value(row, col, subcontrib.get_disaggregation_target_of(category, type) or '')
+                                        col += 1
                             row += 1
 
     # output
diff --git a/akvo/rsr/views/py_reports/utils.py b/akvo/rsr/views/py_reports/utils.py
--- a/akvo/rsr/views/py_reports/utils.py
+++ b/akvo/rsr/views/py_reports/utils.py
@@ -52,6 +52,14 @@ def make_docx_response(document, filename='report.docx'):
     return response
 
 
+def xl_column_name(n):
+    string = ""
+    while n > 0:
+        n, remainder = divmod(n - 1, 26)
+        string = chr(65 + remainder) + string
+    return string
+
+
 def parse_date(string, default=None):
     try:
         return parse(string)


Test patch: