Skip to content

Commit

Permalink
Merge branch 'develop' into 36049-the-currency-of-the-fam-financial-f…
Browse files Browse the repository at this point in the history
…indings
  • Loading branch information
robertavram authored Jun 7, 2024
2 parents 17a46dd + 05ec7eb commit f3b3a33
Show file tree
Hide file tree
Showing 15 changed files with 435 additions and 13 deletions.
1 change: 1 addition & 0 deletions src/etools/applications/audit/serializers/engagement.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,7 @@ def _validate_financial_findings(self, validated_data):
raise serializers.ValidationError({'financial_findings_local': _('Cannot exceed Audited Expenditure Local')})

def validate(self, validated_data):
validated_data = super().validate(validated_data)
self._validate_financial_findings(validated_data)
return validated_data

Expand Down
27 changes: 27 additions & 0 deletions src/etools/applications/audit/serializers/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,33 @@ def _validate_dates(self, validated_data):
if value and key in date_fields and timezone.now().date() < value:
errors[key] = _('Date should be in past.')

start_date = validated_data.get('start_date', self.instance.start_date if self.instance else None)
end_date = validated_data.get('end_date', self.instance.end_date if self.instance else None)
partner_contacted_at = validated_data.get('partner_contacted_at', self.instance.partner_contacted_at if self.instance else None)
date_of_field_visit = validated_data.get('date_of_field_visit', self.instance.date_of_field_visit if self.instance else None)
date_of_draft_report_to_ip = validated_data.get('date_of_draft_report_to_ip', self.instance.date_of_draft_report_to_ip if self.instance else None)
date_of_comments_by_ip = validated_data.get('date_of_comments_by_ip', self.instance.date_of_comments_by_ip if self.instance else None)
date_of_draft_report_to_unicef = validated_data.get('date_of_draft_report_to_unicef', self.instance.date_of_draft_report_to_unicef if self.instance else None)
date_of_comments_by_unicef = validated_data.get('date_of_comments_by_unicef', self.instance.date_of_comments_by_unicef if self.instance else None)

if start_date and end_date and end_date < start_date:
errors['end_date'] = _('This date should be after Period Start Date.')
if end_date and partner_contacted_at and partner_contacted_at < end_date:
errors['partner_contacted_at'] = _('This date should be after Period End Date.')

if partner_contacted_at and date_of_field_visit and date_of_field_visit < partner_contacted_at:
errors['date_of_field_visit'] = _('This date should be after Date IP was contacted.')

if date_of_field_visit and date_of_draft_report_to_ip and date_of_draft_report_to_ip < date_of_field_visit:
# date of field visit is editable even if date of draft report is readonly, map error to field visit date
errors['date_of_field_visit'] = _('This date should be before Date Draft Report Issued to IP.')
if date_of_draft_report_to_ip and date_of_comments_by_ip and date_of_comments_by_ip < date_of_draft_report_to_ip:
errors['date_of_comments_by_ip'] = _('This date should be after Date Draft Report Issued to UNICEF.')
if date_of_comments_by_ip and date_of_draft_report_to_unicef and date_of_draft_report_to_unicef < date_of_comments_by_ip:
errors['date_of_draft_report_to_unicef'] = _('This date should be after Date Comments Received from IP.')
if date_of_draft_report_to_unicef and date_of_comments_by_unicef and date_of_comments_by_unicef < date_of_draft_report_to_unicef:
errors['date_of_comments_by_unicef'] = _('This date should be after Date Draft Report Issued to UNICEF.')

if errors:
raise serializers.ValidationError(errors)
return validated_data
11 changes: 11 additions & 0 deletions src/etools/applications/audit/tests/factories.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import random
from datetime import timedelta

from django.db.models import signals
from django.utils import timezone

import factory
from factory import fuzzy
Expand Down Expand Up @@ -133,16 +135,25 @@ class Meta:


class AuditFactory(EngagementFactory):
start_date = timezone.now().date() - timedelta(days=30)
end_date = timezone.now().date() - timedelta(days=10)

class Meta:
model = Audit


class SpecialAuditFactory(EngagementFactory):
start_date = timezone.now().date() - timedelta(days=30)
end_date = timezone.now().date() - timedelta(days=10)

class Meta:
model = SpecialAudit


class SpotCheckFactory(EngagementFactory):
start_date = timezone.now().date() - timedelta(days=30)
end_date = timezone.now().date() - timedelta(days=10)

class Meta:
model = SpotCheck

Expand Down
148 changes: 148 additions & 0 deletions src/etools/applications/audit/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import datetime
import json
import random
from copy import copy
from unittest.mock import Mock, patch

