diff --git a/wildlifecompliance/components/applications/api.py b/wildlifecompliance/components/applications/api.py index 150938f32..aa8358c1c 100644 --- a/wildlifecompliance/components/applications/api.py +++ b/wildlifecompliance/components/applications/api.py @@ -1849,13 +1849,16 @@ def create(self, request, *args, **kwargs): Application.APPLICATION_TYPE_RENEWAL, Application.APPLICATION_TYPE_REISSUE, ]: - # Check that at least one active application exists in this licence category for amendment/renewal + # Check that at least one active application exists in this + # licence category for amendment/renewal. if not latest_active_licence: raise serializers.ValidationError( 'Cannot create amendment application: active licence not found!') - # Ensure purpose ids are in a shared set with the latest current applications purposes - # to prevent front-end tampering. Remove any that aren't valid for renew/amendment/reissue. + # Ensure purpose ids are in a shared set with the latest + # current applications purposes to prevent front-end + # tampering. Remove any that aren't valid for + # renew/amendment/reissue. active_current_purposes = active_current_applications.filter( licence_purposes__licence_activity_id__in=licence_activity_ids ).values_list( @@ -1863,6 +1866,9 @@ def create(self, request, *args, **kwargs): flat=True ) + # Set the previous for these application types. + # Although multiple purposes of the same type can exist for + # a licence, only one can be created for selected activity. previous_application = licence_activities.filter( id=selected_activity ).values_list( diff --git a/wildlifecompliance/components/applications/models.py b/wildlifecompliance/components/applications/models.py index 01333fe16..a5ee89313 100644 --- a/wildlifecompliance/components/applications/models.py +++ b/wildlifecompliance/components/applications/models.py @@ -1117,11 +1117,18 @@ def copy_application_purposes_for_status( NOTE: System Generated Applications cannot be processed by staff and so processing status for activity needs to be accepted. - SYSTEM GENERATED can occur with renewal of licence purposes where + SYSTEM GENERATED occurs with renewal of licence purposes where current licenses still exist. A copy of this application with the renewed purpose is created with the current licence activities and purposes, and set to accepted. This will the latest application for the licence. + + SYSTEM GENERATED occurs with reissue of a licence purpose where + current licenses still exist. A copy of this application with the + issued purpose is created with the current licence activities and + purposes, and set to accepted. This will the latest application for + the licence. + ''' new_app = Application.objects.get(id=original_app_id) new_app.id = None @@ -2511,6 +2518,40 @@ def get_proposed_decisions(self, request): except BaseException: raise + def get_proposed_purposes(self): + ''' + Return a list of ApplicationSelectedActivityPurpose records issued + on this application. + ''' + from wildlifecompliance.components.applications.models import ( + ApplicationSelectedActivityPurpose, + ApplicationSelectedActivity, + ) + + # activities for this licence in current or suspended. + activity_status = [ + ApplicationSelectedActivity.ACTIVITY_STATUS_CURRENT, + ApplicationSelectedActivity.ACTIVITY_STATUS_SUSPENDED, + ] + # latest purposes on the activities which are issued or reissued. + purpose_process_status = [ + ApplicationSelectedActivityPurpose.PROCESSING_STATUS_ISSUED, + ApplicationSelectedActivityPurpose.PROCESSING_STATUS_REISSUE, + ] + + activity_ids = [ + a.id for a in self.get_current_activity_chain( + activity_status__in=activity_status + ) + ] + + purposes = ApplicationSelectedActivityPurpose.objects.filter( + selected_activity__application_id=self.id, + processing_status__in=purpose_process_status + ).order_by('purpose_sequence') + + return purposes + def proposed_licence(self, request, details): ''' Propose licence purposes for issuing by Approver. @@ -2741,6 +2782,12 @@ def proposed_licence(self, request, details): raise def get_parent_licence(self, auto_create=True): + ''' + Function to get a licence for this application. + When no licence (ie new application) one is created. + + :return: WildlifeLicence. + ''' from wildlifecompliance.components.licences.models import WildlifeLicence current_date = timezone.now().date() try: @@ -2759,12 +2806,14 @@ def get_parent_licence(self, auto_create=True): # Only load licence if any associated activities are still current or suspended. # TODO: get current activities in chain. # get_current_activity_chain() - if not existing_licence.current_application.get_current_activity_chain( - activity_status__in=[ - ApplicationSelectedActivity.ACTIVITY_STATUS_CURRENT, - ApplicationSelectedActivity.ACTIVITY_STATUS_SUSPENDED, - ] - ).first(): + # if not existing_licence.current_application.get_current_activity_chain( + # activity_status__in=[ + # ApplicationSelectedActivity.ACTIVITY_STATUS_CURRENT, + # ApplicationSelectedActivity.ACTIVITY_STATUS_SUSPENDED, + # ] + # ).first(): + if not existing_licence.has_proposed_purposes_in_current(): + # the existing licence is not current. raise WildlifeLicence.DoesNotExist else: raise WildlifeLicence.DoesNotExist @@ -2846,6 +2895,10 @@ def reissue_activity( def issue_activity(self, request, selected_activity, parent_licence=None, generate_licence=False, preview=False): + REPLACED = ApplicationSelectedActivity.ACTIVITY_STATUS_REPLACED + ACCEPTED = ApplicationSelectedActivity.PROCESSING_STATUS_ACCEPTED + CURRENT = ApplicationSelectedActivity.ACTIVITY_STATUS_CURRENT + if not preview and not selected_activity.is_licence_fee_paid(): raise Exception("Cannot issue activity: licence fee has not been paid!") @@ -2856,13 +2909,13 @@ def issue_activity(self, request, selected_activity, parent_licence=None, genera raise Exception("Cannot issue activity: licence not found!") latest_application_in_function = self - application_selected_purpose_ids = self.licence_purposes.all().values_list('id', flat=True) - # FIXME: need to all for multiple activities of same licence purpose - # when issuing licence amendments. Causing all to be replaced. - # licence_latest_activities_for_licence_activity_id = \ - # parent_licence.latest_activities.filter( - # licence_activity_id=selected_activity.licence_activity_id - # ) + # all_purpose_ids = self.licence_purposes.all( + # # All licence purpose ids on this application. + # ).values_list('id', flat=True) + + # All active license purposes on this application. + all_purpose = self.get_proposed_purposes() + all_purpose_ids = [p.purpose_id for p in all_purpose] # Set previous activity if selected_activity is replacing. Required # when issuing activity for licence amendments. @@ -2876,13 +2929,44 @@ def issue_activity(self, request, selected_activity, parent_licence=None, genera with transaction.atomic(): try: for existing_activity in licence_latest_activities_for_licence_activity_id: - # compare each activity's purposes and find the difference from - # the selected_purposes of the new application - issued_activity_purposes = application_selected_purpose_ids.filter( - licence_activity_id=selected_activity.licence_activity_id) - existing_activity_purposes = existing_activity.purposes.values_list('id', flat=True) - common_purpose_ids = list(set(existing_activity_purposes) & set(issued_activity_purposes)) - remaining_purpose_ids_list = list(set(existing_activity_purposes) - set(issued_activity_purposes)) + + # compare each activity's purposes and find the difference + # from the selected_purposes of the new application. + + # selected_lic_id = selected_activity.licence_activity_id + # issued_purposes = all_purpose_ids.filter( + # licence_activity_id=selected_lic_id + # ) + + # existing_purposes = \ + # existing_activity.purposes.values_list('id', flat=True) + + # All active license purposes on the current Activity. + existing_purposes = [ + p.purpose_id + for p in existing_activity.get_proposed_purposes() + ] + + # All purposes on both selected Activity and Licence. + common_purpose_ids = list( + set(existing_purposes) & set(all_purpose_ids) + ) + + # Purposes on the Licence and not on the selected Activity. + remaining_purpose_ids_list = list( + set(existing_purposes) - set(all_purpose_ids) + ) + + # All purposes on the Licence which are currently opened. + opened_purposes = list( + self.licence.get_purposes_in_open_applications() + ) + + # Remove any currently opened licence purposes from the + # remaining list to prevent replacing. + remaining_purpose_ids_list = list( + set(remaining_purpose_ids_list) - set(opened_purposes) + ) # Ignore common purposes to allow addition of multiple # purposes when the application type is a new activity or @@ -2913,21 +2997,35 @@ def issue_activity(self, request, selected_activity, parent_licence=None, genera p_id, sequence_no ) - existing_activity.activity_status = ApplicationSelectedActivity.ACTIVITY_STATUS_REPLACED + # If no other license purposes are opened then mark the + # existing activity as replaced. + if len(opened_purposes) < 2: + existing_activity.activity_status = REPLACED + existing_activity.save() # If only a subset of the existing_activity's purposes are # to be actioned, create new_activity for remaining # purposes. New system generated application is created. elif remaining_purpose_ids_list: - existing_application = existing_activity.application - existing_activity_status = existing_activity.activity_status - new_copied_application = existing_application.copy_application_purposes_for_status( - remaining_purpose_ids_list, existing_activity_status) - - # for each new application created, set its previous_application to latest_application_in_function, - # then update latest_application_in_function to the new_copied_application - new_copied_application.previous_application = latest_application_in_function + + existing_app = existing_activity.application + + existing_activity_status = \ + existing_activity.activity_status + + new_copied_application = \ + existing_app.copy_application_purposes_for_status( + remaining_purpose_ids_list, + existing_activity_status + ) + + # for each new application created, set its previous + # application to latest_application_in_function, + # then update latest_application_in_function to the + # new_copied_application. + new_copied_application.previous_application = \ + latest_application_in_function new_copied_application.save() latest_application_in_function = new_copied_application @@ -2943,11 +3041,11 @@ def issue_activity(self, request, selected_activity, parent_licence=None, genera p_id, sequence_no ) - existing_activity.activity_status = ApplicationSelectedActivity.ACTIVITY_STATUS_REPLACED + existing_activity.activity_status = REPLACED existing_activity.save() - selected_activity.processing_status = ApplicationSelectedActivity.PROCESSING_STATUS_ACCEPTED - selected_activity.activity_status = ApplicationSelectedActivity.ACTIVITY_STATUS_CURRENT + selected_activity.processing_status = ACCEPTED + selected_activity.activity_status = CURRENT if not selected_activity.get_expiry_date() == None: self.generate_returns(parent_licence, selected_activity, request) @@ -3392,13 +3490,15 @@ def update_customer_approval_status(self): self.save() def generate_returns(self, licence, selected_activity, request): + ''' + Generates Returns for Conditions on the selected Activity on this + Application. + ''' from wildlifecompliance.components.returns.utils import ( ReturnSpeciesUtility, ) - # TODO: Delete any previously existing returns with default status - # which may occur if this activity is being reissued or amended. from wildlifecompliance.components.returns.models import Return - # licence_expiry = selected_activity.expiry_date + # Returns are generated at issuing; expiry_date may not be set yet. licence_expiry = selected_activity.get_expiry_date() licence_expiry = datetime.datetime.strptime( @@ -3406,14 +3506,16 @@ def generate_returns(self, licence, selected_activity, request): ).date() if isinstance(licence_expiry, six.string_types) else licence_expiry today = timezone.now().date() timedelta = datetime.timedelta - for condition in self.conditions.all(): + + for condition in selected_activity.get_condition_list(): try: if condition.return_type and condition.due_date and condition.due_date >= today: + already_generated = False current_date = condition.due_date - # create a first Return try: first_return = Return.objects.get( condition=condition, due_date=current_date) + already_generated = True except Return.DoesNotExist: first_return = Return.objects.create( application=self, @@ -3437,7 +3539,8 @@ def generate_returns(self, licence, selected_activity, request): raw_specie_names = returns_utils.get_raw_species_list_for( selected_activity ) - returns_utils.set_species_list(raw_specie_names) + if not already_generated: + returns_utils.set_species_list(raw_specie_names) if condition.recurrence: while current_date < licence_expiry: @@ -3456,6 +3559,8 @@ def generate_returns(self, licence, selected_activity, request): .APPLICATION_CONDITION_RECURRENCE_YEARLY: current_date += timedelta(days=365) # Create the Return + # FIXME: Create species lists for subsequent + # returns. if current_date <= licence_expiry: try: Return.objects.get( @@ -3565,7 +3670,7 @@ def get_first_active_licence_application( A check whether requested_user has any current applications. ''' - applications = Application.get_request_user_applications( + application = Application.get_request_user_applications( request ).filter( @@ -3575,7 +3680,11 @@ def get_first_active_licence_application( ], ).first() - return applications + if application \ + and not application.licence.has_proposed_purposes_in_current(): + application = None + + return application @staticmethod def get_open_applications(request): @@ -5221,8 +5330,23 @@ def reinstate_purposes(self, purpose_ids): self.set_proposed_purposes_status_for(purpose_ids, P_STATUS) - if not self.is_proposed_purposes_status(P_STATUS) and\ - self.activity_status == A_STATUS: + # if not self.is_proposed_purposes_status(P_STATUS) and\ + # self.activity_status == A_STATUS: + # return + + self.activity_status = A_STATUS + self.save() + + def expire_purposes(self, purpose_ids): + ''' + Expire all licence purposes available on this selected activity. + ''' + A_STATUS = ApplicationSelectedActivity.ACTIVITY_STATUS_EXPIRED + P_STATUS = ApplicationSelectedActivityPurpose.PURPOSE_STATUS_EXPIRED + + self.set_proposed_purposes_status_for(purpose_ids, P_STATUS) + + if not self.is_proposed_purposes_status(P_STATUS): return self.activity_status = A_STATUS @@ -5236,25 +5360,26 @@ def reissue_purposes(self, purpose_ids): P_STATUS = ApplicationSelectedActivityPurpose.PURPOSE_STATUS_CURRENT REISSUE = ApplicationSelectedActivityPurpose.PROCESSING_STATUS_REISSUE - self.set_proposed_purposes_status_for(purpose_ids, P_STATUS) - - if not self.is_proposed_purposes_status(P_STATUS) and\ - self.activity_status == A_STATUS: - return - - # Update this activity status. - self.reissue() - # Update purposes processing status so they can be re-issued. selected = [ p for p in self.proposed_purposes.all() - if p.purpose.id in purpose_ids + if p.id in purpose_ids ] for purpose in selected: purpose.processing_status = REISSUE purpose.save() - return True + # Set the status for the selected proposed purposes. + self.set_proposed_purposes_status_for(purpose_ids, P_STATUS) + + if not self.is_proposed_purposes_status(P_STATUS) and\ + self.activity_status == A_STATUS: + # No setting of status for this selected Activity when another + # purpose is current. + return + + # Update this activity status. + self.reissue() def reissue(self): ''' @@ -5337,6 +5462,43 @@ def mark_selected_purpose_as_replaced(self, p_id): return sequence_no + def get_activity_to_replace(self): + ''' + Get the previously issued licence Activity with the correct license + Purpose that this Application Selected Activity will replace. + + :return an ApplicationSelectedActivity with correct license purpose. + ''' + activity_to_replace = None + + valid_status = [ + ApplicationSelectedActivity.ACTIVITY_STATUS_DEFAULT, + ApplicationSelectedActivity.ACTIVITY_STATUS_CURRENT, + # ApplicationSelectedActivity.ACTIVITY_STATUS_REPLACED, + ] + + # current_activities = self.application.get_current_activity_chain() + # NOTE: If applying activity_chain all purposes of the same type will + # be selected and will be difficult to distinguish which to replace. + # Select relevant using the previous application. + current_activities = ApplicationSelectedActivity.objects.filter( + application_id=self.application.previous_application, + activity_status__in=valid_status, + ) + + # NOTE: Multiple purposes of the same type can exist on a License but + # but only one instance on a Selected Activity. Ensure the correct + # Purpose on Activity is selected for replacement. + for activity in current_activities: + purposes = [ # use proposed purpose in Selected Activity. + p for p in activity.proposed_purposes.all() + if p.purpose in self.purposes + ] + if purposes: + activity_to_replace = activity + + return activity_to_replace + def set_selected_purpose_sequence(self, p_id, sequence_no): ''' Set all Selected Purposes for the activity to the same sequence number. @@ -5366,15 +5528,22 @@ def set_selected_purpose_sequence(self, p_id, sequence_no): def is_proposed_purposes_status(self, status): ''' - Check all purposes on this activity have the same status. Used to check - if this activity is still current. + Check all purposes on this activity have the same status. Where not the + same status then this Activity status must not be updated. + + :return boolean indicating this Activity status can be updated. ''' - not_same = [ + DECLINE = ApplicationSelectedActivityPurpose.PROCESSING_STATUS_DECLINED + can_update_status = True + + list_of_not_same = [ p for p in self.proposed_purposes.all() - if not p.purpose_status == status + if not p.purpose_status == status \ + and not p.processing_status == DECLINE # Excluded. ] + can_update_status = not len(list_of_not_same) - return not len(not_same) + return can_update_status @transaction.atomic def set_proposed_purposes_status_for(self, ids, status): @@ -5382,9 +5551,22 @@ def set_proposed_purposes_status_for(self, ids, status): Set the status for the selected proposed purposes. ''' is_updated = False - selected = [ - p for p in self.proposed_purposes.all() if p.purpose.id in ids - ] + CURRENT = ApplicationSelectedActivityPurpose.PURPOSE_STATUS_CURRENT + DECLINE = ApplicationSelectedActivityPurpose.PROCESSING_STATUS_DECLINED + + # Reinstate or Reissue select all applicable purposes for current. Else + # select applicable purposes which have a status of current. + if status == CURRENT: + selected = [ + p for p in self.proposed_purposes.all() + if p.id in ids and not p.processing_status == DECLINE + ] + else: + selected = [ + p for p in self.proposed_purposes.all() + if p.id in ids and not p.processing_status == DECLINE + and p.purpose_status == CURRENT + ] for purpose in selected: purpose.purpose_status = status @@ -5442,38 +5624,75 @@ def store_proposed_attachments(self, proposed_attachments): except BaseException: raise - def has_licence_amendment(self): + def has_licence_amendment(self, purpose_list=[]): + ''' + A check to indicate whether this application selected activity is for a + license with an opened amendment application having the same license + activity and purposes. + :param purpose_list containing purpose ids to be filtered on. + :return boolean. + ''' + logger.debug('AppSelectedActivity.has_licence_amendment - start()') + has_amendment = False valid_status = [ - # Application.CUSTOMER_STATUS_DRAFT, + Application.CUSTOMER_STATUS_DRAFT, Application.CUSTOMER_STATUS_UNDER_REVIEW, Application.CUSTOMER_STATUS_AWAITING_PAYMENT, Application.CUSTOMER_STATUS_AMENDMENT_REQUIRED, Application.CUSTOMER_STATUS_PARTIALLY_APPROVED, ] - has_amendment = Application.objects.filter( + amendments = Application.objects.filter( previous_application_id=self.application.id, + application_type=Application.APPLICATION_TYPE_AMENDMENT, customer_status__in=valid_status, - ).first() + ) + + for app in amendments: + for a in app.activities: + for p in a.proposed_purposes.all(): + if p.purpose_id in purpose_list: + has_amendment = True + break + + logger.debug('AppSelectedActivity.has_licence_amendment - end()') return has_amendment - def get_activity_to_replace(self): + def get_proposed_purposes(self): + ''' + Return a list of ApplicationSelectedActivityPurpose records issued + on this Selected Activity. + ''' + from wildlifecompliance.components.applications.models import ( + ApplicationSelectedActivityPurpose, + ApplicationSelectedActivity, + ) - valid_status = [ - ApplicationSelectedActivity.ACTIVITY_STATUS_DEFAULT, - ApplicationSelectedActivity.ACTIVITY_STATUS_CURRENT, - ApplicationSelectedActivity.ACTIVITY_STATUS_REPLACED, + # latest purposes on the activities which are issued or reissued. + purpose_process_status = [ + ApplicationSelectedActivityPurpose.PROCESSING_STATUS_ISSUED, + ApplicationSelectedActivityPurpose.PROCESSING_STATUS_REISSUE, ] - replace = ApplicationSelectedActivity.objects.filter( - application_id=self.application.previous_application, - activity_status__in=valid_status, - ).first() + purposes = ApplicationSelectedActivityPurpose.objects.filter( + selected_activity_id=self.id, + processing_status__in=purpose_process_status + ).order_by('purpose_sequence') - return replace + return purposes + def get_condition_list(self): + ''' + Get a list of conditions for this Selected Activity. + ''' + condition_list = ApplicationCondition.objects.filter( + application_id=self.application.id, + licence_activity_id=self.licence_activity.id + ) + + return condition_list def get_activity_from_previous(self): ''' @@ -5569,7 +5788,7 @@ class ApplicationSelectedActivityPurpose(models.Model): RENEWABLE = [ PURPOSE_STATUS_DEFAULT, PURPOSE_STATUS_CURRENT, - PURPOSE_STATUS_EXPIRED, + # PURPOSE_STATUS_EXPIRED, ] processing_status = models.CharField( @@ -5804,13 +6023,15 @@ def is_renewable(self): ''' Property to indicate that this purpose has expired or about to expire and can be renewed. + + :return boolean indicating ok and not replaced with Renew application. ''' is_ok = False if self.purpose_status in self.RENEWABLE and self.sent_renewal: is_ok = True - return is_ok + return is_ok and not self.is_replaced() @property def is_active(self): @@ -6093,6 +6314,43 @@ def reinstate(self): ''' self.purpose_status = self.PURPOSE_STATUS_CURRENT + def is_replaced(self): + ''' + Verifies if this selected purpose is going to be replaced by being + associated with a newer application. (amendments, renewals) + + :return boolean indicating this selected purpose is being replaced. + ''' + is_replacing = False + + app_customer_status = [ # customer status to include. + Application.CUSTOMER_STATUS_DRAFT, + Application.CUSTOMER_STATUS_UNDER_REVIEW, + Application.CUSTOMER_STATUS_AWAITING_PAYMENT, + Application.CUSTOMER_STATUS_AMENDMENT_REQUIRED, + Application.CUSTOMER_STATUS_PARTIALLY_APPROVED, + ] + + app_processing_status = [ # processing status to exclude. + Application.PROCESSING_STATUS_DECLINED, + Application.PROCESSING_STATUS_DISCARDED, + ] + + applications = Application.objects.filter( + previous_application_id=self.selected_activity.application_id, + customer_status__in=app_customer_status + ) + + is_replacing = len( + [ + a for a in applications + if a.processing_status not in app_processing_status + and self.purpose in a.licence_purposes.all() + ] + ) + + return is_replacing + def get_purpose_from_previous(self): ''' Gets this Application Selected Activity Purpose from the previous diff --git a/wildlifecompliance/components/applications/serializers.py b/wildlifecompliance/components/applications/serializers.py index 728287b61..200cd4217 100644 --- a/wildlifecompliance/components/applications/serializers.py +++ b/wildlifecompliance/components/applications/serializers.py @@ -1090,6 +1090,11 @@ def get_can_current_user_edit(self, obj): Application.PROCESSING_STATUS_AWAITING_APPLICANT_RESPONSE: # Outstanding amendment request - edit required. result = True + elif not obj.licence: + result = obj.can_user_edit + elif not obj.licence.has_proposed_purposes_in_current(): + # no active purposes ie. licence is expired. + result = False else: result = obj.can_user_edit logger.debug('BaseApplicationSerializer.can_user_edit() - end') diff --git a/wildlifecompliance/components/licences/api.py b/wildlifecompliance/components/licences/api.py index bd80c7fb7..1d6e94d87 100644 --- a/wildlifecompliance/components/licences/api.py +++ b/wildlifecompliance/components/licences/api.py @@ -400,26 +400,29 @@ def surrender_licence(self, request, pk=None, *args, **kwargs): def surrender_purposes(self, request, pk=None, *args, **kwargs): try: purpose_ids_list = request.data.get('purpose_ids_list', None) - if not type(purpose_ids_list) == list: + # if not type(purpose_ids_list) == list: + # raise serializers.ValidationError( + # 'Purpose IDs must be a list') + if not request.user.has_perm('wildlifecompliance.issuing_officer'): raise serializers.ValidationError( - 'Purpose IDs must be a list') - if LicencePurpose.objects.filter(id__in=purpose_ids_list).\ - values_list('licence_activity_id',flat=True).\ - distinct().count() != 1: + 'You are not authorised to surrender licenced activities') + # if LicencePurpose.objects.filter(id__in=purpose_ids_list).\ + # values_list('licence_activity_id',flat=True).\ + # distinct().count() != 1: + # raise serializers.ValidationError( + # 'Selected purposes must all be of the same licence activity') + + if not purpose_ids_list and pk: raise serializers.ValidationError( - 'Selected purposes must all be of the same licence activity') + 'Licence ID and Purpose IDs list must be specified') - if purpose_ids_list and pk: - instance = self.get_object() - LicenceService.request_surrender_licence(instance, request) - serializer = DTExternalWildlifeLicenceSerializer( - instance, context={'request': request} - ) + instance = self.get_object() + LicenceService.request_surrender_licence(instance, request) + serializer = DTExternalWildlifeLicenceSerializer( + instance, context={'request': request}) + + return Response(serializer.data) - return Response(serializer.data) - else: - raise serializers.ValidationError( - 'Licence ID and Purpose IDs list must be specified') except serializers.ValidationError: print(traceback.print_exc()) raise @@ -461,29 +464,29 @@ def cancel_licence(self, request, pk=None, *args, **kwargs): def cancel_purposes(self, request, pk=None, *args, **kwargs): try: purpose_ids_list = request.data.get('purpose_ids_list', None) - if not type(purpose_ids_list) == list: - raise serializers.ValidationError( - 'Purpose IDs must be a list') + # if not type(purpose_ids_list) == list: + # raise serializers.ValidationError( + # 'Purpose IDs must be a list') if not request.user.has_perm('wildlifecompliance.issuing_officer'): raise serializers.ValidationError( 'You are not authorised to cancel licenced activities') - if LicencePurpose.objects.filter(id__in=purpose_ids_list).\ - values_list('licence_activity_id',flat=True).\ - distinct().count() != 1: - raise serializers.ValidationError( - 'Selected purposes must all be of the same licence activity') + # if LicencePurpose.objects.filter(id__in=purpose_ids_list).\ + # values_list('licence_activity_id',flat=True).\ + # distinct().count() != 1: + # raise serializers.ValidationError( + # 'Selected purposes must all be of the same licence activity') - if purpose_ids_list and pk: - instance = self.get_object() - LicenceService.request_cancel_licence(instance, request) - serializer = DTExternalWildlifeLicenceSerializer( - instance, context={'request': request} - ) - - return Response(serializer.data) - else: + if not purpose_ids_list and pk: raise serializers.ValidationError( 'Licence ID and Purpose IDs list must be specified') + + instance = self.get_object() + LicenceService.request_cancel_licence(instance, request) + serializer = DTExternalWildlifeLicenceSerializer( + instance, context={'request': request}) + + return Response(serializer.data) + except serializers.ValidationError: print(traceback.print_exc()) raise @@ -527,31 +530,31 @@ def suspend_purposes(self, request, pk=None, *args, **kwargs): Request to suspend purposes. ''' MSG_NOAUTH = 'You are not authorised to suspend licenced activities' - MSG_NOSAME = 'Purposes must all be of the same licence activity' + # MSG_NOSAME = 'Purposes must all be of the same licence activity' try: purpose_ids_list = request.data.get('purpose_ids_list', None) - if not type(purpose_ids_list) == list: - raise serializers.ValidationError('Purpose IDs must be a list') + # if not type(purpose_ids_list) == list: + # raise serializers.ValidationError('Purpose IDs must be a list') if not request.user.has_perm('wildlifecompliance.issuing_officer'): raise serializers.ValidationError(MSG_NOAUTH) - if LicencePurpose.objects.filter(id__in=purpose_ids_list).\ - values_list('licence_activity_id', flat=True).\ - distinct().count() != 1: - raise serializers.ValidationError(MSG_NOSAME) + # if LicencePurpose.objects.filter(id__in=purpose_ids_list).\ + # values_list('licence_activity_id', flat=True).\ + # distinct().count() != 1: + # raise serializers.ValidationError(MSG_NOSAME) - if purpose_ids_list and pk: - instance = self.get_object() - LicenceService.request_suspend_licence(instance, request) - serializer = DTExternalWildlifeLicenceSerializer( - instance, context={'request': request} - ) - - return Response(serializer.data) - else: + if not purpose_ids_list and pk: raise serializers.ValidationError( 'Licence ID and Purpose IDs list must be specified') + + instance = self.get_object() + LicenceService.request_suspend_licence(instance, request) + serializer = DTExternalWildlifeLicenceSerializer( + instance, context={'request': request}) + + return Response(serializer.data) + except serializers.ValidationError: print(traceback.print_exc()) raise @@ -592,31 +595,31 @@ def reinstate_licence(self, request, pk=None, *args, **kwargs): @detail_route(methods=['POST', ]) def reinstate_purposes(self, request, pk=None, *args, **kwargs): MSG_NOAUTH = 'You are not authorised to reinstate licenced activities' - MSG_NOSAME = 'Purposes must all be of the same licence activity' + # MSG_NOSAME = 'Purposes must all be of the same licence activity' try: purpose_ids_list = request.data.get('purpose_ids_list', None) - if not type(purpose_ids_list) == list: - raise serializers.ValidationError( - 'Purpose IDs must be a list') + # if not type(purpose_ids_list) == list: + # raise serializers.ValidationError( + # 'Purpose IDs must be a list') if not request.user.has_perm('wildlifecompliance.issuing_officer'): raise serializers.ValidationError(MSG_NOAUTH) - if LicencePurpose.objects.filter(id__in=purpose_ids_list).\ - values_list('licence_activity_id',flat=True).\ - distinct().count() != 1: - raise serializers.ValidationError(MSG_NOSAME) - - if purpose_ids_list and pk: - instance = self.get_object() - LicenceService.request_reinstate_licence(instance, request) - serializer = DTExternalWildlifeLicenceSerializer( - instance, context={'request': request} - ) + # if LicencePurpose.objects.filter(id__in=purpose_ids_list).\ + # values_list('licence_activity_id',flat=True).\ + # distinct().count() != 1: + # raise serializers.ValidationError(MSG_NOSAME) - return Response(serializer.data) - else: + if not purpose_ids_list and pk: raise serializers.ValidationError( 'Licence ID and Purpose IDs list must be specified') + + instance = self.get_object() + LicenceService.request_reinstate_licence(instance, request) + serializer = DTExternalWildlifeLicenceSerializer( + instance, context={'request': request}) + + return Response(serializer.data) + except serializers.ValidationError: print(traceback.print_exc()) raise @@ -632,28 +635,29 @@ def reissue_purposes(self, request, pk=None, *args, **kwargs): try: purpose_ids_list = request.data.get('purpose_ids_list', None) - if not type(purpose_ids_list) == list: - raise serializers.ValidationError( - 'Purpose IDs must be a list') + # if not type(purpose_ids_list) == list: + # raise serializers.ValidationError( + # 'Purpose IDs must be a list') if not request.user.has_perm('wildlifecompliance.issuing_officer'): raise serializers.ValidationError( 'You are not authorised to reissue licenced activities') - if LicencePurpose.objects.filter(id__in=purpose_ids_list).\ - values_list('licence_activity_id',flat=True).\ - distinct().count() != 1: - raise serializers.ValidationError( - 'Selected purposes must all be of the same licence activity') + # if LicencePurpose.objects.filter(id__in=purpose_ids_list).\ + # values_list('licence_activity_id',flat=True).\ + # distinct().count() != 1: + # raise serializers.ValidationError( + # 'Selected purposes must all be of the same licence activity') - if purpose_ids_list and pk: - instance = self.get_object() - LicenceService.request_reissue_licence(instance, request) - serializer = DTExternalWildlifeLicenceSerializer( - instance, context={'request': request}) - - return Response(serializer.data) - else: + if not purpose_ids_list and pk: raise serializers.ValidationError( 'Licence ID and Purpose IDs list must be specified') + + instance = self.get_object() + licence = LicenceService.request_reissue_licence(instance, request) + serializer = DTExternalWildlifeLicenceSerializer( + licence, context={'request': request}) + + return Response(serializer.data) + except serializers.ValidationError: print(traceback.print_exc()) raise @@ -775,6 +779,7 @@ def list(self, request, *args, **kwargs): licence_activity_id = request.GET.get('licence_activity') licence_no = request.GET.get('licence_no') select_activity_id = request.GET.get('select_activity') + select_purpose_id = request.GET.get('select_purpose') # active_applications are applications linked with licences that have CURRENT or SUSPENDED activities active_applications = Application.get_active_licence_applications(request, application_type) active_current_applications = active_applications.exclude( @@ -903,7 +908,7 @@ def list(self, request, *args, **kwargs): ] p_ids = [ p.purpose_id for p in activitys[0].proposed_purposes.all() - if p.is_issued + if p.id == int(select_purpose_id) ] # amendable_purpose_ids = active_purpose_id2 amendable_purpose_ids = p_ids @@ -915,8 +920,8 @@ def list(self, request, *args, **kwargs): 'licence_activity_id', flat=True) ) - # Filter by Licence Category ID if specified or - # return empty queryset if available_purpose_records is empty for the Licence Category ID specified + # Filter by Licence Category ID if specified or return empty queryset + # if available_purpose_records is empty for the Licence Category ID. if licence_category_id: if available_purpose_records: available_purpose_records = available_purpose_records.filter( @@ -926,8 +931,11 @@ def list(self, request, *args, **kwargs): else: queryset = LicenceCategory.objects.none() - # Filter out LicenceCategory objects that are not linked with available_purpose_records - queryset = queryset.filter(activity__purpose__in=available_purpose_records).distinct() + # Filter out LicenceCategory objects that are not linked with + # available_purpose_records. + # queryset = queryset.filter( + # activity__purpose__in=available_purpose_records + # ).distinct() # Set any changes to base fees. if application_type == Application.APPLICATION_TYPE_AMENDMENT: diff --git a/wildlifecompliance/components/licences/models.py b/wildlifecompliance/components/licences/models.py index 548a30291..473a71f65 100644 --- a/wildlifecompliance/components/licences/models.py +++ b/wildlifecompliance/components/licences/models.py @@ -8,6 +8,7 @@ from django.forms.models import model_to_dict import json import reversion +import logging from ckeditor.fields import RichTextField from ledger.licence.models import LicenceType @@ -20,6 +21,8 @@ Document ) +logger = logging.getLogger(__name__) +# logger = logging def update_licence_doc_filename(instance, filename): return 'wildlifecompliance/licences/{}/documents/{}'.format( @@ -621,6 +624,7 @@ def get_purposes_in_open_applications(self): from wildlifecompliance.components.applications.models import ( Application, ApplicationSelectedActivity) + logger.debug('WildlifeLicence.get_purposes_in_open_apps() - start') open_applications = Application.objects.filter( Q(org_applicant=self.current_application.org_applicant) if self.current_application.org_applicant @@ -643,6 +647,47 @@ def get_purposes_in_open_applications(self): 'licence_purposes', flat=True) + logger.debug('WildlifeLicence.get_purposes_in_open_apps() - end') + return open_purposes + + def get_proposed_purposes_in_open_applications(self): + ''' + Get proposed purposes which are currently in an application being + processed. + + :return list of ApplicationSelectedActivityPurpose records. + ''' + from wildlifecompliance.components.applications.models import ( + Application, ApplicationSelectedActivity) + + logger.debug('WildlifeLicence.get_proposed_purposes_in_open() - start') + open_purposes = [] + + open_applications = Application.objects.filter( + Q(org_applicant=self.current_application.org_applicant) + if self.current_application.org_applicant + else Q(proxy_applicant=self.current_application.proxy_applicant) + if self.current_application.proxy_applicant + else Q( + submitter=self.current_application.submitter, + proxy_applicant=None, + org_applicant=None) + ).computed_filter( + licence_category_id=self.licence_category.id + ).exclude( + selected_activities__processing_status__in=[ + ApplicationSelectedActivity.PROCESSING_STATUS_ACCEPTED, + ApplicationSelectedActivity.PROCESSING_STATUS_DECLINED, + ApplicationSelectedActivity.PROCESSING_STATUS_DISCARDED + ] + ) + + for application in open_applications: + purposes = application.get_proposed_purposes() + if purposes: + open_purposes += purposes + + logger.debug('WildlifeLicence.get_proposed_purposes_in_open() - end') return open_purposes def get_proposed_purposes_in_applications(self): @@ -655,11 +700,13 @@ def get_proposed_purposes_in_applications(self): ApplicationSelectedActivity, ) - # activities for this licence in current or suspended. + # status applicable for issued purpose which have a sequence number. activity_status = [ ApplicationSelectedActivity.ACTIVITY_STATUS_CURRENT, - ApplicationSelectedActivity.ACTIVITY_STATUS_REPLACED, ApplicationSelectedActivity.ACTIVITY_STATUS_SUSPENDED, + ApplicationSelectedActivity.ACTIVITY_STATUS_SURRENDERED, + ApplicationSelectedActivity.ACTIVITY_STATUS_EXPIRED, + ApplicationSelectedActivity.ACTIVITY_STATUS_CANCELLED, ] # latest purposes on the activities which are issued or reissued. purpose_process_status = [ @@ -680,6 +727,53 @@ def get_proposed_purposes_in_applications(self): return purposes + def has_proposed_purposes_in_current(self): + ''' + A Flag to indicate that there are current licence purposes - + ApplicationSelectedActivityPurpose records available otherwise this + licence is not current. + + NOTE: WildlifeLicence is still current with Suspended purposes. + + :return: boolean. + ''' + from wildlifecompliance.components.applications.models import ( + ApplicationSelectedActivityPurpose, + ApplicationSelectedActivity, + ) + + # status applicable for issued purpose which have a sequence number. + activity_status = [ + ApplicationSelectedActivity.ACTIVITY_STATUS_CURRENT, + ApplicationSelectedActivity.ACTIVITY_STATUS_SUSPENDED, + ] + + purpose_status = [ + ApplicationSelectedActivityPurpose.PURPOSE_STATUS_CURRENT, + ApplicationSelectedActivityPurpose.PURPOSE_STATUS_SUSPENDED, + ] + + # latest purposes on the activities which are proposed/issued/reissued. + purpose_process_status = [ + ApplicationSelectedActivityPurpose.PROCESSING_STATUS_ISSUED, + ApplicationSelectedActivityPurpose.PROCESSING_STATUS_REISSUE, + ApplicationSelectedActivityPurpose.PROCESSING_STATUS_PROPOSED, + ] + + activity_ids = [ + a.id for a in self.current_application.get_current_activity_chain( + activity_status__in=activity_status + ) + ] + + purposes = ApplicationSelectedActivityPurpose.objects.filter( + selected_activity__in=activity_ids, + processing_status__in=purpose_process_status, + purpose_status__in=purpose_status, + ).order_by('purpose_sequence') + + return len(purposes) + @property def latest_activities_merged(self): """ @@ -830,46 +924,6 @@ def is_latest_in_category(self): ) ).filter(licence_category_id=self.licence_category.id).latest('id') == self - # @property - # def can_action(self): - # # Returns DICT of can_ if any of the licence's - # # latest_activities can be actioned - # can_action = { - # 'can_amend': False, - # 'can_renew': False, - # 'can_reactivate_renew': False, - # 'can_surrender': False, - # 'can_cancel': False, - # 'can_suspend': False, - # 'can_reissue': False, - # 'can_reinstate': False, - # } - - # # only check if licence is the latest in its category for the applicant - # if self.is_latest_in_category: - # # set True if any activities can be actioned - # purposes_in_open_applications = self.get_purposes_in_open_applications() - # for activity in self.latest_activities: - # activity_can_action = activity.can_action(purposes_in_open_applications) - # if activity_can_action.get('can_amend'): - # can_action['can_amend'] = True - # if activity_can_action.get('can_renew'): - # can_action['can_renew'] = True - # if activity_can_action.get('can_reactivate_renew'): - # can_action['can_reactivate_renew'] = True - # if activity_can_action.get('can_surrender'): - # can_action['can_surrender'] = True - # if activity_can_action.get('can_cancel'): - # can_action['can_cancel'] = True - # if activity_can_action.get('can_suspend'): - # can_action['can_suspend'] = True - # if activity_can_action.get('can_reissue'): - # can_action['can_reissue'] = True - # if activity_can_action.get('can_reinstate'): - # can_action['can_reinstate'] = True - - # return can_action - @property def has_inspection_open(self): """ @@ -953,6 +1007,7 @@ def apply_action_to_purposes(self, request, action): from wildlifecompliance.components.applications.models import ( Application, ApplicationSelectedActivity ) + logger.debug('Licence.apply_action_to_purpose() - start') CANCEL = WildlifeLicence.ACTIVITY_PURPOSE_ACTION_CANCEL SUSPEND = WildlifeLicence.ACTIVITY_PURPOSE_ACTION_SUSPEND SURRENDER = WildlifeLicence.ACTIVITY_PURPOSE_ACTION_SURRENDER @@ -987,8 +1042,8 @@ def apply_action_to_purposes(self, request, action): # A Reissue on Activity Purpose occurs on the selected Activity # only and does apply to all activities. - # TODO:AYN requirement that reissue of active licence purposes only - # from the same licence application. + # NOTE: Multiple Purposes can exist on the selected Activity and + # must only reissue one purpose. if action == WildlifeLicence.ACTIVITY_PURPOSE_ACTION_REISSUE: can_action_purposes_ids_list = purpose_ids_list # licence_activity_id = purpose_ids_list[0] @@ -1171,6 +1226,8 @@ def apply_action_to_purposes(self, request, action): for activity in original_activities: activity.mark_as_replaced(request) + logger.debug('Licence.apply_action_to_purpose() - end') + @property def purposes_available_to_add(self): """ @@ -1269,33 +1326,33 @@ def get_purposes_in_sequence(self): ''' Returns selected licence purposes for this licence in sequence order. ''' - from wildlifecompliance.components.applications.models import ( - ApplicationSelectedActivityPurpose, - ApplicationSelectedActivity, - ) - purposes = [] - # activities for this licence in current or suspended. - activity_status = [ - ApplicationSelectedActivity.ACTIVITY_STATUS_CURRENT, - ApplicationSelectedActivity.ACTIVITY_STATUS_REPLACED, - ApplicationSelectedActivity.ACTIVITY_STATUS_SUSPENDED, - ] - # latest purposes on the activities which are issued or reissued. - purpose_process_status = [ - ApplicationSelectedActivityPurpose.PROCESSING_STATUS_ISSUED, - ApplicationSelectedActivityPurpose.PROCESSING_STATUS_REISSUE, - ] - - activity_ids = [ - a.id for a in self.current_application.get_current_activity_chain( - activity_status__in=activity_status - ) - ] - - purposes = ApplicationSelectedActivityPurpose.objects.filter( - selected_activity__in=activity_ids, - processing_status__in=purpose_process_status - ).order_by('purpose_sequence') + # from wildlifecompliance.components.applications.models import ( + # ApplicationSelectedActivityPurpose, + # ApplicationSelectedActivity, + # ) + # purposes = [] + # # activities for this licence in current or suspended. + # activity_status = [ + # ApplicationSelectedActivity.ACTIVITY_STATUS_CURRENT, + # # ApplicationSelectedActivity.ACTIVITY_STATUS_REPLACED, + # ApplicationSelectedActivity.ACTIVITY_STATUS_SUSPENDED, + # ] + # # latest purposes on the activities which are issued or reissued. + # purpose_process_status = [ + # ApplicationSelectedActivityPurpose.PROCESSING_STATUS_ISSUED, + # ApplicationSelectedActivityPurpose.PROCESSING_STATUS_REISSUE, + # ] + + # activity_ids = [ + # a.id for a in self.current_application.get_current_activity_chain( + # activity_status__in=activity_status + # ) + # ] + + # purposes = ApplicationSelectedActivityPurpose.objects.filter( + # selected_activity__in=activity_ids, + # processing_status__in=purpose_process_status + # ).order_by('purpose_sequence') purposes = self.get_proposed_purposes_in_applications() diff --git a/wildlifecompliance/components/licences/serializers.py b/wildlifecompliance/components/licences/serializers.py index 531d7b506..fcffc19cf 100644 --- a/wildlifecompliance/components/licences/serializers.py +++ b/wildlifecompliance/components/licences/serializers.py @@ -62,10 +62,14 @@ def get_can_add_purpose(self, obj): ''' Check if there are purposes left in the category to add on licence. ''' - can_add = obj.is_latest_in_category and\ - obj.purposes_available_to_add.count() > 0 - - return can_add + is_latest = obj.is_latest_in_category + has_available_purposes = obj.purposes_available_to_add.count() > 0 + has_current_purposes = obj.has_proposed_purposes_in_current() + # can_add = obj.is_latest_in_category and\ + # obj.purposes_available_to_add.count() > 0 and\ + # obj.has_proposed_purposes_in_current() + + return is_latest and has_available_purposes and has_current_purposes class DTInternalWildlifeLicenceSerializer(WildlifeLicenceSerializer): diff --git a/wildlifecompliance/components/licences/services.py b/wildlifecompliance/components/licences/services.py index f61127001..81e4b79dd 100644 --- a/wildlifecompliance/components/licences/services.py +++ b/wildlifecompliance/components/licences/services.py @@ -73,6 +73,8 @@ def request_reissue_licence(licence, request): on_licence_actioner = LicenceActioner(licence) on_licence_actioner.apply_action(request, REISSUE) + application = on_licence_actioner.actioned_application + licence.current_application = application except Exception as e: logger.error('ERR request_reissue_licence() ID {0}: {1}'.format( @@ -209,8 +211,9 @@ def get_activities_list_for(licence, request=None): ''' the_list = None try: - on_licence_actioner = LicenceActioner(licence) - the_list = on_licence_actioner.get_latest_activities_for_request() + actioner = LicenceActioner(licence) + # the_list = actioner.get_latest_activities_for_request() + the_list = actioner.get_latest_activity_purposes_for_request() except Exception as e: logger.error('ERR get_activities_list_for() ID {0}: {1}'.format( @@ -225,14 +228,16 @@ def verify_licence_renewals(request=None): ''' Verifies licences which have purposes about to expire and sends a notification the applicant. - ''' - try: - today = date.today() - issued_status = [ - ApplicationSelectedActivityPurpose.PROCESSING_STATUS_ISSUED, - ] + :return a count of total renew notification sent. + ''' + licence_ids_count = 0 # count of licences verified. + today = date.today() + issued_status = [ + ApplicationSelectedActivityPurpose.PROCESSING_STATUS_ISSUED, + ] + try: # build application ids with renewable purposes. apps = ApplicationSelectedActivityPurpose.objects.filter( expiry_date__gte=today, @@ -255,15 +260,16 @@ def verify_licence_renewals(request=None): # for each licence id verify if renewal required. if not licence_id['licence_id']: continue - LicenceService.verify_licence_renewal_for( + this_licence = LicenceService.verify_licence_renewal_for( licence_id['licence_id'] ) + licence_ids_count = licence_ids_count + this_licence except Exception as e: logger.error('ERR verify_licence_renewals: {0}'.format(e)) raise Exception('Failed verifying licence renewal.') - return True + return licence_ids_count @staticmethod def verify_licence_renewal_for(licence_id, request=None): @@ -271,6 +277,7 @@ def verify_licence_renewal_for(licence_id, request=None): Verifies a licence requiring renewal and send a notification to the applicant. ''' + has_sent_notification = 0 # Count of notification sent. try: period_days = GlobalSettings.objects.values('value').filter( key=GlobalSettings.LICENCE_RENEW_DAYS @@ -293,6 +300,7 @@ def verify_licence_renewal_for(licence_id, request=None): # Send out renewal notice. (only with request) send_licence_renewal_notification( licence, purposes_to_renew, request) + has_sent_notification = 1 except Exception as e: logger.error('ERR verify_licence_renewal_for {0}: {1}'.format( @@ -301,13 +309,15 @@ def verify_licence_renewal_for(licence_id, request=None): )) raise Exception('Failed verifying licence renewal.') - return True + return has_sent_notification @staticmethod def verify_expired_licences(request=None): ''' Verifies licences requiring renewing by expiring licence purposes after their expiry date and sending out a renewal notification. + + :return a count of licenses expired and re-generated. ''' try: # raise Exception('LicenceService not implemented') @@ -353,7 +363,7 @@ def verify_expired_licences(request=None): logger.error('ERR verify_licence_renewal: {0}'.format(e)) raise Exception('Failed verifying licence renewal.') - return True + return licence_ids.count() @staticmethod def verify_expired_licence_for(licence_id, request=None): @@ -405,6 +415,7 @@ class LicenceActioner(LicenceActionable): A representation of a Licence that can be actioned. ''' licence = None # Composite licence. + actioned_application = None actioned_purposes = None filter_on_action = None @@ -489,7 +500,7 @@ def set_actioned_purposes(self, purpose_ids_list, selected_activity_id): p for p in self.licence.get_proposed_purposes_in_applications() if p.purpose_status in self.filter_on_action and ( - p.purpose.id in ids_list if ids_list else True + p.id in ids_list if ids_list else True ) and ( p.selected_activity_id == int(selected_activity_id) @@ -529,18 +540,9 @@ def can_action_purposes(self, purpose_list, activity): if not activity.is_in_latest_licence: return can_action - # FIXME: Needs to allow for multiple activity purposes. - # No action should be available if all of an activity's purposes are in - # open applications check if there are any purposes in open - # applications (i.e. can action) return false for all actions if no - # purposes are still actionable. - # activity_purposes = activity.purposes.values_list('id', flat=True) - # if not len(list((set(activity_purposes) - set(purpose_list)))) > 0: - # return can_action - # multiple activity purposes can exist. No action is available if # an open licence amendment application for activity purpose exist. - if activity.has_licence_amendment(): + if activity.has_licence_amendment(purpose_list): return can_action # can_amend is true if the activity can be included in a Amendment @@ -553,7 +555,12 @@ def can_action_purposes(self, purpose_list, activity): activity_ids=[activity.id] ).exclude(activity_status=SUSPENDED).count() > 0 - can_action['can_amend'] = current + amendable = [ + p for p in activity.proposed_purposes.all() + if p.is_issued and p.is_active and not p.is_replaced() + ] + + can_action['can_amend'] = current and len(amendable) # can_renew is true if the activity can be included in a Renewal # Application Extra exclude for SUSPENDED due to get_current_activities @@ -598,7 +605,7 @@ def can_action_purposes(self, purpose_list, activity): surrenderable = [ p for p in activity.proposed_purposes.all() - if p.is_issued and p.is_active + if p.is_issued and p.is_active and not p.is_replaced() ] can_action['can_surrender'] = current and len(surrenderable) @@ -613,7 +620,7 @@ def can_action_purposes(self, purpose_list, activity): cancelable = [ p for p in activity.proposed_purposes.all() - if p.is_issued and p.is_active + if p.is_issued and p.is_active and not p.is_replaced() ] can_action['can_cancel'] = current and len(cancelable) @@ -628,18 +635,17 @@ def can_action_purposes(self, purpose_list, activity): suspendable = [ p for p in activity.proposed_purposes.all() - if p.is_issued and p.is_active + if p.is_issued and p.is_active and not p.is_replaced() ] can_action['can_suspend'] = current and len(suspendable) - # can_reissue is true if the activity can be included in a Reissue + # can_reissue is true if the activity can be included in a Reissue. # Application Extra exclude for SUSPENDED due to # get_current_activities_for_application_type intentionally not # excluding these as part of the default queryset disable if there are # any open applications to maintain licence sequence data integrity. - # if not purpose_list: - if not activity.has_licence_amendment(): + if not activity.has_licence_amendment(purpose_list): current = self.get_current_activities_for_application_type( Application.APPLICATION_TYPE_REISSUE, activity_ids=[activity.id] @@ -648,18 +654,14 @@ def can_action_purposes(self, purpose_list, activity): ).count() > 0 reissuable = [ - p for p in activity.proposed_purposes.all() if p.is_issued + p for p in activity.proposed_purposes.all() + if p.is_issued and not p.is_replaced() ] can_action['can_reissue'] = current and len(reissuable) # can_reinstate is true if the activity has not yet expired and is # currently SUSPENDED, CANCELLED or SURRENDERED. - # current = self.get_current_activities_for_application_type( - # Application.APPLICATION_TYPE_RENEWAL, - # activity_ids=[activity.id] - # ).exclude(activity_status=SUSPENDED).count() > 0 - reinstatable = [ p for p in activity.proposed_purposes.all() if p.is_reinstatable ] @@ -667,6 +669,100 @@ def can_action_purposes(self, purpose_list, activity): return can_action + def get_latest_activity_purposes_for_request(self, request=None): + ''' + Gets a list of current selected licence activities available on this + licence for an action request. The list is a set of purposes for each + activity on the licence which can be actioned. + + :return list of actionable current selected licence activity purposes. + ''' + latest_activity_purposes = {} + opened_proposed_purposes = [] + + licence_purposes = [ + p for p in self.licence.get_purposes_in_sequence() + if p.is_issued + ] + + if self.licence.is_latest_in_category: + # purposes_in_open_applications = list( + # self.licence.get_purposes_in_open_applications()) + + # Use proposed purpose to ensure multiple purposes of the same type + # are included. + opened_proposed_purpose_ids = None + opened_proposed_purposes = \ + self.licence.get_proposed_purposes_in_open_applications() + if len(opened_proposed_purposes) > 0: + opened_proposed_purpose_ids = [ + p.purpose_id for p in opened_proposed_purposes + ] + purposes_in_open_applications = opened_proposed_purpose_ids + else: + purposes_in_open_applications = None + + sequence = 0 + + # for activity in latest_activities: + for purpose in licence_purposes: + + activity = purpose.selected_activity + + # if not purpose.purpose_id in purposes_in_open_applications or\ + # purposes_in_open_applications == []: + if not purpose in opened_proposed_purposes \ + or opened_proposed_purposes == []: + + activity_can_action = self.can_action_purposes( + [purpose.purpose_id], + activity, + ) + + else: + activity_can_action = { + 'licence_activity_id': activity.licence_activity_id, + 'can_renew': False, + 'can_amend': False, + 'can_surrender': False, + 'can_cancel': False, + 'can_suspend': False, + 'can_reissue': False, + 'can_reinstate': False, + } + + sequence = sequence + 1 + latest_activity_purposes[purpose] = { + 'id': activity.id, + 'licence_activity_id': activity.licence_activity_id, + 'activity_purpose_id': purpose.id, + 'activity_name_str': activity.licence_activity.name, + 'issue_date': purpose.issue_date, + 'start_date': purpose.start_date, + 'expiry_date': '\n'.join([ + '{}'.format(purpose.expiry_date.strftime( + '%d/%m/%Y') if purpose.expiry_date else '') + ]), + 'activity_purpose_names_and_status': '\n'.join([ + '{} ({})'.format( + purpose.purpose.name, purpose.purpose_status) + ]), + 'can_action': + { + 'licence_activity_id': activity.licence_activity_id, + 'can_renew': activity_can_action['can_renew'], + 'can_amend': activity_can_action['can_amend'], + 'can_surrender': activity_can_action['can_surrender'], + 'can_cancel': activity_can_action['can_cancel'], + 'can_suspend': activity_can_action['can_suspend'], + 'can_reissue': activity_can_action['can_reissue'], + 'can_reinstate': activity_can_action['can_reinstate'], + }, + 'sequence': sequence, + } + + return latest_activity_purposes.values() + def get_latest_activities_for_request(self, request=None): ''' Gets a list of current selected licence activities available on this @@ -675,14 +771,6 @@ def get_latest_activities_for_request(self, request=None): :return a list of actionable current selected licence activities. ''' - # latest_activities = self.licence.latest_activities - - # include = [ - # ApplicationSelectedActivityPurpose.PURPOSE_STATUS_SUSPENDED, - # ApplicationSelectedActivityPurpose.PURPOSE_STATUS_CURRENT, - # ApplicationSelectedActivityPurpose.PURPOSE_STATUS_DEFAULT, - # ] - licence_purposes = [ p for p in self.licence.get_purposes_in_sequence() if p.is_issued @@ -692,21 +780,6 @@ def get_latest_activities_for_request(self, request=None): for purpose in licence_purposes: latest_activities.append(purpose.selected_activity) - # new_activities = [ - # a for a in latest_activities - # if a.application.application_type in [ - # 'new_activity', 'new_licence'] - # ] - - # merge_ids = [ - # a.id for a in latest_activities - # if a.application.application_type in [ - # 'amend_activity', 'renew_activity', 'system_generated'] and - # a.licence_activity_id in [ - # n.licence_activity_id for n in new_activities - # ] - # ] - new_ids = [ a.id for a in latest_activities ] @@ -823,7 +896,7 @@ def apply_action(self, request, action): licence activity_id and purposes list, the selected activity status will not be updated to allow further management of the activity. ''' - purpose_ids_list = request.data.get('purpose_ids_list', None) + purpose_ids_list = request.data.get('purpose_ids_list', '0') selected_activity_id = request.data.get('selected_activity_id', None) def _log_selected_activity_request(_activity, _action): @@ -864,30 +937,30 @@ def _log_selected_activity_request(_activity, _action): # Set purposes to be actioned on this actioner. self.filter_on_action = self.ACTIONS.get(action, self.ACTIVE) - self.set_actioned_purposes(purpose_ids_list, selected_activity_id) + self.set_actioned_purposes([purpose_ids_list], selected_activity_id) action_licence = True selected_user_action = None selected_activities = self.licence.latest_activities - if selected_activity_id: + if selected_activity_id: # Action purpose from an Activity level. action_licence = False selected_activities = [ a for a in selected_activities if a.id == int(selected_activity_id) ] + purpose_ids_list = [p.id for p in self.actioned_purposes] for selected_activity in selected_activities: - # build and order purposes on selected activity. - if action_licence: + self.actioned_application = selected_activity.application + + if action_licence: # Action purpose at Licence level. purpose_ids_list = [ - p.purpose.id + p.id for p in selected_activity.proposed_purposes.all() if p.is_issued ] - purpose_ids_list = list(set(purpose_ids_list)) - purpose_ids_list.sort() selected_activity.updated_by = request.user diff --git a/wildlifecompliance/components/organisations/api.py b/wildlifecompliance/components/organisations/api.py index ece3554db..0d4d33efd 100644 --- a/wildlifecompliance/components/organisations/api.py +++ b/wildlifecompliance/components/organisations/api.py @@ -44,6 +44,7 @@ OrganisationRequestSerializer, OrganisationRequestDTSerializer, OrganisationContactSerializer, + OrganisationContactCheckSerializer, OrganisationCheckSerializer, OrganisationPinCheckSerializer, OrganisationRequestActionSerializer, @@ -230,17 +231,71 @@ def contacts_exclude(self, request, *args, **kwargs): print(traceback.print_exc()) raise serializers.ValidationError(str(e)) + @detail_route(methods=['POST', ]) + def add_nonuser_contact(self, request, *args, **kwargs): + try: + serializer = OrganisationContactCheckSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + instance = self.get_object() + admin_flag = False + role = OrganisationContact.ORG_CONTACT_ROLE_USER + status = OrganisationContact.ORG_CONTACT_STATUS_DRAFT + + with transaction.atomic(): + + OrganisationContact.objects.create( + organisation=instance, + first_name=request.data.get('first_name'), + last_name=request.data.get('last_name'), + mobile_number=request.data.get('mobile_number',''), + phone_number=request.data.get('phone_number'), + fax_number=request.data.get('fax_number',''), + email=request.data.get('email'), + user_role=role, + user_status=status, + is_admin=admin_flag + ) + + instance.log_user_action( + OrganisationAction.ACTION_CONTACT_ADDED.format( + '{} {}({})'.format( + request.data.get('first_name'), + request.data.get('last_name'), + request.data.get('email') + ) + ), + request + ) + + serializer = self.get_serializer(instance) + + return Response(serializer.data) + + except serializers.ValidationError: + print(traceback.print_exc()) + raise + except ValidationError as e: + print(traceback.print_exc()) + raise serializers.ValidationError(repr(e.error_dict)) + except Exception as e: + print(traceback.print_exc()) + raise serializers.ValidationError(str(e)) + @detail_route(methods=['POST', ]) def validate_pins(self, request, *args, **kwargs): try: instance = Organisation.objects.get(id=request.data.get('id')) serializer = OrganisationPinCheckSerializer(data=request.data) serializer.is_valid(raise_exception=True) - data = { - 'valid': instance.validate_pins( - serializer.validated_data['pin1'], - serializer.validated_data['pin2'], - request)} + + with transaction.atomic(): + data = { + 'valid': instance.validate_pins( + serializer.validated_data['pin1'], + serializer.validated_data['pin2'], + request)} + if data['valid']: # Notify each Admin member of request. instance.send_organisation_request_link_notification(request) diff --git a/wildlifecompliance/components/organisations/models.py b/wildlifecompliance/components/organisations/models.py index 36785e62c..129154475 100644 --- a/wildlifecompliance/components/organisations/models.py +++ b/wildlifecompliance/components/organisations/models.py @@ -73,30 +73,32 @@ def validate_pins(self, pin1, pin2, request): return val def add_user_contact(self, user, request, admin_flag, role): - with transaction.atomic(): - - OrganisationContact.objects.create( - organisation=self, - first_name=user.first_name, - last_name=user.last_name, - mobile_number=user.mobile_number, - phone_number=user.phone_number, - fax_number=user.fax_number, - email=user.email, - user_role=role, - user_status=OrganisationContact.ORG_CONTACT_STATUS_PENDING, - is_admin=admin_flag + ''' + Add user contact for linking to this Organisation. Linking requires + authorisation as validation pins are supplied by admin. + ''' + OrganisationContact.objects.create( + organisation=self, + first_name=user.first_name, + last_name=user.last_name, + mobile_number=user.mobile_number, + phone_number=user.phone_number, + fax_number=user.fax_number, + email=user.email, + user_role=role, + user_status=OrganisationContact.ORG_CONTACT_STATUS_PENDING, + is_admin=admin_flag - ) + ) - # log linking - self.log_user_action( - OrganisationAction.ACTION_CONTACT_ADDED.format( - '{} {}({})'.format( - user.first_name, - user.last_name, - user.email)), - request) + # log linking + self.log_user_action( + OrganisationAction.ACTION_CONTACT_ADDED.format( + '{} {}({})'.format( + user.first_name, + user.last_name, + user.email)), + request) def accept_user(self, user, request): with transaction.atomic(): diff --git a/wildlifecompliance/components/organisations/serializers.py b/wildlifecompliance/components/organisations/serializers.py index ad15254d4..3f59414a6 100644 --- a/wildlifecompliance/components/organisations/serializers.py +++ b/wildlifecompliance/components/organisations/serializers.py @@ -301,6 +301,35 @@ class Meta: fields = ('id', 'name') +class OrganisationContactCheckSerializer(serializers.Serializer): + ''' + Validation Serializer for Organisation Contact. + ''' + last_name = serializers.CharField() + first_name = serializers.CharField() + organisation = serializers.CharField() + email = serializers.CharField() + + def validate(self, data): + is_invalid = False + invalid_attr = '' + + if data['organisation'] == '': + is_invalid = True + + # validate formatting. + # if data['phone_number'] == '': + # is_invalid = True + + if data['email'] == '': + is_invalid = True + + if is_invalid: + raise serializers.ValidationError('Contact details are invalid.') + + return data + + class OrganisationContactSerializer(serializers.ModelSerializer): user_status = CustomChoiceField(read_only=True) user_role = CustomChoiceField(read_only=True) diff --git a/wildlifecompliance/components/returns/services.py b/wildlifecompliance/components/returns/services.py index 5344028f2..fb6fa9dec 100644 --- a/wildlifecompliance/components/returns/services.py +++ b/wildlifecompliance/components/returns/services.py @@ -220,6 +220,8 @@ def verify_due_returns(id=0, for_all=True): ''' Vertification of return due date seven days before it is due and updating the processing status. + + :return a count of total returns due. ''' DUE_DAYS = 7 @@ -264,6 +266,8 @@ def verify_due_returns(id=0, for_all=True): continue a_return.set_processing_status(status) + return due_returns.count() + @staticmethod def get_details_for(a_return): """ diff --git a/wildlifecompliance/frontend/wildlifecompliance/src/components/common/add_contact.vue b/wildlifecompliance/frontend/wildlifecompliance/src/components/common/add_contact.vue index d3c665575..da187a06b 100644 --- a/wildlifecompliance/frontend/wildlifecompliance/src/components/common/add_contact.vue +++ b/wildlifecompliance/frontend/wildlifecompliance/src/components/common/add_contact.vue @@ -137,9 +137,7 @@ export default { } else { let contact = JSON.parse(JSON.stringify(vm.contact)); contact.organisation = vm.org_id; - contact.user_status.id = 'draft' - contact.user_role.id = 'organisation_user' - vm.$http.post(api_endpoints.organisation_contacts,JSON.stringify(contact),{ + vm.$http.post(helpers.add_endpoint_json(api_endpoints.organisations,vm.org_id+'/add_nonuser_contact'),JSON.stringify(contact),{ emulateJSON:true, }).then((response)=>{ //vm.$parent.loading.splice('processing contact',1); diff --git a/wildlifecompliance/frontend/wildlifecompliance/src/components/common/applications_dashboard.vue b/wildlifecompliance/frontend/wildlifecompliance/src/components/common/applications_dashboard.vue index 3e4f8673b..a9caeb67f 100644 --- a/wildlifecompliance/frontend/wildlifecompliance/src/components/common/applications_dashboard.vue +++ b/wildlifecompliance/frontend/wildlifecompliance/src/components/common/applications_dashboard.vue @@ -38,7 +38,7 @@ --> -
+
New Application
@@ -292,11 +292,11 @@ export default { else{ if (full.can_current_user_edit) { links += `Continue
`; - if(vm.canDiscardApplication(full)) { - links += `Discard
`; - } } - else if (full.can_user_view) { + if(vm.canDiscardApplication(full)) { + links += `Discard
`; + } + if (full.can_user_view) { links += `View
`; } if (full.can_pay_application){ @@ -523,6 +523,7 @@ export default { computed: { ...mapGetters([ 'canViewPayments', + 'current_user', ]), visibleHeaders: function() { return this.is_external ? this.application_ex_headers : this.application_headers; @@ -533,6 +534,9 @@ export default { is_external: function(){ return this.level == 'external'; }, + showNewApplicationButton: function() { + return this.is_external && this.current_user.identification + }, }, methods:{ ...mapActions([ diff --git a/wildlifecompliance/frontend/wildlifecompliance/src/components/common/compliance_file.vue b/wildlifecompliance/frontend/wildlifecompliance/src/components/common/compliance_file.vue index 6e27a9afc..f01ea61f8 100644 --- a/wildlifecompliance/frontend/wildlifecompliance/src/components/common/compliance_file.vue +++ b/wildlifecompliance/frontend/wildlifecompliance/src/components/common/compliance_file.vue @@ -33,7 +33,7 @@ - +