Skip to content

Commit 7d7273c

Browse files
committed
Merge branch 'develop' of github.com:unicef/etools into ch33354-UNICEF-cash-unfunded
# Conflicts: # src/etools/applications/partners/locale/ar/LC_MESSAGES/django.mo # src/etools/applications/partners/locale/ar/LC_MESSAGES/django.po # src/etools/applications/partners/locale/es/LC_MESSAGES/django.mo # src/etools/applications/partners/locale/es/LC_MESSAGES/django.po # src/etools/applications/partners/locale/fr/LC_MESSAGES/django.mo # src/etools/applications/partners/locale/fr/LC_MESSAGES/django.po # src/etools/applications/partners/locale/pt/LC_MESSAGES/django.mo # src/etools/applications/partners/locale/pt/LC_MESSAGES/django.po # src/etools/applications/partners/locale/ru/LC_MESSAGES/django.mo # src/etools/applications/partners/locale/ru/LC_MESSAGES/django.po # src/etools/applications/partners/serializers/interventions_v3.py # src/etools/applications/reports/serializers/v2.py
2 parents 35586a0 + a257689 commit 7d7273c

File tree

120 files changed

+13326
-9402
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

120 files changed

+13326
-9402
lines changed

.flake8

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ ignore =
66
F405,
77
; E501 maximum line length
88
E501,
9-
W504
9+
W504,
10+
; E741 ambiguous variable name
11+
E741
1012

1113
exclude =
1214
*/migrations,

Dockerfile-base

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ RUN apk add --no-cache --virtual .build-deps --update \
3131
gettext
3232

3333
RUN pip install --no-cache-dir --upgrade \
34-
pip \
34+
pip==23.1.2 \
3535
pipenv==2022.10.4
3636

3737

Pipfile

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,16 @@ azure-common = "==1.1.27"
2222
azure-storage-blob = "==2.1.0"
2323
azure-storage-common = "==2.1.0"
2424
carto = "==1.11.2"
25-
celery = "==5.1.2"
25+
celery = "==5.2.3"
2626
cryptography = "<3.4"
2727
dj-database-url = "==0.5"
2828
dj-static = "==0.0.6"
29-
Django = "==3.2.6"
29+
Django = "==3.2.19"
3030
django-admin-extra-urls = "*"
3131
django-appconf = "==1.0.4"
3232
django-celery-beat = "==2.2.1"
3333
django-celery-email = "==3.0.0"
34-
django-celery-results = "==2.2"
34+
django-celery-results = "==2.4.0"
3535
django-contrib-comments = "==2.1.0"
3636
django-cors-headers = "==3.7.0"
3737
django-debug-toolbar = "==3.2.1"
@@ -61,11 +61,11 @@ djangorestframework = "==3.12.4"
6161
drf-nested-routers = "==0.93.3"
6262
drf-querystringfilter = "==1.0.0"
6363
etools-validator = "==0.5.0"
64-
flower = "==0.9.5" # issue when locking
64+
flower = "==1.2.0" # issue when locking
6565
GDAL = "==3.0.2"
6666
gunicorn = "<20.0"
6767
newrelic = "*"
68-
Pillow = "==8.1.0"
68+
Pillow = "==9.3.0"
6969
psycopg2-binary = "==2.9.1"
7070
sentry-sdk = "*"
7171
requests = "==2.26"
@@ -83,8 +83,8 @@ xhtml2pdf = "==0.2.5"
8383
unicef-vision = "==0.6"
8484
etools-offline = "==0.1.0"
8585
openpyxl = "==3.0.5"
86-
pyyaml = "==5.4.1"
86+
pyyaml = "==5.3.1"
8787
reportlab = "==3.6.6" # freeze: getStringIO from 'reportlab.lib.utils'
8888

8989
[requires]
90-
python_version = "3.9"
90+
python_version = "3.9"

Pipfile.lock

Lines changed: 1109 additions & 915 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/etools/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
VERSION = __version__ = '10.1.1'
1+
VERSION = __version__ = '11.1.6'
22
NAME = 'eTools'

src/etools/applications/attachments/tests/test_views.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from django.contrib.auth.models import AnonymousUser
12
from django.urls import resolve, reverse
23

