From d727039b5fb26ed0050b9dd6c187820a60a38c53 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Thu, 2 Jan 2025 14:32:49 +0000 Subject: [PATCH 1/8] Update conditions_list (#1366) --- measures/jinja2/includes/measures/conditions.jinja | 4 ++-- measures/jinja2/includes/measures/list.jinja | 2 +- measures/jinja2/includes/measures/workbasket-measures.jinja | 4 ++-- measures/views/search.py | 1 + workbaskets/jinja2/workbaskets/compare.jinja | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/measures/jinja2/includes/measures/conditions.jinja b/measures/jinja2/includes/measures/conditions.jinja index a56791c4d..83e1616f3 100644 --- a/measures/jinja2/includes/measures/conditions.jinja +++ b/measures/jinja2/includes/measures/conditions.jinja @@ -1,6 +1,6 @@ -{% macro conditions_list(measure) -%} +{% macro conditions_list(measure, workbasket) -%}
    - {% for condition in measure.conditions.current().with_reference_price_string() -%} + {% for condition in measure.conditions.approved_up_to_transaction(workbasket.transactions.last()).with_reference_price_string() -%}
  1. diff --git a/measures/jinja2/includes/measures/list.jinja b/measures/jinja2/includes/measures/list.jinja index 51e4fad86..bbb3a31e4 100644 --- a/measures/jinja2/includes/measures/list.jinja +++ b/measures/jinja2/includes/measures/list.jinja @@ -68,7 +68,7 @@ {"text": create_link(measure.order_number.get_url(), measure.order_number.order_number) if measure.order_number else '-'}, {"text": footnotes_display(measure.footnoteassociationmeasure_set.current())}, {"text": create_link(url("regulation-ui-detail", kwargs={"role_type": measure.generating_regulation.role_type,"regulation_id": measure.generating_regulation.regulation_id}), measure.generating_regulation.regulation_id) if measure.generating_regulation.regulation_id else '-'}, - {"text": conditions_list(measure) if measure.conditions.current() else "-", "classes": "govuk-!-width-one-quarter"}, + {"text": conditions_list(measure, workbasket) if measure.conditions.current() else "-", "classes": "govuk-!-width-one-quarter"}, ]) or "" }} {% endfor %} {{ govukTable({ diff --git a/measures/jinja2/includes/measures/workbasket-measures.jinja b/measures/jinja2/includes/measures/workbasket-measures.jinja index c9f2e8811..337575e2c 100644 --- a/measures/jinja2/includes/measures/workbasket-measures.jinja +++ b/measures/jinja2/includes/measures/workbasket-measures.jinja @@ -17,8 +17,8 @@ {"html": create_link(url("additional_code-ui-detail", kwargs={"sid": measure.additional_code.sid}), measure.additional_code.type.sid ~ measure.additional_code.code) if measure.additional_code else '-'}, {"html": create_link(url("geo_area-ui-detail", kwargs={"sid": measure.geographical_area.sid}), measure.geographical_area.area_id ~ " - " ~ measure.geographical_area.get_description().description) if measure.geographical_area else '-'}, {"text": create_link(measure.order_number.get_url(), measure.order_number.order_number) if measure.order_number else '-'}, - {"text": footnotes_display(measure.footnoteassociationmeasure_set.current())}, - {"text": conditions_list(measure) if measure.conditions.current() else "-"}, + {"text": footnotes_display(measure.footnoteassociationmeasure_set.approved_up_to_transaction(workbasket.transactions.last()))}, + {"text": conditions_list(measure, workbasket) if measure.conditions.approved_up_to_transaction(workbasket.transactions.last()) else "-"}, ]) or "" }} {% endfor %} diff --git a/measures/views/search.py b/measures/views/search.py index 3815b8c62..dec0f6ba2 100644 --- a/measures/views/search.py +++ b/measures/views/search.py @@ -241,6 +241,7 @@ def get_context_data(self, **kwargs): page.paginator.num_pages, ), "selected_filter_lists": self.selected_filter_formatter(), + "workbasket": self.workbasket, }, ) if context["has_previous_page"]: diff --git a/workbaskets/jinja2/workbaskets/compare.jinja b/workbaskets/jinja2/workbaskets/compare.jinja index d852903be..20d6fa525 100644 --- a/workbaskets/jinja2/workbaskets/compare.jinja +++ b/workbaskets/jinja2/workbaskets/compare.jinja @@ -92,7 +92,7 @@ {"html": create_link(url("geo_area-ui-detail", kwargs={"sid": measure.geographical_area.sid}), measure.geographical_area.area_id ~ " - " ~ measure.geographical_area.get_description().description) if measure.geographical_area else '-'}, {"text": create_link(measure.order_number.get_url(), measure.order_number.order_number) if measure.order_number else '-'}, {"text": footnotes_display(measure.footnoteassociationmeasure_set.current())}, - {"text": conditions_list(measure) if measure.conditions.current() else "-"}, + {"text": conditions_list(measure, workbasket) if measure.conditions.current() else "-"}, ]) or "" }} {% endfor %} From 01a53a11ca51d3940baaa2399987cb6f44eb5e21 Mon Sep 17 00:00:00 2001 From: Dale Cannon <118175145+dalecannon@users.noreply.github.com> Date: Fri, 3 Jan 2025 14:43:07 +0000 Subject: [PATCH 2/8] TP2000-1636 Fix date-related failing unit tests (#1372) * Replace hardcoded dates in failing unit tests * Use start date from TaricDateRange object --- measures/forms/mixins.py | 2 +- measures/tests/conftest.py | 6 +++--- .../tests/test_ref_quota_definition_range_forms.py | 12 ++++++------ .../tests/test_ref_quota_definition_ranges_model.py | 3 ++- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/measures/forms/mixins.py b/measures/forms/mixins.py index 08c6b6e33..6bf3a2acc 100644 --- a/measures/forms/mixins.py +++ b/measures/forms/mixins.py @@ -247,7 +247,7 @@ def conditions_clean( ) if price: - date = measure_start_date or make_aware(datetime.now()) + date = measure_start_date or make_aware(datetime.datetime.now()) parser = LarkDutySentenceParser(compound_duties=False, date=date) try: components = parser.transform(price) diff --git a/measures/tests/conftest.py b/measures/tests/conftest.py index df5d8e12c..0c836188a 100644 --- a/measures/tests/conftest.py +++ b/measures/tests/conftest.py @@ -425,7 +425,7 @@ def measure_conditions_form_data( }, "kwargs": { "form_kwargs": { - "measure_start_date": datetime.date(2025, 1, 1), + "measure_start_date": date_ranges.normal.lower, "measure_type": factories.MeasureTypeFactory.create( valid_between=date_ranges.normal, ), @@ -451,7 +451,7 @@ def measure_footnotes_form_data(): @pytest.fixture() -def measure_commodities_and_duties_form_data(): +def measure_commodities_and_duties_form_data(date_ranges): commodity_1 = factories.GoodsNomenclatureFactory.create() commodity_2 = factories.GoodsNomenclatureFactory.create() @@ -464,7 +464,7 @@ def measure_commodities_and_duties_form_data(): }, "kwargs": { "min_commodity_count": 1, - "measure_start_date": datetime.date(2025, 1, 1), + "measure_start_date": date_ranges.normal.lower, "form_kwargs": { "measure_type": None, }, diff --git a/reference_documents/tests/test_ref_quota_definition_range_forms.py b/reference_documents/tests/test_ref_quota_definition_range_forms.py index edd4b3330..0ff5dca7e 100644 --- a/reference_documents/tests/test_ref_quota_definition_range_forms.py +++ b/reference_documents/tests/test_ref_quota_definition_range_forms.py @@ -67,16 +67,16 @@ def test_clean_duty_rate(self): ("zz", "Enter a whole number."), ( 0, - "Start year is not valid, it must be a 4 digit year greater than 2010 and less than 2124", + f"Start year is not valid, it must be a 4 digit year greater than 2010 and less than {date.today().year + 100}", ), ( 1980, - "Start year is not valid, it must be a 4 digit year greater than 2010 and less than 2124", + f"Start year is not valid, it must be a 4 digit year greater than 2010 and less than {date.today().year + 100}", ), (date.today().year + 2, None), ( date.today().year + 101, - "Start year is not valid, it must be a 4 digit year greater than 2010 and less than 2124", + f"Start year is not valid, it must be a 4 digit year greater than 2010 and less than {date.today().year + 100}", ), ], ) @@ -139,17 +139,17 @@ def test_clean_start_year(self, value, expected_message): ( 0, "start_year", - "Start year is not valid, it must be a 4 digit year greater than 2010 and less than 2124", + f"Start year is not valid, it must be a 4 digit year greater than 2010 and less than {date.today().year + 100}", ), ( 1980, "start_year", - "Start year is not valid, it must be a 4 digit year greater than 2010 and less than 2124", + f"Start year is not valid, it must be a 4 digit year greater than 2010 and less than {date.today().year + 100}", ), ( date.today().year + 101, "start_year", - "Start year is not valid, it must be a 4 digit year greater than 2010 and less than 2124", + f"Start year is not valid, it must be a 4 digit year greater than 2010 and less than {date.today().year + 100}", ), (date.today().year + 2, "start_year", None), # end year diff --git a/reference_documents/tests/test_ref_quota_definition_ranges_model.py b/reference_documents/tests/test_ref_quota_definition_ranges_model.py index 2bee8f4b6..7c7f02d91 100644 --- a/reference_documents/tests/test_ref_quota_definition_ranges_model.py +++ b/reference_documents/tests/test_ref_quota_definition_ranges_model.py @@ -92,7 +92,8 @@ def test_date_ranges_no_end_date(self): rqdr_date_ranges = target.date_ranges() - assert len(rqdr_date_ranges) == 8 + # date_ranges() adds three years to the current date to calculate target.end_year if None + assert len(rqdr_date_ranges) == (date.today().year + 4) - target.start_year assert rqdr_date_ranges[0] == TaricDateRange( date(2020, 1, 1), date(2020, 12, 31), From 5a1d02dc6901df44ee24c59ce3f323f6e11bc10f Mon Sep 17 00:00:00 2001 From: Charlie Prichard <46421052+CPrich905@users.noreply.github.com> Date: Wed, 8 Jan 2025 08:59:06 +0000 Subject: [PATCH 3/8] TP2000-1537 Bulk create QuotaDefinitionPeriods (#1320) --- .../common/scss/_quota-definitions.scss | 6 + .../common/widgets/decimal_suffix.html | 16 + common/widgets.py | 20 + quotas/forms/definitions.py | 128 +++-- quotas/forms/wizards.py | 457 ++++++++++++++---- quotas/jinja2/includes/quotas/actions.jinja | 8 +- .../bulk-create-definition-edit.jinja | 19 + .../bulk-create-definition-info.jinja | 26 + .../quota-definitions/bulk-create-done.jinja | 43 ++ .../bulk-create-review.jinja | 102 ++++ .../quota-definitions/bulk-create-start.jinja | 23 + .../quota-definitions/bulk-create-step.jinja | 17 + quotas/jinja2/quota-definitions/create.jinja | 22 +- quotas/serializers.py | 72 ++- quotas/tests/test_forms.py | 356 +++++++++++++- quotas/tests/test_views.py | 243 +++++++++- quotas/urls.py | 26 + quotas/views/definitions.py | 37 +- quotas/views/wizards.py | 145 +++++- quotas/wizard.py | 4 + .../jinja2/workbaskets/edit-workbasket.jinja | 1 + 21 files changed, 1624 insertions(+), 147 deletions(-) create mode 100644 common/templates/common/widgets/decimal_suffix.html create mode 100644 quotas/jinja2/quota-definitions/bulk-create-definition-edit.jinja create mode 100644 quotas/jinja2/quota-definitions/bulk-create-definition-info.jinja create mode 100644 quotas/jinja2/quota-definitions/bulk-create-done.jinja create mode 100644 quotas/jinja2/quota-definitions/bulk-create-review.jinja create mode 100644 quotas/jinja2/quota-definitions/bulk-create-start.jinja create mode 100644 quotas/jinja2/quota-definitions/bulk-create-step.jinja diff --git a/common/static/common/scss/_quota-definitions.scss b/common/static/common/scss/_quota-definitions.scss index 4a81d0996..70786d1d6 100644 --- a/common/static/common/scss/_quota-definitions.scss +++ b/common/static/common/scss/_quota-definitions.scss @@ -28,4 +28,10 @@ .definition-original { color: $govuk-secondary-text-colour; +} + +.bulk-create-review { + details.govuk-details[open] { + height: 420px !important; + } } \ No newline at end of file diff --git a/common/templates/common/widgets/decimal_suffix.html b/common/templates/common/widgets/decimal_suffix.html new file mode 100644 index 000000000..aff4b4ce7 --- /dev/null +++ b/common/templates/common/widgets/decimal_suffix.html @@ -0,0 +1,16 @@ +
    + + {% if widget.suffix %} + + {% endif %} + + {{ widget.label }} + +
    diff --git a/common/widgets.py b/common/widgets.py index f64a100ea..05be8f890 100644 --- a/common/widgets.py +++ b/common/widgets.py @@ -71,3 +71,23 @@ class MultipleFileInput(FileInput): """Enable multiple files to be selected using FileInput widget.""" allow_multiple_selected = True + + +class DecimalSuffix(widgets.NumberInput): + """Identical to the Decimal widget but allows the addition of a suffix.""" + + template_name = "common/widgets/decimal_suffix.html" + + def __init__(self, attrs=None, suffix="", **kwargs): + self.suffix = suffix + super().__init__(attrs, **kwargs) + + def render(self, name, value, attrs=None, renderer=None): + if attrs is None: + attrs = {} + context = self.get_context(name, value, attrs) + if self.suffix: + context["widget"]["suffix"] = self.suffix + + template = loader.get_template(self.template_name).render(context) + return mark_safe(template) diff --git a/quotas/forms/definitions.py b/quotas/forms/definitions.py index 66a0f552e..8a03b880b 100644 --- a/quotas/forms/definitions.py +++ b/quotas/forms/definitions.py @@ -2,8 +2,6 @@ from crispy_forms_gds.helper import FormHelper from crispy_forms_gds.layout import HTML -from crispy_forms_gds.layout import Accordion -from crispy_forms_gds.layout import AccordionSection from crispy_forms_gds.layout import Div from crispy_forms_gds.layout import Field from crispy_forms_gds.layout import Layout @@ -14,6 +12,7 @@ from common.forms import ValidityPeriodForm from common.serializers import deserialize_date +from common.widgets import DecimalSuffix from measures.models import MeasurementUnit from quotas import business_rules from quotas import models @@ -145,6 +144,7 @@ class QuotaDefinitionCreateForm( class Meta: model = models.QuotaDefinition fields = [ + "order_number", "valid_between", "description", "volume", @@ -156,9 +156,14 @@ class Meta: "maximum_precision", ] + order_number = forms.ModelChoiceField( + queryset=models.QuotaOrderNumber.objects.current(), + widget=forms.HiddenInput(), + ) description = forms.CharField(label="", widget=forms.Textarea(), required=False) volume = forms.DecimalField( label="Current volume", + help_text="The current volume is the starting balance for the quota", widget=forms.TextInput(), error_messages={ "invalid": "Volume must be a number", @@ -167,6 +172,7 @@ class Meta: ) initial_volume = forms.DecimalField( widget=forms.TextInput(), + help_text="The initial volume is the legal balance applied to the definition period", error_messages={ "invalid": "Initial volume must be a number", "required": "Enter the initial volume", @@ -174,13 +180,14 @@ class Meta: ) measurement_unit = forms.ModelChoiceField( queryset=MeasurementUnit.objects.current(), + empty_label="Choose measurement unit", error_messages={"required": "Select the measurement unit"}, ) quota_critical_threshold = forms.DecimalField( label="Threshold", + widget=DecimalSuffix(suffix="%"), help_text="The point at which this quota definition period becomes critical, as a percentage of the total volume.", - widget=forms.TextInput(), error_messages={ "invalid": "Critical threshold must be a number", "required": "Enter the critical threshold", @@ -199,13 +206,21 @@ class Meta: ) def __init__(self, *args, **kwargs): + self.buttons = kwargs.pop("buttons", None) + self.order_number = kwargs.pop("order_number", None) super().__init__(*args, **kwargs) self.init_layout() self.init_fields() def clean(self): + cleaned_data = super().clean() + """The "order_number" field is hidden, but is not correctly populated by + .initial when creating a single QuotaDefinitionPeriod.""" + if "order_number" in self.errors: + self.cleaned_data["order_number"] = self.order_number + self.errors.pop("order_number") validators.validate_quota_volume(self.cleaned_data) - return super().clean() + return cleaned_data def init_fields(self): # This is always set to 3 for current definitions @@ -230,50 +245,87 @@ def init_fields(self): lambda obj: f"{obj.code} - {obj.description}" ) - def init_layout(self): + self.fields["end_date"].help_text = "" + self.fields["end_date"].required = True + self.fields["measurement_unit_qualifier"].help_text = ( + "A measurement unit qualifier is not always required." + ) + self.fields["measurement_unit_qualifier"].empty_label = ( + "Choose measurement unit qualifier." + ) + + def init_layout(self, *args, **kwargs): self.helper = FormHelper(self) self.helper.label_size = Size.SMALL self.helper.legend_size = Size.SMALL - + link_text = self.buttons["link_text"] + link = self.buttons["link"] self.helper.layout = Layout( - Accordion( - AccordionSection( - "Description", - HTML.p("Adding a description is optional."), - "description", - "order_number", + Div( + HTML( + '

    Validity period

    ', ), - AccordionSection( - "Validity period", - "start_date", - "end_date", + "start_date", + "end_date", + ), + HTML( + "
    ", + ), + Div( + HTML( + '

    Measurements

    ', ), - AccordionSection( - "Measurements", - HTML.p("A measurement unit qualifier is not always required."), - Field("measurement_unit", css_class="govuk-!-width-full"), - Field("measurement_unit_qualifier", css_class="govuk-!-width-full"), + Field("measurement_unit", css_class="govuk-!-width-two-thirds"), + Field( + "measurement_unit_qualifier", + css_class="govuk-!-width-two-thirds", ), - AccordionSection( - "Volume", - HTML.p( - "The initial volume is the legal balance applied to the definition period.

    The current volume is the starting balance for the quota.", - ), - "initial_volume", - "volume", - "maximum_precision", + ), + HTML( + "
    ", + ), + Div( + HTML( + '

    Volume

    ', + ), + Field("initial_volume", css_class="govuk-!-width-one-third"), + Field("volume", css_class="govuk-!-width-one-third"), + "maximum_precision", + ), + HTML( + "
    ", + ), + Div( + HTML( + '

    Criticality

    ', ), - AccordionSection( - "Criticality", - "quota_critical_threshold", - "quota_critical", + "quota_critical_threshold", + "quota_critical", + ), + HTML( + "
    ", + ), + Div( + HTML( + '

    Description

    ', ), + HTML.p("Adding a description is optional."), + Field("description", css_class="govuk-!-width-two-thirds"), ), - Submit( - "submit", - "Save", - data_module="govuk-button", - data_prevent_double_click="true", + HTML( + "
    ", + ), + Div( + Submit( + "submit", + self.buttons["submit"], + data_module="govuk-button", + data_prevent_double_click="true", + ), + HTML( + f'{link_text}', + ), + css_class="govuk-button-group", ), ) diff --git a/quotas/forms/wizards.py b/quotas/forms/wizards.py index e4c17a640..77eea97e1 100644 --- a/quotas/forms/wizards.py +++ b/quotas/forms/wizards.py @@ -1,20 +1,26 @@ -from crispy_forms_gds.helper import FormHelper +import decimal -# from crispy_forms_gds.layout import Button +from crispy_forms_gds.helper import FormHelper from crispy_forms_gds.layout import HTML from crispy_forms_gds.layout import Div from crispy_forms_gds.layout import Field from crispy_forms_gds.layout import Layout from crispy_forms_gds.layout import Size from crispy_forms_gds.layout import Submit +from dateutil.relativedelta import relativedelta from django import forms from django.core.exceptions import ValidationError from common.fields import AutoCompleteField from common.forms import ValidityPeriodForm +from common.serializers import deserialize_date +from common.util import TaricDateRange +from common.widgets import DecimalSuffix from measures.models import MeasurementUnit +from measures.models import MeasurementUnitQualifier from quotas import models -from quotas import validators +from quotas.forms.definitions import QuotaDefinitionCreateForm +from quotas.serializers import serialize_definition_data from quotas.serializers import serialize_duplicate_data from workbaskets.forms import SelectableObjectsForm @@ -38,9 +44,9 @@ class QuotaOrderNumbersSelectForm(forms.Form): def __init__(self, *args, **kwargs): self.request = kwargs.pop("request", None) super().__init__(*args, **kwargs) - self.init_layout(self.request) + self.init_layout() - def init_layout(self, request): + def init_layout(self): self.helper = FormHelper(self) self.helper.label_size = Size.SMALL self.helper.legend_size = Size.SMALL @@ -130,22 +136,71 @@ def clean(self): class BulkQuotaDefinitionCreateStartForm(forms.Form): - pass + def __init__(self, *args, **kwargs): + self.request = kwargs.pop("request") + super().__init__(*args, **kwargs) + self.init_layout(self.request) + quota_order_number = AutoCompleteField( + label="Enter the quota order number", + queryset=models.QuotaOrderNumber.objects.all(), + required=True, + ) -class BulkQuotaDefinitionCreateIntroductoryPeriod(forms.Form): - pass + def save_quota_order_number_to_session(self, cleaned_data): + self.request.session["quota_order_number_pk"] = cleaned_data[ + "quota_order_number" + ].pk + self.request.session["quota_order_number"] = cleaned_data[ + "quota_order_number" + ].order_number + + def clean(self): + cleaned_data = super().clean() + if cleaned_data == {}: + raise ValidationError("A quota order number must be selected") + + self.save_quota_order_number_to_session(cleaned_data) + return cleaned_data + def init_layout(self, request): + self.helper = FormHelper(self) + self.helper.label_size = Size.SMALL + self.helper.legend_size = Size.SMALL + + self.helper.layout = Layout( + Div( + HTML( + '

    Enter quota order number

    ', + ), + Div( + "quota_order_number", + css_class="govuk-!-width-one-third", + ), + Div( + Submit( + "submit", + "Continue", + data_module="govuk-button", + data_prevent_double_click="true", + ), + HTML( + 'Cancel', + ), + css_class="govuk-button-group", + ), + ), + ) -class QuotaDefinitionCreateForm( + +class QuotaDefinitionBulkCreateDefinitionInformation( ValidityPeriodForm, - forms.ModelForm, + forms.Form, ): class Meta: model = models.QuotaDefinition fields = [ "valid_between", - "description", "volume", "initial_volume", "measurement_unit", @@ -155,31 +210,54 @@ class Meta: "maximum_precision", ] - description = forms.CharField(label="", widget=forms.Textarea(), required=False) + instance_count = forms.DecimalField( + required=True, + label="Total number of definitions to create", + help_text="You can create up to 20 definition periods at a time per quota order number", + error_messages={ + "invalid": "Must be a number", + "required": "Enter the number of definition periods to create", + }, + ) + + frequency = forms.ChoiceField( + widget=forms.RadioSelect(attrs={"required": "required"}), + choices=[ + (1, "Every year"), + (2, "Every 6 months"), + (3, "Every 3 months"), + ], + help_text="For non-standard frequencies, pick the closest option and edit it on the review page", + ) + volume = forms.DecimalField( label="Current volume", - widget=forms.TextInput(), + widget=forms.TextInput(attrs={"required": "required"}), + help_text="The current volume is the starting balance for the quota", error_messages={ "invalid": "Volume must be a number", "required": "Enter the volume", }, ) initial_volume = forms.DecimalField( - widget=forms.TextInput(), + widget=forms.TextInput(attrs={"required": "required"}), + help_text="The initial volume is the legal balance applied to the definition period", error_messages={ "invalid": "Initial volume must be a number", "required": "Enter the initial volume", }, ) + measurement_unit = forms.ModelChoiceField( + empty_label="Choose measurement unit", queryset=MeasurementUnit.objects.current(), error_messages={"required": "Select the measurement unit"}, ) quota_critical_threshold = forms.DecimalField( label="Threshold", + widget=DecimalSuffix(suffix="%"), help_text="The point at which this quota definition period becomes critical, as a percentage of the total volume.", - widget=forms.TextInput(), error_messages={ "invalid": "Critical threshold must be a number", "required": "Enter the critical threshold", @@ -196,38 +274,133 @@ class Meta: maximum_precision = forms.IntegerField( widget=forms.HiddenInput(), ) + description = forms.CharField( + label="", + help_text="Adding a description is optional", + widget=forms.Textarea(), + ) def __init__(self, *args, **kwargs): + self.request = kwargs.pop("request") super().__init__(*args, **kwargs) self.init_layout() self.init_fields() - def clean(self): - validators.validate_quota_volume(self.cleaned_data) - return super().clean() - def init_fields(self): # This is always set to 3 for current definitions # see https://uktrade.github.io/tariff-data-manual/documentation/data-structures/quotas.html#the-quota-definition-table self.fields["maximum_precision"].initial = 3 - - # Set these as the default values - self.fields["quota_critical"].initial = False + self.fields["end_date"].help_text = "" + self.fields["end_date"].required = True + self.fields["measurement_unit_qualifier"].help_text = ( + "A measurement unit qualifier is not always required" + ) + self.fields["measurement_unit_qualifier"].empty_label = ( + "Choose measurement unit qualifier" + ) self.fields["quota_critical_threshold"].initial = 90 + self.fields["quota_critical"].initial = False + self.fields["description"].required = False + + def save_definition_data_to_session(self, cleaned_data): + instance_count = decimal.Decimal(cleaned_data["instance_count"]) + frequency = decimal.Decimal(cleaned_data["frequency"]) + definition_data = { + "id": 1, + "maximum_precision": cleaned_data["maximum_precision"], + "initial_volume": cleaned_data["initial_volume"], + "volume": cleaned_data["volume"], + "measurement_unit": cleaned_data["measurement_unit"], + "quota_critical_threshold": cleaned_data["quota_critical_threshold"], + "quota_critical": cleaned_data["quota_critical"], + "valid_between": cleaned_data["valid_between"], + } + # optional fields + if "description" in cleaned_data: + definition_data["description"] = cleaned_data["description"] + if "measurement_unit_qualifier" in cleaned_data: + definition_data["measurement_unit_qualifier"] = cleaned_data[ + "measurement_unit_qualifier" + ] + + staged_definitions = [] + + serialize_first_definition = serialize_definition_data(definition_data) + staged_definitions.append(serialize_first_definition) + + while len(staged_definitions) < instance_count: + id = decimal.Decimal(definition_data["id"]) + 1 + definition_data.update( + {"id": id}, + ) + """ + There are currently three options for the frequency with which + definition periods repeat, selected in + BulkQuotaDefinitionCreateInitialInformation + 1. Annually + 2. Every 6 months + 3. Quarterly + """ + if frequency == 1: + # Repeats annualy + new_start_date = definition_data["valid_between"].lower + relativedelta( + years=1, + ) + new_end_date = definition_data["valid_between"].upper + relativedelta( + years=1, + ) + new_date_range = TaricDateRange( + new_start_date, + new_end_date, + ) + definition_data.update( + { + "valid_between": new_date_range, + }, + ) - self.fields["measurement_unit"].queryset = self.fields[ - "measurement_unit" - ].queryset.order_by("code") - self.fields["measurement_unit"].label_from_instance = ( - lambda obj: f"{obj.code} - {obj.description}" - ) + if frequency == 2: + # Repeats every 6 months + new_start_date = definition_data["valid_between"].upper + relativedelta( + days=1, + ) + new_end_date = new_start_date + relativedelta( + months=6, + days=-1, + ) + new_date_range = TaricDateRange( + new_start_date, + new_end_date, + ) + definition_data.update( + {"valid_between": new_date_range}, + ) + if frequency == 3: + # repeats quarterly + new_start_date = definition_data["valid_between"].upper + relativedelta( + days=1, + ) + new_end_date = new_start_date + relativedelta( + months=3, + days=-1, + ) + new_date_range = TaricDateRange( + new_start_date, + new_end_date, + ) + definition_data.update( + {"valid_between": new_date_range}, + ) + serialized_definition_data = serialize_definition_data(definition_data) + staged_definitions.append(serialized_definition_data) - self.fields["measurement_unit_qualifier"].queryset = self.fields[ - "measurement_unit_qualifier" - ].queryset.order_by("code") - self.fields["measurement_unit_qualifier"].label_from_instance = ( - lambda obj: f"{obj.code} - {obj.description}" - ) + self.request.session["staged_definition_data"] = staged_definitions + + def clean(self): + cleaned_data = super().clean() + if self.is_valid(): + self.save_definition_data_to_session(cleaned_data) + return cleaned_data def init_layout(self): self.helper = FormHelper(self) @@ -237,73 +410,181 @@ def init_layout(self): self.helper.layout = Layout( Div( HTML( - '

    Definitions count

    ', + '

    First definition period

    ', ), - ), - HTML( - '
    ', - ), - Div( - HTML( - '

    Description

    ', + Div( + HTML( + '

    Enter the dates for the first definition period you are creating. Subsequent definition period dates will be calculated based on the dates entered for this first period

    ', + ), + "start_date", + "end_date", + ), + Div( + HTML( + '

    Subsequent definition periods

    ', + ), + ), + Div( + HTML( + '

    Select the frequency at which the subsequent definition periods should be duplicated

    ', + ), + "frequency", + Field( + "instance_count", + css_class="govuk-input govuk-input--width-2", + ), ), - HTML.p("Adding a description is optional."), - "description", - "order_number", - ), - HTML( - '
    ', - ), - Div( HTML( - '

    Validity period

    ', + '
    ', + ), + Div( + HTML( + '

    Measurements

    ', + ), + Field("measurement_unit", css_class="govuk-!-width-two-thirds"), + Field( + "measurement_unit_qualifier", + css_class="govuk-!-width-two-thirds", + ), ), - "start_date", - "end_date", - ), - HTML( - '
    ', - ), - Div( HTML( - '

    Measurements

    ', + "
    ", + ), + Div( + HTML( + '

    Volume

    ', + ), + Field("initial_volume", css_class="govuk-!-width-one-third"), + Field("volume", css_class="govuk-!-width-one-third"), + "maximum_precision", ), - HTML.p("A measurement unit qualifier is not always required."), - Field("measurement_unit", css_class="govuk-!-width-full"), - Field("measurement_unit_qualifier", css_class="govuk-!-width-full"), - ), - HTML( - '
    ', - ), - Div( HTML( - '

    Volume

    ', + "
    ", ), - HTML.p( - "The initial volume is the legal balance applied to the definition period.

    The current volume is the starting balance for the quota.", + Div( + HTML( + '

    Criticality

    ', + ), + Field( + "quota_critical_threshold", + css_class="govuk-!-width-two-thirds", + ), + "quota_critical", ), - "initial_volume", - "volume", - "maximum_precision", - ), - HTML( - '
    ', - ), - Div( HTML( - '

    Criticality

    ', + "
    ", + ), + Div( + HTML( + '

    Description

    ', + ), + Field("description", css_class="govuk-!-width-two-thirds"), + ), + Div( + Submit( + "submit", + "Save and continue", + data_module="govuk-button", + data_prevent_double_click="true", + ), + HTML( + f"Back", + ), + css_class="govuk-button-group", ), - "quota_critical_threshold", - "quota_critical", ), - Submit( - "submit", - "Save", - data_module="govuk-button", - data_prevent_double_click="true", + HTML( + 'Cancel', ), ) -class BulkQuotaDefinitionCreateSummaryForm: - pass +class BulkQuotaDefinitionCreateReviewForm(forms.Form): + def __init__(self, *args, **kwargs): + self.request = kwargs.pop("request") + super().__init__(*args, **kwargs) + + +class BulkDefinitionUpdateData( + QuotaDefinitionCreateForm, + forms.Form, +): + """This is broadly similar to the QuotaDefinitionCreateForm.""" + + class Meta: + model = models.QuotaDefinition + fields = [ + "valid_between", + "description", + "volume", + "initial_volume", + "measurement_unit", + "measurement_unit_qualifier", + "quota_critical_threshold", + "quota_critical", + "maximum_precision", + ] + + def __init__(self, *args, **kwargs): + self.request = kwargs.pop("request") + self.definition_data = self.request.session["staged_definition_data"][ + int(kwargs.pop("pk")) - 1 + ] + super().__init__(*args, **kwargs) + self.init_fields() + + def init_fields(self): + fields = self.fields + definition_data = self.definition_data + # This is always set to 3 for current definitions + # see https://uktrade.github.io/tariff-data-manual/documentation/data-structures/quotas.html#the-quota-definition-table + fields["maximum_precision"].initial = 3 + # The following populate from the data saved in session + fields["start_date"].initial = deserialize_date(definition_data["start_date"]) + fields["end_date"].initial = deserialize_date(definition_data["end_date"]) + fields["end_date"].help_text = "" + fields["initial_volume"].initial = decimal.Decimal( + definition_data["initial_volume"], + ) + fields["volume"].initial = decimal.Decimal(definition_data["volume"]) + fields["measurement_unit"].initial = MeasurementUnit.objects.get( + code=definition_data["measurement_unit_code"], + ) + if "description" in definition_data: + fields["description"].initial = definition_data["description"] + + if ( + "measurement_unit_qualifier" in definition_data + and definition_data["measurement_unit_qualifier"] != "None" + ): + fields["measurement_unit_qualifier"].initial = ( + MeasurementUnitQualifier.objects.get( + code=definition_data["measurement_unit_qualifier"], + ) + ) + else: + self.fields["measurement_unit_qualifier"].empty_label = ( + "Choose measurement unit qualifier." + ) + fields["quota_critical_threshold"].initial = decimal.Decimal( + definition_data["threshold"], + ) + fields["quota_critical"].initial = definition_data["quota_critical"] + self.fields["measurement_unit_qualifier"].help_text = ( + "A measurement unit qualifier is not always required." + ) + + def update_definition_data_in_session(self, cleaned_data): + cleaned_data.update( + { + "id": self.definition_data["id"], + }, + ) + serialized_clean_data = serialize_definition_data(cleaned_data) + self.request.session["staged_definition_data"][ + int(serialized_clean_data["id"]) - 1 + ].update(serialized_clean_data) + + def clean(self): + cleaned_data = super().clean() + self.update_definition_data_in_session(cleaned_data) diff --git a/quotas/jinja2/includes/quotas/actions.jinja b/quotas/jinja2/includes/quotas/actions.jinja index 88e8ee8c0..0428aca41 100644 --- a/quotas/jinja2/includes/quotas/actions.jinja +++ b/quotas/jinja2/includes/quotas/actions.jinja @@ -2,7 +2,7 @@