Skip to content

Commit

Permalink
feat: clone application on applicant side (hl-1447) (#3324)
Browse files Browse the repository at this point in the history
* feat(backend): clone application for less manual form filling

* feat(applicant): add buttons and texts to clone application

* fix: add locale prefix to title url

* fix: typing issue in test
  • Loading branch information
sirtawast authored Sep 20, 2024
1 parent 8c8935d commit 9bd626f
Show file tree
Hide file tree
Showing 12 changed files with 374 additions and 17 deletions.
110 changes: 109 additions & 1 deletion backend/benefit/applications/api/v1/application_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
from django.utils.translation import gettext_lazy as _
from django_filters import DateFromToRangeFilter, rest_framework as filters
from django_filters.widgets import CSVWidget
from drf_spectacular.utils import extend_schema
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema, OpenApiParameter
from rest_framework import filters as drf_filters, status
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
Expand Down Expand Up @@ -60,6 +61,7 @@
ApplicationAlterationCsvService,
)
from applications.services.applications_csv_report import ApplicationsCsvService
from applications.services.clone_application import clone_application_based_on_other
from applications.services.generate_application_summary import (
generate_application_summary_file,
get_context_for_summary_context,
Expand Down Expand Up @@ -593,6 +595,112 @@ def get_queryset(self):
else:
return Application.objects.none()

@extend_schema(
parameters=[
OpenApiParameter(
name="work",
description="Should work position information be cloned",
required=False,
type=OpenApiTypes.BOOL,
location=OpenApiParameter.PATH,
),
OpenApiParameter(
name="employee",
description="Should employee information be cloned",
required=False,
type=OpenApiTypes.BOOL,
location=OpenApiParameter.PATH,
),
OpenApiParameter(
name="pay_subsidy",
description="Should pay subsidy information be cloned",
required=False,
type=OpenApiTypes.BOOL,
location=OpenApiParameter.PATH,
),
],
description="Clone any application as draft.",
)
@action(methods=["GET"], detail=True, url_path="clone_as_draft")
@transaction.atomic
def clone_as_draft(self, request, pk=None) -> HttpResponse:
application_base = self.get_object()

clone_employee = request.query_params.get("employee") or None
clone_work = request.query_params.get("work") or None
clone_subsidies = request.query_params.get("pay_subsidy") or None

cloned_application = clone_application_based_on_other(
application_base, clone_employee, clone_work, clone_subsidies
)

return Response(
{"id": cloned_application.id},
status=status.HTTP_201_CREATED,
)

@extend_schema(
parameters=[
OpenApiParameter(
name="work",
description="Should work position information be cloned",
required=False,
type=OpenApiTypes.BOOL,
location=OpenApiParameter.PATH,
),
OpenApiParameter(
name="employee",
description="Should employee information be cloned",
required=False,
type=OpenApiTypes.BOOL,
location=OpenApiParameter.PATH,
),
OpenApiParameter(
name="pay_subsidy",
description="Should pay subsidy information be cloned",
required=False,
type=OpenApiTypes.BOOL,
location=OpenApiParameter.PATH,
),
],
description="Clone any application as draft.",
)
@action(methods=["GET"], detail=False, url_path="clone_latest")
@transaction.atomic
def clone_latest(self, request, pk=None) -> HttpResponse:
company = get_company_from_request(request)

try:
application_base = Application.objects.filter(
company=company,
status__in=[
ApplicationStatus.RECEIVED,
ApplicationStatus.HANDLING,
ApplicationStatus.ADDITIONAL_INFORMATION_NEEDED,
ApplicationStatus.ACCEPTED,
ApplicationStatus.REJECTED,
],
).latest("submitted_at")

except Application.DoesNotExist:
return Response(
{"detail": _("No applications found for cloning")},
status=status.HTTP_404_NOT_FOUND,
)

clone_employee = request.query_params.get("employee") or None
clone_work = request.query_params.get("work") or None
clone_subsidies = request.query_params.get("pay_subsidy") or None

cloned_application = clone_application_based_on_other(
application_base, clone_employee, clone_work, clone_subsidies
)

return Response(
{"id": cloned_application.id},
status=status.HTTP_201_CREATED,
)


@extend_schema(
description=(
Expand Down
86 changes: 86 additions & 0 deletions backend/benefit/applications/services/clone_application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from applications.enums import ApplicationStep
from applications.models import Application, DeMinimisAid, Employee
from companies.models import Company


def clone_application_based_on_other(
application_base,
clone_employee=False,
clone_work=False,
clone_subsidies=False,
):
company = Company.objects.get(id=application_base.company.id)
cloned_application = Application(
**{
"alternative_company_city": application_base.alternative_company_city,
"alternative_company_postcode": application_base.alternative_company_postcode,
"alternative_company_street_address": application_base.alternative_company_street_address,
"applicant_language": "fi",
"application_step": ApplicationStep.STEP_1,
"archived": False,
"association_has_business_activities": application_base.association_has_business_activities,
"association_immediate_manager_check": application_base.association_immediate_manager_check,
"benefit_type": "salary_benefit",
"co_operation_negotiations": application_base.co_operation_negotiations,
"co_operation_negotiations_description": application_base.co_operation_negotiations_description,
"company_bank_account_number": application_base.company_bank_account_number,
"company_contact_person_email": application_base.company_contact_person_email,
"company_contact_person_first_name": application_base.company_contact_person_first_name,
"company_contact_person_last_name": application_base.company_contact_person_last_name,
"company_contact_person_phone_number": application_base.company_contact_person_phone_number,
"company_department": application_base.company_department,
"company_form_code": company.company_form_code,
"de_minimis_aid": application_base.de_minimis_aid,
"status": "draft",
"use_alternative_address": application_base.use_alternative_address,
}
)

cloned_application.company = company

if clone_employee or clone_work:
employee = Employee.objects.get(id=application_base.employee.id)
employee.pk = None
employee.application = cloned_application
else:
employee = Employee.objects.create(application=cloned_application)

if not clone_employee:
employee.first_name = ""
employee.last_name = ""
employee.social_security_number = ""
employee.is_living_in_helsinki = False

if not clone_work:
employee.job_title = ""
employee.monthly_pay = None
employee.vacation_money = None
employee.other_expenses = None
employee.working_hours = None
employee.collective_bargaining_agreement = ""

employee.save()

if clone_subsidies:
cloned_application.pay_subsidy_granted = application_base.pay_subsidy_granted
cloned_application.apprenticeship_program = (
application_base.apprenticeship_program
)
cloned_application.pay_subsidy_percent = application_base.pay_subsidy_percent

de_minimis_aids = DeMinimisAid.objects.filter(
application__pk=application_base.id
).all()

if de_minimis_aids.exists():
cloned_application.de_minimis_aid = True

last_order = DeMinimisAid.objects.last().ordering + 1
for index, aid in enumerate(de_minimis_aids):
aid.pk = None
aid.ordering = last_order + index
aid.application = cloned_application
aid.save()

cloned_application.save()
return cloned_application
13 changes: 11 additions & 2 deletions frontend/benefit/applicant/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"edit": "Edit",
"close": "Close",
"print": "Print the application",
"clone": "Copy as new application base",
"openTermsAsPDF": "Open the terms as a PDF file",
"continueToService": "Continue to the service",
"pauseAndExit": "Cancel and exit",
Expand Down Expand Up @@ -418,7 +419,11 @@
}
},
"errors": {
"dirtyOrInvalidForm": "Please fill any missing or invalid form fields"
"dirtyOrInvalidForm": "Please fill any missing or invalid form fields",
"cloneError": {
"title": "No previous applications found",
"message": "Please fill in a new application form"
}
},
"decision": {
"description": {
Expand Down Expand Up @@ -614,7 +619,11 @@
"description1": "Welcome to the Helsinki benefit service.",
"description2": "Before filling out the application, review the necessary information and required attachments if needed. You can save an incomplete application as a draft and return to it later.",
"linkText": " Please take a look at the required information and attachments before filling in the application.",
"newApplicationBtnText": "Submit a new application"
"newApplicationBtnText": "Submit a new application",
"cloneApplication": {
"helperText": "The application which was submitted last will be used as a template",
"buttonText": "Copy previous application"
}
},
"decisions": {
"heading": "Archive",
Expand Down
13 changes: 11 additions & 2 deletions frontend/benefit/applicant/public/locales/fi/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"edit": "Muokkaa",
"close": "Sulje",
"print": "Tulosta hakemus",
"clone": "Käytä uuden hakemuksen pohjana",
"openTermsAsPDF": "Avaa ehdot PDF-tiedostona",
"continueToService": "Jatka palveluun",
"pauseAndExit": "Keskeytä ja poistu",
Expand Down Expand Up @@ -418,7 +419,11 @@
}
},
"errors": {
"dirtyOrInvalidForm": "Täytä lomakkeen puuttuvat tai virheelliset kentät"
"dirtyOrInvalidForm": "Täytä lomakkeen puuttuvat tai virheelliset kentät",
"cloneError": {
"title": "Ei aiempia lähetettyjä hakemuksia",
"message": "Ole hyvä ja tee kokonaan uusi hakemus"
}
},
"decision": {
"description": {
Expand Down Expand Up @@ -614,7 +619,11 @@
"description1": "Tervetuloa Helsinki-lisän asiointipalveluun.",
"description2": "Ennen hakemuksen täyttämistä tutustu hakemisessa vaadittaviin tietoihin ja liitteisiin. Voit tallentaa hakemuksen keskeneräisenä luonnokseksi ja jatkaa sen täyttämistä myöhemmin.",
"linkText": " Tutustu vaadittaviin tietoihin ja tarvittaviin liitteisiin ennen hakemuksen täyttämistä.",
"newApplicationBtnText": "Tee uusi hakemus"
"newApplicationBtnText": "Tee uusi hakemus",
"cloneApplication": {
"helperText": "Pohjana käytetään viimeksi lähetettyä hakemusta",
"buttonText": "Kopioi aiempi hakemus"
}
},
"decisions": {
"heading": "Arkisto",
Expand Down
13 changes: 11 additions & 2 deletions frontend/benefit/applicant/public/locales/sv/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"edit": "Redigera",
"close": "Stäng",
"print": "Skriv ut ansökan",
"clone": "Kopiera ansökan",
"openTermsAsPDF": "Öppna villkoren som PDF-fil",
"continueToService": "Fortsätt till tjänsten",
"pauseAndExit": "Avbryt och lämna sidan",
Expand Down Expand Up @@ -418,7 +419,11 @@
}
},
"errors": {
"dirtyOrInvalidForm": "Fyll i eventuella saknade eller ogiltiga formulärfält"
"dirtyOrInvalidForm": "Fyll i eventuella saknade eller ogiltiga formulärfält",
"cloneError": {
"title": "Inga tidigare ansökningar har hittats",
"message": "Vänligen skapa en ny ansökan"
}
},
"decision": {
"description": {
Expand Down Expand Up @@ -614,7 +619,11 @@
"description1": "Välkommen till e-tjänsten för Helsingforstillägg.",
"description2": "Innan du fyller i ansökan, bekanta dig med den information och de bilagor som krävs för att ansöka. Du kan spara en halvfärdig ansökan och komplettera den senare.",
"linkText": " Se vilka uppgifter och bilagor som krävs innan du fyller i ansökan.",
"newApplicationBtnText": "Gör en ny ansökan"
"newApplicationBtnText": "Gör en ny ansökan",
"cloneApplication": {
"helperText": "Den ansökan som skickades in senast kommer att användas som en mall",
"buttonText": "Kopiera den senaste ansökan"
}
},
"decisions": {
"heading": "Arkiv",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import SummarySection from 'benefit/applicant/components/summarySection/SummarySection';
import { SUPPORTED_LANGUAGES } from 'benefit/applicant/constants';
import useCloneApplicationMutation from 'benefit/applicant/hooks/useCloneApplicationMutation';
import { DynamicFormStepComponentProps } from 'benefit/applicant/types/common';
import {
BackendEndpoint,
getBackendUrl,
} from 'benefit-shared/backend-api/backend-api';
import { ATTACHMENT_TYPES, BENEFIT_TYPES } from 'benefit-shared/constants';
import { Button, IconPen, IconPrinter } from 'hds-react';
import { Button, IconPen, IconPlus, IconPrinter } from 'hds-react';
import isEmpty from 'lodash/isEmpty';
import React from 'react';
import { useRouter } from 'next/router';
import React, { useEffect } from 'react';
import {
$Grid,
$GridCell,
Expand Down Expand Up @@ -41,6 +44,25 @@ const ApplicationFormStep5: React.FC<
isSubmit,
} = useApplicationFormStep5(data, setIsSubmittedApplication);

const {
data: clonedData,
isLoading,
mutate: cloneApplication,
} = useCloneApplicationMutation();

const handleCloneApplication = (): void => cloneApplication(data?.id);
const router = useRouter();

useEffect(() => {
if (clonedData?.id && data?.id) {
void router.push(
`${
router.locale !== SUPPORTED_LANGUAGES.FI ? router.locale : ''
}${router.asPath.replace(data?.id, clonedData.id)}`
);
}
}, [clonedData?.id, router, data?.id]);

const theme = useTheme();

return (
Expand Down Expand Up @@ -157,7 +179,7 @@ const ApplicationFormStep5: React.FC<
{t('common:utility.close')}
</Button>
</$GridCell>
<$GridCell $colSpan={11}>
<$GridCell $colSpan={3}>
<Button
iconLeft={<IconPrinter />}
theme="coat"
Expand All @@ -176,6 +198,18 @@ const ApplicationFormStep5: React.FC<
{t(`common:applications.actions.print`)}
</Button>
</$GridCell>
<$GridCell $colSpan={8} justifySelf="end">
<Button
isLoading={isLoading}
iconLeft={<IconPlus />}
theme="coat"
role="link"
variant="secondary"
onClick={() => handleCloneApplication()}
>
{t(`common:applications.actions.clone`)}
</Button>
</$GridCell>
</$Grid>
) : (
<StepperActions
Expand Down
Loading

0 comments on commit 9bd626f

Please sign in to comment.