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"
)