From ea91c28f5ca5ddcde22615fc47747379c49cc115 Mon Sep 17 00:00:00 2001
From: Brendan Smith
Date: Thu, 4 Apr 2024 11:54:48 +0100
Subject: [PATCH 01/25] Ensure caseworkers can view F680 applications
---
caseworker/cases/views/main.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/caseworker/cases/views/main.py b/caseworker/cases/views/main.py
index 51490fcf09..61e9cab035 100644
--- a/caseworker/cases/views/main.py
+++ b/caseworker/cases/views/main.py
@@ -285,7 +285,6 @@ def get_gifting_clearance_application(self):
def get_f680_clearance_application(self):
self.tabs = self.get_tabs()
self.tabs.insert(1, Tabs.LICENCES)
- self.tabs.append(Tabs.ADVICE)
self.slices = [
Slices.GOODS,
Slices.DESTINATIONS,
From 7b70be92a58b32182886db3d0238139a21eca443 Mon Sep 17 00:00:00 2001
From: Tomos Williams
Date: Thu, 4 Apr 2024 12:37:30 +0100
Subject: [PATCH 02/25] f680 exporter application
---
.../forms/triage_questions.py | 108 ++++++++++++++++--
exporter/apply_for_a_licence/urls.py | 1 +
exporter/apply_for_a_licence/views.py | 18 +++
.../exporter/apply_for_a_licence/test_urls.py | 4 +
4 files changed, 122 insertions(+), 9 deletions(-)
diff --git a/exporter/apply_for_a_licence/forms/triage_questions.py b/exporter/apply_for_a_licence/forms/triage_questions.py
index ca59916011..797e7890c8 100644
--- a/exporter/apply_for_a_licence/forms/triage_questions.py
+++ b/exporter/apply_for_a_licence/forms/triage_questions.py
@@ -10,15 +10,7 @@
MODQuestions,
TranshipmentQuestions,
)
-from lite_forms.components import (
- Form,
- RadioButtons,
- Option,
- Breadcrumbs,
- BackLink,
- FormGroup,
- Label,
-)
+from lite_forms.components import Form, RadioButtons, Option, Breadcrumbs, BackLink, FormGroup, Label, TextArea
from lite_forms.helpers import conditional
from django.template.loader import render_to_string
@@ -61,6 +53,7 @@ def opening_question():
),
disabled=settings.FEATURE_FLAG_ONLY_ALLOW_SIEL,
),
+ Option(key="f680", value="F680", description=("Select if f680"), disabled=False),
]
if settings.FEATURE_FLAG_ONLY_ALLOW_SIEL:
description = render_to_string("applications/use-spire-triage.html")
@@ -161,6 +154,100 @@ def export_licence_questions(request, application_type, goodstype_category=None)
return FormGroup(forms)
+def f680_licence_questions(request, application_type, goodstype_category=None):
+ forms = []
+ forms.append(reference_name_form())
+ forms.append(listcompany_or_site())
+ forms.append(clearance_to_use_open_licence())
+ # Product details
+ # product name (text input)
+ # upload tech spec (file upload)
+ # enter CLE (text input)
+ # manufacturers website (text input)
+ # Are any of the items designed or modified to use cryptographic techniques or crypto-analytic functions? (radios)
+ # [NEW] Is there a requirement to release UK MOD owned EW data or information in support of this export? (radios)
+ # End-use (text area)
+ # [NEW] Are the products MOD funded, part funded or private venture? (radios)
+ # [NEW] Are the products due to enter service into the armed forces?
+ # If yes, give details (text area)
+ # Does the product have a security grading? (radios)
+ # If yes, give details (we can re-use the existing SIELs page for this i think)
+ # [NEW] What clearance are you seeking? (radios)
+ # [NEW] Is local assembly / manufacture required? (radios)
+ # If yes give details (text area)
+ # [NEW] Is there any foreign technology or information involved in the release? (radios)
+ # If yes, give details (text area)
+ # Value (text input)
+ # Any further information (text area)
+ # Parties
+ # End-users (re-use current SIEL flow)
+ # Third parties (re-use current SIEL flow)
+ # Ultimate end-users (re-use current SIEL flow)
+ # Supporting documents (file upload)
+ # [NEW] EW data request form (file upload)
+ forms.append(goodstype_category_form())
+ # if application_type == CaseTypes.SIEL:
+ # forms.append(told_by_an_official_form())
+
+ # if goodstype_category in [GoodsTypeCategory.MILITARY, GoodsTypeCategory.UK_CONTINENTAL_SHELF]:
+ # forms.append(firearms_form())
+
+ return FormGroup(forms)
+
+
+def listcompany_or_site():
+ return Form(
+ title="Is this a List X company or site?",
+ questions=[
+ RadioButtons(
+ name="is_list_X_company",
+ options=[
+ Option(key="Site", value=False),
+ Option(
+ key="List X Company",
+ value=True,
+ components=[
+ TextArea(
+ title="other information",
+ description="",
+ name="is_list_x_company_other_information",
+ optional=True,
+ ),
+ ],
+ ),
+ Option(key="company", value="List X Company"),
+ ],
+ )
+ ],
+ )
+
+
+def clearance_to_use_open_licence():
+ return Form(
+ title="Do you require this clearance to use an open licence?",
+ questions=[
+ RadioButtons(
+ name="clearance_required",
+ options=[
+ Option(key="no", value="No"),
+ Option(
+ key="yes",
+ value="Yes",
+ components=[
+ TextArea(
+ title="if yes, provide licence name and details",
+ description="",
+ name="licence_details",
+ optional=True,
+ ),
+ ],
+ ),
+ ],
+ )
+ ],
+ )
+
+
def goodstype_category_form(application_id=None):
return Form(
title=ExportLicenceQuestions.OpenLicenceCategoryQuestion.TITLE,
@@ -279,3 +366,6 @@ def MOD_questions(application_type=None):
reference_name_form(),
]
)
+
+
+# def
diff --git a/exporter/apply_for_a_licence/urls.py b/exporter/apply_for_a_licence/urls.py
index 4c6d51edf2..55aa5df8db 100644
--- a/exporter/apply_for_a_licence/urls.py
+++ b/exporter/apply_for_a_licence/urls.py
@@ -9,6 +9,7 @@
urlpatterns = [
path("", views.LicenceType.as_view(), name="start"),
path("export/", views.ExportLicenceQuestions.as_view(), name="export_licence_questions"),
+ path("f680/", views.F680LicenceQuestions.as_view(), name="f680_questions"),
]
if not settings.FEATURE_FLAG_ONLY_ALLOW_SIEL:
diff --git a/exporter/apply_for_a_licence/views.py b/exporter/apply_for_a_licence/views.py
index bf71d3098d..e446f1355a 100644
--- a/exporter/apply_for_a_licence/views.py
+++ b/exporter/apply_for_a_licence/views.py
@@ -12,6 +12,7 @@
export_licence_questions,
MOD_questions,
transhipment_questions,
+ f680_licence_questions,
)
from exporter.apply_for_a_licence.validators import validate_opening_question, validate_open_general_licences
from exporter.core.constants import PERMANENT, CaseTypes
@@ -55,6 +56,23 @@ def get_success_url(self):
return reverse_lazy("applications:task_list", kwargs={"pk": pk})
+class F680LicenceQuestions(LoginRequiredMixin, MultiFormView):
+ def init(self, request, **kwargs):
+ self.forms = f680_licence_questions(request, None)
+ self.action = post_applications
+ self.data = {"application_type": CaseTypes.F680}
+
+ def on_submission(self, request, **kwargs):
+ copied_req = request.POST.copy()
+ self.forms = f680_licence_questions(
+ request, copied_req.get("application_type"), copied_req.get("goodstype_category")
+ )
+
+ def get_success_url(self):
+ pk = self.get_validated_data()["id"]
+ return reverse_lazy("applications:task_list", kwargs={"pk": pk})
+
+
class TranshipmentQuestions(LoginRequiredMixin, MultiFormView):
def init(self, request, **kwargs):
self.forms = transhipment_questions(request)
diff --git a/unit_tests/exporter/apply_for_a_licence/test_urls.py b/unit_tests/exporter/apply_for_a_licence/test_urls.py
index 81ceb10982..ff0b3e6280 100644
--- a/unit_tests/exporter/apply_for_a_licence/test_urls.py
+++ b/unit_tests/exporter/apply_for_a_licence/test_urls.py
@@ -18,6 +18,7 @@ def test_url_respects_feature_flag_off(settings):
# then SIEL and start url are found
reverse("apply_for_a_licence:start")
reverse("apply_for_a_licence:export_licence_questions")
+ reverse("apply_for_a_licence:f680_questions")
# but non SIEL urls are not found
with pytest.raises(NoReverseMatch):
@@ -38,6 +39,9 @@ def test_url_respects_feature_flag_on(settings):
reverse("apply_for_a_licence:start")
reverse("apply_for_a_licence:export_licence_questions")
+ # f680 questions work
+ reverse("apply_for_a_licence:f680_questions")
+
# and non SIEL urls are not found
reverse("apply_for_a_licence:transhipment_questions")
reverse("apply_for_a_licence:mod_questions")
From b307d81cf8e7ae28e6f2fcf9cef743cabee4193f Mon Sep 17 00:00:00 2001
From: Tomos Williams
Date: Thu, 4 Apr 2024 13:09:40 +0100
Subject: [PATCH 03/25] temporarily remove all questions to allow creation of
basic application for anyone on the branch
---
exporter/apply_for_a_licence/forms/triage_questions.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/exporter/apply_for_a_licence/forms/triage_questions.py b/exporter/apply_for_a_licence/forms/triage_questions.py
index 797e7890c8..0a4eb931d1 100644
--- a/exporter/apply_for_a_licence/forms/triage_questions.py
+++ b/exporter/apply_for_a_licence/forms/triage_questions.py
@@ -157,8 +157,8 @@ def export_licence_questions(request, application_type, goodstype_category=None)
def f680_licence_questions(request, application_type, goodstype_category=None):
forms = []
forms.append(reference_name_form())
- forms.append(listcompany_or_site())
- forms.append(clearance_to_use_open_licence())
+ # forms.append(listcompany_or_site())
+ # forms.append(clearance_to_use_open_licence())
# Product details
# product name (text input)
# upload tech spec (file upload)
@@ -185,7 +185,7 @@ def f680_licence_questions(request, application_type, goodstype_category=None):
# Ultimate end-users (re-use current SIEL flow)
# Supporting documents (file upload)
# [NEW] EW data request form (file upload)
- forms.append(goodstype_category_form())
+ # forms.append(goodstype_category_form())
# if application_type == CaseTypes.SIEL:
# forms.append(told_by_an_official_form())
From 69c42ac0323eeda1c0ced4cf8ba36aea424f4521 Mon Sep 17 00:00:00 2001
From: Tomos Williams
Date: Thu, 4 Apr 2024 13:12:41 +0100
Subject: [PATCH 04/25] clean up some comments
---
.../forms/triage_questions.py | 91 -------------------
1 file changed, 91 deletions(-)
diff --git a/exporter/apply_for_a_licence/forms/triage_questions.py b/exporter/apply_for_a_licence/forms/triage_questions.py
index 0a4eb931d1..58e926b736 100644
--- a/exporter/apply_for_a_licence/forms/triage_questions.py
+++ b/exporter/apply_for_a_licence/forms/triage_questions.py
@@ -157,97 +157,9 @@ def export_licence_questions(request, application_type, goodstype_category=None)
def f680_licence_questions(request, application_type, goodstype_category=None):
forms = []
forms.append(reference_name_form())
- # forms.append(listcompany_or_site())
- # forms.append(clearance_to_use_open_licence())
- # Product details
- # product name (text input)
- # upload tech spec (file upload)
- # enter CLE (text input)
- # manufacturers website (text input)
- # Are any of the items designed or modified to use cryptographic techniques or crypto-analytic functions? (radios)
- # [NEW] Is there a requirement to release UK MOD owned EW data or information in support of this export? (radios)
- # End-use (text area)
- # [NEW] Are the products MOD funded, part funded or private venture? (radios)
- # [NEW] Are the products due to enter service into the armed forces?
- # If yes, give details (text area)
- # Does the product have a security grading? (radios)
- # If yes, give details (we can re-use the existing SIELs page for this i think)
- # [NEW] What clearance are you seeking? (radios)
- # [NEW] Is local assembly / manufacture required? (radios)
- # If yes give details (text area)
- # [NEW] Is there any foreign technology or information involved in the release? (radios)
- # If yes, give details (text area)
- # Value (text input)
- # Any further information (text area)
- # Parties
- # End-users (re-use current SIEL flow)
- # Third parties (re-use current SIEL flow)
- # Ultimate end-users (re-use current SIEL flow)
- # Supporting documents (file upload)
- # [NEW] EW data request form (file upload)
- # forms.append(goodstype_category_form())
- # if application_type == CaseTypes.SIEL:
- # forms.append(told_by_an_official_form())
-
- # if goodstype_category in [GoodsTypeCategory.MILITARY, GoodsTypeCategory.UK_CONTINENTAL_SHELF]:
- # forms.append(firearms_form())
-
return FormGroup(forms)
-def listcompany_or_site():
- return Form(
- title="Is this a List X company or site?",
- questions=[
- RadioButtons(
- name="is_list_X_company",
- options=[
- Option(key="Site", value=False),
- Option(
- key="List X Company",
- value=True,
- components=[
- TextArea(
- title="other information",
- description="",
- name="is_list_x_company_other_information",
- optional=True,
- ),
- ],
- ),
- Option(key="company", value="List X Company"),
- ],
- )
- ],
- )
-
-
-def clearance_to_use_open_licence():
- return Form(
- title="Do you require this clearance to use an open licence?",
- questions=[
- RadioButtons(
- name="clearance_required",
- options=[
- Option(key="no", value="No"),
- Option(
- key="yes",
- value="Yes",
- components=[
- TextArea(
- title="if yes, provide licence name and details",
- description="",
- name="licence_details",
- optional=True,
- ),
- ],
- ),
- ],
- )
- ],
- )
-
-
def goodstype_category_form(application_id=None):
return Form(
title=ExportLicenceQuestions.OpenLicenceCategoryQuestion.TITLE,
@@ -366,6 +278,3 @@ def MOD_questions(application_type=None):
reference_name_form(),
]
)
-
-
-# def
From 15044e6762fdd0ea5aff2e3cc16cf27aaf0b8969 Mon Sep 17 00:00:00 2001
From: Arun Siluvery
Date: Thu, 4 Apr 2024 14:27:09 +0100
Subject: [PATCH 05/25] Fix failing test
---
exporter/apply_for_a_licence/forms/triage_questions.py | 2 +-
.../apply_for_a_licence/forms/test_triage_questions.py | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/exporter/apply_for_a_licence/forms/triage_questions.py b/exporter/apply_for_a_licence/forms/triage_questions.py
index 58e926b736..20a5ab7606 100644
--- a/exporter/apply_for_a_licence/forms/triage_questions.py
+++ b/exporter/apply_for_a_licence/forms/triage_questions.py
@@ -10,7 +10,7 @@
MODQuestions,
TranshipmentQuestions,
)
-from lite_forms.components import Form, RadioButtons, Option, Breadcrumbs, BackLink, FormGroup, Label, TextArea
+from lite_forms.components import Form, RadioButtons, Option, Breadcrumbs, BackLink, FormGroup, Label
from lite_forms.helpers import conditional
from django.template.loader import render_to_string
diff --git a/unit_tests/exporter/apply_for_a_licence/forms/test_triage_questions.py b/unit_tests/exporter/apply_for_a_licence/forms/test_triage_questions.py
index 2727ea991b..8aa85cae7e 100644
--- a/unit_tests/exporter/apply_for_a_licence/forms/test_triage_questions.py
+++ b/unit_tests/exporter/apply_for_a_licence/forms/test_triage_questions.py
@@ -7,8 +7,8 @@
@pytest.mark.parametrize(
"value,expect_enabled,expect_disabled",
(
- (True, ["export_licence"], ["transhipment", "trade_control_licence", "mod"]),
- (False, ["export_licence", "transhipment", "trade_control_licence", "mod"], []),
+ (True, ["export_licence", "f680"], ["transhipment", "trade_control_licence", "mod"]),
+ (False, ["export_licence", "transhipment", "trade_control_licence", "mod", "f680"], []),
),
)
def test_opening_question_feature_flag(settings, value, expect_enabled, expect_disabled):
From 31ef0fbdc325f6c8403a3f7ebadd89f1007e91e8 Mon Sep 17 00:00:00 2001
From: Arun Siluvery
Date: Thu, 4 Apr 2024 19:31:10 +0000
Subject: [PATCH 06/25] Add additional info questions for F680 application
This section uses lite-forms. It is currently using a SummaryListView which
is not working so changed it to use MultiFormView.
The issue with SummaryListView is that after submitting to the last step
we are going back to first step again instead of redirecting.
Field names updated to match the model in API.
---
exporter/applications/constants.py | 18 ++++----
exporter/applications/forms/questions.py | 43 ++++++++-----------
exporter/applications/views/questions.py | 17 +-------
.../lite_exporter_frontend/applications.py | 12 +++---
4 files changed, 35 insertions(+), 55 deletions(-)
diff --git a/exporter/applications/constants.py b/exporter/applications/constants.py
index b122f62517..ca54b31e92 100644
--- a/exporter/applications/constants.py
+++ b/exporter/applications/constants.py
@@ -27,19 +27,17 @@ class F680:
"prospect_value",
]
REQUIRED_FIELDS = [
- "expedited",
- "foreign_technology",
- "locally_manufactured",
- "mtcr_type",
- "electronic_warfare_requirement",
- "uk_service_equipment",
- "prospect_value",
+ "exceptional_circumstances",
+ "foreign_technology_information",
+ "is_local_assembly_manufacture",
+ "product_mtcr_rating_type",
+ "ew_data",
+ "armed_forces_usage",
]
REQUIRED_SECONDARY_FIELDS = {
- "foreign_technology": "foreign_technology_description",
- "expedited": "expedited_date",
- "locally_manufactured": "locally_manufactured_description",
+ "foreign_technology_information": "foreign_technology_information_details",
+ "is_local_assembly_manufacture": "is_local_assembly_manufacture_details",
}
diff --git a/exporter/applications/forms/questions.py b/exporter/applications/forms/questions.py
index a7be9cbcf7..db65d10ea2 100644
--- a/exporter/applications/forms/questions.py
+++ b/exporter/applications/forms/questions.py
@@ -1,6 +1,6 @@
from lite_content.lite_exporter_frontend import generic
from lite_content.lite_exporter_frontend.applications import F680Questions
-from lite_forms.components import FormGroup, Form, RadioButtons, Option, TextArea, DateInput, CurrencyInput, Label
+from lite_forms.components import FormGroup, Form, RadioButtons, Option, TextArea, CurrencyInput, Label
def questions_forms():
@@ -13,7 +13,6 @@ def questions_forms():
electronic_warfare_form(),
uk_service_equipment_form(),
uk_service_equipment_type_form(),
- prospect_value_form(),
],
)
@@ -24,14 +23,11 @@ def expedited_form():
title=F680Questions.Expedited.TITLE,
questions=[
RadioButtons(
- name="expedited",
+ name="exceptional_circumstances",
options=[
Option(
key=True,
value="Yes",
- components=[
- DateInput(title=F680Questions.Expedited.DATE, name="expedited_date", prefix=""),
- ],
),
Option(key=False, value="No"),
],
@@ -47,7 +43,7 @@ def foreign_technology_form():
title=F680Questions.ForeignTechnology.TITLE,
questions=[
RadioButtons(
- name="foreign_technology",
+ name="foreign_technology_information",
options=[
Option(
key=True,
@@ -55,7 +51,7 @@ def foreign_technology_form():
components=[
TextArea(
title=F680Questions.ForeignTechnology.PROVIDE_DETAILS,
- name="foreign_technology_description",
+ name="foreign_technology_information_details",
description=F680Questions.ForeignTechnology.DESCRIPTION,
extras={"max_length": 2200},
optional=False,
@@ -76,7 +72,7 @@ def locally_manufactured_form():
title=F680Questions.LocallyManufactured.TITLE,
questions=[
RadioButtons(
- name="locally_manufactured",
+ name="is_local_assembly_manufacture",
options=[
Option(
key=True,
@@ -84,7 +80,7 @@ def locally_manufactured_form():
components=[
TextArea(
title=F680Questions.LocallyManufactured.PROVIDE_DETAILS,
- name="locally_manufactured_description",
+ name="is_local_assembly_manufacture_details",
extras={"max_length": 2200},
optional=False,
),
@@ -104,12 +100,12 @@ def mtcr_form():
title=F680Questions.MtcrType.TITLE,
questions=[
RadioButtons(
- name="mtcr_type",
+ name="product_mtcr_rating_type",
options=[
- Option(key="mtcr_category_1", value=F680Questions.MtcrType.Categories.ONE),
- Option(key="mtcr_category_2", value=F680Questions.MtcrType.Categories.TWO),
- Option(key="none", value=F680Questions.MtcrType.Categories.NO),
- Option(key="unknown", value=F680Questions.MtcrType.Categories.I_DONT_KNOW, show_or=True),
+ Option(key="YES_MTCR_CAT1", value=F680Questions.MtcrType.Categories.ONE),
+ Option(key="YES_MTCR_CAT2", value=F680Questions.MtcrType.Categories.TWO),
+ Option(key="NO", value=F680Questions.MtcrType.Categories.NO),
+ Option(key="DONT_KNOW", value=F680Questions.MtcrType.Categories.I_DONT_KNOW, show_or=True),
],
)
],
@@ -123,7 +119,7 @@ def electronic_warfare_form():
title=F680Questions.EWRequirement.TITLE,
questions=[
RadioButtons(
- name="electronic_warfare_requirement",
+ name="ew_data",
options=[
Option(
key=True,
@@ -151,7 +147,7 @@ def uk_service_equipment_form():
title=F680Questions.UKServiceEquipment.TITLE,
questions=[
RadioButtons(
- name="uk_service_equipment",
+ name="armed_forces_usage",
options=[
Option(
key=True,
@@ -159,7 +155,7 @@ def uk_service_equipment_form():
components=[
TextArea(
title=F680Questions.UKServiceEquipment.PROVIDE_DETAILS_OPTIONAL,
- name="uk_service_equipment_description",
+ name="armed_forces_usage_details",
extras={"max_length": 2200},
)
],
@@ -178,16 +174,15 @@ def uk_service_equipment_type_form():
title=F680Questions.UKServiceEquipment.TYPE,
questions=[
RadioButtons(
- name="uk_service_equipment_type",
+ name="product_funding",
options=[
- Option(key="mod_funded", value=F680Questions.UKServiceEquipment.Types.MOD_FUNDED),
- Option(
- key="part_mod_part_venture", value=F680Questions.UKServiceEquipment.Types.MOD_VENTURE_FUNDED
- ),
- Option(key="private_venture", value=F680Questions.UKServiceEquipment.Types.PRIVATE_VENTURE),
+ Option(key="MOD", value=F680Questions.UKServiceEquipment.Types.MOD_FUNDED),
+ Option(key="PART_MOD", value=F680Questions.UKServiceEquipment.Types.MOD_VENTURE_FUNDED),
+ Option(key="PRIVATE_VENTURE", value=F680Questions.UKServiceEquipment.Types.PRIVATE_VENTURE),
],
),
],
+ default_button_name=generic.SAVE_AND_CONTINUE,
)
diff --git a/exporter/applications/views/questions.py b/exporter/applications/views/questions.py
index 760316e3c7..4730520991 100644
--- a/exporter/applications/views/questions.py
+++ b/exporter/applications/views/questions.py
@@ -6,21 +6,12 @@
from exporter.applications.constants import F680
from exporter.applications.forms.questions import questions_forms
from exporter.applications.services import put_application, get_application
-from exporter.core.helpers import str_to_bool
-from lite_content.lite_exporter_frontend import applications
-from lite_forms.views import SummaryListFormView
+from lite_forms.views import MultiFormView
from core.auth.views import LoginRequiredMixin
def questions_action(request, pk, data):
- if str_to_bool(data.get("expedited", False)):
- if "year" in data and "month" in data and "day" in data:
- data["expedited_date"] = f"{data['year']}-{str(data['month']).zfill(2)}-{str(data['day']).zfill(2)}"
-
- else:
- if "expedited_date" in data:
- del data["expedited_date"]
empty_keys = []
for key in data:
try:
@@ -36,17 +27,13 @@ def questions_action(request, pk, data):
return put_application(request, pk, data)
-class AdditionalInformationFormView(LoginRequiredMixin, SummaryListFormView):
+class AdditionalInformationFormView(LoginRequiredMixin, MultiFormView):
def init(self, request, **kwargs):
self.object_pk = str(kwargs["pk"])
self.data = self.get_additional_information(request, self.object_pk)
self.forms = questions_forms()
self.action = questions_action
self.success_url = reverse_lazy("applications:task_list", kwargs={"pk": self.object_pk})
- self.summary_list_title = applications.F680ClearanceTaskList.ADDITIONAL_INFORMATION
- self.summary_list_notice_title = applications.F680ClearanceTaskList.NOTICE_TITLE
- self.summary_list_notice_text = applications.F680ClearanceTaskList.NOTICE_TEXT
- self.summary_list_button = applications.F680ClearanceTaskList.SAVE_AND_RETURN
self.validate_only_until_final_submission = False
def prettify_data(self, data):
diff --git a/lite_content/lite_exporter_frontend/applications.py b/lite_content/lite_exporter_frontend/applications.py
index 890e5f6fee..7bf07c7d09 100644
--- a/lite_content/lite_exporter_frontend/applications.py
+++ b/lite_content/lite_exporter_frontend/applications.py
@@ -522,13 +522,13 @@ class MtcrType:
TITLE = "Do you believe the products are rated under the Missile Technology Control Regime (MTCR)"
class Categories:
- ONE = "Yes, Category 1"
- TWO = "Yes, Category 2"
+ ONE = "Yes - MTCR Cat 1"
+ TWO = "Yes - MTCR Cat 2"
NO = "No"
- I_DONT_KNOW = "I don't know"
+ I_DONT_KNOW = "Don't know"
class EWRequirement:
- TITLE = "Is there is a requirement to release UK MOD owned electronic warfare (EW) data or information in support of this export"
+ TITLE = "Is there is a requirement to release UK MOD owned EW data or information in support of this export"
ATTACHMENT = (
"You need to complete part A of the MOD EW Data Release Capture Form "
"and attach it to the application in the supporting documents section."
@@ -540,8 +540,8 @@ class UKServiceEquipment:
PROVIDE_DETAILS_OPTIONAL = "Provide details (optional)"
class Types:
- MOD_FUNDED = "MOD funded"
- MOD_VENTURE_FUNDED = "Part MOD funded / part private venture"
+ MOD_FUNDED = "MOD"
+ MOD_VENTURE_FUNDED = "Part MOD"
PRIVATE_VENTURE = "Private venture"
class ProspectValue:
From 2c3d43ce6a43c2c99751eb632424e3be3077188f Mon Sep 17 00:00:00 2001
From: Arun Siluvery
Date: Thu, 4 Apr 2024 20:09:10 +0000
Subject: [PATCH 07/25] Add required clearances types question for F680
application
---
exporter/applications/constants.py | 27 ++++++++++---------
exporter/applications/forms/f680_details.py | 2 +-
exporter/applications/services.py | 2 +-
exporter/applications/views/f680_details.py | 3 ++-
.../templates/applications/task-list.html | 2 +-
5 files changed, 19 insertions(+), 17 deletions(-)
diff --git a/exporter/applications/constants.py b/exporter/applications/constants.py
index ca54b31e92..c1e708fbe9 100644
--- a/exporter/applications/constants.py
+++ b/exporter/applications/constants.py
@@ -12,19 +12,17 @@ class ApplicationStatus:
class F680:
FIELDS = [
- "expedited",
- "expedited_date",
- "expedited_description",
- "foreign_technology",
- "foreign_technology_description",
- "locally_manufactured",
- "locally_manufactured_description",
- "mtcr_type",
- "electronic_warfare_requirement",
- "uk_service_equipment",
- "uk_service_equipment_description",
- "uk_service_equipment_type",
- "prospect_value",
+ "exceptional_circumstances",
+ "foreign_technology_information",
+ "foreign_technology_information_details",
+ "is_local_assembly_manufacture",
+ "is_local_assembly_manufacture_details",
+ "product_mtcr_rating_type",
+ "product_mtcr_rating_type_details",
+ "ew_data",
+ "product_funding",
+ "armed_forces_usage",
+ "armed_forces_usage_details",
]
REQUIRED_FIELDS = [
"exceptional_circumstances",
@@ -33,11 +31,14 @@ class F680:
"product_mtcr_rating_type",
"ew_data",
"armed_forces_usage",
+ "product_funding",
]
REQUIRED_SECONDARY_FIELDS = {
"foreign_technology_information": "foreign_technology_information_details",
"is_local_assembly_manufacture": "is_local_assembly_manufacture_details",
+ "product_mtcr_rating_type": "product_mtcr_rating_type_details",
+ "armed_forces_usage": "armed_forces_usage_details",
}
diff --git a/exporter/applications/forms/f680_details.py b/exporter/applications/forms/f680_details.py
index 5b5037a800..9a14c55fe4 100644
--- a/exporter/applications/forms/f680_details.py
+++ b/exporter/applications/forms/f680_details.py
@@ -11,7 +11,7 @@ def f680_details_form(request, application_id):
description="",
questions=[
Checkboxes(
- name="types[]",
+ name="clearances[]",
options=[Option(key, value) for key, value in get_f680_clearance_types(request).items()],
),
],
diff --git a/exporter/applications/services.py b/exporter/applications/services.py
index c4146c385d..31ad233bcf 100644
--- a/exporter/applications/services.py
+++ b/exporter/applications/services.py
@@ -98,7 +98,7 @@ def put_temporary_export_details(request, pk, json):
def put_application_with_clearance_types(request, pk, json):
# Inject the clearance types as an empty set into JSON if they are not present
- json["types"] = json.get("types", [])
+ json["clearances"] = json.get("clearances", [])
data = client.put(request, f"/applications/{pk}", json)
return data.json(), data.status_code
diff --git a/exporter/applications/views/f680_details.py b/exporter/applications/views/f680_details.py
index 7ce009a960..5d25f38215 100644
--- a/exporter/applications/views/f680_details.py
+++ b/exporter/applications/views/f680_details.py
@@ -13,5 +13,6 @@ def init(self, request, **kwargs):
application = get_application(request, self.object_pk)
self.form = f680_details_form(request, self.object_pk)
self.action = put_application_with_clearance_types
- self.data = {"types": [f680_clearance_type["name"]["key"] for f680_clearance_type in application["types"]]}
+ clearances = application["clearances"] or []
+ self.data = {"clearances": [f680_clearance_type["key"] for f680_clearance_type in clearances]}
self.success_url = reverse_lazy("applications:task_list", kwargs={"pk": self.object_pk})
diff --git a/exporter/templates/applications/task-list.html b/exporter/templates/applications/task-list.html
index 4d4efa0569..4a7b5e9025 100644
--- a/exporter/templates/applications/task-list.html
+++ b/exporter/templates/applications/task-list.html
@@ -63,7 +63,7 @@
{% if not is_editing or edit_type == 'major_edit' %}
{% url 'applications:f680_details' application.id as f680_details_url %}
{% tld f680detail 'f680detail' 'f680details' as f680_details %}
- {% include "includes/task-list-item.html" with id='types' title=strings.F680_DETAILS url=f680_details_url status=application.types|task_list_item_status description=f680_details %}
+ {% include "includes/task-list-item.html" with id='types' title=strings.F680_DETAILS url=f680_details_url status=application.clearances|task_list_item_status description=f680_details %}
{% endif %}
{% endif %}
From b89f880381b5dfb3c7e3d422df7dc7d85f68bbad Mon Sep 17 00:00:00 2001
From: Arun Siluvery
Date: Fri, 5 Apr 2024 13:53:05 +0100
Subject: [PATCH 08/25] Add product details to F680 application
---
core/summaries/formatters.py | 30 ++++
core/summaries/reducers.py | 21 +++
core/summaries/summaries.py | 53 ++++++
exporter/applications/summaries/f680.py | 31 ++++
exporter/applications/urls.py | 1 +
.../views/goods/common/constants.py | 1 +
.../applications/views/goods/f680/urls.py | 23 +++
.../views/goods/f680/views/add.py | 156 ++++++++++++++++++
.../views/goods/f680/views/constants.py | 11 ++
.../views/goods/f680/views/payloads.py | 26 +++
.../views/goods/f680/views/summary.py | 75 +++++++++
.../applications/views/parties/end_users.py | 4 +
exporter/goods/forms/common.py | 21 +++
.../f680/product-on-application-summary.html | 56 +++++++
.../goods/f680/product-summary.html | 38 +++++
.../templates/applications/goods/index.html | 14 +-
16 files changed, 557 insertions(+), 4 deletions(-)
create mode 100644 exporter/applications/summaries/f680.py
create mode 100644 exporter/applications/views/goods/f680/urls.py
create mode 100644 exporter/applications/views/goods/f680/views/add.py
create mode 100644 exporter/applications/views/goods/f680/views/constants.py
create mode 100644 exporter/applications/views/goods/f680/views/payloads.py
create mode 100644 exporter/applications/views/goods/f680/views/summary.py
create mode 100644 exporter/templates/applications/goods/f680/product-on-application-summary.html
create mode 100644 exporter/templates/applications/goods/f680/product-summary.html
diff --git a/core/summaries/formatters.py b/core/summaries/formatters.py
index 25016dceb6..c6a4dd7baf 100644
--- a/core/summaries/formatters.py
+++ b/core/summaries/formatters.py
@@ -715,6 +715,36 @@ def document_formatter(document, url, link_text=None):
}
+F680_GOOD_DETAILS_FORMATTERS = {
+ "is-good-controlled": key_value_formatter,
+ "control-list-entries": comma_separated_list(itemgetter("rating")),
+ "is-pv-graded": mapping_formatter(
+ {
+ "yes": "Yes",
+ "no": "No",
+ }
+ ),
+}
+
+F680_GOOD_DETAILS_ON_APPLICATION_FORMATTERS = {
+ "number-of-items": integer,
+ "total-value": money_formatter,
+}
+
+F680_GOOD_DETAILS_LABELS = {
+ "name": "Give the product a descriptive name",
+ "is-good-controlled": "Do you know the product's control list entry?",
+ "control-list-entries": "Enter the control list entry",
+ "is-pv-graded": "Does the product have a government security grading or classification?",
+ "product-description": "Describe the product and what it is designed to do",
+}
+
+
+F680_GOOD_DETAILS_ON_APPLICATION_LABELS = {
+ "number-of-items": "Number of items",
+ "total-value": "Total value",
+}
+
def add_edit_links(
summary,
edit_links,
diff --git a/core/summaries/reducers.py b/core/summaries/reducers.py
index 1525ee087d..553d922a63 100644
--- a/core/summaries/reducers.py
+++ b/core/summaries/reducers.py
@@ -745,3 +745,24 @@ def technology_on_application_reducer(good_on_application):
)
summary += is_onward_exported_reducer(good_on_application)
return summary
+
+
+def f680_good_details_reducer(good):
+ summary = (
+ (
+ "name",
+ good["name"],
+ ),
+ )
+ summary += is_good_controlled_reducer(good)
+ summary += is_pv_graded_reducer(good)
+
+ return summary
+
+
+def f680_good_details_on_application_reducer(good_on_application):
+ summary = (
+ ("number-of-items", good_on_application["quantity"]),
+ ("total-value", Decimal(good_on_application["value"])),
+ )
+ return summary
diff --git a/core/summaries/summaries.py b/core/summaries/summaries.py
index bbfda6eab4..9e713266e3 100644
--- a/core/summaries/summaries.py
+++ b/core/summaries/summaries.py
@@ -35,6 +35,10 @@
SOFTWARE_RELATED_TO_FIREARMS_VALUE_FORMATTERS,
TECHNOLOGY_RELATED_TO_FIREARMS_LABELS,
TECHNOLOGY_RELATED_TO_FIREARMS_VALUE_FORMATTERS,
+ F680_GOOD_DETAILS_FORMATTERS,
+ F680_GOOD_DETAILS_LABELS,
+ F680_GOOD_DETAILS_ON_APPLICATION_FORMATTERS,
+ F680_GOOD_DETAILS_ON_APPLICATION_LABELS,
)
from core.summaries.reducers import (
firearm_on_application_reducer,
@@ -52,6 +56,8 @@
component_accessory_reducer,
software_related_to_firearms_reducer,
technology_related_to_firearms_reducer,
+ f680_good_details_reducer,
+ f680_good_details_on_application_reducer,
)
from core.summaries.utils import pick_fields
@@ -813,3 +819,50 @@ def get_summary_url_for_good(good):
return reverse("goods:component_accessory_detail", kwargs={"pk": good["id"]})
raise NoSummaryForType
+
+
+F680_GOOD_DETAILS_FIELDS = (
+ "name",
+ "is-good-controlled",
+ "control-list-entries",
+ "is-pv-graded",
+ "product-description",
+)
+
+
+F680_GOOD_DETAILS_ON_APPLICATION_FIELDS = (
+ "number-of-items",
+ "total-value",
+)
+
+def f680_goods_summary(good, additional_formatters=None):
+ if not additional_formatters:
+ additional_formatters = {}
+
+ summary = f680_good_details_reducer(good)
+ formatters = {
+ **F680_GOOD_DETAILS_FORMATTERS,
+ **additional_formatters,
+ }
+ summary = pick_fields(summary, F680_GOOD_DETAILS_FIELDS)
+ summary = format_values(summary, formatters)
+ summary = add_labels(summary, F680_GOOD_DETAILS_LABELS)
+
+ return summary
+
+
+def f680_good_details_on_application_summary(good_on_application, additional_formatters=None):
+ if not additional_formatters:
+ additional_formatters = {}
+
+ summary = f680_good_details_on_application_reducer(good_on_application)
+ formatters = {
+ **F680_GOOD_DETAILS_ON_APPLICATION_FORMATTERS,
+ **additional_formatters,
+ }
+
+ summary = pick_fields(summary, F680_GOOD_DETAILS_ON_APPLICATION_FIELDS)
+ summary = format_values(summary, formatters)
+ summary = add_labels(summary, F680_GOOD_DETAILS_ON_APPLICATION_LABELS)
+
+ return summary
diff --git a/exporter/applications/summaries/f680.py b/exporter/applications/summaries/f680.py
new file mode 100644
index 0000000000..9cf3169dc6
--- /dev/null
+++ b/exporter/applications/summaries/f680.py
@@ -0,0 +1,31 @@
+from django.urls import reverse
+
+from core.summaries.formatters import (
+ add_edit_links,
+)
+from core.summaries.summaries import (
+ f680_goods_summary,
+ f680_good_details_on_application_summary as core_f680_good_details_on_application_summary,
+)
+
+
+def get_complete_item_summary_edit_link_factory(application, good):
+ def get_edit_link(name):
+ return reverse(
+ f"applications:complete_item_edit_{name}",
+ kwargs={
+ "pk": application["id"],
+ "good_pk": good["id"],
+ },
+ )
+
+ return get_edit_link
+
+
+def f680_good_details_summary(good, *args, **kwargs):
+
+ return f680_goods_summary(good)
+
+
+def f680_good_details_on_application_summary(good_on_application, *args, **kwargs):
+ return core_f680_good_details_on_application_summary(good_on_application)
diff --git a/exporter/applications/urls.py b/exporter/applications/urls.py
index dfb382b260..1258501b3d 100644
--- a/exporter/applications/urls.py
+++ b/exporter/applications/urls.py
@@ -198,6 +198,7 @@
name="is_material_substance",
),
# F680 details
+ path("/f680/good-details/", include("exporter.applications.views.goods.f680.urls")),
path("/f680-details/", f680_details.F680Details.as_view(), name="f680_details"),
path("/questions/", questions.AdditionalInformationFormView.as_view(), name="questions"),
# Goods Types
diff --git a/exporter/applications/views/goods/common/constants.py b/exporter/applications/views/goods/common/constants.py
index 017cd6be86..de9b545277 100644
--- a/exporter/applications/views/goods/common/constants.py
+++ b/exporter/applications/views/goods/common/constants.py
@@ -16,3 +16,4 @@
QUANTITY_AND_VALUE = "QUANTITY_AND_VALUE"
PART_NUMBER = "PART_NUMBER"
PRODUCT_DESCRIPTION = "PRODUCT_DESCRIPTION"
+PROSPECT_VALUE = "PROSPECT_VALUE"
diff --git a/exporter/applications/views/goods/f680/urls.py b/exporter/applications/views/goods/f680/urls.py
new file mode 100644
index 0000000000..21e00792be
--- /dev/null
+++ b/exporter/applications/views/goods/f680/urls.py
@@ -0,0 +1,23 @@
+from django.urls import path
+
+from .views import add, summary
+
+
+urlpatterns = [
+ path("add/", add.AddF680GoodDetails.as_view(), name="f680_good_details"),
+ path(
+ "/product-summary/",
+ summary.CompleteF680GoodDetailsSummary.as_view(),
+ name="f680_good_details_summary",
+ ),
+ path(
+ "/add-to-application/",
+ add.AddF680GoodDetailsToApplication.as_view(),
+ name="f680_good_details_to_application",
+ ),
+ path(
+ "/product-on-application-summary/",
+ summary.CompleteF680GoodDetailsOnApplicationSummary.as_view(),
+ name="f680_good_details_on_application_summary",
+ ),
+]
diff --git a/exporter/applications/views/goods/f680/views/add.py b/exporter/applications/views/goods/f680/views/add.py
new file mode 100644
index 0000000000..131b512b28
--- /dev/null
+++ b/exporter/applications/views/goods/f680/views/add.py
@@ -0,0 +1,156 @@
+import logging
+
+from http import HTTPStatus
+
+from django.shortcuts import redirect
+from django.urls import reverse
+
+from core.auth.views import LoginRequiredMixin
+from core.decorators import expect_status
+
+from core.wizard.views import BaseSessionWizardView
+from exporter.goods.forms.common import (
+ ProductControlListEntryForm,
+ ProductDescriptionForm,
+ ProductNameForm,
+ ProductProspectValueForm,
+)
+
+from exporter.goods.services import post_complete_item
+from exporter.applications.services import post_complete_item_good_on_application
+from exporter.applications.views.goods.common.mixins import (
+ ApplicationMixin,
+ GoodMixin,
+)
+
+from .constants import (
+ AddF680GoodDetailsSteps,
+ AddF680GoodDetailsToApplicationSteps,
+)
+from .payloads import (
+ AddF680GoodDetailsPayloadBuilder,
+ AddF680GoodDetailsToApplicationPayloadBuilder,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class AddF680GoodDetails(
+ LoginRequiredMixin,
+ ApplicationMixin,
+ BaseSessionWizardView,
+):
+ form_list = [
+ (AddF680GoodDetailsSteps.NAME, ProductNameForm),
+ (AddF680GoodDetailsSteps.PRODUCT_CONTROL_LIST_ENTRY, ProductControlListEntryForm),
+ (AddF680GoodDetailsSteps.PRODUCT_DESCRIPTION, ProductDescriptionForm),
+ ]
+
+ def get_form_kwargs(self, step=None):
+ kwargs = super().get_form_kwargs(step)
+
+ if step == AddF680GoodDetailsSteps.PRODUCT_CONTROL_LIST_ENTRY:
+ kwargs["request"] = self.request
+
+ return kwargs
+
+ def get_context_data(self, form, **kwargs):
+ ctx = super().get_context_data(form, **kwargs)
+
+ ctx["title"] = form.Layout.TITLE
+
+ return ctx
+
+ def get_payload(self, form_dict):
+ good_payload = AddF680GoodDetailsPayloadBuilder().build(form_dict)
+ good_payload["is_pv_graded"] = "no"
+ return good_payload
+
+ def get_success_url(self):
+ return reverse(
+ "applications:f680_good_details_summary",
+ kwargs={"pk": self.application["id"], "good_pk": self.good["id"]},
+ )
+
+ @expect_status(
+ HTTPStatus.CREATED,
+ "Error creating complete product",
+ "Unexpected error adding complete product",
+ )
+ def post_complete_item(self, form_dict):
+ breakpoint()
+ payload = self.get_payload(form_dict)
+
+ return post_complete_item(
+ self.request,
+ payload,
+ )
+
+ def done(self, form_list, form_dict, **kwargs):
+ good, _ = self.post_complete_item(form_dict)
+ self.good = good["good"]
+
+ return redirect(self.get_success_url())
+
+
+class AddF680GoodDetailsToApplication(
+ LoginRequiredMixin,
+ ApplicationMixin,
+ GoodMixin,
+ BaseSessionWizardView,
+):
+ form_list = [
+ (AddF680GoodDetailsToApplicationSteps.PROSPECT_VALUE, ProductProspectValueForm),
+ ]
+
+ def get_form_kwargs(self, step=None):
+ kwargs = super().get_form_kwargs(step)
+ return kwargs
+
+ def get_success_url(self):
+ return reverse(
+ "applications:f680_good_details_on_application_summary",
+ kwargs={
+ "pk": self.kwargs["pk"],
+ "good_on_application_pk": self.good_on_application["id"],
+ },
+ )
+
+ def get_payload(self, form_dict):
+ good_on_application_payload = AddF680GoodDetailsToApplicationPayloadBuilder().build(form_dict)
+ return good_on_application_payload
+
+ @expect_status(
+ HTTPStatus.CREATED,
+ "Error adding complete item to application",
+ "Unexpected error adding complete item to application",
+ )
+ def post_complete_item_to_application(self, form_dict):
+ payload = self.get_payload(form_dict)
+ return post_complete_item_good_on_application(
+ self.request,
+ self.application["id"],
+ self.good["id"],
+ payload,
+ )
+
+ def get_context_data(self, form, **kwargs):
+ ctx = super().get_context_data(form, **kwargs)
+
+ ctx["back_link_url"] = reverse(
+ "applications:f680_good_details_summary",
+ kwargs={
+ "pk": self.kwargs["pk"],
+ "good_pk": self.good["id"],
+ },
+ )
+ ctx["title"] = form.Layout.TITLE
+
+ return ctx
+
+ def done(self, form_list, form_dict, **kwargs):
+ good_on_application, _ = self.post_complete_item_to_application(form_dict)
+ good_on_application = good_on_application["good"]
+ self.good_on_application = good_on_application
+
+ return redirect(self.get_success_url())
diff --git a/exporter/applications/views/goods/f680/views/constants.py b/exporter/applications/views/goods/f680/views/constants.py
new file mode 100644
index 0000000000..122bb2fedc
--- /dev/null
+++ b/exporter/applications/views/goods/f680/views/constants.py
@@ -0,0 +1,11 @@
+from exporter.applications.views.goods.common import constants
+
+
+class AddF680GoodDetailsSteps:
+ NAME = constants.NAME
+ PRODUCT_CONTROL_LIST_ENTRY = constants.PRODUCT_CONTROL_LIST_ENTRY
+ PRODUCT_DESCRIPTION = constants.PRODUCT_DESCRIPTION
+
+
+class AddF680GoodDetailsToApplicationSteps:
+ PROSPECT_VALUE = constants.PROSPECT_VALUE
diff --git a/exporter/applications/views/goods/f680/views/payloads.py b/exporter/applications/views/goods/f680/views/payloads.py
new file mode 100644
index 0000000000..cc882be53b
--- /dev/null
+++ b/exporter/applications/views/goods/f680/views/payloads.py
@@ -0,0 +1,26 @@
+from core.wizard.payloads import MergingPayloadBuilder
+
+from .constants import AddF680GoodDetailsSteps, AddF680GoodDetailsToApplicationSteps
+from exporter.applications.views.goods.common.payloads import get_cleaned_data
+
+
+def get_prospect_value_payload(form):
+ return {
+ "quantity": 1,
+ "unit": "NAR",
+ "value": str(form.cleaned_data["value"]),
+ }
+
+
+class AddF680GoodDetailsPayloadBuilder(MergingPayloadBuilder):
+ payload_dict = {
+ AddF680GoodDetailsSteps.NAME: get_cleaned_data,
+ AddF680GoodDetailsSteps.PRODUCT_CONTROL_LIST_ENTRY: get_cleaned_data,
+ AddF680GoodDetailsSteps.PRODUCT_DESCRIPTION: get_cleaned_data,
+ }
+
+
+class AddF680GoodDetailsToApplicationPayloadBuilder(MergingPayloadBuilder):
+ payload_dict = {
+ AddF680GoodDetailsToApplicationSteps.PROSPECT_VALUE: get_prospect_value_payload,
+ }
diff --git a/exporter/applications/views/goods/f680/views/summary.py b/exporter/applications/views/goods/f680/views/summary.py
new file mode 100644
index 0000000000..6275dd5384
--- /dev/null
+++ b/exporter/applications/views/goods/f680/views/summary.py
@@ -0,0 +1,75 @@
+from django.utils.functional import cached_property
+from django.views.generic import TemplateView
+
+from core.auth.views import LoginRequiredMixin
+
+from exporter.applications.views.goods.common.mixins import (
+ ApplicationMixin,
+ GoodMixin,
+ GoodOnApplicationMixin,
+)
+from exporter.applications.summaries.f680 import (
+ f680_good_details_summary,
+ f680_good_details_on_application_summary,
+)
+from exporter.core.helpers import get_organisation_documents
+
+
+class BaseF680GoodDetailsApplicationSummary(
+ LoginRequiredMixin,
+ ApplicationMixin,
+ GoodOnApplicationMixin,
+ TemplateView,
+):
+ template_name = "applications/goods/f680/product-on-application-summary.html"
+
+ @cached_property
+ def organisation_documents(self):
+ return get_organisation_documents(self.application)
+
+ def get_f680_good_details_summary(self):
+ product_summary = f680_good_details_summary(
+ self.good,
+ )
+ return product_summary
+
+ def get_f680_good_details_on_application_summary(self):
+ product_on_application_summary = f680_good_details_on_application_summary(
+ self.good_on_application,
+ )
+ return product_on_application_summary
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+
+ return {
+ **context,
+ "application": self.application,
+ "good": self.good,
+ "good_on_application": self.good_on_application,
+ "product_summary": self.get_f680_good_details_summary(),
+ "product_on_application_summary": self.get_f680_good_details_on_application_summary(),
+ }
+
+
+class CompleteF680GoodDetailsOnApplicationSummary(BaseF680GoodDetailsApplicationSummary):
+ summary_type = "f680_good_details-on-application-summary"
+
+
+class CompleteF680GoodDetailsSummary(
+ LoginRequiredMixin,
+ ApplicationMixin,
+ GoodMixin,
+ TemplateView,
+):
+ template_name = "applications/goods/f680/product-summary.html"
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["application_id"] = self.application["id"]
+ context["good"] = self.good
+
+ summary = f680_good_details_summary(self.good)
+
+ context["summary"] = summary
+ return context
diff --git a/exporter/applications/views/parties/end_users.py b/exporter/applications/views/parties/end_users.py
index a4eba248ad..a43cf02712 100644
--- a/exporter/applications/views/parties/end_users.py
+++ b/exporter/applications/views/parties/end_users.py
@@ -164,6 +164,10 @@ def done(self, form_list, **kwargs):
party_eng_translation_document = all_data.pop("party_eng_translation_document", None)
party_letterhead_document = all_data.pop("party_letterhead_document", None)
+ # F680 - clearance level is mandatory
+ if self.application["case_type"]["sub_type"]["key"] == "f680_clearance":
+ all_data["clearance_level"] = self.application["clearance_level"]
+
response, status_code = post_party(self.request, self.kwargs["pk"], dict(all_data))
if status_code != HTTPStatus.CREATED:
log.error(
diff --git a/exporter/goods/forms/common.py b/exporter/goods/forms/common.py
index b99ff58152..cb31f2e1b8 100644
--- a/exporter/goods/forms/common.py
+++ b/exporter/goods/forms/common.py
@@ -747,3 +747,24 @@ class Layout:
def get_layout_fields(self):
return ("product_description",)
+
+
+class ProductProspectValueForm(BaseForm):
+ class Layout:
+ TITLE = "What is the estimated value of the Prospect?"
+
+ value = forms.DecimalField(
+ decimal_places=2,
+ error_messages={
+ "invalid": "Total value must be a number, like 16.32",
+ "required": "Enter the total value",
+ "max_decimal_places": "Total value must not be more than 2 decimals",
+ "min_value": "Total value must be 0.01 or more",
+ },
+ label="Total value",
+ min_value=Decimal("0.01"),
+ widget=forms.TextInput,
+ )
+
+ def get_layout_fields(self):
+ return (Prefixed("£", "value", css_class="govuk-input--width-10 input-force-default-width"),)
diff --git a/exporter/templates/applications/goods/f680/product-on-application-summary.html b/exporter/templates/applications/goods/f680/product-on-application-summary.html
new file mode 100644
index 0000000000..44bc831e1e
--- /dev/null
+++ b/exporter/templates/applications/goods/f680/product-on-application-summary.html
@@ -0,0 +1,56 @@
+{% extends 'layouts/base.html' %}
+
+{% load date firearm_details firearm_details_summary %}
+
+{% block back_link %}
+{% endblock %}
+
+{% block body %}
+
+
+
+ {% block title %}
+ '{{ good.name }}' has been added to the application
+ {% endblock %}
+
+
+
+
+
+ {% for key, value, label in product_on_application_summary %}
+
+
-
+ {{ label }}
+
+
-
+ {{ value }}
+
+
-
+ {% if edit_link %}
+ Change
+ {% endif %}
+
+
+ {% endfor %}
+
+
+
+ Continue
+
+
+ Saved product details for '{{ good.name }}'
+
+ {% for key, value, label in product_summary %}
+
+
-
+ {{ label }}
+
+ -
+ {{ value }}
+
+ -
+
+
+ {% endfor %}
+
+{% endblock %}
diff --git a/exporter/templates/applications/goods/f680/product-summary.html b/exporter/templates/applications/goods/f680/product-summary.html
new file mode 100644
index 0000000000..543b51e638
--- /dev/null
+++ b/exporter/templates/applications/goods/f680/product-summary.html
@@ -0,0 +1,38 @@
+{% extends 'layouts/base.html' %}
+
+{% load date firearm_details_summary %}
+
+{% block back_link %}
+{% endblock %}
+
+{% block body %}
+
+
+
+ {% block title %}'{{ good.name }}' has been saved{% endblock %}
+
+
This item has been saved to your product list so that you can reuse it for future
+ applications.
+
+
+
+
+ {% for key, value, label in summary %}
+
+
-
+ {{ label }}
+
+
-
+ {{ value }}
+
+
-
+ {% if edit_link %}
+ Change
+ {% endif %}
+
+
+ {% endfor %}
+
+
+ Continue
+{% endblock %}
diff --git a/exporter/templates/applications/goods/index.html b/exporter/templates/applications/goods/index.html
index 852966822c..d8b8b0f199 100644
--- a/exporter/templates/applications/goods/index.html
+++ b/exporter/templates/applications/goods/index.html
@@ -21,10 +21,16 @@
{% if application.status.key in 'applicant_editing,draft' %}
-
+ {% if application.sub_type == 'f680_clearance' %}
+
+ {% else %}
+
+ {% endif %}
{% endif %}
{% if goods %}
From 8da332a2ef1a4cfa6dbb29fc66c01591513e95a5 Mon Sep 17 00:00:00 2001
From: Brendan Smith
Date: Thu, 4 Apr 2024 15:45:41 +0100
Subject: [PATCH 09/25] Ensure F680 case detail page shows correct tabs
---
caseworker/activities/views.py | 2 +-
caseworker/advice/views/views.py | 2 +-
caseworker/cases/views/main.py | 24 ++++++++++++++++++++++--
3 files changed, 24 insertions(+), 4 deletions(-)
diff --git a/caseworker/activities/views.py b/caseworker/activities/views.py
index 288f715c40..cd84bc4e4b 100644
--- a/caseworker/activities/views.py
+++ b/caseworker/activities/views.py
@@ -99,7 +99,7 @@ def get_context_data(self, **kwargs):
"filtering_by": list(self.request.GET.keys()),
"queue": self.queue,
"team_filters": self.get_team_filters(),
- "tabs": self.get_standard_application_tabs(),
+ "tabs": self.get_tabs_by_case_type(self.case.sub_type),
"current_tab": "cases:activities:notes-and-timeline",
"activities": get_activity(self.request, self.case_id, activity_filters=self.request.GET),
}
diff --git a/caseworker/advice/views/views.py b/caseworker/advice/views/views.py
index 67a258e737..e6a7cadb77 100644
--- a/caseworker/advice/views/views.py
+++ b/caseworker/advice/views/views.py
@@ -319,7 +319,7 @@ def get_context(self, **kwargs):
"security_approvals_classified_display": self.security_approvals_classified_display,
"assessed_trigger_list_goods": self.assessed_trigger_list_goods,
"unassessed_trigger_list_goods": self.unassessed_trigger_list_goods,
- "tabs": self.get_standard_application_tabs(),
+ "tabs": self.get_tabs_by_case_type(self.case.sub_type),
"current_tab": "cases:advice_view",
**services.get_advice_tab_context(
self.case,
diff --git a/caseworker/cases/views/main.py b/caseworker/cases/views/main.py
index 61e9cab035..a962f7cb20 100644
--- a/caseworker/cases/views/main.py
+++ b/caseworker/cases/views/main.py
@@ -116,6 +116,27 @@ def get_standard_application_tabs(self):
return tabs
+ def get_f680_application_tabs(self):
+ tabs = [
+ Tabs.QUICK_SUMMARY,
+ Tabs.DETAILS,
+ Tabs.ECJU_QUERIES,
+ Tabs.DOCUMENTS,
+ ]
+ tabs.append(self.get_notes_and_timelines_tab())
+ tabs.append(self.get_advice_tab())
+
+ return tabs
+
+ def get_tabs_by_case_type(self, case_type):
+ # TODO: this is pretty naff - the problem is that the place we choose tabs based on case type
+ # is over in cases.helpers.case:CaseView. We need to make this more DRY.
+ tabs_by_case_type = {
+ "f680_clearance": self.get_f680_application_tabs(),
+ "standard": self.get_standard_application_tabs(),
+ }
+ return tabs_by_case_type[case_type]
+
def get_advice_tab(self):
data, _ = get_gov_user(self.request, str(self.request.session["lite_api_user_id"]))
return Tab(
@@ -283,8 +304,7 @@ def get_gifting_clearance_application(self):
self.additional_context = self.get_advice_additional_context()
def get_f680_clearance_application(self):
- self.tabs = self.get_tabs()
- self.tabs.insert(1, Tabs.LICENCES)
+ self.tabs = self.get_f680_application_tabs()
self.slices = [
Slices.GOODS,
Slices.DESTINATIONS,
From 58a89950f1d334ba84f5a2bba00c80dc6f185e1b Mon Sep 17 00:00:00 2001
From: Brendan Smith
Date: Fri, 5 Apr 2024 11:14:29 +0100
Subject: [PATCH 10/25] Improve F680 case details page
---
caseworker/cases/views/main.py | 5 +
.../templates/case/slices/f680-details.html | 131 +++++++-----------
2 files changed, 57 insertions(+), 79 deletions(-)
diff --git a/caseworker/cases/views/main.py b/caseworker/cases/views/main.py
index a962f7cb20..9b1b5a977a 100644
--- a/caseworker/cases/views/main.py
+++ b/caseworker/cases/views/main.py
@@ -308,9 +308,14 @@ def get_f680_clearance_application(self):
self.slices = [
Slices.GOODS,
Slices.DESTINATIONS,
+ conditional(self.case.data["denial_matches"], Slices.DENIAL_MATCHES),
+ conditional(self.case.data["sanction_matches"], Slices.SANCTION_MATCHES),
+ conditional(self.case.data["end_user"], Slices.END_USER_DOCUMENTS),
+ Slices.LOCATIONS,
Slices.F680_DETAILS,
Slices.END_USE_DETAILS,
Slices.SUPPORTING_DOCUMENTS,
+ Slices.FREEDOM_OF_INFORMATION,
]
self.additional_context = self.get_advice_additional_context()
diff --git a/caseworker/templates/case/slices/f680-details.html b/caseworker/templates/case/slices/f680-details.html
index 84b58e37f6..c167889605 100644
--- a/caseworker/templates/case/slices/f680-details.html
+++ b/caseworker/templates/case/slices/f680-details.html
@@ -1,79 +1,52 @@
-{% load humanize %}
-
-
-
-
-
-
-
-
-
-
-
- 1. |
-
- {{ case.data.electronic_warfare_requirement|friendly_boolean }} |
-
-
- 2. |
-
-
- {{ case.data.expedited|friendly_boolean }}
- {% if case.data.expedited_date %}
- {{ case.data.expedited_date }}
- {% endif %}
- |
-
-
- 3. |
-
-
- {{ case.data.foreign_technology|friendly_boolean }}
- {% if case.data.foreign_technology_description %}
-
- {{ case.data.foreign_technology_description }}
-
- {% endif %}
- |
-
-
- 4. |
-
-
- {{ case.data.locally_manufactured|friendly_boolean }}
- {% if case.data.locally_manufactured_description %}
-
- {{ case.data.locally_manufactured_description }}
-
- {% endif %}
- |
-
-
- 5. |
-
- {{ case.data.mtcr_type.value }} |
-
-
- 6. |
-
- {{ case.data.uk_service_equipment|friendly_boolean }} |
-
-
- 7. |
-
-
- {{ case.data.uk_service_equipment_type.value }}
- {% if case.data.uk_service_equipment_description %}
-
- {{ case.data.uk_service_equipment_description }}
-
- {% endif %}
- |
-
-
- 8. |
-
- £{{ case.data.prospect_value|intcomma }} |
-
-
-
+
+
+
- Select the types of clearance you need
+ - {{ case.data.clearances }}
+
+
+
- Do you need the F680 clearance in less than 30 days due to exceptional circumstances?
+ - {{ case.data.exceptional_circumstances|friendly_boolean }}
+
+
+
- Is there any foreign technology or information involved in the release?
+ - {{ case.data.foreign_technology_information|friendly_boolean }}
+
+ {% if case.data.foreign_technology_information %}
+
+
- Foreign technology details
+ - {{ case.data.foreign_technology_information_details }}
+
+ {% endif %}
+
+
- Is local assembly or manufacture required?
+ - {{ case.data.is_local_assembly_manufacture|friendly_boolean }}
+
+ {% if case.data.is_local_assembly_manufacture %}
+
+
- Local assembly manufacture details
+ - {{ case.data.is_local_assembly_manufacture_details }}
+
+ {% endif %}
+
+
- Do you believe the products are rated under the Missile Technology Control Regime (MTCR)
+ - {{ case.data.product_mtcr_rating_type }}
+
+
+
- Is there is a requirement to release UK MOD owned EW data or information in support of this export
+ - {{ case.data.ew_data|friendly_boolean }}
+
+
+
- Is the equipment or a version of it due to enter service with the UK armed forces?
+ - {{ case.data.armed_forces_usage|friendly_boolean }}
+
+ {% if case.data.armed_forces_usage %}
+
+
- Armed forces usage details
+ - {{ case.data.armed_forces_usage_details }}
+
+ {% endif %}
+
+
- Select how the product is funded
+ - {{ case.data.product_funding }}
+
+
From 02985c8dd833d1b7473aa05119b60b45ef16e8c8 Mon Sep 17 00:00:00 2001
From: Gurdeep Atwal
Date: Fri, 5 Apr 2024 12:51:09 +0100
Subject: [PATCH 11/25] fix post f680 advise
---
caseworker/advice/constants.py | 1 +
caseworker/advice/services.py | 8 +++++++-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/caseworker/advice/constants.py b/caseworker/advice/constants.py
index dc13a41352..74a48d03de 100644
--- a/caseworker/advice/constants.py
+++ b/caseworker/advice/constants.py
@@ -4,6 +4,7 @@
"Refuse": "refused",
"Conflicting": "given conflicting advice",
"No Licence Required": "no licence required",
+ "F680": "F680 Approval",
}
diff --git a/caseworker/advice/services.py b/caseworker/advice/services.py
index 7a661c7eeb..f3b65bcf46 100644
--- a/caseworker/advice/services.py
+++ b/caseworker/advice/services.py
@@ -343,9 +343,15 @@ def get_advice_subjects(case, countries=None):
def post_approval_advice(request, case, data, level="user-advice"):
+ advice_type = "approve"
+ if data["proviso"]:
+ advice_type = "proviso"
+ elif case.sub_type == "f680_clearance":
+ advice_type = "f680"
+
json = [
{
- "type": "proviso" if data.get("proviso", False) else "approve",
+ "type": advice_type,
"text": data["approval_reasons"],
"proviso": data.get("proviso", ""),
"note": data.get("instructions_to_exporter", ""),
From 5ef8b427e27108140a8abdc5a48285dab771811b Mon Sep 17 00:00:00 2001
From: Brendan Smith
Date: Fri, 5 Apr 2024 14:54:31 +0100
Subject: [PATCH 12/25] Add case type filter to queue view
---
caseworker/queues/views/forms.py | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/caseworker/queues/views/forms.py b/caseworker/queues/views/forms.py
index ab82849574..7e99e46f51 100644
--- a/caseworker/queues/views/forms.py
+++ b/caseworker/queues/views/forms.py
@@ -25,6 +25,15 @@ class CasesFiltersForm(forms.Form):
widget=forms.TextInput(attrs={"id": "case_reference"}),
required=False,
)
+ case_type = forms.ChoiceField(
+ label="Case type",
+ choices=(
+ ("", ""),
+ ("siel", "SIEL"),
+ ("f680", "F680"),
+ ),
+ required=False,
+ )
export_type = forms.ChoiceField(
label="Permanent or temporary",
choices=(
@@ -216,6 +225,7 @@ def __init__(self, queue, filters_data, all_flags, all_cles, all_regimes, countr
case_filters = [
"case_reference",
+ "case_type",
"status",
"sub_status",
Field("case_officer", css_class="single-select-filter"),
From 14b72b6c43a3c052ff5746f19c89860bb075aae5 Mon Sep 17 00:00:00 2001
From: Brendan Smith
Date: Fri, 5 Apr 2024 15:01:21 +0100
Subject: [PATCH 13/25] Fix unit tests
---
unit_tests/caseworker/queues/views/test_cases.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/unit_tests/caseworker/queues/views/test_cases.py b/unit_tests/caseworker/queues/views/test_cases.py
index 73449e1730..809f4e7128 100644
--- a/unit_tests/caseworker/queues/views/test_cases.py
+++ b/unit_tests/caseworker/queues/views/test_cases.py
@@ -143,6 +143,7 @@ def test_cases_home_page_view_context(authorized_client):
assert isinstance(response.context["form"], CasesFiltersForm)
expected_fields = [
"case_reference",
+ "case_type",
"export_type",
"exporter_application_reference",
"organisation_name",
From 6399be649ce5bc4d87b350fe6923a701130e7eff Mon Sep 17 00:00:00 2001
From: Arun Siluvery
Date: Fri, 5 Apr 2024 16:34:46 +0100
Subject: [PATCH 14/25] Fix linting issue and a failing test
---
caseworker/templates/case/slices/f680-details.html | 10 +++++++++-
core/summaries/formatters.py | 1 +
core/summaries/summaries.py | 1 +
exporter/applications/summaries/f680.py | 3 ---
exporter/applications/views/goods/f680/views/add.py | 1 -
.../applications/views/parties/test_end_users.py | 9 +++++++--
6 files changed, 18 insertions(+), 7 deletions(-)
diff --git a/caseworker/templates/case/slices/f680-details.html b/caseworker/templates/case/slices/f680-details.html
index c167889605..a5d559eadc 100644
--- a/caseworker/templates/case/slices/f680-details.html
+++ b/caseworker/templates/case/slices/f680-details.html
@@ -1,7 +1,15 @@
- Select the types of clearance you need
- - {{ case.data.clearances }}
+ -
+ {% for item in case.data.clearances %}
+ {% if not forloop.last %}
+ {{ item.value }},
+ {% else %}
+ {{ item.value }}
+ {% endif %}
+ {% endfor %}
+
- Do you need the F680 clearance in less than 30 days due to exceptional circumstances?
diff --git a/core/summaries/formatters.py b/core/summaries/formatters.py
index c6a4dd7baf..de0606855d 100644
--- a/core/summaries/formatters.py
+++ b/core/summaries/formatters.py
@@ -745,6 +745,7 @@ def document_formatter(document, url, link_text=None):
"total-value": "Total value",
}
+
def add_edit_links(
summary,
edit_links,
diff --git a/core/summaries/summaries.py b/core/summaries/summaries.py
index 9e713266e3..65ff83269b 100644
--- a/core/summaries/summaries.py
+++ b/core/summaries/summaries.py
@@ -835,6 +835,7 @@ def get_summary_url_for_good(good):
"total-value",
)
+
def f680_goods_summary(good, additional_formatters=None):
if not additional_formatters:
additional_formatters = {}
diff --git a/exporter/applications/summaries/f680.py b/exporter/applications/summaries/f680.py
index 9cf3169dc6..45968d4f60 100644
--- a/exporter/applications/summaries/f680.py
+++ b/exporter/applications/summaries/f680.py
@@ -1,8 +1,5 @@
from django.urls import reverse
-from core.summaries.formatters import (
- add_edit_links,
-)
from core.summaries.summaries import (
f680_goods_summary,
f680_good_details_on_application_summary as core_f680_good_details_on_application_summary,
diff --git a/exporter/applications/views/goods/f680/views/add.py b/exporter/applications/views/goods/f680/views/add.py
index 131b512b28..a669de0e40 100644
--- a/exporter/applications/views/goods/f680/views/add.py
+++ b/exporter/applications/views/goods/f680/views/add.py
@@ -78,7 +78,6 @@ def get_success_url(self):
"Unexpected error adding complete product",
)
def post_complete_item(self, form_dict):
- breakpoint()
payload = self.get_payload(form_dict)
return post_complete_item(
diff --git a/unit_tests/exporter/applications/views/parties/test_end_users.py b/unit_tests/exporter/applications/views/parties/test_end_users.py
index 700b88f2f9..85240598df 100644
--- a/unit_tests/exporter/applications/views/parties/test_end_users.py
+++ b/unit_tests/exporter/applications/views/parties/test_end_users.py
@@ -17,7 +17,13 @@
@pytest.fixture
def mock_application(requests_mock, data_standard_case):
url = client._build_absolute_uri(f'/applications/{data_standard_case["case"]["id"]}/')
- yield requests_mock.get(url=url, json={"id": data_standard_case["case"]["id"]})
+ yield requests_mock.get(
+ url=url,
+ json={
+ "id": data_standard_case["case"]["id"],
+ "case_type": data_standard_case["case"]["case_type"],
+ },
+ )
@pytest.fixture
@@ -102,7 +108,6 @@ def test_set_end_user_view(url, authorized_client, requests_mock, data_standard_
"size": 0,
}
- _ = requests_mock.request_history.pop()
end_user_data = requests_mock.request_history.pop().json()
assert end_user_data == {
"sub_type": "government",
From c14771d94aea374a7bf0338e5087e696a8be5840 Mon Sep 17 00:00:00 2001
From: Brendan Smith
Date: Mon, 8 Apr 2024 10:48:19 +0100
Subject: [PATCH 15/25] Ensure OGDs can give advice/move case forward on F680
applications
---
caseworker/advice/services.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/caseworker/advice/services.py b/caseworker/advice/services.py
index f3b65bcf46..b8e4c7da83 100644
--- a/caseworker/advice/services.py
+++ b/caseworker/advice/services.py
@@ -124,7 +124,7 @@ def filter_current_user_advice(all_advice, user_id):
advice
for advice in all_advice
if advice["level"] == constants.AdviceLevel.USER
- and advice["type"]["key"] in ["approve", "proviso", "refuse"]
+ and advice["type"]["key"] in ["approve", "proviso", "refuse", "f680"]
and (advice["user"]["id"] == user_id)
]
From d9496d1f73ea19d39f458c041e218f57b6b904c7 Mon Sep 17 00:00:00 2001
From: Brendan Smith
Date: Mon, 8 Apr 2024 11:55:45 +0100
Subject: [PATCH 16/25] Ensure MOD-ECJU can finalise F680 applications
---
caseworker/advice/views/views.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/caseworker/advice/views/views.py b/caseworker/advice/views/views.py
index e6a7cadb77..bc685e7322 100644
--- a/caseworker/advice/views/views.py
+++ b/caseworker/advice/views/views.py
@@ -687,6 +687,10 @@ def get_context(self, **kwargs):
lu_countersign_required = False
finalise_case = False
+ # Hack to ensure that F680 cases can be finalised by MOD-ECJU
+ if user_team_alias == services.MOD_ECJU_TEAM and self.case.case_type["reference"]["key"] == "f680":
+ finalise_case = True
+
if user_team_alias == services.LICENSING_UNIT_TEAM:
rejected_lu_countersignature = self.rejected_countersign_advice()
lu_countersign_required = self.get_lu_countersign_required(rejected_lu_countersignature)
From 15d4d1f69be12f71a909130ec3c86857c9890a35 Mon Sep 17 00:00:00 2001
From: Arun Siluvery
Date: Mon, 8 Apr 2024 13:29:17 +0100
Subject: [PATCH 17/25] Update description for F680 application option
---
exporter/apply_for_a_licence/forms/triage_questions.py | 9 ++++++++-
.../apply_for_a_licence/forms/test_triage_questions.py | 2 +-
2 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/exporter/apply_for_a_licence/forms/triage_questions.py b/exporter/apply_for_a_licence/forms/triage_questions.py
index 20a5ab7606..b400143c4f 100644
--- a/exporter/apply_for_a_licence/forms/triage_questions.py
+++ b/exporter/apply_for_a_licence/forms/triage_questions.py
@@ -26,6 +26,14 @@ def opening_question():
"before you provide access to controlled technology, software or data."
),
),
+ Option(
+ key="f680",
+ value="MOD F680",
+ description=(
+ "Select if you need approval to release security classified products or information to foreign entities."
+ ),
+ disabled=False,
+ ),
Option(
key="transhipment",
value="Transhipment licence",
@@ -53,7 +61,6 @@ def opening_question():
),
disabled=settings.FEATURE_FLAG_ONLY_ALLOW_SIEL,
),
- Option(key="f680", value="F680", description=("Select if f680"), disabled=False),
]
if settings.FEATURE_FLAG_ONLY_ALLOW_SIEL:
description = render_to_string("applications/use-spire-triage.html")
diff --git a/unit_tests/exporter/apply_for_a_licence/forms/test_triage_questions.py b/unit_tests/exporter/apply_for_a_licence/forms/test_triage_questions.py
index 8aa85cae7e..9ee85241e9 100644
--- a/unit_tests/exporter/apply_for_a_licence/forms/test_triage_questions.py
+++ b/unit_tests/exporter/apply_for_a_licence/forms/test_triage_questions.py
@@ -8,7 +8,7 @@
"value,expect_enabled,expect_disabled",
(
(True, ["export_licence", "f680"], ["transhipment", "trade_control_licence", "mod"]),
- (False, ["export_licence", "transhipment", "trade_control_licence", "mod", "f680"], []),
+ (False, ["export_licence", "f680", "transhipment", "trade_control_licence", "mod"], []),
),
)
def test_opening_question_feature_flag(settings, value, expect_enabled, expect_disabled):
From c41a4e34b13dfebc22633038baad4dc7d669fec3 Mon Sep 17 00:00:00 2001
From: Brendan Smith
Date: Mon, 13 Jan 2025 15:22:09 +0000
Subject: [PATCH 18/25] Rebase fixup: Remove problematic reference number
generation
---
exporter/applications/helpers/task_list_sections.py | 2 +-
exporter/applications/views/common.py | 3 +--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/exporter/applications/helpers/task_list_sections.py b/exporter/applications/helpers/task_list_sections.py
index aea3fb693e..ab0f07e5bc 100644
--- a/exporter/applications/helpers/task_list_sections.py
+++ b/exporter/applications/helpers/task_list_sections.py
@@ -3,8 +3,8 @@
def get_reference_number_description(application):
- have_you_been_informed = application["have_you_been_informed"]
reference_number_on_information_form = application["reference_number_on_information_form"]
+ have_you_been_informed = application["have_you_been_informed"]
if have_you_been_informed == "yes":
if not reference_number_on_information_form:
reference_number_on_information_form = "not provided"
diff --git a/exporter/applications/views/common.py b/exporter/applications/views/common.py
index 485e7b377d..a1d88f945f 100644
--- a/exporter/applications/views/common.py
+++ b/exporter/applications/views/common.py
@@ -28,7 +28,6 @@
get_application_type_string,
)
from exporter.applications.helpers.summaries import draft_summary
-from exporter.applications.helpers.task_list_sections import get_reference_number_description
from exporter.applications.helpers.task_lists import get_application_task_list
from exporter.applications.helpers.validators import (
validate_withdraw_application,
@@ -352,7 +351,7 @@ def get_context_data(self, **kwargs):
"summary_page": True,
"application_type": get_application_type_string(self.application),
"notes": get_case_notes(self.request, self.case_id)["case_notes"],
- "reference_code": get_reference_number_description(self.application),
+ # "reference_code": get_reference_number_description(self.application),
}
)
return context
From 71b2d931ba35df824347b0a2e1b561bd12f3d5ae Mon Sep 17 00:00:00 2001
From: Brendan Smith
Date: Thu, 16 Jan 2025 11:34:26 +0000
Subject: [PATCH 19/25] Tidy up F680 MTCR answers in caseworker screen
---
caseworker/templates/case/slices/f680-details.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/caseworker/templates/case/slices/f680-details.html b/caseworker/templates/case/slices/f680-details.html
index a5d559eadc..8ef839e6d7 100644
--- a/caseworker/templates/case/slices/f680-details.html
+++ b/caseworker/templates/case/slices/f680-details.html
@@ -37,7 +37,7 @@
{% endif %}
- Do you believe the products are rated under the Missile Technology Control Regime (MTCR)
- - {{ case.data.product_mtcr_rating_type }}
+ - {{ case.data.product_mtcr_rating_type|sentence_case }}
- Is there is a requirement to release UK MOD owned EW data or information in support of this export
From d7d09b2cff194cfec2ba33cb03d6493f9fd2d559 Mon Sep 17 00:00:00 2001
From: "mark.j0hnst0n"
Date: Thu, 16 Jan 2025 15:15:11 +0000
Subject: [PATCH 20/25] add in f680 approval advice type
---
caseworker/advice/constants.py | 1 +
caseworker/advice/templatetags/advice_tags.py | 2 ++
2 files changed, 3 insertions(+)
diff --git a/caseworker/advice/constants.py b/caseworker/advice/constants.py
index 74a48d03de..768ad6019b 100644
--- a/caseworker/advice/constants.py
+++ b/caseworker/advice/constants.py
@@ -9,6 +9,7 @@
TEAM_DECISION_APPROVED = "has approved"
+TEAM_DECISION_APPROVED_F680 = "has approved this F680 application"
TEAM_DECISION_APPROVED_REFUSED = "has approved and refused"
TEAM_DECISION_PROVISO = "has approved with licence conditions"
TEAM_DECISION_REFUSED = "has refused"
diff --git a/caseworker/advice/templatetags/advice_tags.py b/caseworker/advice/templatetags/advice_tags.py
index 6db1e9e523..39886bfb94 100644
--- a/caseworker/advice/templatetags/advice_tags.py
+++ b/caseworker/advice/templatetags/advice_tags.py
@@ -220,5 +220,7 @@ def _add_team_decisions(grouped_advice):
team_advice["decision"] = constants.TEAM_DECISION_PROVISO
elif decisions == {"Refuse"}:
team_advice["decision"] = constants.TEAM_DECISION_REFUSED
+ elif decisions == {"F680"}:
+ team_advice["decision"] = constants.TEAM_DECISION_APPROVED_F680
else:
team_advice["decision"] = constants.TEAM_DECISION_APPROVED_REFUSED
From 3da3896e177d597f42a4d46f158da82be385ec12 Mon Sep 17 00:00:00 2001
From: "mark.j0hnst0n"
Date: Fri, 17 Jan 2025 09:38:12 +0000
Subject: [PATCH 21/25] add f680 png
---
.../images/letter_templates/f680_letter.png | Bin 0 -> 30370 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 caseworker/assets/images/letter_templates/f680_letter.png
diff --git a/caseworker/assets/images/letter_templates/f680_letter.png b/caseworker/assets/images/letter_templates/f680_letter.png
new file mode 100644
index 0000000000000000000000000000000000000000..384be86c6e2aa361daa7cba5a030e113bcd75341
GIT binary patch
literal 30370
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