diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 21e26f6aa..540ba792c 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -69,3 +69,4 @@ services: - CHARGEBEE_SITE_API_KEY=test_31lcdE7L3grqdkGcvy24ik3lmlJrnA0Ez - CHARGEBEE_SITE=toladata-test - TOLA_TRACK_SYNC_ENABLED=False + - DEFAULT_REPLY_TO=noreply@test.com diff --git a/feed/tests/test_budgetview.py b/feed/tests/test_budgetview.py index d302e22d3..1e26c69d0 100644 --- a/feed/tests/test_budgetview.py +++ b/feed/tests/test_budgetview.py @@ -1,3 +1,5 @@ +from decimal import Decimal + from django.test import TestCase from rest_framework.test import APIRequestFactory from rest_framework.reverse import reverse @@ -76,9 +78,10 @@ def test_create_budget(self): response = view(request) self.assertEqual(response.status_code, 201) - self.assertEqual(response.data['proposed_value'], + self.assertEqual(Decimal(response.data['proposed_value']), data['proposed_value']) - self.assertEqual(response.data['actual_value'], data['actual_value']) + self.assertEqual(Decimal(response.data['actual_value']), + data['actual_value']) self.assertEqual(response.data['created_by'], user_url) # Check WorkflowLevel2 @@ -101,9 +104,10 @@ def test_create_budget_without_wkfl2(self): response = view(request) self.assertEqual(response.status_code, 201) - self.assertEqual(response.data['proposed_value'], + self.assertEqual(Decimal(response.data['proposed_value']), data['proposed_value']) - self.assertEqual(response.data['actual_value'], data['actual_value']) + self.assertEqual(Decimal(response.data['actual_value']), + data['actual_value']) self.assertEqual(response.data['created_by'], user_url) @@ -129,14 +133,42 @@ def test_update_budget(self): budget = Budget.objects.get(pk=response.data['id']) self.assertEqual(response.status_code, 200) - self.assertEqual(response.data['proposed_value'], budget.proposed_value) - self.assertEqual(response.data['actual_value'], budget.actual_value) + self.assertEqual(Decimal(response.data['proposed_value']), + budget.proposed_value) + self.assertEqual(Decimal(response.data['actual_value']), + budget.actual_value) # Check WorkflowLevel2 wkfl2 = budget.workflowlevel2 self.assertEquals(wkfl2.total_estimated_budget, data['proposed_value']) self.assertEquals(wkfl2.actual_cost, data['actual_value']) + def test_update_budget_without_actual_value(self): + wflvl1 = factories.WorkflowLevel1( + name='WorkflowLevel1', organization=self.tola_user.organization) + wflvl2 = factories.WorkflowLevel2( + name='WorkflowLevel2', actual_cost=100, total_estimated_budget=0, + workflowlevel1=wflvl1) + budget = factories.Budget(workflowlevel2=wflvl2, + proposed_value=None, actual_value=None) + + data = {'proposed_value': 5678} + request = self.factory.post('/api/budget/', data) + request.user = self.tola_user.user + view = BudgetViewSet.as_view({'post': 'update'}) + response = view(request, pk=budget.pk) + + budget = Budget.objects.get(pk=response.data['id']) + self.assertEqual(response.status_code, 200) + self.assertEqual(Decimal(response.data['proposed_value']), + budget.proposed_value) + self.assertEqual(Decimal(response.data['actual_value']), 0) + + # Check WorkflowLevel2 + wkfl2 = budget.workflowlevel2 + self.assertEquals(wkfl2.total_estimated_budget, data['proposed_value']) + self.assertEquals(wkfl2.actual_cost, 100) + def test_update_budget_without_wkfl2(self): budget = factories.Budget() @@ -149,8 +181,10 @@ def test_update_budget_without_wkfl2(self): budget = Budget.objects.get(pk=response.data['id']) self.assertEqual(response.status_code, 201) - self.assertEqual(response.data['proposed_value'], budget.proposed_value) - self.assertEqual(response.data['actual_value'], budget.actual_value) + self.assertEqual(Decimal(response.data['proposed_value']), + budget.proposed_value) + self.assertEqual(Decimal(response.data['actual_value']), + budget.actual_value) class BudgetFilterViewTest(TestCase): diff --git a/tola/settings/local.py b/tola/settings/local.py index 4c3e455d3..127866865 100755 --- a/tola/settings/local.py +++ b/tola/settings/local.py @@ -179,3 +179,9 @@ chargebee.configure(os.getenv('CHARGEBEE_SITE_API_KEY'), os.getenv('CHARGEBEE_SITE')) ########## END CHARGEBEE CONFIGURATION + +########## EMAIL CONFIGURATION + +DEFAULT_REPLY_TO = os.getenv('DEFAULT_REPLY_TO', '') + +########## END EMAIL CONFIGURATION diff --git a/workflow/migrations/0018_auto_20180302_0814.py b/workflow/migrations/0018_auto_20180302_0814.py new file mode 100644 index 000000000..91c7c6559 --- /dev/null +++ b/workflow/migrations/0018_auto_20180302_0814.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2018-03-02 16:14 +from __future__ import unicode_literals + +from decimal import Decimal +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflow', '0017_auto_20180226_0031'), + ] + + operations = [ + migrations.AlterField( + model_name='budget', + name='actual_value', + field=models.DecimalField(blank=True, decimal_places=2, default=Decimal('0.00'), help_text='Monetary value positive or negative', max_digits=12, verbose_name='Actual'), + ), + migrations.AlterField( + model_name='budget', + name='proposed_value', + field=models.DecimalField(blank=True, decimal_places=2, default=Decimal('0.00'), help_text='Approximate value if not a monetary fund', max_digits=12, verbose_name='Budget'), + ), + migrations.AlterField( + model_name='historicalbudget', + name='actual_value', + field=models.DecimalField(blank=True, decimal_places=2, default=Decimal('0.00'), help_text='Monetary value positive or negative', max_digits=12, verbose_name='Actual'), + ), + migrations.AlterField( + model_name='historicalbudget', + name='proposed_value', + field=models.DecimalField(blank=True, decimal_places=2, default=Decimal('0.00'), help_text='Approximate value if not a monetary fund', max_digits=12, verbose_name='Budget'), + ), + ] diff --git a/workflow/models.py b/workflow/models.py index 6eeb355cb..f2b84e156 100755 --- a/workflow/models.py +++ b/workflow/models.py @@ -1341,12 +1341,12 @@ class Meta: class Budget(models.Model): contributor = models.CharField(max_length=135, blank=True, null=True, help_text="Source of budget fund") - account_code = models.CharField("Accounting Code",max_length=135, blank=True, null=True, help_text="Label or coded field") - cost_center = models.CharField("Cost Center",max_length=135, blank=True, null=True, help_text="Associate a cost with a type of expense") - donor_code = models.CharField("Donor Code",max_length=135, blank=True, null=True, help_text="Third Party coded field") + account_code = models.CharField("Accounting Code", max_length=135, blank=True, null=True, help_text="Label or coded field") + cost_center = models.CharField("Cost Center", max_length=135, blank=True, null=True, help_text="Associate a cost with a type of expense") + donor_code = models.CharField("Donor Code", max_length=135, blank=True, null=True, help_text="Third Party coded field") description_of_contribution = models.CharField(max_length=255, blank=True, null=True, help_text="Purpose or use for funds") - proposed_value = models.IntegerField("Budget",default=0, blank=True, null=True, help_text="Approximate value if not a monetary fund") - actual_value = models.IntegerField("Actual", default=0, blank=True, null=True, help_text="Monetary value positive or negative") + proposed_value = models.DecimalField("Budget", decimal_places=2, max_digits=12, default=Decimal("0.00"), blank=True, help_text="Approximate value if not a monetary fund") + actual_value = models.DecimalField("Actual", decimal_places=2, max_digits=12, default=Decimal("0.00"), blank=True, help_text="Monetary value positive or negative") workflowlevel2 = models.ForeignKey(WorkflowLevel2, blank=True, null=True, on_delete=models.SET_NULL, help_text="Releated workflow level 2") local_currency = models.ForeignKey(Currency, blank=True, null=True, related_name="local", help_text="Primary Currency") donor_currency = models.ForeignKey(Currency, blank=True, null=True, related_name="donor", help_text="Secondary Currency") @@ -1360,6 +1360,11 @@ def save(self, *args, **kwargs): self.create_date = timezone.now() self.edit_date = timezone.now() + if self.proposed_value is None: + self.proposed_value = Decimal("0.00") + if self.actual_value is None: + self.actual_value = Decimal("0.00") + if self.workflowlevel2: wflvl2 = self.workflowlevel2 try: diff --git a/workflow/signals.py b/workflow/signals.py index a90aedacb..991480a73 100644 --- a/workflow/signals.py +++ b/workflow/signals.py @@ -6,6 +6,7 @@ except ImportError: pass from django.conf import settings +from django.core.mail import EmailMessage from django.contrib.auth.models import Group from django.db.models import signals from django.dispatch import receiver @@ -107,8 +108,18 @@ def check_seats_save_team(sender, instance, **kwargs): if user_addon: available_seats = user_addon.quantity if available_seats < org.chargebee_used_seats: - # TODO: Notify the Org admin - pass + user_email = instance.workflow_user.user.email + email = EmailMessage( + subject='Exceeded the number of editors', + body='The number of editors has exceeded the amount of ' + 'users set in your Subscription. Please check it out!' + '\nCurrent amount of editors: {}.\nSelected amount ' + 'of editors: {}.'.format( + org.chargebee_used_seats, available_seats), + to=[user_email], + reply_to=[settings.DEFAULT_REPLY_TO], + ) + email.send() @receiver(signals.pre_delete, sender=WorkflowTeam) @@ -189,8 +200,18 @@ def check_seats_save_user_groups(sender, instance, **kwargs): if user_addon: available_seats = user_addon.quantity if available_seats < org.chargebee_used_seats: - # TODO: Notify the Org admin - pass + user_email = instance.email + email = EmailMessage( + subject='Exceeded the number of editors', + body='The number of editors has exceeded the amount of ' + 'users set in your Subscription. Please check it ' + 'out!\nCurrent amount of editors: {}.\nSelected ' + 'amount of editors: {}.'.format( + org.chargebee_used_seats, available_seats), + to=[user_email], + reply_to=[settings.DEFAULT_REPLY_TO], + ) + email.send() # ORGANIZATION SIGNALS diff --git a/workflow/tests/test_signals.py b/workflow/tests/test_signals.py index c00351cb1..2a5a9c397 100644 --- a/workflow/tests/test_signals.py +++ b/workflow/tests/test_signals.py @@ -5,6 +5,7 @@ from chargebee import Addon, Subscription except ImportError: pass +from django.core import mail from django.test import TestCase, override_settings, tag from mock import Mock, patch @@ -109,7 +110,7 @@ def __init__(self, values): addon = Addon(values) addon.id = 'user' - addon.quantity = 0 + addon.quantity = 1 self.subscription.addons = [addon] def setUp(self): @@ -216,6 +217,34 @@ def test_check_seats_save_team_org_admin(self): organization = Organization.objects.get(pk=self.org.id) self.assertEqual(organization.chargebee_used_seats, 1) + @override_settings(DEFAULT_REPLY_TO='noreply@test.com') + def test_check_seats_save_team_exceed_notify(self): + self.tola_user.user.groups.add(self.group_org_admin) + self.tola_user.user.save() + self.org = Organization.objects.get(pk=self.org.id) + user = factories.User(first_name='John', last_name='Lennon') + tolauser = factories.TolaUser(user=user, organization=self.org) + + external_response = self.ExternalResponse(None) + Subscription.retrieve = Mock(return_value=external_response) + wflvl1 = factories.WorkflowLevel1(name='WorkflowLevel1') + factories.WorkflowTeam(workflow_user=tolauser, + workflowlevel1=wflvl1, + role=self.group_program_admin) + + # It should notify the OrgAdmin + organization = Organization.objects.get(pk=self.org.id) + self.assertEqual(organization.chargebee_used_seats, 2) + self.assertEqual(len(mail.outbox), 1) + self.assertIn('Exceeded the number of editors', mail.outbox[0].subject) + self.assertEqual(mail.outbox[0].to, [user.email]) + self.assertEqual(mail.outbox[0].reply_to, ['noreply@test.com']) + self.assertEqual(mail.outbox[0].body, + 'The number of editors has exceeded the amount of ' + 'users set in your Subscription. Please check it ' + 'out!\nCurrent amount of editors: 2.\nSelected ' + 'amount of editors: 1.') + class CheckSeatsDeleteWFTeamsTest(TestCase): class ExternalResponse: @@ -225,7 +254,7 @@ def __init__(self, values): addon = Addon(values) addon.id = 'user' - addon.quantity = 0 + addon.quantity = 1 self.subscription.addons = [addon] def setUp(self): @@ -251,7 +280,7 @@ def test_check_seats_delete_team_decrease(self): organization = Organization.objects.get(pk=self.org.id) self.assertEqual(organization.chargebee_used_seats, 0) - def test_check_seats_save_team_not_decrease(self): + def test_check_seats_delete_team_not_decrease(self): external_response = self.ExternalResponse(None) Subscription.retrieve = Mock(return_value=external_response) wflvl1_1 = factories.WorkflowLevel1(name='WorkflowLevel1_1') @@ -310,7 +339,7 @@ def __init__(self, values): addon = Addon(values) addon.id = 'user' - addon.quantity = 0 + addon.quantity = 1 self.subscription.addons = [addon] def setUp(self): @@ -378,6 +407,32 @@ def test_check_seats_save_user_groups_demo(self, mock_tsync): organization = Organization.objects.get(pk=self.org.id) self.assertEqual(organization.chargebee_used_seats, 0) + @override_settings(DEFAULT_REPLY_TO='noreply@test.com') + def test_check_seats_save_user_groups_exceed_notify(self): + external_response = self.ExternalResponse(None) + Subscription.retrieve = Mock(return_value=external_response) + self.tola_user.user.groups.add(self.group_org_admin) + self.tola_user.user.save() + + self.org = Organization.objects.get(pk=self.org.id) + user = factories.User(first_name='John', last_name='Lennon') + tolauser = factories.TolaUser(user=user, organization=self.org) + tolauser.user.groups.add(self.group_org_admin) + tolauser.user.save() + + # It should notify the OrgAdmin + organization = Organization.objects.get(pk=self.org.id) + self.assertEqual(organization.chargebee_used_seats, 2) + self.assertEqual(len(mail.outbox), 1) + self.assertIn('Exceeded the number of editors', mail.outbox[0].subject) + self.assertEqual(mail.outbox[0].to, [user.email]) + self.assertEqual(mail.outbox[0].reply_to, ['noreply@test.com']) + self.assertEqual(mail.outbox[0].body, + 'The number of editors has exceeded the amount of ' + 'users set in your Subscription. Please check it ' + 'out!\nCurrent amount of editors: 2.\nSelected ' + 'amount of editors: 1.') + class SignalSyncTrackTest(TestCase): def setUp(self):