34
from rest_framework import status
@@ -125,8 +126,9 @@ def test_unauthenticated_user_forbidden(self):
125126
factory = APIRequestFactory()
126127
view_info = resolve(self.url)
127128
request = factory.get(self.url)
129+
request.user = AnonymousUser()
128130
response = view_info.func(request)
129-
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
131+
self.assertEqual(response.status_code, status.HTTP_302_FOUND)
130132

131133
def test_non_schema_user(self):
132134
user = UserFactory(profile=None, realms__data=[])

src/etools/applications/audit/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class EngagementAdmin(admin.ModelAdmin):
2929
list_filter = [
3030
'status', 'start_date', 'end_date', 'status', 'engagement_type',
3131
]
32-
search_fields = 'partner__organization__name', 'agreement__auditor_firm__name',
32+
search_fields = 'partner__organization__name', 'agreement__auditor_firm__organization__name',
3333
filter_horizontal = ('authorized_officers', 'active_pd', 'staff_members', 'users_notified', 'sections', 'offices')
3434
raw_id_fields = ('po_item', 'partner', 'active_pd', 'staff_members', 'authorized_officers', 'users_notified', )
3535

src/etools/applications/audit/filters.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
from django.contrib.auth import get_user_model
21
from django.db import models
32
from django.db.models.functions import TruncYear
43

54
from django_filters import rest_framework as filters
65
from rest_framework.filters import BaseFilterBackend, OrderingFilter
76

87
from etools.applications.audit.models import Engagement
8+
from etools.applications.users.models import Country
99

1010

1111
class DisplayStatusFilter(BaseFilterBackend):
@@ -87,12 +87,20 @@ class Meta:
8787
}
8888

8989

90-
class AuditorStaffMembersFilterSet(filters.FilterSet):
91-
user__profile__countries_available__name = filters.CharFilter(field_name='realms__country__name', distinct=True)
90+
class StaffMembersCountriesAvailableFilter(BaseFilterBackend):
91+
"""
92+
manual filtering on countries available without extra join
93+
"""
94+
def filter_queryset(self, request, queryset, view):
95+
country_name = request.query_params.get('user__profile__countries_available__name', '')
96+
if not country_name:
97+
return queryset
9298

93-
class Meta:
94-
model = get_user_model()
95-
fields = {}
99+
country = Country.objects.filter(name=country_name).only('id').first()
100+
if not country:
101+
return queryset.none()
102+
103+
return queryset.filter(realms__country=country.id).distinct()
96104

97105

98106
class StaffMembersOrderingFilter(OrderingFilter):

src/etools/applications/audit/management/commands/update_audit_permissions.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,20 +349,35 @@ def assign_permissions(self):
349349
'audit.engagement.cancel',
350350
condition=partner_contacted_condition
351351
)
352+
self.add_permissions(
353+
self.everybody, 'view',
354+
'audit.engagement.send_back_comment',
355+
condition=partner_contacted_condition
356+
)
352357

353-
# report submitted. focal point can finalize. all can view
358+
# report submitted. focal point can finalize or send back. all can view
354359
report_submitted_condition = self.engagement_status(Engagement.STATUSES.report_submitted)
355360
self.add_permissions(self.auditor, 'edit', self.report_attachments_block, condition=report_submitted_condition)
356361
self.add_permissions(
357362
self.focal_point, 'action',
358363
'audit.engagement.finalize',
359364
condition=report_submitted_condition
360365
)
366+
self.add_permissions(
367+
self.focal_point, 'action',
368+
'audit.engagement.send_back',
369+
condition=report_submitted_condition
370+
)
361371
self.add_permissions(
362372
self.everybody, 'view',
363373
self.report_block,
364374
condition=report_submitted_condition
365375
)
376+
self.add_permissions(
377+
self.everybody, 'view',
378+
'audit.engagement.send_back_comment',
379+
condition=report_submitted_condition
380+
)
366381

367382
# final report. everybody can view. focal point can add action points
368383
final_engagement_condition = self.engagement_status(Engagement.STATUSES.final)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 3.2.19 on 2024-01-03 16:06
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('audit', '0029_merge_20230523_1049'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='engagement',
15+
name='send_back_comment',
16+
field=models.TextField(blank=True, verbose_name='Send Back Comment'),
17+
),
18+
]

