diff --git a/common/forms.py b/common/forms.py index fd3d7d6b8..f3c5a04c0 100644 --- a/common/forms.py +++ b/common/forms.py @@ -338,6 +338,32 @@ def compress(self, data_list): return None +class DateInputFieldTakesParameters(DateInputField): + def __init__(self, day, month, year, **kwargs): + error_messages = { + "required": "Enter the day, month and year", + "incomplete": "Enter the day, month and year", + } + fields = (day, month, year) + + forms.MultiValueField.__init__( + self, + error_messages=error_messages, + fields=fields, + **kwargs, + ) + + def compress(self, data_list): + day, month, year = data_list or [None, None, None] + if day and month and year: + try: + return date(day=int(day), month=int(month), year=int(year)) + except ValueError as e: + raise ValidationError(str(e).capitalize()) from e + else: + return None + + class GovukDateRangeField(DateRangeField): base_field = DateInputFieldFixed diff --git a/common/static/common/js/addNewQuotaDefinitionForm.js b/common/static/common/js/addNewQuotaDefinitionForm.js new file mode 100644 index 000000000..4e2d1f39d --- /dev/null +++ b/common/static/common/js/addNewQuotaDefinitionForm.js @@ -0,0 +1,37 @@ +const addNewForm = (event) => { + event.preventDefault(); + + let numForms = document.querySelectorAll(".quota-definition-row").length; + let fieldset = document.querySelector(".quota-definition-row"); + let formset = fieldset.parentNode; + let newForm = fieldset.cloneNode(true); + + newForm.innerHTML = newForm.innerHTML.replaceAll('name="volume_0"', 'name="volume_' + numForms + '"'); + newForm.innerHTML = newForm.innerHTML.replaceAll('name="start_date_0_0"', 'name="start_date_' + numForms + '_0"'); + newForm.innerHTML = newForm.innerHTML.replaceAll('name="start_date_0_1"', 'name="start_date_' + numForms + '_1"'); + newForm.innerHTML = newForm.innerHTML.replaceAll('name="start_date_0_2"', 'name="start_date_' + numForms + '_2"'); + newForm.innerHTML = newForm.innerHTML.replaceAll('name="end_date_0_0"', 'name="end_date_' + numForms + '_0"'); + newForm.innerHTML = newForm.innerHTML.replaceAll('name="end_date_0_1"', 'name="end_date_' + numForms + '_1"'); + newForm.innerHTML = newForm.innerHTML.replaceAll('name="end_date_0_2"', 'name="end_date_' + numForms + '_2"'); + + let formFields = newForm.querySelectorAll("input"); + for (let field of formFields.values()) { + field.value = ""; + } + + let submitButton = document.getElementById("submit-id-submit"); + formset.insertBefore(newForm, submitButton); + + let addNewButton = document.querySelector("#add-new-definition"); + addNewButton.scrollIntoView(false); + } + +const initAddNewDefinition = () => { + const btn = document.querySelector("#add-new-definition"); + + if (btn) { + btn.addEventListener("click", addNewForm); + } + } + +export { initAddNewDefinition } diff --git a/common/static/common/js/application.js b/common/static/common/js/application.js index 82ef1e73f..2c9c9732e 100644 --- a/common/static/common/js/application.js +++ b/common/static/common/js/application.js @@ -5,6 +5,7 @@ require.context('govuk-frontend/govuk/assets'); import showHideCheckboxes from './showHideCheckboxes'; import { initAutocomplete } from './autocomplete'; import { initAutocompleteProgressiveEnhancement } from './autocompleteProgressiveEnhancement'; +import { initAddNewDefinition } from './addNewQuotaDefinitionForm'; import { initAddNewEnhancement } from './addNewForm'; import { initCopyToNextDuties } from './copyDuties'; import { initAll } from 'govuk-frontend'; @@ -20,6 +21,7 @@ showHideCheckboxes(); // Initialise accessible-autocomplete components without a `name` attr in order // to avoid the "dummy" autocomplete field being submitted as part of the form // to the server. +initAddNewDefinition(); initAddNewEnhancement(); initAutocomplete(false); initCopyToNextDuties(); diff --git a/reference_documents/forms/preferential_quota_forms.py b/reference_documents/forms/preferential_quota_forms.py index da10dd2df..00b55e6b5 100644 --- a/reference_documents/forms/preferential_quota_forms.py +++ b/reference_documents/forms/preferential_quota_forms.py @@ -1,3 +1,5 @@ +from datetime import date + from crispy_forms_gds.helper import FormHelper from crispy_forms_gds.layout import Div from crispy_forms_gds.layout import Field @@ -9,7 +11,11 @@ from django import forms from django.core.exceptions import ValidationError +from common.forms import DateInputFieldFixed +from common.forms import DateInputFieldTakesParameters +from common.forms import GovukDateRangeField from common.forms import ValidityPeriodForm +from common.util import TaricDateRange from reference_documents.models import PreferentialQuota from reference_documents.models import PreferentialQuotaOrderNumber from reference_documents.validators import commodity_code_validator @@ -110,7 +116,7 @@ def clean_quota_duty_rate(self): return data -class PreferentialQuotaBulkCreate(ValidityPeriodForm, forms.ModelForm): +class PreferentialQuotaBulkCreate(forms.Form): commodity_codes = forms.CharField( label="Commodity codes", widget=forms.Textarea, @@ -138,15 +144,6 @@ class PreferentialQuotaBulkCreate(ValidityPeriodForm, forms.ModelForm): }, ) - volume = forms.CharField( - validators=[], - error_messages={ - "invalid": "Volume invalid", - "required": "Volume is required", - }, - help_text="
", - ) - measurement = forms.CharField( validators=[], error_messages={ @@ -155,8 +152,56 @@ class PreferentialQuotaBulkCreate(ValidityPeriodForm, forms.ModelForm): }, ) + def get_variant_index(self, post_data): + result = [0] + if "data" in post_data.keys(): + for key in post_data["data"].keys(): + if key.startswith("start_date_"): + variant_index = int(key.replace("start_date_", "").split("_")[0]) + result.append(variant_index) + result = list(set(result)) + result.sort() + return result + def __init__(self, reference_document_version, *args, **kwargs): super().__init__(*args, **kwargs) + self.variant_indices = self.get_variant_index(kwargs) + self.fields["start_date_0"] = DateInputFieldFixed( + label="Start date", + required=True, + ) + self.fields["end_date_0"] = DateInputFieldFixed(label="End date", required=True) + self.fields["volume_0"] = forms.CharField( + error_messages={ + "invalid": "Volume invalid", + "required": "Volume is required", + }, + help_text="
", + ) + for index in self.variant_indices: + self.fields[f"start_date_{index}_0"] = forms.CharField() + self.fields[f"start_date_{index}_1"] = forms.CharField() + self.fields[f"start_date_{index}_2"] = forms.CharField() + self.fields[f"start_date_{index}"] = DateInputFieldTakesParameters( + day=self.fields[f"start_date_{index}_0"], + month=self.fields[f"start_date_{index}_1"], + year=self.fields[f"start_date_{index}_2"], + label="Start date", + ) + self.fields[f"end_date_{index}_0"] = forms.CharField() + self.fields[f"end_date_{index}_1"] = forms.CharField() + self.fields[f"end_date_{index}_2"] = forms.CharField() + self.fields[f"end_date_{index}"] = DateInputFieldTakesParameters( + day=self.fields[f"end_date_{index}_0"], + month=self.fields[f"end_date_{index}_1"], + year=self.fields[f"end_date_{index}_2"], + label="End date", + ) + self.fields[f"valid_between_{index}"] = GovukDateRangeField() + self.fields[f"volume_{index}"] = forms.CharField( + label="Volume", + help_text="
", + ) self.fields["preferential_quota_order_number"].queryset = ( PreferentialQuotaOrderNumber.objects.all() .filter(reference_document_version=reference_document_version) @@ -165,7 +210,7 @@ def __init__(self, reference_document_version, *args, **kwargs): self.fields[ "preferential_quota_order_number" ].label_from_instance = lambda obj: f"{obj.quota_order_number}" - self.fields["end_date"].help_text = "" + self.fields["end_date_0"].help_text = "" self.helper = FormHelper(self) self.helper.label_size = Size.SMALL self.helper.legend_size = Size.SMALL @@ -184,18 +229,24 @@ def __init__(self, reference_document_version, *args, **kwargs): ), Fieldset( Div( - Field("start_date"), + Field( + "start_date_0", + ), ), Div( - Field("end_date"), + Field( + "end_date_0", + ), ), Div( Field( - "volume", + "volume_0", + label="Volume", field_width=Fixed.TEN, ), ), style="display: grid; grid-template-columns: 2fr 2fr 1fr", + css_class="quota-definition-row", ), Submit( "submit", @@ -204,23 +255,98 @@ def __init__(self, reference_document_version, *args, **kwargs): data_prevent_double_click="true", ), ) + for index in self.variant_indices[1:]: + self.helper.layout.insert( + -1, + Fieldset( + Div( + Field( + f"start_date_{index}", + ), + ), + Div( + Field( + f"end_date_{index}", + ), + ), + Div( + Field( + f"volume_{index}", + field_width=Fixed.TEN, + ), + ), + style="display: grid; grid-template-columns: 2fr 2fr 1fr", + css_class="quota-definition-row", + ), + ) def clean(self): cleaned_data = super().clean() - commodity_codes = cleaned_data.get("commodity_codes").splitlines() - for commodity_code in commodity_codes: - try: - commodity_code_validator(commodity_code) - except ValidationError: + # Clean commodity codes + commodity_codes = cleaned_data.get("commodity_codes") + if commodity_codes: + for commodity_code in commodity_codes.splitlines(): + try: + commodity_code_validator(commodity_code) + except ValidationError: + self.add_error( + "commodity_codes", + "Ensure all commodity codes are 10 digits and each on a new line", + ) + # Clean validity periods + for index in self.variant_indices: + self.clean_validity_period( + self, + cleaned_data, + valid_between_field_name=f"valid_between_{index}", + start_date_field_name=f"start_date_{index}", + end_date_field_name=f"end_date_{index}", + ) + + @staticmethod + def clean_validity_period( + self, + cleaned_data, + valid_between_field_name, + start_date_field_name, + end_date_field_name, + ): + start_date = cleaned_data.pop(start_date_field_name, None) + end_date = cleaned_data.pop(end_date_field_name, None) + + # Data may not be present, e.g. if the user skips ahead in the sidebar + valid_between = self.initial.get(valid_between_field_name) + if end_date and start_date and end_date < start_date: + if valid_between: + if start_date != valid_between.lower: + self.add_error( + start_date_field_name, + "The start date must be the same as or before the end date.", + ) + if end_date != self.initial[valid_between_field_name].upper: + self.add_error( + end_date_field_name, + "The end date must be the same as or after the start date.", + ) + else: self.add_error( - "commodity_codes", - "Ensure all commodity codes are 10 digits and each on a new line", + end_date_field_name, + "The end date must be the same as or after the start date.", ) + cleaned_data[valid_between_field_name] = TaricDateRange(start_date, end_date) - class Meta: - model = PreferentialQuota - fields = [ - "preferential_quota_order_number", - "quota_duty_rate", - "measurement", - ] + if start_date: + day, month, year = (start_date.day, start_date.month, start_date.year) + self.fields[start_date_field_name].initial = date( + day=int(day), + month=int(month), + year=int(year), + ) + + if end_date: + day, month, year = (end_date.day, end_date.month, end_date.year) + self.fields[end_date_field_name].initial = date( + day=int(day), + month=int(month), + year=int(year), + ) diff --git a/reference_documents/views/preferential_quota_views.py b/reference_documents/views/preferential_quota_views.py index 398dde170..19908e476 100644 --- a/reference_documents/views/preferential_quota_views.py +++ b/reference_documents/views/preferential_quota_views.py @@ -1,5 +1,3 @@ -from datetime import date - from django.contrib.auth.mixins import PermissionRequiredMixin from django.shortcuts import redirect from django.urls import reverse @@ -7,7 +5,6 @@ from django.views.generic import FormView from django.views.generic import UpdateView -from common.util import TaricDateRange from reference_documents.forms.preferential_quota_forms import ( PreferentialQuotaBulkCreate, ) @@ -80,7 +77,6 @@ def get_success_url(self): class PreferentialQuotaBulkCreateView(PermissionRequiredMixin, FormView): template_name = "reference_documents/preferential_quotas/bulk_create.jinja" permission_required = "reference_documents.add_preferentialquota" - # model = PreferentialQuota form_class = PreferentialQuotaBulkCreate queryset = ReferenceDocumentVersion.objects.all() @@ -102,54 +98,32 @@ def get_context_data(self, **kwargs): ) return context_data - @staticmethod - def get_validity_and_volume_data(data): - entries = [] - for entry in range(len(data.getlist("volume"))): - start_date = date( - day=int(data.getlist("start_date_0")[entry]), - month=int(data.getlist("start_date_1")[entry]), - year=int(data.getlist("start_date_2")[entry]), - ) - end_date = date( - day=int(data.getlist("end_date_0")[entry]), - month=int(data.getlist("end_date_1")[entry]), - year=int(data.getlist("end_date_2")[entry]), - ) - valid_between = TaricDateRange(start_date, end_date) - volume = data.getlist("volume")[entry] - entries.append({f"valid_between": valid_between, f"volume": volume}) - return entries - def form_valid(self, form): - dates_and_volumes = self.get_validity_and_volume_data(form.data) + cleaned_data = form.cleaned_data commodity_codes = form.cleaned_data["commodity_codes"].splitlines() - self.reference_document_version = ReferenceDocumentVersion.objects.all().get( + reference_document_version = ReferenceDocumentVersion.objects.all().get( pk=self.kwargs["pk"], ) for commodity_code in commodity_codes: - for entry in dates_and_volumes: - instance = form.save(commit=False) - instance.order = ( - len(self.reference_document_version.preferential_quotas()) + 1 - ) - instance = PreferentialQuota( + for index in form.variant_indices: + PreferentialQuota.objects.create( commodity_code=commodity_code, - quota_duty_rate=instance.quota_duty_rate, - volume=entry["volume"], - valid_between=entry["valid_between"], - measurement=instance.measurement, - order=instance.order, - preferential_quota_order_number=instance.preferential_quota_order_number, + quota_duty_rate=cleaned_data["quota_duty_rate"], + volume=cleaned_data[f"volume_{index}"], + valid_between=cleaned_data[f"valid_between_{index}"], + measurement=cleaned_data["measurement"], + order=len(reference_document_version.preferential_quotas()) + 1, + preferential_quota_order_number=cleaned_data[ + "preferential_quota_order_number" + ], ) - instance.save() - return redirect(self.get_success_url()) + return redirect(self.get_success_url(reference_document_version)) - def get_success_url(self): + def get_success_url(self, reference_document_version): return ( reverse( "reference_documents:version-details", - args=[self.reference_document_version.pk], + args=[reference_document_version.pk], ) + "#tariff-quotas" )