from django.contrib.contenttypes.models import ContentType
Expand Down Expand Up @@ -539,6 +540,16 @@ def setUp(self):

def _do_create(self, user, data):
data = data or {}
for date_field in [
'start_date', 'end_date', 'partner_contacted_at',
'date_of_field_visit', 'date_of_draft_report_to_ip',
'date_of_comments_by_ip', 'date_of_draft_report_to_unicef',
'date_of_comments_by_unicef'
]:
if data.get(date_field):
if isinstance(data[date_field], datetime.datetime):
data[date_field] = data[date_field].date()
data[date_field] = data[date_field].isoformat()
response = self.forced_auth_req(
'post',
self.engagements_url(),
Expand Down Expand Up @@ -602,6 +613,14 @@ class TestMicroAssessmentCreateViewSet(TestEngagementCreateActivePDViewSet, Base
BaseTenantTestCase):
engagement_factory = MicroAssessmentFactory

def test_partner_contacted_at_validation(self):
# date should be in past
data = copy(self.create_data)
data['partner_contacted_at'] = datetime.datetime.now() + datetime.timedelta(days=1)
response = self._do_create(self.unicef_focal_point, data)
self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('partner_contacted_at', response.data)


class TestAuditCreateViewSet(TestEngagementCreateActivePDViewSet, BaseTestEngagementsCreateViewSet, BaseTenantTestCase):
engagement_factory = AuditFactory
Expand All @@ -610,6 +629,20 @@ def setUp(self):
super().setUp()
self.create_data['year_of_audit'] = timezone.now().year

def test_end_date_validation(self):
data = copy(self.create_data)
data['end_date'] = data['start_date'] - datetime.timedelta(days=1)
response = self._do_create(self.unicef_focal_point, data)
self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('end_date', response.data)

def test_partner_contacted_at_validation(self):
data = copy(self.create_data)
data['partner_contacted_at'] = data['end_date'] - datetime.timedelta(days=1)
response = self._do_create(self.unicef_focal_point, data)
self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('partner_contacted_at', response.data)


class TestSpotCheckCreateViewSet(TestEngagementCreateActivePDViewSet, BaseTestEngagementsCreateViewSet,
BaseTenantTestCase):
Expand Down Expand Up @@ -690,6 +723,20 @@ def test_offices(self):
sorted([office_1.pk, office_2.pk]),
)

def test_end_date_validation(self):
data = copy(self.create_data)
data['end_date'] = data['start_date'] - datetime.timedelta(days=1)
response = self._do_create(self.unicef_focal_point, data)
self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('end_date', response.data)

def test_partner_contacted_at_validation(self):
data = copy(self.create_data)
data['partner_contacted_at'] = data['end_date'] - datetime.timedelta(days=1)
response = self._do_create(self.unicef_focal_point, data)
self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('partner_contacted_at', response.data)


class SpecialAuditCreateViewSet(BaseTestEngagementsCreateViewSet, BaseTenantTestCase):
engagement_factory = SpecialAuditFactory
Expand All @@ -715,12 +762,36 @@ def test_engagement_without_active_pd(self):

self.assertEquals(response.status_code, status.HTTP_201_CREATED)

def test_end_date_validation(self):
data = copy(self.create_data)
data['end_date'] = data['start_date'] - datetime.timedelta(days=1)
response = self._do_create(self.unicef_focal_point, data)
self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('end_date', response.data)

def test_partner_contacted_at_validation(self):
data = copy(self.create_data)
data['partner_contacted_at'] = data['end_date'] - datetime.timedelta(days=1)
response = self._do_create(self.unicef_focal_point, data)
self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('partner_contacted_at', response.data)


class TestEngagementsUpdateViewSet(EngagementTransitionsTestCaseMixin, BaseTenantTestCase):
engagement_factory = AuditFactory

def _do_update(self, user, data):
data = data or {}
for date_field in [
'start_date', 'end_date', 'partner_contacted_at',
'date_of_field_visit', 'date_of_draft_report_to_ip',
'date_of_comments_by_ip', 'date_of_draft_report_to_unicef',
'date_of_comments_by_unicef'
]:
if data.get(date_field):
if isinstance(data[date_field], datetime.datetime):
data[date_field] = data[date_field].date()
data[date_field] = data[date_field].isoformat()
response = self.forced_auth_req(
'patch',
'/api/audit/audits/{}/'.format(self.engagement.id),
Expand Down Expand Up @@ -753,6 +824,83 @@ def test_percent_of_audited_expenditure_valid(self):
})
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_date_of_field_visit_after_partner_contacted_at_validation(self):
self.engagement.partner_contacted_at = self.engagement.end_date + datetime.timedelta(days=1)
self.engagement.save()
response = self._do_update(self.auditor, {
'date_of_field_visit': self.engagement.end_date,
'date_of_draft_report_to_ip': None,
'date_of_comments_by_ip': None,
'date_of_draft_report_to_unicef': None,
'date_of_comments_by_unicef': None,
})
self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('date_of_field_visit', response.data)