src/etools/applications/audit/models.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
ValidateMARiskCategories,
2929
ValidateMARiskExtra,
3030
)
31-
from etools.applications.audit.transitions.serializers import EngagementCancelSerializer
31+
from etools.applications.audit.transitions.serializers import EngagementCancelSerializer, EngagementSendBackSerializer
3232
from etools.applications.audit.utils import generate_final_report
3333
from etools.applications.core.urlresolvers import build_frontend_url
3434
from etools.applications.environment.notifications import send_notification_with_template
@@ -174,6 +174,8 @@ class Engagement(InheritedModelMixin, TimeStampedModel, models.Model):
174174

175175
cancel_comment = models.TextField(blank=True, verbose_name=_('Cancel Comment'))
176176

177+
send_back_comment = models.TextField(blank=True, verbose_name=_('Send Back Comment'))
178+
177179
active_pd = models.ManyToManyField('partners.Intervention', verbose_name=_('Active PDs'), blank=True)
178180

179181
authorized_officers = models.ManyToManyField(
@@ -282,6 +284,13 @@ def submit(self):
282284

283285
self._notify_focal_points('audit/engagement/reported_by_auditor')
284286

287+
@transition(status, source=STATUSES.report_submitted, target=STATUSES.partner_contacted,
288+
permission=has_action_permission(action='send_back'),
289+
custom={'serializer': EngagementSendBackSerializer})
290+
def send_back(self, send_back_comment):
291+
self.date_of_report_submit = None
292+
self.send_back_comment = send_back_comment
293+
285294
@transition(status, source=[STATUSES.partner_contacted, STATUSES.report_submitted], target=STATUSES.cancelled,
286295
permission=has_action_permission(action='cancel'),
287296
custom={'serializer': EngagementCancelSerializer})

src/etools/applications/audit/serializers/auditor.py

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -26,57 +26,13 @@ def update(self, instance, validated_data):
2626

2727
class AuditorStaffMemberSerializer(UserSerializer):
2828
user = UserSerializer(required=False, source='*')
29-
# TODO: REALMS - do cleanup - we don't need this field since this serializer is not to be used for editing anymore
30-
# user_pk = serializers.PrimaryKeyRelatedField(
31-
# write_only=True, required=False,
32-
# queryset=get_user_model().objects.all()
33-
# )
3429
hidden = serializers.SerializerMethodField()
3530

3631
def get_hidden(self, obj):
3732
return False
3833

39-
# TODO: REALMS - do cleanup
40-
# # TODO: make sure email provided is lower_case
41-
# def validate(self, attrs):
42-
# validated_data = super().validate(attrs)
43-
# user_pk = validated_data.pop('user_pk', None)
44-
#
45-
# if user_pk:
46-
# if hasattr(user_pk, 'purchase_order_auditorstaffmember'):
47-
# firm = user_pk.purchase_order_auditorstaffmember.auditor_firm
48-
# raise serializers.ValidationError({'user': _('User is already assigned to ') + str(firm)})
49-
# if not self.instance:
50-
# validated_data['user'] = user_pk
51-
# elif 'user' not in validated_data:
52-
# raise serializers.ValidationError({'user': _('This field is required.')})
53-
# elif 'user' in validated_data:
54-
# email = validated_data['user'].get('email', None)
55-
# if not AuditorStaffMember.objects.filter(user__email=email).exists():
56-
# try:
57-
# validated_data['user'] = get_user_model().objects.get(email=email, email__isnull=False)
58-
# except get_user_model().DoesNotExist:
59-
# pass
60-
#
61-
# return validated_data
62-
63-
# TODO: REALMS
64-
# def update(self, instance, validated_data):
65-
# instance = super().update(instance, validated_data)
66-
# if 'hidden' in validated_data:
67-
# Realm.objects.update_or_create(
68-
# user=instance,
69-
# country=connection.tenant,
70-
# organization=self.context['firm'].organization,
71-
# group=Auditor.as_group(),
72-
# defaults={'is_active': not validated_data['hidden']}
73-
# )
74-
# return instance
75-
7634
class Meta(UserSerializer.Meta):
7735
model = get_user_model()
78-
# TODO: REALMS
79-
# fields = ['id', 'user', 'user_pk', 'hidden']
8036
fields = ['id', 'user', 'hidden']
8137

8238

src/etools/applications/audit/serializers/engagement.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ class Meta(EngagementListSerializer.Meta):
292292
'start_date', 'end_date', 'partner_contacted_at', 'date_of_field_visit', 'date_of_draft_report_to_ip',
293293
'date_of_comments_by_ip', 'date_of_draft_report_to_unicef', 'date_of_comments_by_unicef',
294294
'date_of_report_submit', 'date_of_final_report', 'date_of_cancel',
295-
'cancel_comment', 'specific_procedures',
295+
'cancel_comment', 'send_back_comment', 'specific_procedures',
296296
'engagement_attachments',
297297
'report_attachments',
298298
'final_report',

src/etools/applications/audit/serializers/risks.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -361,21 +361,42 @@ def calculate_risk(category):
361361
if category.applicable_questions:
362362
category.risk_score = category.risk_points / category.applicable_questions
363363

364-
lowest_score_possible = category.applicable_questions
365-
highest_score_possible = 4 * lowest_score_possible
366-
banding_width = highest_score_possible - lowest_score_possible
367-
low_points_below = lowest_score_possible + banding_width * 0.15
368-
moderate_points_below = lowest_score_possible + banding_width * 0.3
369-
significant_points_below = lowest_score_possible + banding_width * 0.5
370-
371-
if category.risk_points < low_points_below:
372-
category.risk_rating = 'low'
373-
elif category.risk_points < moderate_points_below:
374-
category.risk_rating = 'medium'
375-
elif category.risk_points < significant_points_below:
376-
category.risk_rating = 'significant'
364+
if category.code in ['ma_questionnaire', 'ma_subject_areas']:
365+
# v1 calculations
366+
lowest_score_possible = 1
367+
highest_score_possible = (4 * category.applicable_questions + 4 * category.applicable_key_questions)
368+
highest_score_possible = highest_score_possible / category.applicable_questions
369+
banding_width = (highest_score_possible - lowest_score_possible) / 4
370+
low_scores_below = lowest_score_possible + banding_width
371+
moderate_scores_below = low_scores_below + banding_width
372+
significant_score_below = moderate_scores_below + banding_width
373+
374+
category.risk_rating = 0
375+
if category.risk_score < low_scores_below:
376+
category.risk_rating = 'low'
377+
elif category.risk_score < moderate_scores_below:
378+
category.risk_rating = 'medium'
379+
elif category.risk_score < significant_score_below:
380+
category.risk_rating = 'significant'
381+
else:
382+
category.risk_rating = 'high'
377383
else:
378-
category.risk_rating = 'high'
384+
# v2 calculations
385+
lowest_score_possible = category.applicable_questions
386+
highest_score_possible = 4 * lowest_score_possible
387+
banding_width = highest_score_possible - lowest_score_possible
388+
low_points_below = lowest_score_possible + banding_width * 0.15
389+
moderate_points_below = lowest_score_possible + banding_width * 0.3
390+
significant_points_below = lowest_score_possible + banding_width * 0.5
391+
392+
if category.risk_points < low_points_below:
393+
category.risk_rating = 'low'
394+
elif category.risk_points < moderate_points_below:
395+
category.risk_rating = 'medium'
396+
elif category.risk_points < significant_points_below:
397+
category.risk_rating = 'significant'
398+
else:
399+
category.risk_rating = 'high'
379400
else:
380401
category.risk_score = None
381402
category.risk_rating = 0

src/etools/applications/audit/templates/audit/audit_pdf.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<td colspan="2">{{ serializer.fields.financial_findings_local.label }}</td>
2020
{% endif %}
2121
<td>{{ serializer.fields.percent_of_audited_expenditure.label }}</td>
22-
<td>{{ serializer.fields.audit_opinion.label }}</td>
22+
<td colspan="2">{{ serializer.fields.audit_opinion.label }}</td>
2323
<td>No. of Financial Findings</td>
2424
<td colspan="3" style="padding: 0">
2525
<table>
@@ -45,7 +45,7 @@
4545
<td colspan="2">{{ engagement.financial_findings_local|default_if_none:"0.00"|currency }}</td>
4646
{% endif %}
4747
<td>{{ engagement.percent_of_audited_expenditure|default_if_none:"0.00"|currency }}</td>
48-
<td>{{ engagement.audit_opinion|default:"-"|title }}</td>
48+
<td colspan="2">{{ engagement.audit_opinion|default:"-"|title }}</td>
4949
<td>{{ engagement.financial_finding_set|length }}</td>
5050
<td>{{ engagement.key_internal_weakness.high_risk_count }}</td>
5151
<td>{{ engagement.key_internal_weakness.medium_risk_count }}</td>

0 commit comments

Comments
 (0)