diff --git a/hypha/apply/projects/forms/payment.py b/hypha/apply/projects/forms/payment.py index 0d16ff7c40a25539caebd85c74dd60b5348009e8..3cf7f3e2abfc89545ad5a1ff63b4ead1cf87fdf1 100644 --- a/hypha/apply/projects/forms/payment.py +++ b/hypha/apply/projects/forms/payment.py @@ -1,6 +1,7 @@ import json from django import forms +from django.conf import settings from django.core.files.base import ContentFile from django.db import transaction from django.db.models.fields.files import FieldFile @@ -56,16 +57,21 @@ class ChangeInvoiceStatusForm(forms.ModelForm): user_choices ), CHANGES_REQUESTED_BY_FINANCE_1: filter_request_choices([CHANGES_REQUESTED_BY_STAFF, DECLINED], user_choices), - CHANGES_REQUESTED_BY_FINANCE_2: filter_request_choices( - [ - CHANGES_REQUESTED_BY_FINANCE_1, APPROVED_BY_FINANCE_1, - ], - user_choices - ), - APPROVED_BY_FINANCE_1: filter_request_choices([CHANGES_REQUESTED_BY_FINANCE_2, APPROVED_BY_FINANCE_2], user_choices), - APPROVED_BY_FINANCE_2: filter_request_choices([CONVERTED, PAID], user_choices), + APPROVED_BY_FINANCE_1: filter_request_choices([CONVERTED, PAID], user_choices), CONVERTED: filter_request_choices([PAID], user_choices), } + if settings.INVOICE_EXTENDED_WORKFLOW: + possible_status_transitions_lut.update({ + CHANGES_REQUESTED_BY_FINANCE_2: filter_request_choices( + [ + CHANGES_REQUESTED_BY_FINANCE_1, APPROVED_BY_FINANCE_1, + ], + user_choices + ), + APPROVED_BY_FINANCE_1: filter_request_choices([CHANGES_REQUESTED_BY_FINANCE_2, APPROVED_BY_FINANCE_2], + user_choices), + APPROVED_BY_FINANCE_2: filter_request_choices([CONVERTED, PAID], user_choices), + }) status_field.choices = possible_status_transitions_lut.get(instance.status, []) def clean(self): diff --git a/hypha/apply/projects/models/payment.py b/hypha/apply/projects/models/payment.py index ffbf79f448dfa0b8bfba2a9bcc9b9ae1a6954f2a..b9d4ffe614b2c477bfa351f8e3ab8b504e508530 100644 --- a/hypha/apply/projects/models/payment.py +++ b/hypha/apply/projects/models/payment.py @@ -48,8 +48,11 @@ INVOICE_TRANISTION_TO_RESUBMITTED = [ ] INVOICE_STATUS_PM_CHOICES = [CHANGES_REQUESTED_BY_STAFF, APPROVED_BY_STAFF, DECLINED] -INVOICE_STATUS_FINANCE_1_CHOICES = [CHANGES_REQUESTED_BY_FINANCE_1, APPROVED_BY_FINANCE_1] -INVOICE_STATUS_FINANCE_2_CHOICES = [CHANGES_REQUESTED_BY_FINANCE_2, APPROVED_BY_FINANCE_2, CONVERTED, PAID] +INVOICE_STATUS_FINANCE_1_CHOICES = [CHANGES_REQUESTED_BY_FINANCE_1, APPROVED_BY_FINANCE_1, CONVERTED, PAID] +INVOICE_STATUS_FINANCE_2_CHOICES = [] +if settings.INVOICE_EXTENDED_WORKFLOW: + INVOICE_STATUS_FINANCE_1_CHOICES = [CHANGES_REQUESTED_BY_FINANCE_1, APPROVED_BY_FINANCE_1] + INVOICE_STATUS_FINANCE_2_CHOICES = [CHANGES_REQUESTED_BY_FINANCE_2, APPROVED_BY_FINANCE_2, CONVERTED, PAID] def invoice_status_user_choices(user): @@ -77,10 +80,14 @@ class InvoiceQueryset(models.QuerySet): return self.filter(status=APPROVED_BY_FINANCE_1) def for_finance_1(self): - return self.filter(status__in=[APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_2]) + if settings.INVOICE_EXTENDED_WORKFLOW: + return self.filter(status__in=[APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_2]) + return self.filter(status__in=[APPROVED_BY_STAFF, APPROVED_BY_FINANCE_1, CONVERTED]) def for_finance_2(self): - return self.filter(status__in=[APPROVED_BY_FINANCE_1, APPROVED_BY_FINANCE_2, CONVERTED]) + if settings.INVOICE_EXTENDED_WORKFLOW: + return self.filter(status__in=[APPROVED_BY_FINANCE_1, APPROVED_BY_FINANCE_2, CONVERTED]) + return [] def rejected(self): return self.filter(status=DECLINED) @@ -218,8 +225,12 @@ class Invoice(models.Model): return True if user.is_finance_level_1: - if self.status in {APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_2}: - return True + if settings.INVOICE_EXTENDED_WORKFLOW: + if self.status in {APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_2}: + return True + else: + if self.status in {APPROVED_BY_STAFF, APPROVED_BY_FINANCE_1, CONVERTED}: + return True if user.is_finance_level_2: if self.status in {APPROVED_BY_FINANCE_1, APPROVED_BY_FINANCE_2, CONVERTED}: @@ -229,7 +240,9 @@ class Invoice(models.Model): def can_user_complete_required_checks(self, user): if user.is_finance_level_1: - if self.status in [APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_2]: + if self.status in [APPROVED_BY_STAFF]: + return True + elif settings.INVOICE_EXTENDED_WORKFLOW and self.status in [CHANGES_REQUESTED_BY_FINANCE_2]: return True return False @@ -245,7 +258,9 @@ class Invoice(models.Model): if self.status in {SUBMITTED, RESUBMITTED, CHANGES_REQUESTED_BY_FINANCE_1}: return True if user.is_finance_level_1: - if self.status in {APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_2}: + if self.status in {APPROVED_BY_STAFF}: + return True + elif settings.INVOICE_EXTENDED_WORKFLOW and self.status in [CHANGES_REQUESTED_BY_FINANCE_2]: return True if user.is_finance_level_2: if self.status in {APPROVED_BY_FINANCE_1}: diff --git a/hypha/apply/projects/tests/test_forms.py b/hypha/apply/projects/tests/test_forms.py index 20d2c53d1f83fcbb841fd025cd9f83c2eacb4b05..194964988be1cc0415e9d68806d19680f8bbb34e 100644 --- a/hypha/apply/projects/tests/test_forms.py +++ b/hypha/apply/projects/tests/test_forms.py @@ -35,7 +35,9 @@ from ..models.payment import ( CHANGES_REQUESTED_BY_FINANCE_1, CHANGES_REQUESTED_BY_FINANCE_2, CHANGES_REQUESTED_BY_STAFF, + CONVERTED, DECLINED, + PAID, RESUBMITTED, SUBMITTED, invoice_status_user_choices, @@ -206,7 +208,8 @@ class TestChangeInvoiceStatusFormForm(TestCase): actual = set(form.fields['status'].choices) self.assertEqual(expected, actual) - def test_finance1_choices_with_approved_by_finance1_status(self): + @override_settings(INVOICE_EXTENDED_WORKFLOW=True) + def test_finance1_choices_with_approved_by_finance1_status_with_extended_flow(self): invoice = InvoiceFactory(status=APPROVED_BY_FINANCE_1) user = FinanceFactory() form = ChangeInvoiceStatusForm(instance=invoice, user=user) @@ -216,6 +219,18 @@ class TestChangeInvoiceStatusFormForm(TestCase): actual = set(form.fields['status'].choices) self.assertEqual(expected, actual) + @override_settings(INVOICE_EXTENDED_WORKFLOW=False) + def test_finance1_choices_with_approved_by_finance1_status(self): + invoice = InvoiceFactory(status=APPROVED_BY_FINANCE_1) + user = FinanceFactory() + form = ChangeInvoiceStatusForm(instance=invoice, user=user) + + expected = set(filter_request_choices([CONVERTED, PAID], + invoice_status_user_choices(user))) + actual = set(form.fields['status'].choices) + self.assertEqual(expected, actual) + + @override_settings(INVOICE_EXTENDED_WORKFLOW=True) def test_finance2_choices_with_approved_by_finance1_status(self): invoice = InvoiceFactory(status=APPROVED_BY_FINANCE_1) user = Finance2Factory() @@ -226,6 +241,7 @@ class TestChangeInvoiceStatusFormForm(TestCase): actual = set(form.fields['status'].choices) self.assertEqual(expected, actual) + @override_settings(INVOICE_EXTENDED_WORKFLOW=True) def test_staff_choices_with_changes_requested_by_finance2_status(self): invoice = InvoiceFactory(status=CHANGES_REQUESTED_BY_FINANCE_2) user = StaffFactory() @@ -236,6 +252,7 @@ class TestChangeInvoiceStatusFormForm(TestCase): actual = set(form.fields['status'].choices) self.assertEqual(expected, actual) + @override_settings(INVOICE_EXTENDED_WORKFLOW=True) def test_finance1_choices_with_changes_requested_by_finance2_status(self): invoice = InvoiceFactory(status=CHANGES_REQUESTED_BY_FINANCE_2) user = FinanceFactory() @@ -246,6 +263,7 @@ class TestChangeInvoiceStatusFormForm(TestCase): actual = set(form.fields['status'].choices) self.assertEqual(expected, actual) + @override_settings(INVOICE_EXTENDED_WORKFLOW=True) def test_finance2_choices_with_changes_requested_by_finance2_status(self): invoice = InvoiceFactory(status=CHANGES_REQUESTED_BY_FINANCE_2) user = Finance2Factory() diff --git a/hypha/apply/projects/tests/test_models.py b/hypha/apply/projects/tests/test_models.py index 8fd8d9fe019cb9f1f88159c5f9cafb70c944e447..8bed7524a44da1b231dfb851bb37a0973a3163a6 100644 --- a/hypha/apply/projects/tests/test_models.py +++ b/hypha/apply/projects/tests/test_models.py @@ -1,7 +1,7 @@ from decimal import Decimal from dateutil.relativedelta import relativedelta -from django.test import TestCase +from django.test import TestCase, override_settings from django.utils import timezone from hypha.apply.funds.tests.factories import ApplicationSubmissionFactory @@ -219,14 +219,24 @@ class TestInvoiceModel(TestCase): invoice = InvoiceFactory(status=status) self.assertFalse(invoice.can_user_change_status(user)) - def test_finance1_can_change_status(self): + @override_settings(INVOICE_EXTENDED_WORKFLOW=True) + def test_finance1_can_change_status_with_extended_flow(self): statuses = [APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_2] user = FinanceFactory() for status in statuses: invoice = InvoiceFactory(status=status) self.assertTrue(invoice.can_user_change_status(user)) - def test_finance1_cant_change_status(self): + @override_settings(INVOICE_EXTENDED_WORKFLOW=False) + def test_finance1_can_change_status(self): + statuses = [APPROVED_BY_STAFF, APPROVED_BY_FINANCE_1, CONVERTED] + user = FinanceFactory() + for status in statuses: + invoice = InvoiceFactory(status=status) + self.assertTrue(invoice.can_user_change_status(user)) + + @override_settings(INVOICE_EXTENDED_WORKFLOW=True) + def test_finance1_cant_change_status_with_extended_flow(self): statuses = [ APPROVED_BY_FINANCE_1, APPROVED_BY_FINANCE_2, CHANGES_REQUESTED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_1, DECLINED, PAID, RESUBMITTED, SUBMITTED @@ -236,13 +246,26 @@ class TestInvoiceModel(TestCase): invoice = InvoiceFactory(status=status) self.assertFalse(invoice.can_user_change_status(user)) - def test_finance2_can_change_status(self): + @override_settings(INVOICE_EXTENDED_WORKFLOW=False) + def test_finance1_cant_change_status(self): + statuses = [ + CHANGES_REQUESTED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_1, + DECLINED, PAID, RESUBMITTED, SUBMITTED + ] + user = FinanceFactory() + for status in statuses: + invoice = InvoiceFactory(status=status) + self.assertFalse(invoice.can_user_change_status(user)) + + @override_settings(INVOICE_EXTENDED_WORKFLOW=True) + def test_finance2_can_change_status_with_extended_flow(self): statuses = [APPROVED_BY_FINANCE_1, APPROVED_BY_FINANCE_2, CONVERTED] user = Finance2Factory() for status in statuses: invoice = InvoiceFactory(status=status) self.assertTrue(invoice.can_user_change_status(user)) + @override_settings(INVOICE_EXTENDED_WORKFLOW=True) def test_finance2_cant_change_status(self): statuses = [ APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_1, CHANGES_REQUESTED_BY_FINANCE_2, @@ -301,13 +324,23 @@ class TestInvoiceModel(TestCase): invoice = InvoiceFactory(status=status) self.assertFalse(invoice.can_user_complete_required_checks(user)) - def test_finance1_can_complete_required_checks(self): + @override_settings(INVOICE_EXTENDED_WORKFLOW=True) + def test_finance1_can_complete_required_checks_with_extended_flow(self): statuses = [APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_2] user = FinanceFactory() for status in statuses: invoice = InvoiceFactory(status=status) self.assertTrue(invoice.can_user_complete_required_checks(user)) + @override_settings(INVOICE_EXTENDED_WORKFLOW=False) + def test_finance1_can_complete_required_checks(self): + statuses = [APPROVED_BY_STAFF] + user = FinanceFactory() + for status in statuses: + invoice = InvoiceFactory(status=status) + self.assertTrue(invoice.can_user_complete_required_checks(user)) + + @override_settings(INVOICE_EXTENDED_WORKFLOW=True) def test_finance2_cant_complete_required_checks(self): statuses = [APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_2] user = Finance2Factory() @@ -330,6 +363,7 @@ class TestInvoiceModel(TestCase): invoice = InvoiceFactory(status=APPROVED_BY_FINANCE_1) self.assertTrue(invoice.can_user_view_required_checks(user)) + @override_settings(INVOICE_EXTENDED_WORKFLOW=True) def test_finance2_can_view_required_checks(self): user = Finance2Factory() invoice = InvoiceFactory(status=APPROVED_BY_FINANCE_1) @@ -362,13 +396,22 @@ class TestInvoiceModel(TestCase): invoice = InvoiceFactory(status=status) self.assertFalse(invoice.can_user_edit_deliverables(user)) - def test_finance1_can_edit_deliverables(self): + @override_settings(INVOICE_EXTENDED_WORKFLOW=True) + def test_finance1_can_edit_deliverables_with_extended_flow(self): statuses = [APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_2] user = FinanceFactory() for status in statuses: invoice = InvoiceFactory(status=status) self.assertTrue(invoice.can_user_edit_deliverables(user)) + @override_settings(INVOICE_EXTENDED_WORKFLOW=False) + def test_finance1_can_edit_deliverables(self): + statuses = [APPROVED_BY_STAFF] + user = FinanceFactory() + for status in statuses: + invoice = InvoiceFactory(status=status) + self.assertTrue(invoice.can_user_edit_deliverables(user)) + def test_finance1_cant_edit_deliverables(self): statuses = [ APPROVED_BY_FINANCE_1, APPROVED_BY_FINANCE_2, CHANGES_REQUESTED_BY_FINANCE_1, CHANGES_REQUESTED_BY_STAFF, @@ -379,6 +422,7 @@ class TestInvoiceModel(TestCase): invoice = InvoiceFactory(status=status) self.assertFalse(invoice.can_user_edit_deliverables(user)) + @override_settings(INVOICE_EXTENDED_WORKFLOW=True) def test_finance2_can_edit_deliverables(self): statuses = [APPROVED_BY_FINANCE_1] user = Finance2Factory() @@ -386,6 +430,7 @@ class TestInvoiceModel(TestCase): invoice = InvoiceFactory(status=status) self.assertTrue(invoice.can_user_edit_deliverables(user)) + @override_settings(INVOICE_EXTENDED_WORKFLOW=True) def test_finance2_cant_edit_deliverables(self): statuses = [ APPROVED_BY_FINANCE_2, APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_1, @@ -398,13 +443,22 @@ class TestInvoiceModel(TestCase): class TestInvoiceQueryset(TestCase): - def test_in_progress(self): + @override_settings(INVOICE_EXTENDED_WORKFLOW=True) + def test_in_progress_with_extended_workflow(self): InvoiceFactory(status=SUBMITTED) InvoiceFactory(status=APPROVED_BY_STAFF) InvoiceFactory(status=CHANGES_REQUESTED_BY_FINANCE_2) InvoiceFactory(status=DECLINED) self.assertEqual(Invoice.objects.in_progress().count(), 3) + @override_settings(INVOICE_EXTENDED_WORKFLOW=True) + def test_in_progress(self): + InvoiceFactory(status=SUBMITTED) + InvoiceFactory(status=APPROVED_BY_STAFF) + InvoiceFactory(status=CHANGES_REQUESTED_BY_FINANCE_1) + InvoiceFactory(status=DECLINED) + self.assertEqual(Invoice.objects.in_progress().count(), 3) + def test_approved_by_staff(self): InvoiceFactory(status=APPROVED_BY_STAFF) self.assertEqual(Invoice.objects.approved_by_staff().count(), 1) @@ -413,12 +467,20 @@ class TestInvoiceQueryset(TestCase): InvoiceFactory(status=APPROVED_BY_FINANCE_1) self.assertEqual(Invoice.objects.approved_by_finance_1().count(), 1) - def test_for_finance_1(self): + @override_settings(INVOICE_EXTENDED_WORKFLOW=True) + def test_for_finance_1_with_extended_flow(self): InvoiceFactory(status=APPROVED_BY_STAFF) InvoiceFactory(status=CHANGES_REQUESTED_BY_FINANCE_2) InvoiceFactory(status=SUBMITTED) self.assertEqual(Invoice.objects.for_finance_1().count(), 2) + @override_settings(INVOICE_EXTENDED_WORKFLOW=False) + def test_for_finance_1(self): + InvoiceFactory(status=APPROVED_BY_STAFF) + InvoiceFactory(status=APPROVED_BY_FINANCE_1) + InvoiceFactory(status=SUBMITTED) + self.assertEqual(Invoice.objects.for_finance_1().count(), 2) + def test_rejected(self): InvoiceFactory(status=DECLINED) InvoiceFactory(status=SUBMITTED) diff --git a/hypha/apply/users/models.py b/hypha/apply/users/models.py index 6ba8ea727da451cdc268be22e26f99be7e2f385a..056de0fe74fd6fdd828d7e74e3a0eb562a01419d 100644 --- a/hypha/apply/users/models.py +++ b/hypha/apply/users/models.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.contrib.auth.hashers import make_password from django.contrib.auth.models import AbstractUser, BaseUserManager, Group from django.db import models @@ -191,6 +192,9 @@ class User(AbstractUser): @cached_property def is_finance_level_2(self): + # disable finance2 user if invoice flow in not extended + if not settings.INVOICE_EXTENDED_WORKFLOW: + return False return self.groups.filter(name=FINANCE_GROUP_NAME).exists() & self.groups.filter(name=APPROVER_GROUP_NAME).exists() @cached_property diff --git a/hypha/settings/base.py b/hypha/settings/base.py index af65832bb0d03a93b91b0b2700d1e0d8df63dafb..2a52dc88ea707ec88db90e7a6e6ac4fccd911da3 100644 --- a/hypha/settings/base.py +++ b/hypha/settings/base.py @@ -455,6 +455,8 @@ INTACCT_USER_ID = env.str('INTACCT_USER_ID', '') INTACCT_COMPANY_ID = env.str('INTACCT_COMPANY_ID', '') INTACCT_USER_PASSWORD = env.str('INTACCT_USER_PASSWORD', '') +# Finance extension to finance2 for Project Invoicing +INVOICE_EXTENDED_WORKFLOW = env.bool('INVOICE_EXTENDED_WORKFLOW', True) # Misc settings