From 4ba4ad2a0dbd7cb0867604430b723b70fdc3815f Mon Sep 17 00:00:00 2001 From: Roman Karpovich Date: Tue, 10 May 2022 21:40:39 +0800 Subject: [PATCH 1/2] fix interventions auto transition in periodic task --- src/etools/applications/partners/tasks.py | 18 ++--- .../applications/partners/tests/test_tasks.py | 77 ++++++++++++++++++- 2 files changed, 81 insertions(+), 14 deletions(-) diff --git a/src/etools/applications/partners/tasks.py b/src/etools/applications/partners/tasks.py index 5ff24207b..c167defc3 100644 --- a/src/etools/applications/partners/tasks.py +++ b/src/etools/applications/partners/tasks.py @@ -4,7 +4,6 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.db import connection, transaction -from django.db.models import F, Sum from celery.utils.log import get_task_logger from django_tenants.utils import schema_context @@ -119,17 +118,12 @@ def _make_intervention_status_automatic_transitions(country_name): admin_user = get_user_model().objects.get(username=settings.TASK_ADMIN_USER) bad_interventions = [] - active_ended = Intervention.objects.filter(status=Intervention.ACTIVE, - end__lt=datetime.date.today()) - qs = Intervention.objects\ - .prefetch_related('frs')\ - .filter(status=Intervention.ENDED)\ - .annotate(frs_total_outstanding=Sum('frs__outstanding_amt_local'), - frs_total_actual_amt=Sum('frs__total_amt_local'), - frs_intervention_amt=Sum('frs__actual_amt_local'))\ - .filter(frs_total_outstanding=0, frs_total_actual_amt=F('frs_intervention_amt')) + # we should try all interventions except the ones in terminal statuses + possible_interventions = Intervention.objects.exclude(status__in=[ + Intervention.DRAFT, Intervention.TERMINATED, Intervention.CLOSED, Intervention.SUSPENDED + ]) processed = 0 - for intervention in itertools.chain(active_ended, qs): + for intervention in possible_interventions: old_status = intervention.status with transaction.atomic(): validator = InterventionValid(intervention, user=admin_user, disable_rigid_check=True) @@ -142,7 +136,7 @@ def _make_intervention_status_automatic_transitions(country_name): logger.error('Bad interventions {}'.format(len(bad_interventions))) logger.error('Bad interventions ids: ' + ' '.join(str(a.id) for a in bad_interventions)) - logger.info('Total interventions {}'.format(active_ended.count() + qs.count())) + logger.info('Total interventions {}'.format(possible_interventions.count())) logger.info("Transitioned interventions {} ".format(processed)) diff --git a/src/etools/applications/partners/tests/test_tasks.py b/src/etools/applications/partners/tests/test_tasks.py index 9302e0f8e..4c6afe9b2 100644 --- a/src/etools/applications/partners/tests/test_tasks.py +++ b/src/etools/applications/partners/tests/test_tasks.py @@ -11,19 +11,29 @@ from django.utils import timezone from unicef_attachments.models import Attachment +from unicef_locations.tests.factories import LocationFactory import etools.applications.partners.tasks from etools.applications.attachments.tests.factories import AttachmentFactory, AttachmentFileTypeFactory from etools.applications.core.tests.cases import BaseTenantTestCase from etools.applications.funds.tests.factories import FundsReservationHeaderFactory -from etools.applications.partners.models import Agreement, Intervention +from etools.applications.partners.models import Agreement, Intervention, InterventionBudget +from etools.applications.partners.tasks import _make_intervention_status_automatic_transitions from etools.applications.partners.tests.factories import ( AgreementFactory, CoreValuesAssessmentFactory, InterventionFactory, + InterventionResultLinkFactory, PartnerFactory, ) -from etools.applications.reports.tests.factories import CountryProgrammeFactory +from etools.applications.reports.models import ResultType +from etools.applications.reports.tests.factories import ( + CountryProgrammeFactory, + LowerResultFactory, + OfficeFactory, + ReportingRequirementFactory, + SectionFactory, +) from etools.applications.users.tests.factories import CountryFactory, UserFactory @@ -680,6 +690,69 @@ def mock_intervention_valid_class_side_effect(*args, **kwargs): ] self._assertCalls(mock_logger.error, expected_call_args) + def test_activate_intervention_with_task(self, _mock_db_connection, _mock_logger): + today = datetime.date.today() + unicef_staff = UserFactory(is_staff=True, groups__data=['UNICEF User']) + + partner = PartnerFactory(name='Partner 2') + active_agreement = AgreementFactory( + partner=partner, + status=Agreement.SIGNED, + signed_by_unicef_date=today - datetime.timedelta(days=2), + signed_by_partner_date=today - datetime.timedelta(days=2), + start=today - datetime.timedelta(days=2), + ) + + active_intervention = InterventionFactory( + agreement=active_agreement, + title='Active Intervention', + document_type=Intervention.PD, + start=today - datetime.timedelta(days=1), + end=today + datetime.timedelta(days=365), + status=Intervention.SIGNED, + country_programme=active_agreement.country_programme, + # budget_owner=unicef_staff, + # date_sent_to_partner=today - datetime.timedelta(days=1), + signed_by_unicef_date=today - datetime.timedelta(days=1), + signed_by_partner_date=today - datetime.timedelta(days=1), + unicef_signatory=unicef_staff, + partner_authorized_officer_signatory=partner.staff_members.all().first(), + # cash_transfer_modalities=[Intervention.CASH_TRANSFER_DIRECT], + ) + InterventionBudget.objects.get_or_create( + intervention=active_intervention, + defaults={ + 'unicef_cash': 100, + 'unicef_cash_local': 10, + 'partner_contribution': 200, + 'partner_contribution_local': 20, + 'in_kind_amount_local': 10, + } + ) + active_intervention.flat_locations.add(LocationFactory()) + active_intervention.partner_focal_points.add(partner.staff_members.all().first()) + active_intervention.unicef_focal_points.add(unicef_staff) + active_intervention.offices.add(OfficeFactory()) + active_intervention.sections.add(SectionFactory()) + ReportingRequirementFactory(intervention=active_intervention) + AttachmentFactory( + code='partners_intervention_signed_pd', + content_object=active_intervention, + ) + FundsReservationHeaderFactory(intervention=active_intervention) + + result_link = InterventionResultLinkFactory( + intervention=active_intervention, + cp_output__result_type__name=ResultType.OUTPUT, + ) + LowerResultFactory(result_link=result_link) + # activity = InterventionActivityFactory(result=pd_output) # epd related stuff + # activity.time_frames.add(active_intervention.quarters.first()) + + _make_intervention_status_automatic_transitions(self.country_name) + active_intervention.refresh_from_db() + self.assertEqual(active_intervention.status, Intervention.ACTIVE) + @mock.patch('etools.applications.partners.tasks.logger', spec=['info']) @mock.patch('etools.applications.partners.tasks.connection', spec=['set_tenant']) From 1748a0ac6f3161eddd88616f64c8bbe2035bc24d Mon Sep 17 00:00:00 2001 From: Roman Karpovich Date: Fri, 13 May 2022 20:50:11 +0800 Subject: [PATCH 2/2] fix tests --- src/etools/applications/partners/tasks.py | 2 +- .../applications/partners/tests/test_tasks.py | 39 ++++--------------- 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/src/etools/applications/partners/tasks.py b/src/etools/applications/partners/tasks.py index c167defc3..5712d20a0 100644 --- a/src/etools/applications/partners/tasks.py +++ b/src/etools/applications/partners/tasks.py @@ -121,7 +121,7 @@ def _make_intervention_status_automatic_transitions(country_name): # we should try all interventions except the ones in terminal statuses possible_interventions = Intervention.objects.exclude(status__in=[ Intervention.DRAFT, Intervention.TERMINATED, Intervention.CLOSED, Intervention.SUSPENDED - ]) + ]).order_by('id') processed = 0 for intervention in possible_interventions: old_status = intervention.status diff --git a/src/etools/applications/partners/tests/test_tasks.py b/src/etools/applications/partners/tests/test_tasks.py index 4c6afe9b2..58197d9d5 100644 --- a/src/etools/applications/partners/tests/test_tasks.py +++ b/src/etools/applications/partners/tests/test_tasks.py @@ -559,23 +559,11 @@ def test_make_intervention_status_automatic_transitions_with_valid_interventions interventions.append(intervention) # Create a few items that should be ignored. If they're not ignored, this test will fail. - # Ignored because of end date - InterventionFactory(status=Intervention.ACTIVE, end=datetime.date.today() + datetime.timedelta(days=2)) # Ignored because of status - InterventionFactory(status=Intervention.IMPLEMENTED, end=end_date) - # Ignored because funds total outstanding != 0 - intervention = InterventionFactory(status=Intervention.ENDED, end=end_date) - for i in range(3): - FundsReservationHeaderFactory(intervention=intervention, outstanding_amt=Decimal(i), - intervention_amt=_make_decimal(i), - actual_amt=_make_decimal(i), total_amt=_make_decimal(i)) - - # Ignored because funds totals don't match - intervention = InterventionFactory(status=Intervention.ENDED, end=end_date) - for i in range(3): - FundsReservationHeaderFactory(intervention=intervention, outstanding_amt=Decimal(0.00), - intervention_amt=_make_decimal(i), - actual_amt=_make_decimal(i + 1), total_amt=_make_decimal(i)) + InterventionFactory(status=Intervention.TERMINATED) + InterventionFactory(status=Intervention.CLOSED) + InterventionFactory(status=Intervention.SUSPENDED) + InterventionFactory(status=Intervention.DRAFT) # Mock InterventionValid() to always return True. mock_validator = mock.Mock(spec=['is_valid']) @@ -629,22 +617,11 @@ def test_make_intervention_status_automatic_transitions_with_mixed_interventions interventions.append(intervention) # Create a few items that should be ignored. If they're not ignored, this test will fail. - # Ignored because of end date - InterventionFactory(status=Intervention.ACTIVE, end=datetime.date.today() + datetime.timedelta(days=2)) # Ignored because of status - InterventionFactory(status=Intervention.IMPLEMENTED, end=end_date) - # Ignored because funds total outstanding != 0 - intervention = InterventionFactory(status=Intervention.ENDED, end=end_date) - for i in range(3): - FundsReservationHeaderFactory(intervention=intervention, outstanding_amt=Decimal(i), - intervention_amt=_make_decimal(i), - actual_amt=_make_decimal(i), total_amt=_make_decimal(i)) - # Ignored because funds totals don't match - intervention = InterventionFactory(status=Intervention.ENDED, end=end_date) - for i in range(3): - FundsReservationHeaderFactory(intervention=intervention, outstanding_amt=Decimal(0.00), - intervention_amt=_make_decimal(i), - actual_amt=_make_decimal(i + 1), total_amt=_make_decimal(i)) + InterventionFactory(status=Intervention.TERMINATED) + InterventionFactory(status=Intervention.CLOSED) + InterventionFactory(status=Intervention.SUSPENDED) + InterventionFactory(status=Intervention.DRAFT) def mock_intervention_valid_class_side_effect(*args, **kwargs): """Side effect for my mock InterventionValid() that gets called each time my mock InterventionValid() class