diff --git a/api/applications/creators.py b/api/applications/creators.py index 104e17994..c26011b91 100644 --- a/api/applications/creators.py +++ b/api/applications/creators.py @@ -8,7 +8,6 @@ GoodOnApplication, SiteOnApplication, ExternalLocationOnApplication, - StandardApplication, ) from api.cases.enums import CaseTypeSubTypeEnum from api.core.helpers import str_to_bool @@ -33,27 +32,6 @@ def _validate_locations(application, errors): return errors -def _validate_siel_locations(application, errors): - old_locations_invalid = ( - not SiteOnApplication.objects.filter(application=application).exists() - and not ExternalLocationOnApplication.objects.filter(application=application).exists() - and not getattr(application, "have_goods_departed", False) - and not getattr(application, "goodstype_category", None) == GoodsTypeCategory.CRYPTOGRAPHIC - ) - - new_locations_invalid = ( - not getattr(application, "export_type", False) - and not getattr(application, "goods_recipients", False) - and not getattr(application, "goods_starting_point", False) - and getattr(application, "is_shipped_waybill_or_lading") is None - ) - - if old_locations_invalid and new_locations_invalid: - errors["location"] = [strings.Applications.Generic.NO_LOCATION_SET] - - return errors - - def _get_document_errors(documents, processing_error, virus_error): document_statuses = documents.values_list("safe", flat=True) @@ -130,21 +108,16 @@ def _validate_end_user(draft, errors, is_mandatory, open_application=False): def _validate_consignee(draft, errors, is_mandatory): - """ - Checks there is an consignee if goods_recipients is set to VIA_CONSIGNEE or VIA_CONSIGNEE_AND_THIRD_PARTIES - (with a document if is_document_mandatory) - """ + """ Checks there is an consignee (with a document if is_document_mandatory) """ - # This logic includes old style applications where the goods_recipients field will be "" - if draft.goods_recipients != StandardApplication.DIRECT_TO_END_USER: - consignee_errors = check_party_error( - draft.consignee.party if draft.consignee else None, - object_not_found_error=strings.Applications.Standard.NO_CONSIGNEE_SET, - is_mandatory=is_mandatory, - is_document_mandatory=False, - ) - if consignee_errors: - errors["consignee"] = [consignee_errors] + consignee_errors = check_party_error( + draft.consignee.party if draft.consignee else None, + object_not_found_error=strings.Applications.Standard.NO_CONSIGNEE_SET, + is_mandatory=is_mandatory, + is_document_mandatory=False, + ) + if consignee_errors: + errors["consignee"] = [consignee_errors] return errors @@ -351,7 +324,7 @@ def _validate_exhibition_details(draft, errors): def _validate_standard_licence(draft, errors): """ Checks that a standard licence has all party types & goods """ - errors = _validate_siel_locations(draft, errors) + errors = _validate_locations(draft, errors) errors = _validate_end_user(draft, errors, is_mandatory=True) errors = _validate_consignee(draft, errors, is_mandatory=True) errors = _validate_third_parties(draft, errors, is_mandatory=False) diff --git a/api/applications/helpers.py b/api/applications/helpers.py index 8117cd13e..643fa68cc 100644 --- a/api/applications/helpers.py +++ b/api/applications/helpers.py @@ -3,6 +3,7 @@ from elasticsearch_dsl import Search, Q from elasticsearch.exceptions import NotFoundError +from api.applications.enums import ApplicationExportType from api.applications.models import BaseApplication, GoodOnApplication from api.applications.serializers.end_use_details import ( F680EndUseDetailsUpdateSerializer, @@ -40,6 +41,7 @@ StandardApplicationViewSerializer, ) from api.applications.serializers.good import GoodOnStandardLicenceSerializer +from api.applications.serializers.temporary_export_details import TemporaryExportDetailsUpdateSerializer from api.cases.enums import CaseTypeSubTypeEnum, CaseTypeEnum, AdviceType, AdviceLevel from api.core.exceptions import BadRequestError from api.documents.models import Document @@ -128,6 +130,15 @@ def get_application_end_use_details_update_serializer(application: BaseApplicati ) +def get_temp_export_details_update_serializer(export_type): + if export_type == ApplicationExportType.TEMPORARY: + return TemporaryExportDetailsUpdateSerializer + else: + raise BadRequestError( + {f"get_temp_export_details_update_serializer does " f"not support this export type: {export_type}"} + ) + + def validate_and_create_goods_on_licence(application_id, licence_id, data): errors = {} good_on_applications = ( diff --git a/api/applications/migrations/0050_auto_20211210_1618.py b/api/applications/migrations/0050_auto_20211210_1618.py deleted file mode 100644 index 725a341ba..000000000 --- a/api/applications/migrations/0050_auto_20211210_1618.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 3.1.12 on 2021-12-10 16:18 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('applications', '0049_auto_20211206_1031'), - ] - - operations = [ - migrations.AddField( - model_name='standardapplication', - name='goods_recipients', - field=models.TextField(choices=[('direct_to_end_user', 'Directly to the end-user'), ('via_consignee', 'To an end-user via a consignee'), ('via_consignee_and_third_parties', 'To an end-user via a consignee, with additional third parties')], default=''), - ), - migrations.AddField( - model_name='standardapplication', - name='goods_starting_point', - field=models.TextField(choices=[('GB', 'Great Britain'), ('NI', 'Northern Ireland')], default=''), - ), - migrations.AlterField( - model_name='standardapplication', - name='export_type', - field=models.TextField(blank=True, choices=[('permanent', 'Permanent'), ('temporary', 'Temporary')], default=''), - ), - ] diff --git a/api/applications/models.py b/api/applications/models.py index a6ce9e7ed..e7192256d 100644 --- a/api/applications/models.py +++ b/api/applications/models.py @@ -179,23 +179,7 @@ class Meta: # Licence Applications class StandardApplication(BaseApplication): - GB = "GB" - NI = "NI" - GOODS_STARTING_POINT_CHOICES = [ - (GB, "Great Britain"), - (NI, "Northern Ireland"), - ] - DIRECT_TO_END_USER = "direct_to_end_user" - VIA_CONSIGNEE = "via_consignee" - VIA_CONSIGNEE_AND_THIRD_PARTIES = "via_consignee_and_third_parties" - - GOODS_RECIPIENTS_CHOICES = [ - (DIRECT_TO_END_USER, "Directly to the end-user"), - (VIA_CONSIGNEE, "To an end-user via a consignee"), - (VIA_CONSIGNEE_AND_THIRD_PARTIES, "To an end-user via a consignee, with additional third parties"), - ] - - export_type = models.TextField(choices=ApplicationExportType.choices, blank=True, default="") + export_type = models.CharField(choices=ApplicationExportType.choices, default=None, max_length=50) reference_number_on_information_form = models.CharField(blank=True, null=True, max_length=255) have_you_been_informed = models.CharField( choices=ApplicationExportLicenceOfficialType.choices, blank=True, null=True, default=None, max_length=50, @@ -213,8 +197,6 @@ class StandardApplication(BaseApplication): trade_control_product_categories = SeparatedValuesField( choices=TradeControlProductCategory.choices, blank=False, null=True, max_length=50 ) - goods_recipients = models.TextField(choices=GOODS_RECIPIENTS_CHOICES, default="") - goods_starting_point = models.TextField(choices=GOODS_STARTING_POINT_CHOICES, default="") class OpenApplication(BaseApplication): diff --git a/api/applications/serializers/standard_application.py b/api/applications/serializers/standard_application.py index 97b053811..6643d749e 100644 --- a/api/applications/serializers/standard_application.py +++ b/api/applications/serializers/standard_application.py @@ -39,8 +39,6 @@ class StandardApplicationViewSerializer(PartiesSerializerMixin, GenericApplicati trade_control_product_categories = serializers.SerializerMethodField() sanction_matches = serializers.SerializerMethodField() is_amended = serializers.SerializerMethodField() - goods_starting_point = serializers.CharField() - goods_recipients = serializers.CharField() class Meta: model = StandardApplication @@ -77,8 +75,6 @@ class Meta: "trade_control_product_categories", "sanction_matches", "is_amended", - "goods_starting_point", - "goods_recipients", ) ) @@ -137,7 +133,9 @@ def get_is_amended(self, instance): class StandardApplicationCreateSerializer(GenericApplicationCreateSerializer): - export_type = KeyValueChoiceField(choices=ApplicationExportType.choices, required=False) + export_type = KeyValueChoiceField( + choices=ApplicationExportType.choices, error_messages={"required": strings.Applications.Generic.NO_EXPORT_TYPE}, + ) have_you_been_informed = KeyValueChoiceField( choices=ApplicationExportLicenceOfficialType.choices, error_messages={"required": strings.Goods.INFORMED}, ) @@ -194,21 +192,15 @@ def create(self, validated_data): class StandardApplicationUpdateSerializer(GenericApplicationUpdateSerializer): - export_type = KeyValueChoiceField(choices=ApplicationExportType.choices, required=False) - goods_starting_point = serializers.CharField() - goods_recipients = serializers.CharField() reference_number_on_information_form = CharField(max_length=100, required=False, allow_blank=True, allow_null=True) class Meta: model = StandardApplication fields = GenericApplicationUpdateSerializer.Meta.fields + ( - "export_type", "have_you_been_informed", "reference_number_on_information_form", "is_shipped_waybill_or_lading", "non_waybill_or_lading_route_details", - "goods_starting_point", - "goods_recipients", ) def __init__(self, *args, **kwargs): diff --git a/api/applications/tests/test_create_application.py b/api/applications/tests/test_create_application.py index ff900a158..84bfb5f14 100644 --- a/api/applications/tests/test_create_application.py +++ b/api/applications/tests/test_create_application.py @@ -45,25 +45,6 @@ def test_create_draft_standard_individual_export_application_successful(self): self.assertEqual(response_data["id"], str(standard_application.id)) self.assertEqual(StandardApplication.objects.count(), 1) - def test_create_draft_standard_individual_export_application_empty_export_type_successful(self): - """ - Ensure we can create a new standard individual export application draft without the export_type field populated - """ - data = { - "name": "Test", - "application_type": CaseTypeReferenceEnum.SIEL, - "have_you_been_informed": ApplicationExportLicenceOfficialType.YES, - "reference_number_on_information_form": "123", - } - - response = self.client.post(self.url, data, **self.exporter_headers) - response_data = response.json() - standard_application = StandardApplication.objects.get() - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response_data["id"], str(standard_application.id)) - self.assertEqual(StandardApplication.objects.count(), 1) - def test_create_draft_exhibition_clearance_application_successful(self): """ Ensure we can create a new Exhibition Clearance draft object @@ -82,7 +63,7 @@ def test_create_draft_exhibition_clearance_application_successful(self): def test_create_draft_gifting_clearance_application_successful(self): """ - Ensure we can create a new Gifting Clearance draft object + Ensure we can create a new Exhibition Clearance draft object """ self.assertEqual(GiftingClearanceApplication.objects.count(), 0) @@ -101,7 +82,7 @@ def test_create_draft_gifting_clearance_application_successful(self): def test_create_draft_f680_clearance_application_successful(self): """ - Ensure we can create a new F680 Clearance draft object + Ensure we can create a new Exhibition Clearance draft object """ self.assertEqual(F680ClearanceApplication.objects.count(), 0) diff --git a/api/applications/tests/test_edit_application.py b/api/applications/tests/test_edit_application.py index fc29a82a3..1a5452637 100644 --- a/api/applications/tests/test_edit_application.py +++ b/api/applications/tests/test_edit_application.py @@ -38,46 +38,6 @@ def test_edit_unsubmitted_application_name_success(self): # Unsubmitted (draft) applications should not create audit entries when edited self.assertEqual(Audit.objects.count(), 0) - def test_edit_unsubmitted_application_export_type_success(self): - """ Test edit the application export_type of an unsubmitted application. An unsubmitted application - has the 'draft' status. - """ - application = self.create_draft_standard_application(self.organisation) - # export_type is set to permanent in create_draft_standard_application - - url = reverse("applications:application", kwargs={"pk": application.id}) - updated_at = application.updated_at - - response = self.client.put(url, {"export_type": "temporary"}, **self.exporter_headers) - - application.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(application.export_type, "temporary") - self.assertGreater(application.updated_at, updated_at) - # Unsubmitted (draft) applications should not create audit entries when edited - self.assertEqual(Audit.objects.count(), 0) - - def test_edit_unsubmitted_application_locations_success(self): - application = self.create_draft_standard_application(self.organisation) - - url = reverse("applications:application", kwargs={"pk": application.id}) - updated_at = application.updated_at - - data = { - "goods_starting_point": "GB", - "goods_recipients": "via_consignee", - } - - response = self.client.put(url, data, **self.exporter_headers) - - application.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(application.goods_starting_point, "GB") - self.assertEqual(application.goods_recipients, "via_consignee") - self.assertGreater(application.updated_at, updated_at) - # Unsubmitted (draft) applications should not create audit entries when edited - self.assertEqual(Audit.objects.count(), 0) - @parameterized.expand(get_case_statuses(read_only=False)) def test_edit_application_name_in_editable_status_success(self, editable_status): old_name = "Old Name" diff --git a/api/applications/tests/test_edit_temporary_export_details.py b/api/applications/tests/test_edit_temporary_export_details.py index 234feba65..9e8d5bc26 100644 --- a/api/applications/tests/test_edit_temporary_export_details.py +++ b/api/applications/tests/test_edit_temporary_export_details.py @@ -28,7 +28,7 @@ def test_perform_action_on_non_temporary_export_type_standard_applications_failu self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( response.json()["errors"], - {"temp_export_details": ["Cannot update temporary export details for a permanent export type"]}, + ["{'get_temp_export_details_update_serializer does not support this export type: permanent'}"], ) def test_perform_action_on_non_open_or_standard_applications_failure(self): diff --git a/api/applications/tests/test_standard_application_submit.py b/api/applications/tests/test_standard_application_submit.py index 8fd899bfd..a0f49ba00 100644 --- a/api/applications/tests/test_standard_application_submit.py +++ b/api/applications/tests/test_standard_application_submit.py @@ -5,7 +5,7 @@ from uuid import UUID from api.applications.enums import ApplicationExportType -from api.applications.models import SiteOnApplication, GoodOnApplication, PartyOnApplication, StandardApplication +from api.applications.models import SiteOnApplication, GoodOnApplication, PartyOnApplication from api.audit_trail.enums import AuditType from api.audit_trail.models import Audit from api.cases.enums import CaseTypeEnum, CaseDocumentState @@ -60,35 +60,12 @@ def test_submit_standard_application_with_invalid_id_failure(self): self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_submit_standard_application_old_location_info_success(self): - SiteOnApplication(site=self.organisation.primary_site, application=self.draft).save() + def test_submit_standard_application_without_site_or_external_location_failure(self): + SiteOnApplication.objects.get(application=self.draft).delete() url = reverse("applications:application_submit", kwargs={"pk": self.draft.id}) response = self.client.put(url, **self.exporter_headers) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_submit_standard_application_with_new_location_info_success(self): - url = reverse("applications:application_submit", kwargs={"pk": self.draft.id}) - SiteOnApplication.objects.filter(application_id=self.draft.id).delete() - self.draft.goods_recipients = StandardApplication.DIRECT_TO_END_USER - self.draft.goods_starting_point = StandardApplication.GB - - response = self.client.put(url, **self.exporter_headers) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_submit_standard_application_with_no_new_or_old_location_info_failure(self): - url = reverse("applications:application_submit", kwargs={"pk": self.draft.id}) - SiteOnApplication.objects.filter(application_id=self.draft.id).delete() - self.draft.export_type = "" - self.draft.goods_recipients = "" - self.draft.goods_starting_point = "" - self.draft.is_shipped_waybill_or_lading = None - self.draft.save() - - response = self.client.put(url, **self.exporter_headers) - self.assertContains( response, text=strings.Applications.Generic.NO_LOCATION_SET, status_code=status.HTTP_400_BAD_REQUEST, ) @@ -117,17 +94,16 @@ def test_submit_standard_application_without_end_user_document_failure(self): status_code=status.HTTP_400_BAD_REQUEST, ) - def test_submit_standard_application_without_consignee_success(self): - # Consignee is optional if goods_recipients is DIRECT_TO_END_USER + def test_submit_standard_application_without_consignee_failure(self): self.draft.delete_party(self.draft.consignee) - self.draft.goods_recipients = StandardApplication.DIRECT_TO_END_USER - self.draft.save() url = reverse("applications:application_submit", kwargs={"pk": self.draft.id}) response = self.client.put(url, **self.exporter_headers) - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertContains( + response, text=strings.Applications.Standard.NO_CONSIGNEE_SET, status_code=status.HTTP_400_BAD_REQUEST, + ) def test_submit_standard_application_without_consignee_document_success(self): # Consignee document is optional diff --git a/api/applications/views/applications.py b/api/applications/views/applications.py index a29de3c2f..ba39d008e 100644 --- a/api/applications/views/applications.py +++ b/api/applications/views/applications.py @@ -203,12 +203,10 @@ def put(self, request, pk): Update an application instance """ application = get_application(pk) - update_serializer = get_application_update_serializer(application) + serializer = get_application_update_serializer(application) case = application.get_case() data = request.data.copy() - serializer = update_serializer( - application, data=data, context=get_request_user_organisation(request), partial=True - ) + serializer = serializer(application, data=data, context=get_request_user_organisation(request), partial=True) # Prevent minor edits of the clearance level if not application.is_major_editable() and request.data.get("clearance_level"): @@ -280,7 +278,6 @@ def put(self, request, pk): if application.case_type.sub_type == CaseTypeSubTypeEnum.STANDARD: save_and_audit_have_you_been_informed_ref(request, application, serializer) - serializer.save() return JsonResponse(data={}, status=status.HTTP_200_OK) diff --git a/api/applications/views/temporary_export_details.py b/api/applications/views/temporary_export_details.py index 93d515366..6e35c5534 100644 --- a/api/applications/views/temporary_export_details.py +++ b/api/applications/views/temporary_export_details.py @@ -1,13 +1,11 @@ from django.http import JsonResponse from rest_framework import status from rest_framework.generics import UpdateAPIView -from rest_framework.exceptions import ValidationError -from api.applications.serializers.temporary_export_details import TemporaryExportDetailsUpdateSerializer +from api.applications.helpers import get_temp_export_details_update_serializer from api.applications.libraries.edit_applications import save_and_audit_temporary_export_details from api.applications.libraries.get_applications import get_application from api.cases.enums import CaseTypeSubTypeEnum -from api.applications.enums import ApplicationExportType from api.core.authentication import ExporterAuthentication from api.core.decorators import ( authorised_to_view_application, @@ -25,12 +23,8 @@ class TemporaryExportDetails(UpdateAPIView): @application_in_state(is_major_editable=True) def put(self, request, pk): application = get_application(pk) - if not application.export_type or application.export_type == ApplicationExportType.PERMANENT: - raise ValidationError( - {"temp_export_details": ["Cannot update temporary export details for a permanent export type"]} - ) - - serializer = TemporaryExportDetailsUpdateSerializer(application, data=request.data, partial=True) + serializer = get_temp_export_details_update_serializer(application.export_type) + serializer = serializer(application, data=request.data, partial=True) if serializer.is_valid(raise_exception=True): save_and_audit_temporary_export_details(request, application, serializer) diff --git a/test_helpers/clients.py b/test_helpers/clients.py index d12c080a7..886acd2d5 100644 --- a/test_helpers/clients.py +++ b/test_helpers/clients.py @@ -40,6 +40,7 @@ Case, CaseDocument, CaseAssignment, + GoodCountryDecision, EcjuQuery, CaseType, Advice, @@ -53,10 +54,12 @@ from api.flags.tests.factories import FlagFactory from api.addresses.tests.factories import AddressFactoryGB from api.goods.enums import ( + GoodControlled, GoodPvGraded, PvGrading, ItemCategory, MilitaryUse, + Component, FirearmGoodType, ) from api.goods.models import Good, GoodDocument, PvGradingDetails, FirearmGoodDetails @@ -94,7 +97,7 @@ from api.teams.models import Team from api.users.tests.factories import GovUserFactory from test_helpers import colours -from api.users.enums import SystemUser, UserType +from api.users.enums import UserStatuses, SystemUser, UserType from api.users.libraries.user_to_token import user_to_token from api.users.models import ExporterUser, UserOrganisationRelationship, BaseUser, GovUser, Role from api.workflow.flagging_rules_automation import apply_flagging_rules_to_case