def test_date_of_draft_report_to_ip_after_date_of_field_visit_validation(self):
self.engagement.partner_contacted_at = self.engagement.end_date + datetime.timedelta(days=1)
self.engagement.save()
response = self._do_update(self.auditor, {
'date_of_field_visit': self.engagement.end_date + datetime.timedelta(days=2),
'date_of_draft_report_to_ip': self.engagement.end_date,
'date_of_comments_by_ip': None,
'date_of_draft_report_to_unicef': None,
'date_of_comments_by_unicef': None,
})
self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('date_of_field_visit', response.data)

def test_date_of_comments_by_ip_after_date_of_draft_report_to_ip_validation(self):
self.engagement.partner_contacted_at = self.engagement.end_date + datetime.timedelta(days=1)
self.engagement.save()
response = self._do_update(self.auditor, {
'date_of_field_visit': self.engagement.end_date + datetime.timedelta(days=2),
'date_of_draft_report_to_ip': self.engagement.end_date + datetime.timedelta(days=3),
'date_of_comments_by_ip': self.engagement.end_date,
'date_of_draft_report_to_unicef': None,
'date_of_comments_by_unicef': None,
})
self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('date_of_comments_by_ip', response.data)

def test_date_of_draft_report_to_unicef_after_date_of_comments_by_ip_validation(self):
self.engagement.partner_contacted_at = self.engagement.end_date + datetime.timedelta(days=1)
self.engagement.save()
response = self._do_update(self.auditor, {
'date_of_field_visit': self.engagement.end_date + datetime.timedelta(days=2),
'date_of_draft_report_to_ip': self.engagement.end_date + datetime.timedelta(days=3),
'date_of_comments_by_ip': self.engagement.end_date + datetime.timedelta(days=4),
'date_of_draft_report_to_unicef': self.engagement.end_date,
'date_of_comments_by_unicef': None,
})
self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('date_of_draft_report_to_unicef', response.data)

def test_date_of_comments_by_unicef_after_date_of_draft_report_to_unicef_validation(self):
self.engagement.partner_contacted_at = self.engagement.end_date + datetime.timedelta(days=1)
self.engagement.save()
response = self._do_update(self.auditor, {
'date_of_field_visit': self.engagement.end_date + datetime.timedelta(days=2),
'date_of_draft_report_to_ip': self.engagement.end_date + datetime.timedelta(days=3),
'date_of_comments_by_ip': self.engagement.end_date + datetime.timedelta(days=4),
'date_of_draft_report_to_unicef': self.engagement.end_date + datetime.timedelta(days=5),
'date_of_comments_by_unicef': self.engagement.end_date,
})
self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('date_of_comments_by_unicef', response.data)

def test_dates_update_ok(self):
self.engagement.partner_contacted_at = self.engagement.end_date + datetime.timedelta(days=1)
self.engagement.save()
response = self._do_update(self.auditor, {
'date_of_field_visit': self.engagement.end_date + datetime.timedelta(days=2),
'date_of_draft_report_to_ip': self.engagement.end_date + datetime.timedelta(days=3),
'date_of_comments_by_ip': self.engagement.end_date + datetime.timedelta(days=4),
'date_of_draft_report_to_unicef': self.engagement.end_date + datetime.timedelta(days=5),
'date_of_comments_by_unicef': self.engagement.end_date + datetime.timedelta(days=6),
})
self.assertEquals(response.status_code, status.HTTP_200_OK)


class TestEngagementActionPointViewSet(EngagementTransitionsTestCaseMixin, BaseTenantTestCase):
engagement_factory = MicroAssessmentFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ def handle(self, *args, **options):
'enabled': False,
'interval': every_day})

PeriodicTask.objects.get_or_create(name='Notification Partner Assessment expires', defaults={
'task': 'partners.tasks.notify_partner_assessment_expires',
'enabled': False,
'interval': every_day})

PeriodicTask.objects.get_or_create(name='Intervention Notification Ending', defaults={
'task': 'partners.tasks.intervention_notification_ending',
'enabled': False,
Expand Down
20 changes: 11 additions & 9 deletions src/etools/applications/last_mile/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ class WaybillTransferAttachmentInline(AttachmentSingleInline):

@admin.register(models.PointOfInterest)
class PointOfInterestAdmin(XLSXImportMixin, admin.ModelAdmin):
list_display = ('name', 'parent', 'poi_type')
list_display = ('name', 'parent', 'poi_type', 'p_code')
list_select_related = ('parent',)
list_filter = ('private', 'is_active')
search_fields = ('name', )
search_fields = ('name', 'p_code')
raw_id_fields = ('partner_organizations',)
formfield_overrides = {
models.PointField: {'widget': forms.OSMWidget(attrs={'display_raw': True})},
Expand All @@ -44,7 +44,8 @@ class PointOfInterestAdmin(XLSXImportMixin, admin.ModelAdmin):
'PRIMARY TYPE *': 'poi_type',
'IS PRIVATE***': 'private',
'LATITUDE': 'latitude',
'LONGITUDE': 'longitude'
'LONGITUDE': 'longitude',
'P CODE': 'p_code'
}

def has_import_permission(self, request):
Expand All @@ -60,8 +61,9 @@ def import_data(self, workbook):
continue
poi_dict[self.import_field_mapping[col[0].value]] = str(col[row].value).strip()

# add a pcode as it doesn't exist:
poi_dict['p_code'] = generate_hash(poi_dict['partner_org_vendor_no'] + poi_dict['name'], 12)
if not poi_dict.get('p_code'):
# add a pcode if it doesn't exist:
poi_dict['p_code'] = generate_hash(poi_dict['partner_org_vendor_no'] + poi_dict['name'] + poi_dict['poi_type'], 12)
long = poi_dict.pop('longitude')
lat = poi_dict.pop('latitude')
try:
Expand All @@ -88,11 +90,11 @@ def import_data(self, workbook):
continue

poi_obj, _ = models.PointOfInterest.all_objects.update_or_create(
point=poi_dict['point'],
name=poi_dict['name'],
p_code=poi_dict['p_code'],
poi_type=poi_dict.get('poi_type'),
defaults={'private': poi_dict['private']}
defaults={'private': poi_dict['private'],
'point': poi_dict['point'],
'name': poi_dict['name'],
'poi_type': poi_dict.get('poi_type')}
)
poi_obj.partner_organizations.add(partner_org_obj)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.19 on 2024-05-17 08:33

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('last_mile', '0002_auto_20240426_1157'),
]

operations = [
migrations.AlterField(
model_name='pointofinterest',
name='p_code',
field=models.CharField(max_length=32, unique=True, verbose_name='P Code'),
),
]
2 changes: 1 addition & 1 deletion src/etools/applications/last_mile/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class PointOfInterest(TimeStampedModel, models.Model):
null=True, blank=True
)
name = models.CharField(verbose_name=_("Name"), max_length=254)
p_code = models.CharField(verbose_name=_("P Code"), max_length=32, blank=True, default='')
p_code = models.CharField(verbose_name=_("P Code"), max_length=32, unique=True)
description = models.CharField(verbose_name=_("Description"), max_length=254)
poi_type = models.ForeignKey(
PointOfInterestType,
Expand Down
4 changes: 4 additions & 0 deletions src/etools/applications/last_mile/views_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ def get_annotated_qs(qs):
if qs.model == models.Transfer:
return qs.annotate(vendor_number=F('partner_organization__organization__vendor_number'),
checked_out_by_email=F('checked_out_by__email'),
checked_out_by_first_name=F('checked_out_by__first_name'),
checked_out_by_last_name=F('checked_out_by__last_name'),
checked_in_by_email=F('checked_in_by__email'),
checked_in_by_last_name=F('checked_in_by__last_name'),
checked_in_by_first_name=F('checked_in_by__first_name'),
origin_name=F('origin_point__name'),
destination_name=F('destination_point__name'),
).values()
Expand Down
16 changes: 16 additions & 0 deletions src/etools/applications/partners/notifications/expiring_partner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name = 'partners/expiring_partner'
defaults = {
'description': 'Partner Assessment due to expire',
'subject': 'eTools Partnership {{ partner_name }} Assessment is expiring in {{ days }} days',
'content': """
Dear Colleague,
The assessment for the "HACT" or "Core Value" of Partner {{ partner_name }}
with Vendor number {{ partner_number }} in {{ country }} is due to expire in {{ days }} days.
Kindly complete the new assessment and ensure the vendor record is updated with the
latest risk information promptly to avoid any potential transaction blocks in the system.
Please note that this is an automated message and any response to this email cannot be replied to.
"""
}
Loading

0 comments on commit f3b3a33

Please sign in to comment.