From 4469e05cea96ce171618d6ba8acafd6688c937d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilia=20M=C3=A4kel=C3=A4?= Date: Tue, 27 Feb 2024 15:31:58 +0200 Subject: [PATCH] feat: add alteration notice API --- .../applications/api/v1/application_views.py | 41 +- .../api/v1/serializers/application.py | 15 + .../v1/serializers/application_alteration.py | 177 +++++++ backend/benefit/applications/enums.py | 6 + .../0060_applicationalteration_state.py | 18 + backend/benefit/applications/models.py | 7 + .../tests/test_applications_api.py | 476 +++++++++++++++++- backend/benefit/helsinkibenefit/urls.py | 6 + .../benefit/locale/en/LC_MESSAGES/django.po | 127 ++++- .../benefit/locale/fi/LC_MESSAGES/django.po | 144 +++++- .../benefit/locale/sv/LC_MESSAGES/django.po | 157 +++++- backend/benefit/users/utils.py | 2 +- 12 files changed, 1121 insertions(+), 55 deletions(-) create mode 100644 backend/benefit/applications/api/v1/serializers/application_alteration.py create mode 100644 backend/benefit/applications/migrations/0060_applicationalteration_state.py diff --git a/backend/benefit/applications/api/v1/application_views.py b/backend/benefit/applications/api/v1/application_views.py index 0f15b7d91e..0b073f3452 100755 --- a/backend/benefit/applications/api/v1/application_views.py +++ b/backend/benefit/applications/api/v1/application_views.py @@ -28,13 +28,16 @@ ApplicantApplicationSerializer, HandlerApplicationSerializer, ) +from applications.api.v1.serializers.application_alteration import ( + ApplicationAlterationSerializer, +) from applications.api.v1.serializers.attachment import AttachmentSerializer from applications.enums import ( ApplicationBatchStatus, ApplicationOrigin, ApplicationStatus, ) -from applications.models import Application, ApplicationBatch +from applications.models import Application, ApplicationAlteration, ApplicationBatch from applications.services.ahjo_integration import ( ExportFileInfo, generate_zip, @@ -363,6 +366,42 @@ def get_application_template(self, request, pk=None): ) +class ApplicationAlterationViewSet(AuditLoggingModelViewSet): + serializer_class = ApplicationAlterationSerializer + queryset = ApplicationAlteration.objects.all() + http_method_names = ["post", "patch", "head"] + + APPLICANT_UNEDITABLE_FIELDS = [ + "state", + "recovery_start_date", + "recovery_end_date", + "handled_at", + "recovery_amount", + ] + + class Meta: + model = ApplicationAlteration + fields = "__all__" + read_only_fields = [ + "handled_at", + "recovery_amount", + ] + + def _prune_fields(self, request): + if not request.user.is_handler(): + for field in self.APPLICANT_UNEDITABLE_FIELDS: + if field in request.data.keys(): + request.data.pop(field) + + return request + + def create(self, request, *args, **kwargs): + return super().create(self._prune_fields(request), *args, **kwargs) + + def update(self, request, *args, **kwargs): + return super().update(self._prune_fields(request), *args, **kwargs) + + @extend_schema( description=( "API for create/read/update/delete operations on Helsinki benefit applications" diff --git a/backend/benefit/applications/api/v1/serializers/application.py b/backend/benefit/applications/api/v1/serializers/application.py index 2d97567400..d241bfa0fb 100755 --- a/backend/benefit/applications/api/v1/serializers/application.py +++ b/backend/benefit/applications/api/v1/serializers/application.py @@ -12,6 +12,9 @@ from drf_spectacular.utils import extend_schema_field from rest_framework import serializers +from applications.api.v1.serializers.application_alteration import ( + ApplicationAlterationSerializer, +) from applications.api.v1.serializers.attachment import AttachmentSerializer from applications.api.v1.serializers.batch import ApplicationBatchSerializer from applications.api.v1.serializers.de_minimis import DeMinimisAidSerializer @@ -37,6 +40,7 @@ AhjoStatus, Application, ApplicationBasis, + ApplicationBatch, ApplicationLogEntry, Employee, ) @@ -126,6 +130,12 @@ class BaseApplicationSerializer(DynamicFieldsModelSerializer): ), ) + alterations = ApplicationAlterationSerializer( + source="alteration_set", + read_only=True, + many=True, + ) + class Meta: model = Application fields = [ @@ -189,6 +199,7 @@ class Meta: "ahjo_status", "changes", "archived_for_applicant", + "alterations", ] read_only_fields = [ "submitted_at", @@ -1391,6 +1402,10 @@ def to_representation(self, instance): return True return None + class Meta: + model = ApplicationBatch + fields = [] + class ApplicantApplicationSerializer(BaseApplicationSerializer): status = ApplicantApplicationStatusChoiceField( diff --git a/backend/benefit/applications/api/v1/serializers/application_alteration.py b/backend/benefit/applications/api/v1/serializers/application_alteration.py new file mode 100644 index 0000000000..34502e0fdf --- /dev/null +++ b/backend/benefit/applications/api/v1/serializers/application_alteration.py @@ -0,0 +1,177 @@ +from django.conf import settings +from django.contrib.auth import get_user_model +from django.core.exceptions import ValidationError +from django.utils.text import format_lazy +from django.utils.translation import gettext_lazy as _ +from rest_framework.exceptions import PermissionDenied + +from applications.api.v1.serializers.utils import DynamicFieldsModelSerializer +from applications.enums import ApplicationAlterationState, ApplicationAlterationType +from applications.models import ApplicationAlteration +from users.utils import get_company_from_request, get_request_user_from_context + + +class ApplicationAlterationSerializer(DynamicFieldsModelSerializer): + class Meta: + model = ApplicationAlteration + fields = "__all__" + + ALLOWED_APPLICANT_EDIT_STATES = [ + ApplicationAlterationState.RECEIVED, + ] + + ALLOWED_HANDLER_EDIT_STATES = [ + ApplicationAlterationState.RECEIVED, + ApplicationAlterationState.OPENED, + ] + + def _get_merged_object_for_validation(self, new_data): + def _get_field(field_name): + if field_name in new_data: + return new_data[field_name] + elif self.instance is not None and hasattr(self.instance, field_name): + return getattr(self.instance, field_name) + else: + return None + + return {field: _get_field(field) for field in self.fields} + + def _validate_conditional_required_fields(self, is_suspended, data): + errors = [] + + # Verify fields that are required based on if values in other fields are filled + if is_suspended and data["resume_date"] is None: + errors.append( + ValidationError( + _("Resume date is required if the benefit period wasn't terminated") + ) + ) + if data["use_alternate_einvoice_provider"] is True: + for field, field_label in [ + ("einvoice_provider_name", _("E-invoice provider name")), + ("einvoice_provider_identifier", _("E-invoice provider identifier")), + ("einvoice_address", _("E-invoice address")), + ]: + if data[field] == "" or data[field] is None: + errors.append( + ValidationError( + format_lazy( + _( + "{field} must be filled if using an alternative e-invoice address" + ), + field=field_label, + ) + ) + ) + + return errors + + def _validate_date_range_within_application_date_range( + self, application, start_date, end_date + ): + errors = [] + if start_date < application.start_date: + errors.append( + ValidationError(_("Alteration cannot start before first benefit day")) + ) + if start_date > application.end_date: + errors.append( + ValidationError(_("Alteration cannot start after last benefit day")) + ) + if end_date is not None: + if end_date > application.end_date: + errors.append( + ValidationError(_("Alteration cannot end after last benefit day")) + ) + if start_date > end_date: + errors.append( + ValidationError( + _("Alteration end date cannot be before start date") + ) + ) + + return errors + + def _validate_date_range_overlaps(self, application, start_date, end_date): + errors = [] + + for alteration in application.alteration_set.all(): + if ( + alteration.recovery_start_date is None + or alteration.recovery_end_date is None + ): + continue + + if start_date > alteration.recovery_end_date: + continue + if end_date < alteration.recovery_start_date: + continue + + errors.append( + ValidationError( + _("Another alteration already overlaps the alteration period") + ) + ) + + return errors + + def validate(self, data): + merged_data = self._get_merged_object_for_validation(data) + + # Verify that the user is allowed to make the request + user = get_request_user_from_context(self) + request = self.context.get("request") + + application = merged_data["application"] + + if settings.NEXT_PUBLIC_MOCK_FLAG: + if not (user and user.is_authenticated): + user = get_user_model().objects.all().order_by("username").first() + + if not user.is_handler(): + company = get_company_from_request(request) + print(company, application.company) + print(company.id, application.company.id) + if company != application.company: + raise PermissionDenied(_("You are not allowed to do this action")) + + # Verify that the alteration can be edited in its current state + if self.instance is not None: + current_state = self.instance.state + allowed_states = ( + self.ALLOWED_HANDLER_EDIT_STATES + if user.is_handler() + else self.ALLOWED_APPLICANT_EDIT_STATES + ) + + if current_state not in allowed_states: + raise PermissionDenied( + _("The alteration cannot be edited in this state") + ) + + # Verify that any fields that are required based on another field are filled + errors = [] + is_suspended = ( + merged_data["alteration_type"] == ApplicationAlterationType.SUSPENSION + ) + errors += self._validate_conditional_required_fields(is_suspended, merged_data) + + # Verify that the date range is coherent + alteration_start_date = merged_data["end_date"] + alteration_end_date = ( + merged_data["resume_date"] if is_suspended else application.end_date + ) + + errors += self._validate_date_range_within_application_date_range( + application, alteration_start_date, alteration_end_date + ) + + if alteration_end_date is not None: + errors += self._validate_date_range_overlaps( + application, alteration_start_date, alteration_end_date + ) + + if len(errors) > 0: + raise ValidationError(errors) + + return data diff --git a/backend/benefit/applications/enums.py b/backend/benefit/applications/enums.py index 0855afb7dd..5e9fbd0772 100644 --- a/backend/benefit/applications/enums.py +++ b/backend/benefit/applications/enums.py @@ -212,3 +212,9 @@ class DecisionType(models.TextChoices): class ApplicationAlterationType(models.TextChoices): TERMINATION = "termination", _("Termination") SUSPENSION = "suspension", _("Suspension") + + +class ApplicationAlterationState(models.TextChoices): + RECEIVED = "received", _("Received") + OPENED = "opened", _("Opened") + HANDLED = "handled", _("Handled") diff --git a/backend/benefit/applications/migrations/0060_applicationalteration_state.py b/backend/benefit/applications/migrations/0060_applicationalteration_state.py new file mode 100644 index 0000000000..d512b46c0c --- /dev/null +++ b/backend/benefit/applications/migrations/0060_applicationalteration_state.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.23 on 2024-02-26 15:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('applications', '0059_applicationalteration'), + ] + + operations = [ + migrations.AddField( + model_name='applicationalteration', + name='state', + field=models.TextField(choices=[('received', 'Received'), ('opened', 'Opened'), ('handled', 'Handled')], default='received', verbose_name='state of alteration'), + ), + ] diff --git a/backend/benefit/applications/models.py b/backend/benefit/applications/models.py index 311affa46b..300826f52c 100755 --- a/backend/benefit/applications/models.py +++ b/backend/benefit/applications/models.py @@ -13,6 +13,7 @@ from applications.enums import ( AhjoDecision, AhjoStatus, + ApplicationAlterationState, ApplicationAlterationType, ApplicationBatchStatus, ApplicationOrigin, @@ -1058,6 +1059,12 @@ class ApplicationAlteration(TimeStampedModel): verbose_name=_("type of alteration"), choices=ApplicationAlterationType.choices ) + state = models.TextField( + verbose_name=_("state of alteration"), + choices=ApplicationAlterationState.choices, + default=ApplicationAlterationState.RECEIVED, + ) + end_date = models.DateField(verbose_name=_("new benefit end date")) resume_date = models.DateField( diff --git a/backend/benefit/applications/tests/test_applications_api.py b/backend/benefit/applications/tests/test_applications_api.py index 0c258f1051..fbcd52051d 100755 --- a/backend/benefit/applications/tests/test_applications_api.py +++ b/backend/benefit/applications/tests/test_applications_api.py @@ -27,6 +27,8 @@ ) from applications.enums import ( AhjoStatus, + ApplicationAlterationState, + ApplicationAlterationType, ApplicationBatchStatus, ApplicationStatus, ApplicationStep, @@ -35,7 +37,13 @@ OrganizationType, PaySubsidyGranted, ) -from applications.models import Application, ApplicationLogEntry, Attachment, Employee +from applications.models import ( + Application, + ApplicationAlteration, + ApplicationLogEntry, + Attachment, + Employee, +) from applications.tests.conftest import * # noqa from applications.tests.factories import ( ApplicationBatchFactory, @@ -2328,6 +2336,459 @@ def test_application_pdf_print_denied(api_client, anonymous_client): assert response.status_code == 403 +def test_application_alteration_create_terminated(api_client, application): + pk = application.id + + response = api_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "termination", + "reason": "Päättynyt", + "end_date": application.start_date + relativedelta(days=7), + "use_alternate_einvoice_provider": False, + }, + ) + assert response.status_code == 201 + + +def test_application_alteration_create_suspended(api_client, application): + pk = application.id + + response = api_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "suspension", + "reason": "Keskeytynyt", + "end_date": application.start_date + relativedelta(days=7), + "resume_date": application.start_date + relativedelta(days=14), + "use_alternate_einvoice_provider": False, + }, + ) + assert response.status_code == 201 + + +def test_application_alteration_create_missing_resume_date(api_client, application): + pk = application.id + + response = api_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "suspension", + "reason": "Keskeytynyt", + "end_date": application.start_date + relativedelta(days=7), + "use_alternate_einvoice_provider": False, + }, + ) + assert response.status_code == 400 + assert "non_field_errors" in response.data + assert len(response.data["non_field_errors"]) == 1 + + +def test_application_alteration_create_missing_einvoice_fields(api_client, application): + pk = application.id + + response = api_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "termination", + "reason": "Päättynyt", + "end_date": application.start_date + relativedelta(days=7), + "use_alternate_einvoice_provider": True, + }, + ) + assert response.status_code == 400 + assert "non_field_errors" in response.data + assert len(response.data["non_field_errors"]) == 3 + + +def test_application_alteration_create_outside_application_date_range( + api_client, application +): + pk = application.id + + response = api_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "termination", + "reason": "Päättynyt", + "end_date": application.start_date + relativedelta(days=-7), + "use_alternate_einvoice_provider": False, + }, + ) + assert response.status_code == 400 + assert "non_field_errors" in response.data + + response = api_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "termination", + "reason": "Päättynyt", + "end_date": application.end_date + relativedelta(days=7), + "use_alternate_einvoice_provider": False, + }, + ) + assert response.status_code == 400 + assert "non_field_errors" in response.data + + response = api_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "suspension", + "reason": "Päättynyt", + "end_date": application.start_date + relativedelta(days=7), + "resume_date": application.end_date + relativedelta(days=7), + "use_alternate_einvoice_provider": False, + }, + ) + assert response.status_code == 400 + assert "non_field_errors" in response.data + + +def test_application_alteration_create_reversed_suspension_dates( + api_client, application +): + pk = application.id + + response = api_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "suspension", + "reason": "Keskeytynyt", + "end_date": application.start_date + relativedelta(days=14), + "resume_date": application.start_date + relativedelta(days=7), + "use_alternate_einvoice_provider": False, + }, + ) + assert response.status_code == 400 + assert "non_field_errors" in response.data + assert len(response.data["non_field_errors"]) == 1 + + +def test_application_alteration_create_overlapping_alteration(api_client, application): + pk = application.id + + response = api_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "suspension", + "reason": "Keskeytynyt", + "end_date": application.start_date + relativedelta(days=7), + "resume_date": application.start_date + relativedelta(days=14), + "use_alternate_einvoice_provider": False, + }, + ) + assert response.status_code == 201 + + alteration_pk = response.data["id"] + alteration = ApplicationAlteration.objects.get(pk=alteration_pk) + alteration.recovery_start_date = application.start_date + relativedelta(days=7) + alteration.recovery_end_date = application.start_date + relativedelta(days=14) + alteration.recovery_amount = 600 + alteration.save() + + response = api_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "suspension", + "reason": "Keskeytynyt", + "end_date": application.start_date + relativedelta(days=9), + "resume_date": application.start_date + relativedelta(days=16), + "use_alternate_einvoice_provider": False, + }, + ) + assert response.status_code == 400 + assert "non_field_errors" in response.data + assert len(response.data["non_field_errors"]) == 1 + + response = api_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "suspension", + "reason": "Keskeytynyt", + "end_date": application.start_date + relativedelta(days=5), + "resume_date": application.start_date + relativedelta(days=12), + "use_alternate_einvoice_provider": False, + }, + ) + assert response.status_code == 400 + assert "non_field_errors" in response.data + assert len(response.data["non_field_errors"]) == 1 + + response = api_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "suspension", + "reason": "Keskeytynyt", + "end_date": application.start_date + relativedelta(days=5), + "resume_date": application.start_date + relativedelta(days=16), + "use_alternate_einvoice_provider": False, + }, + ) + assert response.status_code == 400 + assert "non_field_errors" in response.data + assert len(response.data["non_field_errors"]) == 1 + + +def test_application_alteration_create_non_overlapping_alteration( + api_client, application +): + pk = application.id + + response = api_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "suspension", + "reason": "Keskeytynyt", + "end_date": application.start_date + relativedelta(days=7), + "resume_date": application.start_date + relativedelta(days=14), + "use_alternate_einvoice_provider": False, + }, + ) + assert response.status_code == 201 + + alteration_pk = response.data["id"] + alteration = ApplicationAlteration.objects.get(pk=alteration_pk) + alteration.recovery_start_date = application.start_date + relativedelta(days=7) + alteration.recovery_end_date = application.start_date + relativedelta(days=14) + alteration.recovery_amount = 600 + alteration.save() + + response = api_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "suspension", + "reason": "Keskeytynyt", + "end_date": application.start_date + relativedelta(days=16), + "resume_date": application.start_date + relativedelta(days=23), + "use_alternate_einvoice_provider": False, + }, + ) + assert response.status_code == 201 + + +def test_application_alteration_create_forbidden_anonymous( + anonymous_client, application +): + pk = application.id + + response = anonymous_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "termination", + "reason": "Päättynyt", + "end_date": application.start_date + relativedelta(days=7), + "use_alternate_einvoice_provider": False, + }, + ) + assert response.status_code == 403 + + +def test_application_alteration_create_forbidden_another_company( + api_client, application +): + another_company = CompanyFactory() + application.company = another_company + application.save() + + pk = application.id + + response = api_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "termination", + "reason": "Päättynyt", + "end_date": application.start_date + relativedelta(days=7), + "use_alternate_einvoice_provider": False, + }, + ) + assert response.status_code == 403 + + +def test_application_alteration_create_ignored_fields_applicant( + api_client, application +): + pk = application.id + response = api_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "termination", + "state": "handled", + "reason": "Päättynyt", + "end_date": application.start_date + relativedelta(days=7), + "use_alternate_einvoice_provider": False, + "handled_at": application.start_date + relativedelta(days=10), + "recovery_start_date": application.start_date + relativedelta(days=7), + "recovery_end_date": application.end_date, + "recovery_amount": 4000, + }, + ) + + assert response.status_code == 201 + assert response.data["state"] == "received" + assert response.data["recovery_start_date"] is None + assert response.data["recovery_end_date"] is None + assert response.data["recovery_amount"] is None + assert response.data["handled_at"] is None + + +def test_application_alteration_create_ignored_fields_handler( + handler_api_client, application +): + pk = application.id + + response = handler_api_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "termination", + "state": "handled", + "reason": "Päättynyt", + "end_date": application.start_date + relativedelta(days=7), + "use_alternate_einvoice_provider": False, + "handled_at": application.start_date + relativedelta(days=10), + "recovery_start_date": application.start_date + relativedelta(days=7), + "recovery_end_date": application.end_date, + "recovery_amount": 4000, + }, + ) + + assert response.status_code == 201 + assert response.data["state"] == "handled" + assert ( + response.data["recovery_start_date"] + == (application.start_date + relativedelta(days=7)).isoformat() + ) + assert response.data["recovery_end_date"] == application.end_date.isoformat() + assert response.data["recovery_amount"] == "4000.00" + assert ( + response.data["handled_at"] + == (application.start_date + relativedelta(days=10)).isoformat() + ) + + +def test_application_alteration_patch_applicant( + api_client, application, mock_get_organisation_roles_and_create_company +): + pk = application.id + response = api_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "termination", + "reason": "Päättynyt", + "end_date": application.start_date + relativedelta(days=7), + "use_alternate_einvoice_provider": False, + }, + ) + + response = api_client.patch( + reverse("v1:application-alteration-detail", kwargs={"pk": response.data["id"]}), + { + "end_date": application.start_date + relativedelta(days=12), + "recovery_amount": 4000, + }, + ) + assert response.status_code == 200 + assert ( + response.data["end_date"] + == (application.start_date + relativedelta(days=12)).isoformat() + ) + assert response.data["recovery_amount"] is None + + +def test_application_alteration_patch_handler(handler_api_client, application): + pk = application.id + response = handler_api_client.post( + reverse("v1:application-alteration-list"), + { + "application": pk, + "alteration_type": "termination", + "reason": "Päättynyt", + "end_date": application.start_date + relativedelta(days=7), + "use_alternate_einvoice_provider": False, + }, + ) + + response = handler_api_client.patch( + reverse("v1:application-alteration-detail", kwargs={"pk": response.data["id"]}), + { + "end_date": application.start_date + relativedelta(days=12), + "recovery_amount": 4000, + }, + ) + assert response.status_code == 200 + assert ( + response.data["end_date"] + == (application.start_date + relativedelta(days=12)).isoformat() + ) + assert response.data["recovery_amount"] == "4000.00" + + +@pytest.mark.parametrize( + "initial_state,result", + [ + (ApplicationAlterationState.RECEIVED, 200), + (ApplicationAlterationState.OPENED, 200), + (ApplicationAlterationState.HANDLED, 403), + ], +) +def test_application_alteration_patch_allowed_edit_states_handler( + handler_api_client, application, initial_state, result +): + alteration = _create_application_alteration(application) + alteration.state = initial_state + alteration.save() + + response = handler_api_client.patch( + reverse("v1:application-alteration-detail", kwargs={"pk": alteration.pk}), + { + "end_date": application.start_date + relativedelta(days=12), + }, + ) + assert response.status_code == result + + +@pytest.mark.parametrize( + "initial_state,result", + [ + (ApplicationAlterationState.RECEIVED, 200), + (ApplicationAlterationState.OPENED, 403), + (ApplicationAlterationState.HANDLED, 403), + ], +) +def test_application_alteration_patch_allowed_edit_states_applicant( + api_client, application, initial_state, result +): + alteration = _create_application_alteration(application) + alteration.state = initial_state + alteration.save() + + response = api_client.patch( + reverse("v1:application-alteration-detail", kwargs={"pk": alteration.pk}), + { + "end_date": application.start_date + relativedelta(days=12), + }, + ) + assert response.status_code == result + + def _create_random_applications(): f = faker.Faker() combos = [ @@ -2345,3 +2806,16 @@ def _create_random_applications(): Calculation.objects.filter(application__id=application.pk).update( modified_at=random_datetime ) + + +def _create_application_alteration(application): + f = faker.Faker() + + alteration = ApplicationAlteration() + alteration.application = application + alteration.alteration_type = ApplicationAlterationType.TERMINATION + alteration.end_date = application.start_date + relativedelta(days=7) + alteration.reason = f.sentence() + alteration.save() + + return alteration diff --git a/backend/benefit/helsinkibenefit/urls.py b/backend/benefit/helsinkibenefit/urls.py index d34d9bf7f9..662d3dd770 100644 --- a/backend/benefit/helsinkibenefit/urls.py +++ b/backend/benefit/helsinkibenefit/urls.py @@ -65,6 +65,12 @@ router.register(r"applicationbatches", application_batch_views.ApplicationBatchViewSet) router.register(r"previousbenefits", calculator_views.PreviousBenefitViewSet) +router.register( + r"applicationalterations", + application_views.ApplicationAlterationViewSet, + basename="application-alteration", +) + urlpatterns = [ path("admin/", admin.site.urls), path( diff --git a/backend/benefit/locale/en/LC_MESSAGES/django.po b/backend/benefit/locale/en/LC_MESSAGES/django.po index fdce927071..281e7ce766 100644 --- a/backend/benefit/locale/en/LC_MESSAGES/django.po +++ b/backend/benefit/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-02-19 10:35+0200\n" +"POT-Creation-Date: 2024-02-27 15:46+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -35,6 +35,19 @@ msgstr "" msgid "TALPA export {date}" msgstr "" +msgid "Displayed in the archive in the applicant view" +msgstr "" + +msgid "Operation not allowed for this application status." +msgstr "" + +msgid "File not found." +msgstr "" + +#, python-brace-format +msgid "Helsinki-lisan hakemukset viety {date}" +msgstr "" + msgid "This should be unreachable" msgstr "" @@ -136,6 +149,43 @@ msgstr "" msgid "Reading {localized_model_name} data failed: {errors}" msgstr "" +msgid "Resume date is required if the benefit period wasn't terminated" +msgstr "" + +msgid "E-invoice provider name" +msgstr "" + +msgid "E-invoice provider identifier" +msgstr "" + +msgid "E-invoice address" +msgstr "" + +#, python-brace-format +msgid "{field} must be filled if using an alternative e-invoice address" +msgstr "" + +msgid "Alteration cannot start before first benefit day" +msgstr "" + +msgid "Alteration cannot start after last benefit day" +msgstr "" + +msgid "Alteration cannot end after last benefit day" +msgstr "" + +msgid "Alteration end date cannot be before start date" +msgstr "" + +msgid "Another alteration already overlaps the alteration period" +msgstr "" + +msgid "You are not allowed to do this action" +msgstr "" + +msgid "The alteration cannot be edited in this state" +msgstr "" + #, python-brace-format msgid "Upload file size cannot be greater than {size} bytes" msgstr "" @@ -185,19 +235,6 @@ msgstr "" msgid "Initial status must be {initial_status}" msgstr "" -msgid "Displayed in the archive in the applicant view" -msgstr "" - -msgid "Operation not allowed for this application status." -msgstr "" - -msgid "File not found." -msgstr "" - -#, python-brace-format -msgid "Helsinki-lisan hakemukset viety {date}" -msgstr "" - msgid "Benefit can not be granted before 24-month waiting period expires" msgstr "" @@ -293,6 +330,12 @@ msgstr "" msgid "pdf summary" msgstr "" +msgid "public decision text xml attachment" +msgstr "" + +msgid "non-public decision text xml attachment" +msgstr "" + msgid "attachment is required" msgstr "" @@ -392,12 +435,35 @@ msgstr "" msgid "Update application in Ahjo" msgstr "" +msgid "Send decision to Ahjo" +msgstr "" + +msgid "" +"Template part for the decision section of a application decision proposal" +msgstr "" + +msgid "" +"Template part for the decision justification section of a decision proposal" +msgstr "" + +msgid "An accepted decision" +msgstr "" + +msgid "A denied decision" +msgstr "" + msgid "Termination" msgstr "" msgid "Suspension" msgstr "" +msgid "Opened" +msgstr "" + +msgid "Handled" +msgstr "" + #, python-brace-format msgid "" "Your application {id} will be deleted soon. If you want to continue the " @@ -641,12 +707,42 @@ msgstr "" msgid "ahjo statuses" msgstr "" +msgid "type of the decision proposal template section" +msgstr "" + +msgid "type of the decision" +msgstr "" + +msgid "decision proposal section text content" +msgstr "" + +msgid "name of the decision proposal template section" +msgstr "" + +msgid "decision proposal template section" +msgstr "" + +msgid "decision proposal template sections" +msgstr "" + +msgid "decision text content" +msgstr "" + +msgid "ahjo decision text" +msgstr "" + +msgid "ahjo decision texts" +msgstr "" + msgid "alteration of application" msgstr "" msgid "type of alteration" msgstr "" +msgid "state of alteration" +msgstr "" + msgid "new benefit end date" msgstr "" @@ -1066,9 +1162,6 @@ msgstr "" msgid "Application not found" msgstr "" -msgid "You are not allowed to do this action" -msgstr "" - msgid "Cannot do this action because application is not in the correct status" msgstr "" diff --git a/backend/benefit/locale/fi/LC_MESSAGES/django.po b/backend/benefit/locale/fi/LC_MESSAGES/django.po index 1335cba64b..c79674d754 100644 --- a/backend/benefit/locale/fi/LC_MESSAGES/django.po +++ b/backend/benefit/locale/fi/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-02-19 10:35+0200\n" +"POT-Creation-Date: 2024-02-27 15:46+0200\n" "PO-Revision-Date: 2022-11-01 10:45+0200\n" "Last-Translator: Kari Salminen \n" "Language-Team: \n" @@ -35,6 +35,19 @@ msgstr "Ei vietävää hakemusta, yritä myöhemmin uudelleen" msgid "TALPA export {date}" msgstr "TALPA-vienti {date}" +msgid "Displayed in the archive in the applicant view" +msgstr "" + +msgid "Operation not allowed for this application status." +msgstr "Toiminto ei ole sallittu tässä hakemuksen tilassa." + +msgid "File not found." +msgstr "Tiedostoa ei löydy." + +#, python-brace-format +msgid "Helsinki-lisan hakemukset viety {date}" +msgstr "" + msgid "This should be unreachable" msgstr "Tämän pitäisi olla tavoittamattomissa" @@ -143,6 +156,43 @@ msgstr "Laskelman tunnus ei vastaa nykyistä tunnusta" msgid "Reading {localized_model_name} data failed: {errors}" msgstr "{localized_model_name} -tietojen lukeminen epäonnistui: {errors}" +msgid "Resume date is required if the benefit period wasn't terminated" +msgstr "Muutosjakson päättymispäivämäärä vaaditaan, mikäli tukijakso keskeytyi" + +msgid "E-invoice provider name" +msgstr "Verkkolaskuoperaattorin nimi" + +msgid "E-invoice provider identifier" +msgstr "Verkkolaskuoperaattorin tunnus" + +msgid "E-invoice address" +msgstr "Verkkolaskuosoite" + +#, python-brace-format +msgid "{field} must be filled if using an alternative e-invoice address" +msgstr "{field} on pakollinen tieto, mikäli käytössä on poikkeava verkkolaskutunnus" + +msgid "Alteration cannot start before first benefit day" +msgstr "Muutos työsuhteessa ei voi alkaa ennen tukijakson alkua" + +msgid "Alteration cannot start after last benefit day" +msgstr "Muutos työsuhteessa ei voi alkaa tukijakson päättymisen jälkeen" + +msgid "Alteration cannot end after last benefit day" +msgstr "Muutosjakson päättymispäivämäärä ei voi olla tukijakson päättymisen jälkeen" + +msgid "Alteration end date cannot be before start date" +msgstr "Muutosjakson päättymispäivä ei voi olla aiemmin kuin aloituspäivä" + +msgid "Another alteration already overlaps the alteration period" +msgstr "Valitulla aikavälillä on jo toinen muutos työsuhteessa" + +msgid "You are not allowed to do this action" +msgstr "Sinä et saa tehdä tätä toimenpidettä" + +msgid "The alteration cannot be edited in this state" +msgstr "Muutosta työsuhteessa ei voida muuttaa tässä tilassa" + #, python-brace-format msgid "Upload file size cannot be greater than {size} bytes" msgstr "Ladattavan tiedoston koko ei voi ylittää {size} tavua" @@ -193,19 +243,6 @@ msgstr "Tilan siirto ei ole sallittu: {status} arvoon {value}" msgid "Initial status must be {initial_status}" msgstr "Alkutilan on oltava {initial_status}" -msgid "Displayed in the archive in the applicant view" -msgstr "" - -msgid "Operation not allowed for this application status." -msgstr "Toiminto ei ole sallittu tässä hakemuksen tilassa." - -msgid "File not found." -msgstr "Tiedostoa ei löydy." - -#, python-brace-format -msgid "Helsinki-lisan hakemukset viety {date}" -msgstr "" - msgid "Benefit can not be granted before 24-month waiting period expires" msgstr "Avustusta ei voida myöntää ennen 24 kuukauden odotusajan päättymistä" @@ -308,6 +345,12 @@ msgstr "liite" msgid "pdf summary" msgstr "Vaihe 4 – yhteenveto" +msgid "public decision text xml attachment" +msgstr "" + +msgid "non-public decision text xml attachment" +msgstr "" + msgid "attachment is required" msgstr "liite on pakollinen" @@ -433,12 +476,37 @@ msgstr "Puutteellinen hakemus" msgid "Update application in Ahjo" msgstr "Puutteellinen hakemus" +#, fuzzy +#| msgid "date of the decision in Ahjo" +msgid "Send decision to Ahjo" +msgstr "päätöksen päivämäärä Ahjossa" + +msgid "" +"Template part for the decision section of a application decision proposal" +msgstr "" + +msgid "" +"Template part for the decision justification section of a decision proposal" +msgstr "" + +msgid "An accepted decision" +msgstr "Hyväksytty päätös" + +msgid "A denied decision" +msgstr "Hylätty päätös" + msgid "Termination" msgstr "Päättynyt" msgid "Suspension" msgstr "Keskeytynyt" +msgid "Opened" +msgstr "Avattu" + +msgid "Handled" +msgstr "Käsitelty" + #, python-brace-format msgid "" "Your application {id} will be deleted soon. If you want to continue the " @@ -702,12 +770,54 @@ msgstr "tila" msgid "ahjo statuses" msgstr "tila" +msgid "type of the decision proposal template section" +msgstr "" + +#, fuzzy +#| msgid "date of the decision in Ahjo" +msgid "type of the decision" +msgstr "päätöksen päivämäärä Ahjossa" + +#, fuzzy +#| msgid "Decision date" +msgid "decision proposal section text content" +msgstr "Päätöksen päivämäärä" + +msgid "name of the decision proposal template section" +msgstr "" + +#, fuzzy +#| msgid "Decision date" +msgid "decision proposal template section" +msgstr "Päätöksen päivämäärä" + +#, fuzzy +#| msgid "Decision date" +msgid "decision proposal template sections" +msgstr "Päätöksen päivämäärä" + +#, fuzzy +#| msgid "application attachment content" +msgid "decision text content" +msgstr "hakemuksen liitteen sisältö" + +msgid "ahjo decision text" +msgstr "" + +#, fuzzy +#| msgid "status" +msgid "ahjo decision texts" +msgstr "tila" + msgid "alteration of application" msgstr "Muutos hakemukseen" msgid "type of alteration" msgstr "Muutoksen tyyppi" +msgid "state of alteration" +msgstr "Muutoksen tila" + msgid "new benefit end date" msgstr "Uusi avustuksen päättymispäivä" @@ -732,7 +842,8 @@ msgstr "Takaisinperittävä summa" msgid "" "whether to use a separate e-invoice address from the one of the applicant " "organization" -msgstr "Käytetäänkö eri verkkolaskuosoitetta kuin hakijaorganisaation " +msgstr "" +"Käytetäänkö eri verkkolaskuosoitetta kuin hakijaorganisaation " "verkkolaskuosoitetta?" msgid "name of the e-invoice provider" @@ -1146,9 +1257,6 @@ msgstr "Sinulla ei ole oikeutta muuttaa tätä viestiä" msgid "Application not found" msgstr "Hakemusta ei löytynyt" -msgid "You are not allowed to do this action" -msgstr "Sinä et saa tehdä tätä toimenpidettä" - msgid "Cannot do this action because application is not in the correct status" msgstr "" "Tätä toimenpidettä ei voida tehdä, koska hakemus ei ole käsittelytilassa" diff --git a/backend/benefit/locale/sv/LC_MESSAGES/django.po b/backend/benefit/locale/sv/LC_MESSAGES/django.po index 0e59a83e9a..7a469c1c46 100644 --- a/backend/benefit/locale/sv/LC_MESSAGES/django.po +++ b/backend/benefit/locale/sv/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-02-19 10:35+0200\n" +"POT-Creation-Date: 2024-02-27 15:46+0200\n" "PO-Revision-Date: 2022-11-01 10:47+0200\n" "Last-Translator: Kari Salminen \n" "Language-Team: \n" @@ -36,6 +36,19 @@ msgstr "" msgid "TALPA export {date}" msgstr "TALPA-export {date}" +msgid "Displayed in the archive in the applicant view" +msgstr "" + +msgid "Operation not allowed for this application status." +msgstr "Åtgärd tillåts inte för denna ansökningsstatus" + +msgid "File not found." +msgstr "Fil hittades inte." + +#, python-brace-format +msgid "Helsinki-lisan hakemukset viety {date}" +msgstr "" + msgid "This should be unreachable" msgstr "Detta bör vara onåbart" @@ -139,6 +152,51 @@ msgstr "Beräkningens ID motsvarar inte befintlig ID." msgid "Reading {localized_model_name} data failed: {errors}" msgstr "Data {localized_model_name} kan inte läsas: {errors}" +msgid "Resume date is required if the benefit period wasn't terminated" +msgstr "" + +#, fuzzy +#| msgid "Name of the employer" +msgid "E-invoice provider name" +msgstr "Arbetsgivarens namn" + +msgid "E-invoice provider identifier" +msgstr "" + +#, fuzzy +#| msgid "Delivery address" +msgid "E-invoice address" +msgstr "Leveransadress" + +#, python-brace-format +msgid "{field} must be filled if using an alternative e-invoice address" +msgstr "" + +msgid "Alteration cannot start before first benefit day" +msgstr "" + +msgid "Alteration cannot start after last benefit day" +msgstr "" + +msgid "Alteration cannot end after last benefit day" +msgstr "" + +#, fuzzy +#| msgid "application end_date can not be less than start_date" +msgid "Alteration end date cannot be before start date" +msgstr "end_date för ansökan kan inte vara tidigare än start_date" + +msgid "Another alteration already overlaps the alteration period" +msgstr "" + +msgid "You are not allowed to do this action" +msgstr "Du får inte utföra denna åtgärd" + +#, fuzzy +#| msgid "Application can not be changed in this status" +msgid "The alteration cannot be edited in this state" +msgstr "Ansökan kan inte ändras i denna status" + #, python-brace-format msgid "Upload file size cannot be greater than {size} bytes" msgstr "Den fil som laddas upp kan inte vara större än {size} byte" @@ -188,19 +246,6 @@ msgstr "Tillståndsövergång tillåts inte: {status} till {value}" msgid "Initial status must be {initial_status}" msgstr "Initialstatus måste vara {initial_status}" -msgid "Displayed in the archive in the applicant view" -msgstr "" - -msgid "Operation not allowed for this application status." -msgstr "Åtgärd tillåts inte för denna ansökningsstatus" - -msgid "File not found." -msgstr "Fil hittades inte." - -#, python-brace-format -msgid "Helsinki-lisan hakemukset viety {date}" -msgstr "" - msgid "Benefit can not be granted before 24-month waiting period expires" msgstr "Understöd kan inte beviljas före utgång av väntetid på 24 månader" @@ -304,6 +349,12 @@ msgstr "bilaga" msgid "pdf summary" msgstr "Steg 4 – sammanfattning" +msgid "public decision text xml attachment" +msgstr "" + +msgid "non-public decision text xml attachment" +msgstr "" + msgid "attachment is required" msgstr "bilaga är obligatorisk" @@ -429,12 +480,43 @@ msgstr "Ofullständig ansökan" msgid "Update application in Ahjo" msgstr "Ofullständig ansökan" +#, fuzzy +#| msgid "date of the decision in Ahjo" +msgid "Send decision to Ahjo" +msgstr "datum då beslut fattas vid Ahjo" + +msgid "" +"Template part for the decision section of a application decision proposal" +msgstr "" + +msgid "" +"Template part for the decision justification section of a decision proposal" +msgstr "" + +#, fuzzy +#| msgid "Accepted in Ahjo" +msgid "An accepted decision" +msgstr "Godkänd i Ahjo" + +#, fuzzy +#| msgid "pay subsidy decision" +msgid "A denied decision" +msgstr "beslut om lönesubvention" + msgid "Termination" msgstr "" msgid "Suspension" msgstr "" +msgid "Opened" +msgstr "" + +#, fuzzy +#| msgid "Handler" +msgid "Handled" +msgstr "Handläggare" + #, python-brace-format msgid "" "Your application {id} will be deleted soon. If you want to continue the " @@ -698,6 +780,45 @@ msgstr "status" msgid "ahjo statuses" msgstr "status" +msgid "type of the decision proposal template section" +msgstr "" + +#, fuzzy +#| msgid "date of the decision in Ahjo" +msgid "type of the decision" +msgstr "datum då beslut fattas vid Ahjo" + +#, fuzzy +#| msgid "Decision date" +msgid "decision proposal section text content" +msgstr "Beslutsdatum" + +msgid "name of the decision proposal template section" +msgstr "" + +#, fuzzy +#| msgid "Decision date" +msgid "decision proposal template section" +msgstr "Beslutsdatum" + +#, fuzzy +#| msgid "Decision date" +msgid "decision proposal template sections" +msgstr "Beslutsdatum" + +#, fuzzy +#| msgid "application attachment content" +msgid "decision text content" +msgstr "innehåll i bilaga till ansökan" + +msgid "ahjo decision text" +msgstr "" + +#, fuzzy +#| msgid "status" +msgid "ahjo decision texts" +msgstr "status" + #, fuzzy #| msgid "application batches" msgid "alteration of application" @@ -708,6 +829,11 @@ msgstr "ansökningssatser" msgid "type of alteration" msgstr "typ av villkor" +#, fuzzy +#| msgid "type of terms" +msgid "state of alteration" +msgstr "typ av villkor" + #, fuzzy #| msgid "benefit end date" msgid "new benefit end date" @@ -1148,9 +1274,6 @@ msgstr "Du får inte ändra detta meddelande" msgid "Application not found" msgstr "Ansökan hittades inte" -msgid "You are not allowed to do this action" -msgstr "Du får inte utföra denna åtgärd" - msgid "Cannot do this action because application is not in the correct status" msgstr "Kan inte utföra åtgärden eftersom ansökan inte har handläggningsstatus" diff --git a/backend/benefit/users/utils.py b/backend/benefit/users/utils.py index e80bedb502..76692f58b1 100644 --- a/backend/benefit/users/utils.py +++ b/backend/benefit/users/utils.py @@ -30,7 +30,7 @@ def get_company_from_request(request): ) # unique constraint ensures at most one is returned except Company.DoesNotExist: # In case we cannot find the Company in DB, try to query it from 3rd party source - # This should cover the case when first applicant of company log in because his company + # This should cover the case when first applicant of company log in because their company # hasn't been created yet return get_or_create_organisation_with_business_id(business_id) else: