Repository: akvo/akvo-rsr

Issue:
Rearrange disaggregations position in project excel report



Discussion:

Resolving patch:
diff --git a/akvo/rsr/views/py_reports/project_results_indicators_excel_report.py b/akvo/rsr/views/py_reports/project_results_indicators_excel_report.py
--- a/akvo/rsr/views/py_reports/project_results_indicators_excel_report.py
+++ b/akvo/rsr/views/py_reports/project_results_indicators_excel_report.py
@@ -7,8 +7,9 @@
 see < http://www.gnu.org/licenses/agpl.html >.
 """
 
-from akvo.rsr.models import Project, IndicatorPeriodDisaggregation, IndicatorPeriod
+from akvo.rsr.models import Project, IndicatorPeriod
 from akvo.rsr.decorators import with_download_indicator
+from akvo.rsr.project_overview import get_disaggregations
 from datetime import datetime
 from dateutil.relativedelta import relativedelta
 from django.db.models import Q
@@ -45,10 +46,14 @@ def render_report(request, project_id):
     project_view = build_view_object(project, start_date, end_date)
     in_eutf_hierarchy = project_view.in_eutf_hierarchy
     use_indicator_target = project_view.use_indicator_target
-    has_disaggregation = IndicatorPeriodDisaggregation.objects\
-        .filter(period__indicator__result__project=project).count() > 0
 
-    max_column = 14 if has_disaggregation else 12
+    disaggregations = get_disaggregations(project)
+    disaggregation_types_length = 0
+    for category, types in disaggregations.items():
+        disaggregation_types_length += len(types.keys())
+    disaggregations_last_colnum = 11 + (disaggregation_types_length * 2)
+
+    max_column = disaggregations_last_colnum + 1
 
     results_by_types = {}
     for result in project_view.results:
@@ -73,10 +78,6 @@ def render_report(request, project_id):
         ws.set_col_style(9, Style(size=20))
         ws.set_col_style(10, Style(size=25))
         ws.set_col_style(11, Style(size=20))
-        ws.set_col_style(12, Style(size=30))
-        if has_disaggregation:
-            ws.set_col_style(13, Style(size=30))
-            ws.set_col_style(14, Style(size=30))
         ws.set_col_style(max_column, Style(size=25))
 
         # r1
@@ -114,6 +115,13 @@ def render_report(request, project_id):
                 ws.set_cell_style(row, i, result_header1_style)
             ws.set_cell_value(row, 1, 'Result title:')
             ws.set_cell_value(row, 3, 'Result description:')
+            if disaggregation_types_length:
+                ws.set_cell_style(row, 12, 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, 12, 'Disaggregations')
+                ws.range('L' + str(row), utils.xl_column_name(disaggregations_last_colnum) + str(row)).merge()
             row += 1
 
             # r6
@@ -122,12 +130,26 @@ def render_report(request, project_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), 'B' + str(row)).merge()
             ws.set_cell_style(row, 1, result_header2_style)
             ws.set_cell_value(row, 1, result.title)
-            ws.range('C' + str(row), ('N' if has_disaggregation else 'L') + str(row)).merge()
+            ws.range('C' + str(row), ('K') + str(row)).merge()
             ws.set_cell_style(row, 3, result_header2_style)
             ws.set_cell_value(row, 3, result.description)
+            if disaggregation_types_length:
+                col = 12
+                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
+            ws.set_cell_style(row, max_column, result_header2_style)
             row += 1
 
             # r7
@@ -159,12 +181,30 @@ def render_report(request, project_id):
                 ws.set_cell_value(row, col, 'Target comment')
             ws.set_cell_value(row, 10, 'Actual value')
             ws.set_cell_value(row, 11, 'Actual comment')
-            if has_disaggregation:
-                ws.set_cell_value(row, 12, 'Disaggregation label')
-                ws.set_cell_value(row, 13, 'Disaggregation value')
+            if disaggregation_types_length:
+                col = 12
+                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
             ws.set_cell_value(row, max_column, 'Aggregation status')
             row += 1
 
+            if disaggregation_types_length:
+                # r7+1
+                row7_1_style = Style(fill=Fill(background=Color(211, 211, 211)))
+                for i in range(1, max_column + 1):
+                    ws.set_cell_style(row, i, row7_1_style)
+                col = 12
+                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
+
             ws.set_cell_value(row, max_column, 'Yes' if result.aggregation_status else 'No')
             for indicator in result.indicators:
                 # r8
@@ -210,26 +250,15 @@ def render_report(request, project_id):
                     ws.set_cell_value(row, 10, period.actual_value)
                     ws.set_cell_style(row, 11, Style(alignment=Alignment(wrap_text=True)))
                     ws.set_cell_value(row, 11, period.actual_comment)
-
-                    disaggregations = period.disaggregations.order_by('dimension_value__name__id')
-                    if has_disaggregation and disaggregations.count():
-                        category = None
-                        last_category = None
-                        for disaggregation in disaggregations.all():
-                            if disaggregation.value is None:
-                                continue
-                            category = disaggregation.dimension_value.name.name
-                            if category != last_category:
-                                ws.set_cell_style(row, 12, Style(alignment=Alignment(wrap_text=True)))
-                                ws.set_cell_value(row, 12, disaggregation.dimension_value.name.name)
-                            last_category = category
-                            ws.set_cell_style(row, 13, Style(alignment=Alignment(wrap_text=True)))
-                            ws.set_cell_value(
-                                row, 13,
-                                disaggregation.dimension_value.value + ': ' + str(disaggregation.value))
-                            row += 1
-                    else:
-                        row += 1
+                    if disaggregation_types_length:
+                        col = 12
+                        for category, types in disaggregations.items():
+                            for type in [t for t in types.keys()]:
+                                ws.set_cell_value(row, col, period.get_disaggregation_of(category, type) or '')
+                                col += 1
+                                ws.set_cell_value(row, col, period.get_disaggregation_target_of(category, type) or '')
+                                col += 1
+                    row += 1
 
     filename = '{}-{}-results-indicators-report.xlsx'.format(
         datetime.today().strftime('%Y%b%d'), project.id)
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
@@ -15,6 +15,7 @@
 from weasyprint import HTML
 from weasyprint.fonts import FontConfiguration
 from akvo.rsr.models import IndicatorPeriodData
+from akvo.rsr.project_overview import DisaggregationTarget, IndicatorType
 from akvo.rsr.models.result.utils import QUANTITATIVE, QUALITATIVE, PERCENTAGE_MEASURE, calculate_percentage
 from akvo.utils import ObjectReaderProxy, ensure_decimal
 
@@ -329,6 +330,7 @@ def disaggregations(self):
 class PeriodProxy(ObjectReaderProxy):
     def __init__(self, period, indicator):
         super().__init__(period)
+        self.type = IndicatorType.get_type(period.indicator)
         self._indicator = indicator
         self._period_start = None
         self._period_end = None
@@ -337,6 +339,7 @@ def __init__(self, period, indicator):
         self._progress = None
         self._approved_updates = None
         self._has_qualitative_data = None
+        self._disaggregation_targets = None
 
     @property
     def indicator(self):
@@ -385,8 +388,7 @@ def grade(self):
     @property
     def approved_updates(self):
         if self._approved_updates is None:
-            updates = self.data.filter(status=IndicatorPeriodData.STATUS_APPROVED_CODE)
-            self._approved_updates = [PeriodUpdateProxy(u, self) for u in updates]
+            self._approved_updates = ApprovedUpdateCollection(self, self.type)
         return self._approved_updates
 
     @property
@@ -400,6 +402,104 @@ def has_qualitative_data(self):
                         break
         return self._has_qualitative_data
 
+    @property
+    def disaggregation_targets(self):
+        if self._disaggregation_targets is None:
+            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)
+
+    def get_disaggregation_of(self, category, type):
+        key = (category, type)
+        if key not in self.approved_updates.disaggregations:
+            return None
+        return self.approved_updates.disaggregations[key]['value']
+
+
+class ApprovedUpdateCollection(ObjectReaderProxy):
+    def __init__(self, period, type):
+        self.period = period
+        self.type = type
+        self._updates = None
+        self._total_value = None
+        self._total_numerator = None
+        self._total_denominator = None
+        self._disaggregations = None
+
+    @property
+    def total_value(self):
+        self._build()
+        return self._total_value
+
+    @property
+    def total_numerator(self):
+        self._build()
+        return self._total_numerator
+
+    @property
+    def total_denominator(self):
+        self._build()
+        return self._total_denominator
+
+    @property
+    def disaggregations(self):
+        self._build()
+        return self._disaggregations
+
+    def __iter__(self):
+        self._build()
+        return iter(self._updates)
+
+    def __len__(self):
+        self._build()
+        return len(self._updates)
+
+    def _build(self):
+        if self._updates is not None:
+            return
+        self._updates = []
+        self._total_value = 0
+        if self.type == IndicatorType.PERCENTAGE:
+            self._total_numerator = 0
+            self._total_denominator = 0
+        self._disaggregations = {}
+
+        for update in self.period.data.all():
+            if update.status != IndicatorPeriodData.STATUS_APPROVED_CODE:
+                continue
+            self._updates.append(PeriodUpdateProxy(update, self.period))
+            if self.type == IndicatorType.PERCENTAGE:
+                if update.numerator is not None and update.denominator is not None:
+                    self._total_numerator += update.numerator
+                    self._total_denominator += update.denominator
+            elif update.value:
+                self._total_value += update.value
+
+            for d in update.disaggregations.all():
+                key = (d.dimension_value.name.name, d.dimension_value.value)
+                if key not in self._disaggregations:
+                    self._disaggregations[key] = {
+                        'category': d.dimension_value.name.name,
+                        'type': d.dimension_value.value,
+                        'value': 0,
+                        'numerator': d.numerator,
+                        'denominator': d.denominator,
+                    }
+
+                self._disaggregations[key]['value'] += d.value
+
+        if self.type == IndicatorType.PERCENTAGE and self._total_denominator > 0:
+            self._total_value = calculate_percentage(self._total_numerator, self._total_denominator)
+
 
 class PeriodUpdateProxy(ObjectReaderProxy):
     def __init__(self, update, period):


Test patch: