Repository: akvo/akvo-rsr

Issue:
Disaggregation aggregation bug



Discussion:

Resolving patch:
diff --git a/akvo/rsr/management/commands/recalculate_disaggregation_aggregation.py b/akvo/rsr/management/commands/recalculate_disaggregation_aggregation.py
new file mode 100644
--- /dev/null
+++ b/akvo/rsr/management/commands/recalculate_disaggregation_aggregation.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+
+# Akvo Reporting is covered by the GNU Affero General Public License.
+# See more details in the license.txt file located at the root folder of the Akvo RSR module.
+# For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.
+
+from django.core.management.base import BaseCommand
+from akvo.rsr.models import Disaggregation, DisaggregationContribution, IndicatorPeriodDisaggregation
+from akvo.rsr.models.result.disaggregation_aggregation import DisaggregationAggregation
+
+
+class Command(BaseCommand):
+    help = "Reset and recalculate period disaggregation aggregation data."
+
+    def handle(self, *args, **options):
+        print("Deleting the disaggregation contribution data...")
+        DisaggregationContribution.objects.all().delete()
+
+        print("Deleting the period disaggregation data...")
+        IndicatorPeriodDisaggregation.objects.all().delete()
+
+        print("Recalculate period disaggregation aggregation...")
+        disaggregation_aggregation = DisaggregationAggregation(
+            Disaggregation.objects,
+            IndicatorPeriodDisaggregation.objects
+        )
+
+        disaggregation_leafs = Disaggregation.objects.filter(
+            update__period__child_periods__isnull=True,
+            dimension_value__isnull=False
+        )
+
+        for disaggregation in disaggregation_leafs:
+            disaggregation_aggregation.aggregate(
+                disaggregation.update.period,
+                disaggregation.dimension_value
+            )
+
+        print("Done!")
diff --git a/akvo/rsr/models/result/disaggregation.py b/akvo/rsr/models/result/disaggregation.py
--- a/akvo/rsr/models/result/disaggregation.py
+++ b/akvo/rsr/models/result/disaggregation.py
@@ -69,7 +69,7 @@ def update_incomplete_data(self):
             self.siblings().update(incomplete_data=incomplete_data)
 
 
-@receiver(signals.post_save, sender=Disaggregation)
+@receiver([signals.post_save, signals.post_delete], sender=Disaggregation)
 def aggregate_period_disaggregation_up_to_parent_hierarchy(sender, **kwargs):
 
     from .disaggregation_aggregation import DisaggregationAggregation
diff --git a/akvo/rsr/models/result/disaggregation_aggregation.py b/akvo/rsr/models/result/disaggregation_aggregation.py
--- a/akvo/rsr/models/result/disaggregation_aggregation.py
+++ b/akvo/rsr/models/result/disaggregation_aggregation.py
@@ -4,7 +4,9 @@
 # See more details in the license.txt file located at the root folder of the Akvo RSR module.
 # For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.
 
+from decimal import Decimal, InvalidOperation
 from django.db.models import Sum
+from .indicator_period_data import IndicatorPeriodData
 
 
 class DisaggregationAggregation(object):
@@ -14,32 +16,69 @@ def __init__(self, disaggregations, period_disaggregations):
         self.period_disaggregations = period_disaggregations
 
     def aggregate(self, period, dimension_value):
-        queryset = self.disaggregations.filter(update__period=period, dimension_value=dimension_value)
-        self._aggregate_period_disaggregation(queryset, period, dimension_value)
+        local = self._get_local_values(period, dimension_value)
+        contributed = self._get_contributed_values(period, dimension_value)
 
-    def aggregate_parent(self, period, dimension_value):
-        child_dimension_values_pks = [
-            child_dimension_value.id
-            for child_dimension_value
-            in dimension_value.child_dimension_values.all()
-        ]
-        queryset = self.period_disaggregations\
-            .filter(dimension_value__in=child_dimension_values_pks)
-        self._aggregate_period_disaggregation(queryset, period, dimension_value)
-
-    def _aggregate_period_disaggregation(self, queryset, period, dimension_value):
-        values = queryset.aggregate(value=Sum('value'), numerator=Sum('numerator'), denominator=Sum('denominator'))
         period_disaggregation, _ = self.period_disaggregations.get_or_create(
             period=period,
             dimension_value=dimension_value
         )
-        period_disaggregation.value = values['value']
-        period_disaggregation.numerator = values['numerator']
-        period_disaggregation.denominator = values['denominator']
+        period_disaggregation.value = _sum_attr(local, contributed, 'value')
+        period_disaggregation.numerator = _sum_attr(local, contributed, 'numerator')
+        period_disaggregation.denominator = _sum_attr(local, contributed, 'denominator')
         period_disaggregation.save()
 
         if period.parent_period is not None \
                 and period.parent_period.indicator.result.project.aggregate_children \
                 and period.indicator.result.project.aggregate_to_parent \
                 and dimension_value.parent_dimension_value:
-            self.aggregate_parent(period.parent_period, dimension_value.parent_dimension_value)
+            self.aggregate(period.parent_period, dimension_value.parent_dimension_value)
+
+    def _get_local_values(self, period, dimension_value):
+        return self.disaggregations.filter(
+            update__period=period,
+            update__status=IndicatorPeriodData.STATUS_APPROVED_CODE,
+            dimension_value=dimension_value
+        ).aggregate(
+            value=Sum('value'),
+            numerator=Sum('numerator'),
+            denominator=Sum('denominator')
+        )
+
+    def _get_contributed_values(self, period, dimension_value):
+        child_dimension_values_pks = [
+            child_dimension_value.id
+            for child_dimension_value
+            in dimension_value.child_dimension_values.all()
+        ]
+        child_periods_pks = [
+            child_period.id
+            for child_period
+            in period.child_periods.all()
+        ]
+        return self.period_disaggregations\
+            .filter(
+                dimension_value__in=child_dimension_values_pks,
+                period__in=child_periods_pks
+            ).aggregate(
+                value=Sum('value'),
+                numerator=Sum('numerator'),
+                denominator=Sum('denominator')
+            )
+
+
+def _sum_attr(a, b, attr):
+    left = a[attr]
+    right = b[attr]
+
+    if left is None and right is None:
+        return None
+
+    return _d(left) + _d(right)
+
+
+def _d(value):
+    try:
+        return Decimal(value)
+    except (InvalidOperation, TypeError):
+        return Decimal(0)


Test patch:
diff --git a/akvo/rsr/tests/results_framework/test_disaggregation_aggregation.py b/akvo/rsr/tests/results_framework/test_disaggregation_aggregation.py
--- a/akvo/rsr/tests/results_framework/test_disaggregation_aggregation.py
+++ b/akvo/rsr/tests/results_framework/test_disaggregation_aggregation.py
@@ -20,166 +20,605 @@ def setUp(self):
             period_end=datetime.date.today() + datetime.timedelta(days=1),
             target_value="100"
         )
-        self.dimension_name = IndicatorDimensionName.objects.create(project=self.project, name="Gender")
-        self.dimension_value1 = IndicatorDimensionValue.objects.create(name=self.dimension_name, value="Men")
-        self.dimension_value2 = IndicatorDimensionValue.objects.create(name=self.dimension_name, value="Women")
-        self.indicator.dimension_names.add(self.dimension_name)
+        self.type1 = "Type 1"
+        self.type2 = "Type 2"
+        category = IndicatorDimensionName.objects.create(project=self.project, name="Category 1")
+        IndicatorDimensionValue.objects.create(name=category, value=self.type1)
+        IndicatorDimensionValue.objects.create(name=category, value=self.type2)
+        self.indicator.dimension_names.add(category)
 
     def test_aggregate_to_period(self):
         # Given
+        disaggregations = self._get_disaggregations(self.project)
+        type1 = disaggregations.filter(value=self.type1).first()
+        type2 = disaggregations.filter(value=self.type2).first()
 
         # When
-        update1 = IndicatorPeriodData.objects.create(period=self.period, user=self.user, value=50, status='A')
-        update1_disaggregation1 = Disaggregation.objects.create(dimension_value=self.dimension_value1, update=update1, value=20)
-        update1_disaggregation2 = Disaggregation.objects.create(dimension_value=self.dimension_value2, update=update1, value=30)
-
-        update2 = IndicatorPeriodData.objects.create(period=self.period, user=self.user, value=30, status='A')
-        update2_disaggregation1 = Disaggregation.objects.create(dimension_value=self.dimension_value1, update=update2, value=15)
-        update2_disaggregation2 = Disaggregation.objects.create(dimension_value=self.dimension_value2, update=update2, value=15)
+        self._create_period_update(
+            period=self.period,
+            disaggregations=[
+                {'type': type1, 'value': 20},
+                {'type': type2, 'value': 30},
+            ],
+            update={'value': 50}
+        )
+        self._create_period_update(
+            period=self.period,
+            disaggregations=[
+                {'type': type1, 'value': 15},
+                {'type': type2, 'value': 15},
+            ],
+            update={'value': 30}
+        )
 
         # Then
         self.assertEqual(self.period.disaggregations.count(), 2)
-
-        period_disaggregation1 = self.period.disaggregations.filter(dimension_value=self.dimension_value1).get()
         self.assertEqual(
-            period_disaggregation1.value,
-            update1_disaggregation1.value + update2_disaggregation1.value
+            self._get_period_disaggregation(self.period, self.type1).value,
+            20 + 15
         )
-
-        period_disaggregation2 = self.period.disaggregations.filter(dimension_value=self.dimension_value2).get()
         self.assertEqual(
-            period_disaggregation2.value,
-            update1_disaggregation2.value + update2_disaggregation2.value
+            self._get_period_disaggregation(self.period, self.type2).value,
+            30 + 15
         )
 
     def test_aggregate_percentages_to_period(self):
         # Given
+        disaggregations = self._get_disaggregations(self.project)
+        type1 = disaggregations.filter(value=self.type1).first()
+        type2 = disaggregations.filter(value=self.type2).first()
+
+        # When
+        self._create_period_update(
+            period=self.period,
+            disaggregations=[
+                {'type': type1, 'numerator': 20, 'denominator': 50},
+                {'type': type2, 'numerator': 30, 'denominator': 50},
+            ],
+            update={'numerator': 50, 'denominator': 100}
+        )
+        self._create_period_update(
+            period=self.period,
+            disaggregations=[
+                {'type': type1, 'numerator': 15, 'denominator': 50},
+                {'type': type2, 'numerator': 15, 'denominator': 50},
+            ],
+            update={'numerator': 30, 'denominator': 100}
+        )
+
+        # Then
+        self.assertEqual(self.period.disaggregations.count(), 2)
+
+        period_disaggregation1 = self._get_period_disaggregation(self.period, self.type1)
+        self.assertEqual(period_disaggregation1.numerator, 20 + 15)
+        self.assertEqual(period_disaggregation1.denominator, 50 + 50)
+
+        period_disaggregation2 = self._get_period_disaggregation(self.period, self.type2)
+        self.assertEqual(period_disaggregation2.numerator, 30 + 15)
+        self.assertEqual(period_disaggregation2.denominator, 50 + 50)
+
+    def test_aggregate_child_period_to_parent(self):
+        # Given
+        child1 = self._make_contributor("Child 1", self.project)
+        child1_period = self._get_period(child1)
+        child1_disaggregations = self._get_disaggregations(child1)
+        child1_type1 = child1_disaggregations.filter(value=self.type1).first()
+        child1_type2 = child1_disaggregations.filter(value=self.type2).first()
+
+        child2 = self._make_contributor("Child 2", self.project)
+        child2_period = self._get_period(child2)
+        child2_disaggregations = self._get_disaggregations(child1)
+        child2_type1 = child2_disaggregations.filter(value=self.type1).first()
+        child2_type2 = child2_disaggregations.filter(value=self.type2).first()
 
         # When
-        update1 = IndicatorPeriodData.objects.create(period=self.period, user=self.user, numerator=50, denominator=100, status='A')
-        update1_disaggregation1 = Disaggregation.objects.create(dimension_value=self.dimension_value1, update=update1, numerator=20, denominator=50)
-        update1_disaggregation2 = Disaggregation.objects.create(dimension_value=self.dimension_value2, update=update1, numerator=30, denominator=50)
+        self._create_period_update(child1_period, [
+            {'type': child1_type1, 'value': 20},
+            {'type': child1_type2, 'value': 30},
+        ])
+        self._create_period_update(child2_period, [
+            {'type': child2_type1, 'value': 15},
+            {'type': child2_type2, 'value': 15},
+        ])
+
+        # Then
+        self.assertEqual(
+            self._get_period_disaggregation(self.period, self.type1).value,
+            20 + 15
+        )
+        self.assertEqual(
+            self._get_period_disaggregation(self.period, self.type2).value,
+            30 + 15
+        )
+
+    def test_should_not_aggregate_unapproved_updates(self):
+        # Given
+        child = self._make_contributor("Child", self.project)
+        child_period = self._get_period(child)
+        child_disaggregations = self._get_disaggregations(child)
+        child_type1 = child_disaggregations.filter(value=self.type1).first()
+        child_type2 = child_disaggregations.filter(value=self.type2).first()
 
-        update2 = IndicatorPeriodData.objects.create(period=self.period, user=self.user, numerator=30, denominator=100, status='A')
-        update2_disaggregation1 = Disaggregation.objects.create(dimension_value=self.dimension_value1, update=update2, numerator=15, denominator=50)
-        update2_disaggregation2 = Disaggregation.objects.create(dimension_value=self.dimension_value2, update=update2, numerator=15, denominator=50)
+        # When
+        self._create_period_update(
+            period=child_period,
+            disaggregations=[
+                {'type': child_type1, 'value': 20},
+                {'type': child_type2, 'value': 30},
+            ],
+            status=IndicatorPeriodData.STATUS_PENDING_CODE
+        )
 
         # Then
         self.assertEqual(self.period.disaggregations.count(), 2)
+        self.assertEqual(
+            self._get_period_disaggregation(self.period, self.type1).value,
+            None
+        )
+        self.assertEqual(
+            self._get_period_disaggregation(self.period, self.type2).value,
+            None
+        )
 
-        period_disaggregation1 = self.period.disaggregations.filter(dimension_value=self.dimension_value1).get()
+        self.assertEqual(child_period.disaggregations.count(), 2)
         self.assertEqual(
-            period_disaggregation1.numerator,
-            update1_disaggregation1.numerator + update2_disaggregation1.numerator
+            self._get_period_disaggregation(child_period, self.type1).value,
+            None
         )
         self.assertEqual(
-            period_disaggregation1.denominator,
-            update1_disaggregation1.denominator + update2_disaggregation1.denominator
+            self._get_period_disaggregation(child_period, self.type2).value,
+            None
+        )
+
+    def test_project_with_aggregate_to_parent_off(self):
+        # Given
+        child = self._make_contributor("Child", self.project)
+        child.aggregate_to_parent = False
+        child.save()
+
+        child_period = self._get_period(child)
+        child_disaggregations = self._get_disaggregations(child)
+        child_type1 = child_disaggregations.filter(value=self.type1).first()
+        child_type2 = child_disaggregations.filter(value=self.type2).first()
+
+        # When
+        self._create_period_update(
+            period=child_period,
+            disaggregations=[
+                {'type': child_type1, 'value': 20},
+                {'type': child_type2, 'value': 30},
+            ]
         )
 
-        period_disaggregation2 = self.period.disaggregations.filter(dimension_value=self.dimension_value2).get()
+        # Then
+        self.assertEqual(self.period.disaggregations.count(), 0)
+
+        self.assertEqual(child_period.disaggregations.count(), 2)
         self.assertEqual(
-            period_disaggregation2.numerator,
-            update1_disaggregation2.numerator + update2_disaggregation2.numerator
+            self._get_period_disaggregation(child_period, self.type1).value,
+            20
         )
         self.assertEqual(
-            period_disaggregation2.denominator,
-            update1_disaggregation2.denominator + update2_disaggregation2.denominator
+            self._get_period_disaggregation(child_period, self.type2).value,
+            30
         )
 
-    def test_aggregate_child_period_to_parent(self):
+    def test_project_with_aggregate_children_off(self):
         # Given
-        child1 = self.create_project("Child 1")
-        self.make_parent(self.project, child1)
-        child1.import_results()
-
-        child1_period = IndicatorPeriod.objects.filter(indicator__result__project=child1).get()
-        child1_dimension_value1 = IndicatorDimensionValue.objects\
-            .filter(name__project=child1, parent_dimension_value=self.dimension_value1)\
-            .get()
-        child1_dimension_value2 = IndicatorDimensionValue.objects\
-            .filter(name__project=child1, parent_dimension_value=self.dimension_value2)\
-            .get()
-
-        child2 = self.create_project("Child 2")
-        self.make_parent(self.project, child2)
-        child2.import_results()
-
-        child2_period = IndicatorPeriod.objects.filter(indicator__result__project=child2).get()
-        child2_dimension_value1 = IndicatorDimensionValue.objects\
-            .filter(name__project=child2, parent_dimension_value=self.dimension_value1)\
-            .get()
-        child2_dimension_value2 = IndicatorDimensionValue.objects\
-            .filter(name__project=child2, parent_dimension_value=self.dimension_value2)\
-            .get()
+        self.project.aggregate_children = False
+        self.project.save()
+
+        child = self._make_contributor("Child", self.project)
+        child_period = self._get_period(child)
+        child_disaggregations = self._get_disaggregations(child)
+        child_type1 = child_disaggregations.filter(value=self.type1).first()
+        child_type2 = child_disaggregations.filter(value=self.type2).first()
 
         # When
-        update1 = IndicatorPeriodData.objects.create(period=child1_period, user=self.user, value=50, status='A')
-        update1_disaggregation1 = Disaggregation.objects.create(dimension_value=child1_dimension_value1, update=update1, value=20)
-        update1_disaggregation2 = Disaggregation.objects.create(dimension_value=child1_dimension_value2, update=update1, value=30)
+        self._create_period_update(
+            period=child_period,
+            disaggregations=[
+                {'type': child_type1, 'value': 20},
+                {'type': child_type2, 'value': 30},
+            ]
+        )
+
+        # Then
+        self.assertEqual(self.period.disaggregations.count(), 0)
+
+        self.assertEqual(child_period.disaggregations.count(), 2)
+        self.assertEqual(
+            self._get_period_disaggregation(child_period, self.type1).value,
+            20
+        )
+        self.assertEqual(
+            self._get_period_disaggregation(child_period, self.type2).value,
+            30
+        )
+
+    def test_aggregate_multi_level_hierarchy(self):
+        # Given
+        child = self._make_contributor("Child", self.project)
+        child_period = self._get_period(child)
 
-        update2 = IndicatorPeriodData.objects.create(period=child2_period, user=self.user, value=30, status='A')
-        update2_disaggregation1 = Disaggregation.objects.create(dimension_value=child2_dimension_value1, update=update2, value=15)
-        update2_disaggregation2 = Disaggregation.objects.create(dimension_value=child2_dimension_value2, update=update2, value=15)
+        grandchild = self._make_contributor("Grandchild", child)
+        grandchild_period = self._get_period(grandchild)
+        grandchild_disaggregations = self._get_disaggregations(grandchild)
+        grandchild_type1 = grandchild_disaggregations.filter(value=self.type1).first()
+        grandchild_type2 = grandchild_disaggregations.filter(value=self.type2).first()
+
+        # When
+        self._create_period_update(grandchild_period, [
+            {'type': grandchild_type1, 'value': 20},
+            {'type': grandchild_type2, 'value': 30},
+        ])
+        self._create_period_update(grandchild_period, [
+            {'type': grandchild_type1, 'value': 15},
+            {'type': grandchild_type2, 'value': 15},
+        ])
 
         # Then
-        period_disaggregation1 = self.period.disaggregations.filter(dimension_value=self.dimension_value1).get()
+        self.assertEqual(grandchild_period.disaggregations.count(), 2)
+        self.assertEqual(
+            self._get_period_disaggregation(grandchild_period, self.type1).value,
+            20 + 15
+        )
         self.assertEqual(
-            period_disaggregation1.value,
-            update1_disaggregation1.value + update2_disaggregation1.value
+            self._get_period_disaggregation(grandchild_period, self.type2).value,
+            30 + 15
         )
 
-        period_disaggregation2 = self.period.disaggregations.filter(dimension_value=self.dimension_value2).get()
+        self.assertEqual(child_period.disaggregations.count(), 2)
+        self.assertEqual(
+            self._get_period_disaggregation(child_period, self.type1).value,
+            20 + 15
+        )
         self.assertEqual(
-            period_disaggregation2.value,
-            update1_disaggregation2.value + update2_disaggregation2.value
+            self._get_period_disaggregation(child_period, self.type2).value,
+            30 + 15
         )
 
-    def test_aggregate_multi_level_hierarchy(self):
+        self.assertEqual(self.period.disaggregations.count(), 2)
+        self.assertEqual(
+            self._get_period_disaggregation(self.period, self.type1).value,
+            20 + 15
+        )
+        self.assertEqual(
+            self._get_period_disaggregation(self.period, self.type2).value,
+            30 + 15
+        )
+
+    def test_aggregate_multiple_periods(self):
         # Given
-        child = self.create_project("Child")
-        self.make_parent(self.project, child)
-        child.import_results()
+        period2 = IndicatorPeriod.objects.create(
+            indicator=self.indicator,
+            period_start=datetime.date.today(),
+            period_end=datetime.date.today() + datetime.timedelta(days=1),
+            target_value="200"
+        )
+        child = self._make_contributor("Child", self.project)
+        child_period1 = self.period.child_periods.first()
+        child_period2 = period2.child_periods.first()
+        child_disaggregations = self._get_disaggregations(child)
+        child_type1 = child_disaggregations.filter(value=self.type1).first()
+        child_type2 = child_disaggregations.filter(value=self.type2).first()
+
+        # When
+        self._create_period_update(child_period1, [
+            {'type': child_type1, 'value': 20},
+            {'type': child_type2, 'value': 30},
+        ])
+        self._create_period_update(child_period2, [
+            {'type': child_type1, 'value': 15},
+            {'type': child_type2, 'value': 15},
+        ])
+
+        # Then
+        self.assertEqual(child_period1.disaggregations.count(), 2)
+        self.assertEqual(
+            self._get_period_disaggregation(child_period1, self.type1).value,
+            20
+        )
+        self.assertEqual(
+            self._get_period_disaggregation(child_period1, self.type2).value,
+            30
+        )
+        self.assertEqual(self.period.disaggregations.count(), 2)
+        self.assertEqual(
+            self._get_period_disaggregation(self.period, self.type1).value,
+            20
+        )
+        self.assertEqual(
+            self._get_period_disaggregation(self.period, self.type2).value,
+            30
+        )
+
+        self.assertEqual(child_period2.disaggregations.count(), 2)
+        self.assertEqual(
+            self._get_period_disaggregation(child_period2, self.type1).value,
+            15
+        )
+        self.assertEqual(
+            self._get_period_disaggregation(child_period2, self.type2).value,
+            15
+        )
+        self.assertEqual(period2.disaggregations.count(), 2)
+        self.assertEqual(
+            self._get_period_disaggregation(period2, self.type1).value,
+            15
+        )
+        self.assertEqual(
+            self._get_period_disaggregation(period2, self.type2).value,
+            15
+        )
+
+    def test_sum_local_and_aggregated_updates(self):
+        # Given
+        child = self._make_contributor("Child", self.project)
+        child_period = self._get_period(child)
+        child_disaggregations = self._get_disaggregations(child)
+        child_type1 = child_disaggregations.filter(value=self.type1).first()
+        child_type2 = child_disaggregations.filter(value=self.type2).first()
+
+        grandchild = self._make_contributor("Grandchild", child)
+        grandchild_period = self._get_period(grandchild)
+        grandchild_disaggregations = self._get_disaggregations(grandchild)
+        grandchild_type1 = grandchild_disaggregations.filter(value=self.type1).first()
+        grandchild_type2 = grandchild_disaggregations.filter(value=self.type2).first()
+
+        # When
+        self._create_period_update(grandchild_period, [
+            {'type': grandchild_type1, 'value': 20},
+            {'type': grandchild_type2, 'value': 30},
+        ])
+        self._create_period_update(child_period, [
+            {'type': child_type1, 'value': 15},
+            {'type': child_type2, 'value': 15},
+        ])
+
+        # Then
+        self.assertEqual(grandchild_period.disaggregations.count(), 2)
+        self.assertEqual(
+            self._get_period_disaggregation(grandchild_period, self.type1).value,
+            20
+        )
+        self.assertEqual(
+            self._get_period_disaggregation(grandchild_period, self.type2).value,
+            30
+        )
+
+        self.assertEqual(child_period.disaggregations.count(), 2)
+        self.assertEqual(
+            self._get_period_disaggregation(child_period, self.type1).value,
+            20 + 15
+        )
+        self.assertEqual(
+            self._get_period_disaggregation(child_period, self.type2).value,
+            30 + 15
+        )
+
+        self.assertEqual(self.period.disaggregations.count(), 2)
+        self.assertEqual(
+            self._get_period_disaggregation(self.period, self.type1).value,
+            20 + 15
+        )
+        self.assertEqual(
+            self._get_period_disaggregation(self.period, self.type2).value,
+            30 + 15
+        )
 
-        child_dimension_value1 = IndicatorDimensionValue.objects\
-            .filter(name__project=child, parent_dimension_value=self.dimension_value1)\
-            .get()
-        child_dimension_value2 = IndicatorDimensionValue.objects\
-            .filter(name__project=child, parent_dimension_value=self.dimension_value2)\
-            .get()
-
-        grandchild = self.create_project("Grandchild")
-        self.make_parent(child, grandchild)
-        grandchild.import_results()
-
-        grandchild_period = IndicatorPeriod.objects.filter(indicator__result__project=grandchild).get()
-        grandchild_dimension_value1 = IndicatorDimensionValue.objects\
-            .filter(name__project=grandchild, parent_dimension_value=child_dimension_value1)\
-            .get()
-        grandchild_dimension_value2 = IndicatorDimensionValue.objects\
-            .filter(name__project=grandchild, parent_dimension_value=child_dimension_value2)\
-            .get()
+    def test_amend_on_the_lowest_level_case1(self):
+        # Given
+        child = self._make_contributor("Child", self.project)
+        child_period = self._get_period(child)
+
+        grandchild1 = self._make_contributor("Grandchild 1", child)
+        grandchild1_period = self._get_period(grandchild1)
+        grandchild1_disaggregations = self._get_disaggregations(grandchild1)
+        grandchild1_type1 = grandchild1_disaggregations.filter(value=self.type1).first()
+        grandchild1_type2 = grandchild1_disaggregations.filter(value=self.type2).first()
+
+        grandchild2 = self._make_contributor("Grandchild r", child)
+        grandchild2_period = self._get_period(grandchild2)
+        grandchild2_disaggregations = self._get_disaggregations(grandchild2)
+        grandchild2_type1 = grandchild2_disaggregations.filter(value=self.type1).first()
+        grandchild2_type2 = grandchild2_disaggregations.filter(value=self.type2).first()
+
+        self._create_period_update(grandchild1_period, [
+            {'type': grandchild1_type1, 'value': 20},
+            {'type': grandchild1_type2, 'value': 30},
+        ])
+        target_amend_update = self._create_period_update(grandchild1_period, [
+            {'type': grandchild1_type1, 'value': 15},
+            {'type': grandchild1_type2, 'value': 15},
+        ])
+        self._create_period_update(grandchild2_period, [
+            {'type': grandchild2_type1, 'value': 30},
+            {'type': grandchild2_type2, 'value': 30},
+        ])
 
         # When
-        update1 = IndicatorPeriodData.objects.create(period=grandchild_period, user=self.user, value=50, status='A')
-        update1_disaggregation1 = Disaggregation.objects.create(dimension_value=grandchild_dimension_value1, update=update1, value=20)
-        update1_disaggregation2 = Disaggregation.objects.create(dimension_value=grandchild_dimension_value2, update=update1, value=30)
+        self._amend_period_update_type(target_amend_update, self.type1, 20)
+
+        # Then
+        self.assertEqual(self.period.disaggregations.count(), 2)
+        self.assertEqual(
+            self._get_period_disaggregation(self.period, self.type1).value,
+            20 + 15 + 30 + (-15 + 20)
+        )
+        self.assertEqual(
+            self._get_period_disaggregation(self.period, self.type2).value,
+            30 + 15 + 30
+        )
+
+        self.assertEqual(child_period.disaggregations.count(), 2)
+        self.assertEqual(
+            self._get_period_disaggregation(child_period, self.type1).value,
+            20 + 15 + 30 + (-15 + 20)
+        )
+        self.assertEqual(
+            self._get_period_disaggregation(child_period, self.type2).value,
+            30 + 15 + 30
+        )
 
-        update2 = IndicatorPeriodData.objects.create(period=grandchild_period, user=self.user, value=30, status='A')
-        update2_disaggregation1 = Disaggregation.objects.create(dimension_value=grandchild_dimension_value1, update=update2, value=15)
-        update2_disaggregation2 = Disaggregation.objects.create(dimension_value=grandchild_dimension_value2, update=update2, value=15)
+    def test_amend_on_the_lowest_level_case2(self):
+        # Given
+        child1 = self._make_contributor("Child 1", self.project)
+        child1_period = self._get_period(child1)
+
+        child2 = self._make_contributor("Child 2", self.project)
+        child2_period = self._get_period(child2)
+
+        grandchild1 = self._make_contributor("Grandchild 1", child1)
+        grandchild1_period = self._get_period(grandchild1)
+        grandchild1_disaggregations = self._get_disaggregations(grandchild1)
+        grandchild1_type1 = grandchild1_disaggregations.filter(value=self.type1).first()
+        grandchild1_type2 = grandchild1_disaggregations.filter(value=self.type2).first()
+
+        grandchild2 = self._make_contributor("Grandchild r", child2)
+        grandchild2_period = self._get_period(grandchild2)
+        grandchild2_disaggregations = self._get_disaggregations(grandchild2)
+        grandchild2_type1 = grandchild2_disaggregations.filter(value=self.type1).first()
+        grandchild2_type2 = grandchild2_disaggregations.filter(value=self.type2).first()
+
+        self._create_period_update(grandchild1_period, [
+            {'type': grandchild1_type1, 'value': 20},
+            {'type': grandchild1_type2, 'value': 30},
+        ])
+        target_amend_update = self._create_period_update(grandchild1_period, [
+            {'type': grandchild1_type1, 'value': 15},
+            {'type': grandchild1_type2, 'value': 15},
+        ])
+        self._create_period_update(grandchild2_period, [
+            {'type': grandchild2_type1, 'value': 30},
+            {'type': grandchild2_type2, 'value': 30},
+        ])
+
+        # When
+        self._amend_period_update_type(target_amend_update, self.type1, 20)
 
         # Then
         self.assertEqual(self.period.disaggregations.count(), 2)
+        self.assertEqual(
+            self._get_period_disaggregation(self.period, self.type1).value,
+            20 + 15 + 30 + (-15 + 20)
+        )
+        self.assertEqual(
+            self._get_period_disaggregation(self.period, self.type2).value,
+            30 + 15 + 30
+        )
+
+        self.assertEqual(child1_period.disaggregations.count(), 2)
+        self.assertEqual(
+            self._get_period_disaggregation(child1_period, self.type1).value,
+            20 + 15 + (-15 + 20)
+        )
+        self.assertEqual(
+            self._get_period_disaggregation(child1_period, self.type2).value,
+            30 + 15
+        )
+
+        self.assertEqual(child2_period.disaggregations.count(), 2)
+        self.assertEqual(
+            self._get_period_disaggregation(child2_period, self.type1).value,
+            30
+        )
+        self.assertEqual(
+            self._get_period_disaggregation(child2_period, self.type2).value,
+            30
+        )
+
+    def test_delete_on_the_lowest_level(self):
+        # Given
+        child1 = self._make_contributor("Child 1", self.project)
+        child1_period = self._get_period(child1)
+        grandchild1 = self._make_contributor("Grandchild 1", child1)
+        grandchild1_period = self._get_period(grandchild1)
+        grandchild1_disaggregations = self._get_disaggregations(grandchild1)
+        grandchild1_type1 = grandchild1_disaggregations.filter(value=self.type1).first()
+        grandchild1_type2 = grandchild1_disaggregations.filter(value=self.type2).first()
+
+        child2 = self._make_contributor("Child 2", self.project)
+        grandchild2 = self._make_contributor("Grandchild r", child2)
+        grandchild2_period = self._get_period(grandchild2)
+        grandchild2_disaggregations = self._get_disaggregations(grandchild2)
+        grandchild2_type1 = grandchild2_disaggregations.filter(value=self.type1).first()
+        grandchild2_type2 = grandchild2_disaggregations.filter(value=self.type2).first()
+
+        self._create_period_update(grandchild1_period, [
+            {'type': grandchild1_type1, 'value': 20},
+            {'type': grandchild1_type2, 'value': 30},
+        ])
+        target_amend_update = self._create_period_update(grandchild1_period, [
+            {'type': grandchild1_type1, 'value': 15},
+            {'type': grandchild1_type2, 'value': 15},
+        ])
+        self._create_period_update(grandchild2_period, [
+            {'type': grandchild2_type1, 'value': 30},
+            {'type': grandchild2_type2, 'value': 30},
+        ])
 
-        period_disaggregation1 = self.period.disaggregations.filter(dimension_value=self.dimension_value1).get()
+        # When
+        target_amend_update.delete()
+
+        # Then
+        self.assertEqual(self.period.disaggregations.count(), 2)
+        self.assertEqual(
+            self._get_period_disaggregation(self.period, self.type1).value,
+            20 + 15 + 30 + (-15)
+        )
         self.assertEqual(
-            period_disaggregation1.value,
-            update1_disaggregation1.value + update2_disaggregation1.value
+            self._get_period_disaggregation(self.period, self.type2).value,
+            30 + 15 + 30 + (-15)
         )
 
-        period_disaggregation2 = self.period.disaggregations.filter(dimension_value=self.dimension_value2).get()
+        self.assertEqual(child1_period.disaggregations.count(), 2)
+        self.assertEqual(
+            self._get_period_disaggregation(child1_period, self.type1).value,
+            20 + 15 + (-15)
+        )
         self.assertEqual(
-            period_disaggregation2.value,
-            update1_disaggregation2.value + update2_disaggregation2.value
+            self._get_period_disaggregation(child1_period, self.type2).value,
+            30 + 15 + (-15)
+        )
+
+    def _amend_period_update_type(self, update, type, value):
+        disaggregation = update.disaggregations.filter(dimension_value__value=type).first()
+        disaggregation.value = value
+        disaggregation.save()
+
+    def _create_period_update(self, period, disaggregations=[], update={}, status=IndicatorPeriodData.STATUS_APPROVED_CODE):
+        data = IndicatorPeriodData.objects.create(
+            period=period,
+            user=self.user,
+            value=update.get('value', None),
+            numerator=update.get('numerator', None),
+            denominator=update.get('denominator', None),
+            status=status
         )
+        for d in disaggregations:
+            Disaggregation.objects.create(
+                update=data,
+                dimension_value=d['type'],
+                value=d.get('value', None),
+                numerator=d.get('numerator', None),
+                denominator=d.get('denominator', None)
+            )
+
+        return data
+
+    def _make_contributor(self, title, parent):
+        child = self.create_project(title)
+        self.make_parent(parent, child)
+        child.import_results()
+
+        return child
+
+    def _get_period(self, project):
+        return IndicatorPeriod.objects.filter(indicator__result__project=project).first()
+
+    def _get_disaggregations(self, project):
+        return IndicatorDimensionValue.objects.filter(name__project=project)
+
+    def _get_period_disaggregation(self, period, type):
+        return period.disaggregations.filter(dimension_value__value=type).first()