diff --git a/mooringlicensing/components/approvals/api.py b/mooringlicensing/components/approvals/api.py index 023a7f9ec..49e811d6a 100755 --- a/mooringlicensing/components/approvals/api.py +++ b/mooringlicensing/components/approvals/api.py @@ -1,4 +1,5 @@ import traceback +from django.core.paginator import Paginator, EmptyPage from confy import env import datetime import pytz @@ -112,11 +113,21 @@ class GetSticker(views.APIView): renderer_classes = [JSONRenderer, ] def get(self, request, format=None): - search_term = request.GET.get('term', '') + search_term = request.GET.get('search_term', '') + page_number = request.GET.get('page_number', 1) + items_per_page = 10 + if search_term: - data = Sticker.objects.filter(number__icontains=search_term)[:10] + data = Sticker.objects.filter(number__icontains=search_term) + paginator = Paginator(data, items_per_page) + try: + current_page = paginator.page(page_number) + my_objects = current_page.object_list + except EmptyPage: + my_objects = [] + data_transform = [] - for sticker in data: + for sticker in my_objects: approval_history = sticker.approvalhistory_set.order_by('id').first() # Should not be None, but could be None for the data generated at the early stage of development. if approval_history and approval_history.approval: data_transform.append({ @@ -134,7 +145,12 @@ def get(self, request, format=None): # Should not reach here pass - return Response({"results": data_transform}) + return Response({ + "results": data_transform, + "pagination": { + "more": current_page.has_next() + } + }) return Response() @@ -349,7 +365,7 @@ def get_queryset(self): all = Approval.objects.all() # We may need to exclude the approvals created from the Waiting List Application # target_email_user_id = int(self.request.GET.get('target_email_user_id', 0)) - target_email_user_id = int(self.request.data.get('target_email_user_id', 0)) + target_email_user_id = int(self.request.GET.get('target_email_user_id', 0)) if is_internal(self.request): if target_email_user_id: @@ -1294,13 +1310,9 @@ def create_mooring_licence_application(self, request, *args, **kwargs): waiting_list_allocation=waiting_list_allocation, date_invited=current_date, ) + waiting_list_allocation.proposal_applicant.copy_self_to_proposal(new_proposal) logger.info(f'Offering new Mooring Site Licence application: [{new_proposal}], which has been created from the waiting list allocation: [{waiting_list_allocation}].') - # Copy applicant details to the new proposal - proposal_applicant = ProposalApplicant.objects.get(proposal=waiting_list_allocation.current_proposal) - # proposal_applicant.copy_self_to_proposal(new_proposal) - logger.info(f'ProposalApplicant: [{proposal_applicant}] has been copied from the proposal: [{waiting_list_allocation.current_proposal}] to the mooring site licence application: [{new_proposal}].') - # Copy vessel details to the new proposal waiting_list_allocation.current_proposal.copy_vessel_details(new_proposal) logger.info(f'Vessel details have been copied from the proposal: [{waiting_list_allocation.current_proposal}] to the mooring site licence application: [{new_proposal}].') diff --git a/mooringlicensing/components/approvals/models.py b/mooringlicensing/components/approvals/models.py index 7169f5dd3..4ad5f1d62 100755 --- a/mooringlicensing/components/approvals/models.py +++ b/mooringlicensing/components/approvals/models.py @@ -1,3 +1,4 @@ +import math from dateutil.relativedelta import relativedelta import ledger_api_client.utils @@ -344,10 +345,44 @@ def postal_address_obj(self): address_obj = self.submitter_obj.postal_address return address_obj + @property + def proposal_applicant(self): + proposal_applicant = None + if self.current_proposal: + proposal_applicant = self.current_proposal.proposal_applicant + return proposal_applicant + + @property + def postal_first_name(self): + try: + ret_value = self.proposal_applicant.first_name + except: + logger.error(f'Postal address first_name cannot be retrieved for the approval [{self}].') + return '' + + if not ret_value: + logger.warning(f'Empty postal_first_name found for the Approval: [{self}].') + + return ret_value + + @property + def postal_last_name(self): + try: + ret_value = self.proposal_applicant.last_name + except: + logger.error(f'Postal address last_name cannot be retrieved for the approval [{self}].') + return '' + + if not ret_value: + logger.warning(f'Empty postal_last_name found for the Approval: [{self}].') + + return ret_value + + @property def postal_address_line1(self): try: - ret_value = self.postal_address_obj.line1 + ret_value = self.proposal_applicant.postal_address_line1 except: logger.error(f'Postal address line1 cannot be retrieved for the approval [{self}].') return '' @@ -360,7 +395,7 @@ def postal_address_line1(self): @property def postal_address_line2(self): try: - ret_value = self.postal_address_obj.line2 + ret_value = self.proposal_applicant.postal_address_line2 except: logger.error(f'Postal address line2 cannot be retrieved for the approval [{self}]') return '' @@ -370,7 +405,7 @@ def postal_address_line2(self): @property def postal_address_state(self): try: - ret_value = self.postal_address_obj.state + ret_value = self.proposal_applicant.postal_address_state except: logger.error(f'Postal address state cannot be retrieved for the approval [{self}]') return '' @@ -383,7 +418,7 @@ def postal_address_state(self): @property def postal_address_suburb(self): try: - ret_value = self.postal_address_obj.locality + ret_value = self.proposal_applicant.postal_address_suburb except: logger.error(f'Postal address locality cannot be retrieved for the approval [{self}]') return '' @@ -396,7 +431,7 @@ def postal_address_suburb(self): @property def postal_address_postcode(self): try: - ret_value = self.postal_address_obj.postcode + ret_value = self.proposal_applicant.postal_address_postcode except: logger.error(f'Postal address postcode cannot be retrieved for the approval [{self}]') return '' @@ -2333,6 +2368,13 @@ def create_fee_lines(self): private_visit = 'YES' if dcv_admission_arrival.private_visit else 'NO' + if settings.DEBUG: + # In debug environment, we want to avoid decimal number which may cuase some kind of error. + total_amount = math.ceil(total_amount) + total_amount_excl_tax = math.ceil(calculate_excl_gst(total_amount)) if fee_constructor.incur_gst else math.ceil(total_amount) + else: + total_amount_excl_tax = calculate_excl_gst(total_amount) if fee_constructor.incur_gst else total_amount + line_item = { 'ledger_description': '{} Fee: {} (Arrival: {}, Private: {}, {})'.format( fee_constructor.application_type.description, @@ -2343,7 +2385,7 @@ def create_fee_lines(self): ), 'oracle_code': oracle_code, 'price_incl_tax': total_amount, - 'price_excl_tax': calculate_excl_gst(total_amount) if fee_constructor.incur_gst else total_amount, + 'price_excl_tax': total_amount_excl_tax, 'quantity': 1, } line_items.append(line_item) @@ -2522,6 +2564,14 @@ def create_fee_lines(self): db_processes_after_success['season_end_date'] = fee_constructor.fee_season.end_date.__str__() db_processes_after_success['datetime_for_calculating_fee'] = target_datetime.__str__() + if settings.DEBUG: + # In debug environment, we want to avoid decimal number which may cuase some kind of error. + total_amount = math.ceil(fee_item.amount) + total_amount_excl_tax = math.ceil(ledger_api_client.utils.calculate_excl_gst(fee_item.amount)) if fee_constructor.incur_gst else math.ceil(fee_item.amount), + else: + total_amount = fee_item.amount + total_amount_excl_tax = ledger_api_client.utils.calculate_excl_gst(fee_item.amount) if fee_constructor.incur_gst else fee_item.amount, + line_items = [ { # 'ledger_description': '{} Fee: {} (Season: {} to {}) @{}'.format( @@ -2534,8 +2584,8 @@ def create_fee_lines(self): ), # 'oracle_code': application_type.oracle_code, 'oracle_code': ApplicationType.get_current_oracle_code_by_application(application_type.code), - 'price_incl_tax': fee_item.amount, - 'price_excl_tax': ledger_api_client.utils.calculate_excl_gst(fee_item.amount) if fee_constructor.incur_gst else fee_item.amount, + 'price_incl_tax': total_amount, + 'price_excl_tax': total_amount_excl_tax, 'quantity': 1, }, ] @@ -2968,15 +3018,17 @@ def save(self, *args, **kwargs): @property def first_name(self): - if self.approval and self.approval.submitter: - return self.approval.submitter_obj.first_name - return '---' + # if self.approval and self.approval.submitter: + # return self.approval.submitter_obj.first_name + # return '---' + return self.approval.postal_first_name @property def last_name(self): - if self.approval and self.approval.submitter: - return self.approval.submitter_obj.last_name - return '---' + # if self.approval and self.approval.submitter: + # return self.approval.submitter_obj.last_name + # return '---' + return self.approval.postal_last_name @property def postal_address_line1(self): @@ -3006,6 +3058,13 @@ def postal_address_suburb(self): # return '---' return self.approval.postal_address_suburb + @property + def postal_address_postcode(self): + # if self.approval and self.approval.submitter and self.approval.submitter_obj.postal_address: + # return self.approval.submitter_obj.postal_address.postcode + # return '---' + return self.approval.postal_address_postcode + @property def vessel_registration_number(self): if self.vessel_ownership and self.vessel_ownership.vessel: @@ -3018,13 +3077,6 @@ def vessel_applicable_length(self): return self.vessel_ownership.vessel.latest_vessel_details.vessel_applicable_length raise ValueError('Vessel size not found for the sticker: {}'.format(self)) - @property - def postal_address_postcode(self): - # if self.approval and self.approval.submitter and self.approval.submitter_obj.postal_address: - # return self.approval.submitter_obj.postal_address.postcode - # return '---' - return self.approval.postal_address_postcode - class StickerActionDetail(models.Model): sticker = models.ForeignKey(Sticker, blank=True, null=True, related_name='sticker_action_details', on_delete=models.SET_NULL) diff --git a/mooringlicensing/components/compliances/api.py b/mooringlicensing/components/compliances/api.py index 8d8ef02c6..8fbc16e07 100755 --- a/mooringlicensing/components/compliances/api.py +++ b/mooringlicensing/components/compliances/api.py @@ -1,3 +1,4 @@ +import logging import traceback # import os # import datetime @@ -49,6 +50,8 @@ from mooringlicensing.helpers import is_customer, is_internal from rest_framework_datatables.pagination import DatatablesPageNumberPagination +logger = logging.getLogger(__name__) + class ComplianceViewSet(viewsets.ModelViewSet): serializer_class = ComplianceSerializer @@ -372,9 +375,6 @@ def filter_queryset(self, request, queryset, view): if filter_compliance_status and not filter_compliance_status.lower() == 'all': queryset = queryset.filter(customer_status=filter_compliance_status) - # getter = request.query_params.get - # fields = self.get_fields(getter) - # ordering = self.get_ordering(getter, fields) fields = self.get_fields(request) ordering = self.get_ordering(request, view, fields) queryset = queryset.order_by(*ordering) @@ -383,8 +383,13 @@ def filter_queryset(self, request, queryset, view): try: queryset = super(ComplianceFilterBackend, self).filter_queryset(request, queryset, view) + + # Custom search + # search_term = request.GET.get('search[value]') # This has a search term. + # email_users = EmailUser.objects.filter(Q(first_name__icontains=search_term) | Q(last_name__icontains=search_term) | Q(email__icontains=search_term)).values_list('id', flat=True) + # q_set = Compliance.objects.filter(submitter__in=list(email_users)) except Exception as e: - print(e) + logger.error(f'ComplianceFilterBackend raises an error: [{e}]. Query may not work correctly.') setattr(view, '_datatables_total_count', total_count) return queryset diff --git a/mooringlicensing/components/main/models.py b/mooringlicensing/components/main/models.py index 4ac07b7f8..2e84334fe 100755 --- a/mooringlicensing/components/main/models.py +++ b/mooringlicensing/components/main/models.py @@ -68,9 +68,7 @@ class CommunicationsLogEntry(models.Model): subject = models.CharField(max_length=200, blank=True, verbose_name="Subject / Description") text = models.TextField(blank=True) - # customer = models.ForeignKey(EmailUser, null=True, related_name='+') customer = models.IntegerField(null=True) # EmailUserRO - # staff = models.ForeignKey(EmailUser, null=True, related_name='+') staff = models.IntegerField(null=True, blank=True) # EmailUserRO created = models.DateTimeField(auto_now_add=True, null=False, blank=False) diff --git a/mooringlicensing/components/main/utils.py b/mooringlicensing/components/main/utils.py index e0a8b7d89..a193fcb2e 100755 --- a/mooringlicensing/components/main/utils.py +++ b/mooringlicensing/components/main/utils.py @@ -566,8 +566,7 @@ def update_personal_details(request, user_id): # Now we want to update the proposal_applicant of all of this user's proposals with 'draft' status proposals = Proposal.objects.filter(submitter=user_id, processing_status=Proposal.PROCESSING_STATUS_DRAFT) for proposal in proposals: - proposal_applicant = ProposalApplicant.objects.filter(proposal=proposal).order_by('updated_at').last() - serializer = ProposalApplicantSerializer(proposal_applicant, data=payload) + serializer = ProposalApplicantSerializer(proposal.proposal_applicant, data=payload) serializer.is_valid(raise_exception=True) proposal_applicant = serializer.save() logger.info(f'ProposalApplicant: [{proposal_applicant}] has been updated with the data: [{payload}].') diff --git a/mooringlicensing/components/payments_ml/utils.py b/mooringlicensing/components/payments_ml/utils.py index 312d1f538..59948f550 100644 --- a/mooringlicensing/components/payments_ml/utils.py +++ b/mooringlicensing/components/payments_ml/utils.py @@ -1,6 +1,7 @@ import logging # from _pydecimal import Decimal import decimal +import math import pytz from django.http import HttpResponse @@ -106,12 +107,22 @@ def generate_line_item(application_type, fee_amount_adjusted, fee_constructor, i instance.lodgement_number, target_datetime_str, ) + + if settings.DEBUG: + # In debug environment, we want to avoid decimal number which may cuase some kind of error. + total_amount = math.ceil(float(fee_amount_adjusted)) + total_amount_excl_tax = math.ceil(float(calculate_excl_gst(fee_amount_adjusted))) if fee_constructor.incur_gst else math.ceil(float(fee_amount_adjusted)) + else: + total_amount = float(fee_amount_adjusted) + total_amount_excl_tax = float(calculate_excl_gst(fee_amount_adjusted)) if fee_constructor.incur_gst else float(fee_amount_adjusted) return { 'ledger_description': ledger_description, 'oracle_code': application_type.get_oracle_code_by_date(target_datetime.date()), - 'price_incl_tax': float(fee_amount_adjusted), - 'price_excl_tax': float(calculate_excl_gst(fee_amount_adjusted)) if fee_constructor.incur_gst else float(fee_amount_adjusted), + # 'price_incl_tax': float(fee_amount_adjusted), + # 'price_excl_tax': float(calculate_excl_gst(fee_amount_adjusted)) if fee_constructor.incur_gst else float(fee_amount_adjusted), + 'price_incl_tax': total_amount, + 'price_excl_tax': total_amount_excl_tax, 'quantity': 1, } diff --git a/mooringlicensing/components/payments_ml/views.py b/mooringlicensing/components/payments_ml/views.py index 5cbb9fac2..ba0618a0b 100644 --- a/mooringlicensing/components/payments_ml/views.py +++ b/mooringlicensing/components/payments_ml/views.py @@ -1,5 +1,6 @@ import datetime import logging +import math import ledger_api_client.utils # from ledger.checkout.utils import calculate_excl_gst @@ -242,14 +243,23 @@ def post(self, request, *args, **kwargs): application_type = ApplicationType.objects.get(code=settings.APPLICATION_TYPE_REPLACEMENT_STICKER['code']) fee_item = FeeItemStickerReplacement.get_fee_item_by_date(current_datetime.date()) + if settings.DEBUG: + total_amount = 0 if sticker_action_detail.waive_the_fee else math.ceil(fee_item.amount) + total_amount_excl_tax = 0 if sticker_action_detail.waive_the_fee else math.ceil(ledger_api_client.utils.calculate_excl_gst(fee_item.amount)) if fee_item.incur_gst else math.ceil(fee_item.amount) + else: + total_amount = 0 if sticker_action_detail.waive_the_fee else fee_item.amount + total_amount_excl_tax = 0 if sticker_action_detail.waive_the_fee else ledger_api_client.utils.calculate_excl_gst(fee_item.amount) if fee_item.incur_gst else fee_item.amount + lines = [] applicant = None for sticker_action_detail in sticker_action_details: line = { 'ledger_description': 'Sticker Replacement Fee, sticker: {} @{}'.format(sticker_action_detail.sticker, target_datetime_str), 'oracle_code': application_type.get_oracle_code_by_date(current_datetime.date()), - 'price_incl_tax': 0 if sticker_action_detail.waive_the_fee else fee_item.amount, - 'price_excl_tax': 0 if sticker_action_detail.waive_the_fee else ledger_api_client.utils.calculate_excl_gst(fee_item.amount) if fee_item.incur_gst else fee_item.amount, + # 'price_incl_tax': 0 if sticker_action_detail.waive_the_fee else fee_item.amount, + # 'price_excl_tax': 0 if sticker_action_detail.waive_the_fee else ledger_api_client.utils.calculate_excl_gst(fee_item.amount) if fee_item.incur_gst else fee_item.amount, + 'price_incl_tax': total_amount, + 'price_excl_tax': total_amount_excl_tax, 'quantity': 1, } if not applicant: diff --git a/mooringlicensing/components/proposals/api.py b/mooringlicensing/components/proposals/api.py index 5c1960bad..68c396891 100755 --- a/mooringlicensing/components/proposals/api.py +++ b/mooringlicensing/components/proposals/api.py @@ -1,4 +1,5 @@ import json +from django.core.paginator import Paginator, EmptyPage import os import traceback import pathlib @@ -26,7 +27,7 @@ from mooringlicensing.components.main.models import GlobalSettings from mooringlicensing.components.organisations.models import Organisation from mooringlicensing.components.proposals.utils import ( - save_proponent_data, create_proposal_applicant_if_not_exist, make_ownership_ready, + save_proponent_data, update_proposal_applicant, make_ownership_ready, ) from mooringlicensing.components.proposals.models import VesselOwnershipCompanyOwnership, searchKeyWords, search_reference, ProposalUserAction, \ ProposalType, ProposalApplicant, VesselRegistrationDocument @@ -146,43 +147,71 @@ class GetVessel(views.APIView): renderer_classes = [JSONRenderer, ] def get(self, request, format=None): - search_term = request.GET.get('term', '') + search_term = request.GET.get('search_term', '') + page_number = request.GET.get('page_number', 1) + items_per_page = 10 + if search_term: data_transform = [] - + ### VesselDetails ml_data = VesselDetails.filtered_objects.filter( - Q(vessel__rego_no__icontains=search_term) | - Q(vessel_name__icontains=search_term) - ).values( - 'vessel__id', - 'vessel__rego_no', - 'vessel_name' - )[:10] - for vd in ml_data: + Q(vessel__rego_no__icontains=search_term) | + Q(vessel_name__icontains=search_term) + ).values( + 'vessel__id', + 'vessel__rego_no', + 'vessel_name' + ) + paginator = Paginator(ml_data, items_per_page) + try: + current_page = paginator.page(page_number) + my_objects = current_page.object_list + except EmptyPage: + logger.debug(f'VesselDetails empty') + my_objects = [] + + for vd in my_objects: data_transform.append({ 'id': vd.get('vessel__id'), 'rego_no': vd.get('vessel__rego_no'), 'text': vd.get('vessel__rego_no') + ' - ' + vd.get('vessel_name'), 'entity_type': 'ml', - }) + }) + + ### DcvVessel dcv_data = DcvVessel.objects.filter( - Q(rego_no__icontains=search_term) | - Q(vessel_name__icontains=search_term) - ).values( - 'id', - 'rego_no', - 'vessel_name' - )[:10] - for dcv in dcv_data: + Q(rego_no__icontains=search_term) | + Q(vessel_name__icontains=search_term) + ).values( + 'id', + 'rego_no', + 'vessel_name' + ) + paginator2 = Paginator(dcv_data, items_per_page) + try: + current_page2 = paginator2.page(page_number) + my_objects2 = current_page2.object_list + except EmptyPage: + logger.debug(f'DcvVessel empty') + my_objects2 = [] + + for dcv in my_objects2: data_transform.append({ 'id': dcv.get('id'), 'rego_no': dcv.get('rego_no'), 'text': dcv.get('rego_no') + ' - ' + dcv.get('vessel_name'), 'entity_type': 'dcv', - }) + }) + ## order results data_transform.sort(key=lambda item: item.get("id")) - return Response({"results": data_transform}) + + return Response({ + "results": data_transform, + "pagination": { + "more": current_page.has_next() or current_page2.has_next() + } + }) return Response() @@ -190,17 +219,31 @@ class GetMooring(views.APIView): renderer_classes = [JSONRenderer, ] def get(self, request, format=None): + search_term = request.GET.get('search_term', '') + page_number = request.GET.get('page_number', 1) + items_per_page = 10 private_moorings = request.GET.get('private_moorings') - search_term = request.GET.get('term', '') + if search_term: if private_moorings: - # data = Mooring.private_moorings.filter(name__icontains=search_term).values('id', 'name')[:10] data = Mooring.private_moorings.filter(name__icontains=search_term).values('id', 'name') else: - # data = Mooring.objects.filter(name__icontains=search_term).values('id', 'name')[:10] data = Mooring.objects.filter(name__icontains=search_term).values('id', 'name') - data_transform = [{'id': mooring['id'], 'text': mooring['name']} for mooring in data] - return Response({"results": data_transform}) + paginator = Paginator(data, items_per_page) + try: + current_page = paginator.page(page_number) + my_objects = current_page.object_list + except EmptyPage: + my_objects = [] + + data_transform = [{'id': mooring['id'], 'text': mooring['name']} for mooring in my_objects] + + return Response({ + "results": data_transform, + "pagination": { + "more": current_page.has_next() + } + }) return Response() @@ -566,9 +609,10 @@ def get_queryset(self): if is_internal(self.request): if target_email_user_id: + # Internal user may be accessing here via search person result. target_user = EmailUser.objects.get(id=target_email_user_id) - user_orgs = [org.id for org in target_user.mooringlicensing_organisations.all()] - all = all.filter(Q(org_applicant_id__in=user_orgs) | Q(submitter=target_user.id) | Q(site_licensee_email=target_user.email)) + user_orgs = Organisation.objects.filter(delegates__contains=[target_user.id]) + all = all.filter(Q(org_applicant__in=user_orgs) | Q(submitter=target_user.id) | Q(site_licensee_email=target_user.email)) return all elif is_customer(self.request): orgs = Organisation.objects.filter(delegates__contains=[request_user.id]) @@ -814,8 +858,10 @@ def get_serializer_class(self): # return super(ProposalViewSet, self).retrieve(request, *args, **kwargs) def get_object(self): - logger.info(f'Getting object in the ProposalViewSet...') - if self.kwargs.get('id').isnumeric(): + id = self.kwargs.get('id') + logger.info(f'Getting proposal in the ProposalViewSet by the ID: [{id}]...') + + if id.isnumeric(): obj = super(ProposalViewSet, self).get_object() else: # When AUP holder accesses this proposal for endorsement @@ -825,7 +871,6 @@ def get_object(self): return obj def get_queryset(self): - logger.info(f'Getting queryset in the ProposalViewSet...') request_user = self.request.user if is_internal(self.request): qs = Proposal.objects.all() @@ -1241,7 +1286,7 @@ def submit(self, request, *args, **kwargs): # Ensure status is draft and submitter is same as applicant. is_authorised_to_modify(request, instance) - save_proponent_data(instance,request,self) + save_proponent_data(instance, request, self) return Response() @detail_route(methods=['GET',], detail=True) @@ -1873,7 +1918,7 @@ class VesselViewSet(viewsets.ModelViewSet): @detail_route(methods=['POST',], detail=True) @basic_exception_handler def find_related_bookings(self, request, *args, **kwargs): - return Response({}) + # return Response({}) vessel = self.get_object() booking_date_str = request.data.get("selected_date") booking_date = None @@ -1900,23 +1945,23 @@ def find_related_approvals(self, request, *args, **kwargs): for vd in vd_set: for prop in vd.proposal_set.all(): if ( - prop.approval and - selected_date >= prop.approval.start_date and - selected_date <= prop.approval.expiry_date and - # ensure vessel has not been sold - prop.vessel_ownership and not prop.vessel_ownership.end_date - ): + prop.approval and + selected_date >= prop.approval.start_date and + selected_date <= prop.approval.expiry_date and + # ensure vessel has not been sold + prop.vessel_ownership and not prop.vessel_ownership.end_date + ): if prop.approval not in approval_list: approval_list.append(prop.approval) else: for vd in vd_set: for prop in vd.proposal_set.all(): if ( - prop.approval and - prop.approval.status == 'current' and - # ensure vessel has not been sold - prop.vessel_ownership and not prop.vessel_ownership.end_date - ): + prop.approval and + prop.approval.status == 'current' and + # ensure vessel has not been sold + prop.vessel_ownership and not prop.vessel_ownership.end_date + ): if prop.approval not in approval_list: approval_list.append(prop.approval) diff --git a/mooringlicensing/components/proposals/models.py b/mooringlicensing/components/proposals/models.py index b847edde6..d25d450b5 100644 --- a/mooringlicensing/components/proposals/models.py +++ b/mooringlicensing/components/proposals/models.py @@ -2053,7 +2053,7 @@ def clone_proposal_with_status_reset(self): logger.info(f'Cloning the proposal: [{self}] to the proposal: [{proposal}]...') - # self.proposal_applicant.copy_self_to_proposal(proposal) + self.proposal_applicant.copy_self_to_proposal(proposal) proposal.save(no_revision=True) return proposal @@ -2062,7 +2062,7 @@ def clone_proposal_with_status_reset(self): @property def proposal_applicant(self): - proposal_applicant = ProposalApplicant.objects.get(proposal=self) + proposal_applicant = ProposalApplicant.objects.filter(proposal=self).order_by('updated_at').last() return proposal_applicant def renew_approval(self,request): @@ -2386,6 +2386,41 @@ class Meta: def __str__(self): return f'{self.email}: {self.first_name} {self.last_name} (ID: {self.id})' + @property + def postal_address_line1(self): + if self.postal_same_as_residential: + return self.residential_line1 + else: + return self.postal_line1 + + @property + def postal_address_line2(self): + if self.postal_same_as_residential: + return self.residential_line2 + else: + return self.postal_line2 + + @property + def postal_address_state(self): + if self.postal_same_as_residential: + return self.residential_state + else: + return self.postal_state + + @property + def postal_address_suburb(self): + if self.postal_same_as_residential: + return self.residential_locality + else: + return self.postal_locality + + @property + def postal_address_postcode(self): + if self.postal_same_as_residential: + return self.residential_postcode + else: + return self.postal_postcode + def copy_self_to_proposal(self, target_proposal): proposal_applicant = ProposalApplicant.objects.create( proposal=target_proposal, @@ -3385,6 +3420,974 @@ def update_or_create_approval(self, current_datetime, request=None): compliance.save() self.generate_compliances(approval, request) + +def update_sticker_doc_filename(instance, filename): + return '{}/stickers/batch/{}'.format(settings.MEDIA_APP_DIR, filename) + + +def update_sticker_response_doc_filename(instance, filename): + return '{}/stickers/response/{}'.format(settings.MEDIA_APP_DIR, filename) + + +class StickerPrintingContact(models.Model): + TYPE_EMIAL_TO = 'to' + TYPE_EMAIL_CC = 'cc' + TYPE_EMAIL_BCC = 'bcc' + TYPES = ( + (TYPE_EMIAL_TO, 'To'), + (TYPE_EMAIL_CC, 'Cc'), + (TYPE_EMAIL_BCC, 'Bcc'), + ) + email = models.EmailField(blank=True, null=True) + type = models.CharField(max_length=255, choices=TYPES, blank=False, null=False,) + enabled = models.BooleanField(default=True) + + def __str__(self): + return '{} ({})'.format(self.email, self.type) + + class Meta: + app_label = 'mooringlicensing' + + +class StickerPrintedContact(models.Model): + TYPE_EMIAL_TO = 'to' + TYPE_EMAIL_CC = 'cc' + TYPE_EMAIL_BCC = 'bcc' + TYPES = ( + (TYPE_EMIAL_TO, 'To'), + (TYPE_EMAIL_CC, 'Cc'), + (TYPE_EMAIL_BCC, 'Bcc'), + ) + email = models.EmailField(blank=True, null=True) + type = models.CharField(max_length=255, choices=TYPES, blank=False, null=False,) + enabled = models.BooleanField(default=True) + + def __str__(self): + return '{} ({})'.format(self.email, self.type) + + class Meta: + app_label = 'mooringlicensing' + + +class StickerPrintingBatch(Document): + _file = models.FileField(upload_to=update_sticker_doc_filename, max_length=512) + emailed_datetime = models.DateTimeField(blank=True, null=True) # Once emailed, this field has a value + + class Meta: + app_label = 'mooringlicensing' + + +class StickerPrintingResponseEmail(models.Model): + email_subject = models.CharField(max_length=255, blank=True, null=True) + email_body = models.TextField(null=True, blank=True) + email_date = models.CharField(max_length=255, blank=True, null=True) + email_from = models.CharField(max_length=255, blank=True, null=True) + email_message_id = models.CharField(max_length=255, blank=True, null=True) + + class Meta: + app_label = 'mooringlicensing' + + def __str__(self): + return f'Id: {self.id}, subject: {self.email_subject}' + + +class StickerPrintingResponse(Document): + _file = models.FileField(upload_to=update_sticker_response_doc_filename, max_length=512) + sticker_printing_response_email = models.ForeignKey(StickerPrintingResponseEmail, blank=True, null=True, on_delete=models.SET_NULL) + processed = models.BooleanField(default=False) # Processed by a cron to update sticker details + no_errors_when_process = models.NullBooleanField(default=None) + + class Meta: + app_label = 'mooringlicensing' + + def __str__(self): + if self._file: + return f'Id: {self.id}, {self._file.url}' + else: + return f'Id: {self.id}' + + @property + def email_subject(self): + if self.sticker_printing_response_email: + return self.sticker_printing_response_email.email_subject + return '' + + @property + def email_date(self): + if self.sticker_printing_response_email: + return self.sticker_printing_response_email.email_date + return '' + + +class WaitingListApplication(Proposal): + proposal = models.OneToOneField(Proposal, parent_link=True, on_delete=models.CASCADE) + code = 'wla' + prefix = 'WL' + + new_application_text = "I want to be included on the waiting list for a mooring site licence" + + apply_page_visibility = True + description = 'Waiting List Application' + + class Meta: + app_label = 'mooringlicensing' + + def validate_against_existing_proposals_and_approvals(self): + from mooringlicensing.components.approvals.models import Approval, ApprovalHistory, WaitingListAllocation, MooringLicence + today = datetime.datetime.now(pytz.timezone(TIME_ZONE)).date() + + # Get blocking proposals + proposals = Proposal.objects.filter( + vessel_details__vessel=self.vessel_ownership.vessel, + vessel_ownership__end_date__gt=today, # Vessel has not been sold yet + ).exclude(id=self.id) + child_proposals = [proposal.child_obj for proposal in proposals] + proposals_wla = [] + proposals_mla = [] + for proposal in child_proposals: + if proposal.processing_status not in [ + Proposal.PROCESSING_STATUS_APPROVED, + Proposal.PROCESSING_STATUS_DECLINED, + Proposal.PROCESSING_STATUS_DISCARDED, + ]: + if type(proposal) == WaitingListApplication: + proposals_wla.append(proposal) + if type(proposal) == MooringLicenceApplication: + proposals_mla.append(proposal) + + # Get blocking approvals + approval_histories = ApprovalHistory.objects.filter( + end_date=None, + vessel_ownership__vessel=self.vessel_ownership.vessel, + vessel_ownership__end_date__gt=today, # Vessel has not been sold yet + ).exclude(approval_id=self.approval_id) + approvals = [ah.approval for ah in approval_histories] + approvals = list(dict.fromkeys(approvals)) # remove duplicates + approvals_wla = [] + approvals_ml = [] + for approval in approvals: + if approval.status in Approval.APPROVED_STATUSES: + if type(approval.child_obj) == WaitingListAllocation: + approvals_wla.append(approval) + if type(approval.child_obj) == MooringLicence: + approvals_ml.append(approval) + + if (proposals_wla or approvals_wla or proposals_mla or approvals_ml): + raise serializers.ValidationError("The vessel in the application is already listed in " + + ", ".join(['{} {} '.format(proposal.description, proposal.lodgement_number) for proposal in proposals_wla]) + + ", ".join(['{} {} '.format(approval.description, approval.lodgement_number) for approval in approvals_wla]) + ) + # Person can have only one WLA, Waiting Liast application, Mooring Licence and Mooring Licence application + elif ( + WaitingListApplication.get_intermediate_proposals(self.submitter).exclude(id=self.id) or + WaitingListAllocation.get_intermediate_approvals(self.submitter).exclude(approval=self.approval) or + MooringLicenceApplication.get_intermediate_proposals(self.submitter) or + MooringLicence.get_valid_approvals(self.submitter) + ): + raise serializers.ValidationError("Person can have only one WLA, Waiting List application, Mooring Site Licence and Mooring Site Licence application") + + def validate_vessel_length(self, request): + min_mooring_vessel_size_str = GlobalSettings.objects.get(key=GlobalSettings.KEY_MINUMUM_MOORING_VESSEL_LENGTH).value + min_mooring_vessel_size = float(min_mooring_vessel_size_str) + + if self.vessel_details.vessel_applicable_length < min_mooring_vessel_size: + logger.error("Proposal {}: Vessel must be at least {}m in length".format(self, min_mooring_vessel_size_str)) + raise serializers.ValidationError("Vessel must be at least {}m in length".format(min_mooring_vessel_size_str)) + + def process_after_discarded(self): + logger.debug(f'called in [{self}]') + + def process_after_withdrawn(self): + logger.debug(f'called in [{self}]') + + @property + def child_obj(self): + raise NotImplementedError('This method cannot be called on a child_obj') + + @staticmethod + def get_intermediate_proposals(email_user_id): + proposals = WaitingListApplication.objects.filter(submitter=email_user_id).exclude(processing_status__in=[ + Proposal.PROCESSING_STATUS_APPROVED, + Proposal.PROCESSING_STATUS_DECLINED, + Proposal.PROCESSING_STATUS_DISCARDED, + ]) + return proposals + + def create_fee_lines(self): + """ + Create the ledger lines - line item for application fee sent to payment system + """ + logger.info(f'Creating fee lines for the WaitingListApplication: [{self}]...') + + from mooringlicensing.components.payments_ml.models import FeeConstructor + from mooringlicensing.components.payments_ml.utils import generate_line_item + + current_datetime = datetime.datetime.now(pytz.timezone(TIME_ZONE)) + current_datetime_str = current_datetime.astimezone(pytz.timezone(TIME_ZONE)).strftime('%d/%m/%Y %I:%M %p') + target_date = self.get_target_date(current_datetime.date()) + logger.info('Creating fee lines for the proposal: [{}], target date: {}'.format(self, target_date)) + + # Any changes to the DB should be made after the success of payment process + db_processes_after_success = {} + accept_null_vessel = False + + application_type = self.application_type + + if self.vessel_details: + vessel_length = self.vessel_details.vessel_applicable_length + else: + # No vessel specified in the application + if self.does_accept_null_vessel: + # For the amendment application or the renewal application, vessel field can be blank when submit. + vessel_length = -1 + accept_null_vessel = True + else: + # msg = 'No vessel specified for the application {}'.format(self.lodgement_number) + msg = 'The application fee admin data has not been set up correctly for the Waiting List application type. Please contact the Rottnest Island Authority.' + logger.error(msg) + raise Exception(msg) + + logger.info(f'vessel_length: {vessel_length}') + + # Retrieve FeeItem object from FeeConstructor object + fee_constructor = FeeConstructor.get_fee_constructor_by_application_type_and_date(application_type, target_date) + + logger.info(f'FeeConstructor (for main component(WL)): {fee_constructor}') + + if not fee_constructor: + # Fees have not been configured for this application type and date + msg = 'FeeConstructor object for the ApplicationType: {} not found for the date: {} for the application: {}'.format( + application_type, target_date, self.lodgement_number) + logger.error(msg) + raise Exception(msg) + + # Retrieve amounts paid + max_amount_paid = self.get_max_amount_paid_for_main_component() + logger.info(f'Max amount paid so far (for main component(WL)): ${max_amount_paid}') + fee_item = fee_constructor.get_fee_item(vessel_length, self.proposal_type, target_date, accept_null_vessel=accept_null_vessel) + logger.info(f'FeeItem (for main component(WL)): [{fee_item}] has been retrieved for calculation.') + fee_amount_adjusted = self.get_fee_amount_adjusted(fee_item, vessel_length, max_amount_paid) + logger.info(f'Fee amount adjusted (for main component(WL)) to be paid: ${fee_amount_adjusted}') + + db_processes_after_success['season_start_date'] = fee_constructor.fee_season.start_date.__str__() + db_processes_after_success['season_end_date'] = fee_constructor.fee_season.end_date.__str__() + db_processes_after_success['datetime_for_calculating_fee'] = current_datetime_str + db_processes_after_success['fee_item_id'] = fee_item.id if fee_item else 0 + db_processes_after_success['fee_amount_adjusted'] = str(fee_amount_adjusted) + + line_items = [] + line_items.append( + generate_line_item(application_type, fee_amount_adjusted, fee_constructor, self, current_datetime)) + + logger.info(f'line_items calculated: {line_items}') + + return line_items, db_processes_after_success + + @property + def assessor_group(self): + return ledger_api_client.managed_models.SystemGroup.objects.get(name="Mooring Licensing - Assessors: Waiting List") + + @property + def approver_group(self): + return None + + @property + def assessor_recipients(self): + return [retrieve_email_userro(id).email for id in self.assessor_group.get_system_group_member_ids()] + + @property + def approver_recipients(self): + return [] + + def is_assessor(self, user): + if isinstance(user, EmailUserRO): + user = user.id + # return user in self.assessor_group.user_set.all() + return user in self.assessor_group.get_system_group_member_ids() + + #def is_approver(self, user): + # return False + + def is_approver(self, user): + if isinstance(user, EmailUserRO): + user = user.id + #return user in self.approver_group.user_set.all() + # return user in self.assessor_group.user_set.all() + return user in self.assessor_group.get_system_group_member_ids() + + def save(self, *args, **kwargs): + super(WaitingListApplication, self).save(*args, **kwargs) + if self.lodgement_number == '': + new_lodgment_id = '{1}{0:06d}'.format(self.proposal_id, self.prefix) + self.lodgement_number = new_lodgment_id + self.save() + self.proposal.refresh_from_db() + + def send_emails_after_payment_success(self, request): + attachments = [] + if self.invoice: + # invoice_bytes = create_invoice_pdf_bytes('invoice.pdf', self.invoice,) + # api_key = settings.LEDGER_API_KEY + # url = settings.LEDGER_API_URL + '/ledgergw/invoice-pdf/' + api_key + '/' + self.invoice.reference + # url = get_invoice_url(self.invoice.reference, request) + # invoice_pdf = requests.get(url=url) + + api_key = settings.LEDGER_API_KEY + url = settings.LEDGER_API_URL+'/ledgergw/invoice-pdf/'+api_key+'/' + self.invoice.reference + invoice_pdf = requests.get(url=url) + + if invoice_pdf.status_code == 200: + attachment = ('invoice#{}.pdf'.format(self.invoice.reference), invoice_pdf.content, 'application/pdf') + attachments.append(attachment) + try: + ret_value = send_confirmation_email_upon_submit(request, self, True, attachments) + if not self.auto_approve: + send_notification_email_upon_submit_to_assessor(request, self, attachments) + except Exception as e: + logger.exception("Error when sending confirmation/notification email upon submit.", exc_info=True) + + + @property + def does_accept_null_vessel(self): + if self.proposal_type.code in [PROPOSAL_TYPE_AMENDMENT, PROPOSAL_TYPE_RENEWAL,]: + return True + # return False + + def process_after_approval(self, request=None, total_amount=0): + pass + + def does_have_valid_associations(self): + """ + Check if this application has valid associations with other applications and approvals + """ + # TODO: correct the logic. just partially implemented + valid = True + + # Rules for proposal + proposals = WaitingListApplication.objects.\ + filter(vessel_details__vessel=self.vessel_details.vessel).\ + exclude( + Q(id=self.id) | Q(processing_status__in=(Proposal.PROCESSING_STATUS_DECLINED, Proposal.PROCESSING_STATUS_APPROVED, Proposal.PROCESSING_STATUS_DISCARDED)) + ) + if proposals: + # The vessel in this application is already part of another application + valid = False + + return valid + + +class AnnualAdmissionApplication(Proposal): + proposal = models.OneToOneField(Proposal, parent_link=True, on_delete=models.CASCADE) + code = 'aaa' + prefix = 'AA' + new_application_text = "I want to apply for an annual admission permit" + apply_page_visibility = True + description = 'Annual Admission Application' + + def validate_against_existing_proposals_and_approvals(self): + from mooringlicensing.components.approvals.models import Approval, ApprovalHistory, AnnualAdmissionPermit, MooringLicence, AuthorisedUserPermit + today = datetime.datetime.now(pytz.timezone(TIME_ZONE)).date() + + # Get blocking proposals + proposals = Proposal.objects.filter( + vessel_details__vessel=self.vessel_ownership.vessel, + vessel_ownership__end_date__gt=today, # Vessel has not been sold yet + ).exclude(id=self.id) + child_proposals = [proposal.child_obj for proposal in proposals] + proposals_mla = [] + proposals_aaa = [] + proposals_aua = [] + for proposal in child_proposals: + if proposal.processing_status not in [ + Proposal.PROCESSING_STATUS_APPROVED, + Proposal.PROCESSING_STATUS_DECLINED, + Proposal.PROCESSING_STATUS_DISCARDED, + ]: + if type(proposal) == MooringLicenceApplication: + proposals_mla.append(proposal) + if type(proposal) == AnnualAdmissionApplication: + proposals_aaa.append(proposal) + if type(proposal) == AuthorisedUserApplication: + proposals_aua.append(proposal) + + # Get blocking approvals + approval_histories = ApprovalHistory.objects.filter( + end_date=None, + vessel_ownership__vessel=self.vessel_ownership.vessel, + vessel_ownership__end_date__gt=today, # Vessel has not been sold yet + ).exclude(approval_id=self.approval_id) + approvals = [ah.approval for ah in approval_histories] + approvals = list(dict.fromkeys(approvals)) # remove duplicates + approvals_ml = [] + approvals_aap = [] + approvals_aup = [] + for approval in approvals: + if approval.status in Approval.APPROVED_STATUSES: + if type(approval.child_obj) == MooringLicence: + approvals_ml.append(approval) + if type(approval.child_obj) == AnnualAdmissionPermit: + approvals_aap.append(approval) + if type(approval.child_obj) == AuthorisedUserPermit: + approvals_aup.append(approval) + + if proposals_aaa or approvals_aap or proposals_aua or approvals_aup or proposals_mla or approvals_ml: + list_sum = proposals_aaa + proposals_aua + proposals_mla + approvals_aap + approvals_aup + approvals_ml + raise serializers.ValidationError("The vessel in the application is already listed in " + + ", ".join(['{} {} '.format(item.description, item.lodgement_number) for item in list_sum])) + + def validate_vessel_length(self, request): + min_vessel_size_str = GlobalSettings.objects.get(key=GlobalSettings.KEY_MINIMUM_VESSEL_LENGTH).value + min_vessel_size = float(min_vessel_size_str) + + if self.vessel_details.vessel_applicable_length < min_vessel_size: + logger.error("Proposal {}: Vessel must be at least {}m in length".format(self, min_vessel_size_str)) + raise serializers.ValidationError("Vessel must be at least {}m in length".format(min_vessel_size_str)) + + def process_after_discarded(self): + logger.debug(f'called in [{self}]') + + def process_after_withdrawn(self): + logger.debug(f'called in [{self}]') + + class Meta: + app_label = 'mooringlicensing' + + @property + def child_obj(self): + raise NotImplementedError('This method cannot be called on a child_obj') + + def create_fee_lines(self): + """ + Create the ledger lines - line item for application fee sent to payment system + """ + logger.info(f'Creating fee lines for the AnnualAdmissionApplication: [{self}]...') + + from mooringlicensing.components.payments_ml.models import FeeConstructor + from mooringlicensing.components.payments_ml.utils import generate_line_item + + current_datetime = datetime.datetime.now(pytz.timezone(TIME_ZONE)) + current_datetime_str = current_datetime.astimezone(pytz.timezone(TIME_ZONE)).strftime('%d/%m/%Y %I:%M %p') + target_date = self.get_target_date(current_datetime.date()) + annual_admission_type = ApplicationType.objects.get(code=AnnualAdmissionApplication.code) # Used for AUA / MLA + + logger.info('Creating fee lines for the proposal: [{}], target date: {}'.format(self, target_date)) + + # Any changes to the DB should be made after the success of payment process + db_processes_after_success = {} + accept_null_vessel = False + + if self.vessel_details: + vessel_length = self.vessel_details.vessel_applicable_length + else: + # No vessel specified in the application + if self.does_accept_null_vessel: + # For the amendment application or the renewal application, vessel field can be blank when submit. + vessel_length = -1 + accept_null_vessel = True + else: + # msg = 'No vessel specified for the application {}'.format(self.lodgement_number) + msg = 'The application fee admin data has not been set up correctly for the Annual Admission Permit application type. Please contact the Rottnest Island Authority.' + logger.error(msg) + raise Exception(msg) + + logger.info(f'vessel_length: {vessel_length}') + + # Retrieve FeeItem object from FeeConstructor object + fee_constructor = FeeConstructor.get_fee_constructor_by_application_type_and_date(self.application_type, target_date) + + logger.info(f'FeeConstructor (for main component(AA)): {fee_constructor}') + + if self.application_type.code in (AuthorisedUserApplication.code, MooringLicenceApplication.code): + # There is also annual admission fee component for the AUA/MLA. + fee_constructor_for_aa = FeeConstructor.get_fee_constructor_by_application_type_and_date(annual_admission_type, target_date) + if not fee_constructor_for_aa: + # Fees have not been configured for the annual admission application and date + msg = 'FeeConstructor object for the Annual Admission Application not found for the date: {} for the application: {}'.format(target_date, self.lodgement_number) + logger.error(msg) + raise Exception(msg) + if not fee_constructor: + # Fees have not been configured for this application type and date + msg = 'FeeConstructor object for the ApplicationType: {} not found for the date: {} for the application: {}'.format(self.application_type, target_date, self.lodgement_number) + logger.error(msg) + raise Exception(msg) + + # Retrieve amounts paid + max_amount_paid = self.get_max_amount_paid_for_main_component() + logger.info(f'Max amount paid so far (for main component(AA)): ${max_amount_paid}') + fee_item = fee_constructor.get_fee_item(vessel_length, self.proposal_type, target_date, accept_null_vessel=accept_null_vessel) + logger.info(f'FeeItem (for main component(AA)): [{fee_item}] has been retrieved for calculation.') + fee_amount_adjusted = self.get_fee_amount_adjusted(fee_item, vessel_length, max_amount_paid) + logger.info(f'Fee amount adjusted (for main component(AA)) to be paid: ${fee_amount_adjusted}') + + db_processes_after_success['season_start_date'] = fee_constructor.fee_season.start_date.__str__() + db_processes_after_success['season_end_date'] = fee_constructor.fee_season.end_date.__str__() + db_processes_after_success['datetime_for_calculating_fee'] = current_datetime_str + db_processes_after_success['fee_item_id'] = fee_item.id if fee_item else 0 + db_processes_after_success['fee_amount_adjusted'] = str(fee_amount_adjusted) + + line_items = [] + line_items.append(generate_line_item(self.application_type, fee_amount_adjusted, fee_constructor, self, current_datetime)) + + logger.info(f'line_items calculated: {line_items}') + + return line_items, db_processes_after_success + + @property + def assessor_group(self): + # return Group.objects.get(name="Mooring Licensing - Assessors: Annual Admission") + return ledger_api_client.managed_models.SystemGroup.objects.get(name="Mooring Licensing - Assessors: Annual Admission") + + @property + def approver_group(self): + return None + + @property + def assessor_recipients(self): + # return [i.email for i in self.assessor_group.user_set.all()] + return [retrieve_email_userro(id).email for id in self.assessor_group.get_system_group_member_ids()] + + @property + def approver_recipients(self): + return [] + + def is_assessor(self, user): + # return user in dself.assessor_group.user_set.all() + if isinstance(user, EmailUserRO): + user = user.id + return user in self.assessor_group.get_system_group_member_ids() + + #def is_approver(self, user): + # return False + + def is_approver(self, user): + # return user in self.assessor_group.user_set.all() + if isinstance(user, EmailUserRO): + user = user.id + return user in self.assessor_group.get_system_group_member_ids() + + def save(self, *args, **kwargs): + #application_type_acronym = self.application_type.acronym if self.application_type else None + super(AnnualAdmissionApplication, self).save(*args,**kwargs) + if self.lodgement_number == '': + new_lodgment_id = '{1}{0:06d}'.format(self.proposal_id, self.prefix) + self.lodgement_number = new_lodgment_id + self.save() + self.proposal.refresh_from_db() + + def send_emails_after_payment_success(self, request): + attachments = [] + if self.invoice: + # invoice_bytes = create_invoice_pdf_bytes('invoice.pdf', self.invoice,) + # attachment = ('invoice#{}.pdf'.format(self.invoice.reference), invoice_bytes, 'application/pdf') + # attachments.append(attachment) + # url = get_invoice_url(self.invoice.reference, request) + # invoice_pdf = requests.get(url=url) + api_key = settings.LEDGER_API_KEY + url = settings.LEDGER_API_URL+'/ledgergw/invoice-pdf/'+api_key+'/' + self.invoice.reference + invoice_pdf = requests.get(url=url) + + if invoice_pdf.status_code == 200: + attachment = (f'invoice#{self.invoice.reference}', invoice_pdf.content, 'application/pdf') + attachments.append(attachment) + if not self.auto_approve: + try: + send_confirmation_email_upon_submit(request, self, True, attachments) + send_notification_email_upon_submit_to_assessor(request, self, attachments) + except Exception as e: + logger.exception("Error when sending confirmation/notification email upon submit.", exc_info=True) + + + def process_after_approval(self, request=None, total_amount=0): + pass + + @property + def does_accept_null_vessel(self): + # if self.proposal_type.code in (PROPOSAL_TYPE_AMENDMENT,): + # return True + return False + + def does_have_valid_associations(self): + """ + Check if this application has valid associations with other applications and approvals + """ + # TODO: implement logic + return True + + +class AuthorisedUserApplication(Proposal): + proposal = models.OneToOneField(Proposal, parent_link=True, on_delete=models.CASCADE) + code = 'aua' + prefix = 'AU' + new_application_text = "I want to apply for an authorised user permit" + apply_page_visibility = True + description = 'Authorised User Application' + + # This uuid is used to generate the URL for the AUA endorsement link + uuid = models.UUIDField(default=uuid.uuid4, editable=False) + + def validate_against_existing_proposals_and_approvals(self): + from mooringlicensing.components.approvals.models import Approval, ApprovalHistory, AuthorisedUserPermit + today = datetime.datetime.now(pytz.timezone(TIME_ZONE)).date() + + # Get blocking proposals + proposals = Proposal.objects.filter( + vessel_details__vessel=self.vessel_ownership.vessel, + vessel_ownership__end_date__gt=today, # Vessel has not been sold yet + ).exclude(id=self.id) + child_proposals = [proposal.child_obj for proposal in proposals] + proposals_aua = [] + for proposal in child_proposals: + if proposal.processing_status not in [ + Proposal.PROCESSING_STATUS_APPROVED, + Proposal.PROCESSING_STATUS_DECLINED, + Proposal.PROCESSING_STATUS_DISCARDED, + ]: + if type(proposal) == AuthorisedUserApplication: + proposals_aua.append(proposal) + + # Get blocking approvals + approval_histories = ApprovalHistory.objects.filter( + end_date=None, + vessel_ownership__vessel=self.vessel_ownership.vessel, + vessel_ownership__end_date__gt=today, # Vessel has not been sold yet + ).exclude(approval_id=self.approval_id) + approvals = [ah.approval for ah in approval_histories] + approvals = list(dict.fromkeys(approvals)) # remove duplicates + approvals_aup = [] + for approval in approvals: + if approval.status in Approval.APPROVED_STATUSES: + if type(approval.child_obj) == AuthorisedUserPermit: + approvals_aup.append(approval) + + if proposals_aua or approvals_aup: + #association_fail = True + raise serializers.ValidationError("The vessel in the application is already listed in " + + ", ".join(['{} {} '.format(proposal.description, proposal.lodgement_number) for proposal in proposals_aua]) + + ", ".join(['{} {} '.format(approval.description, approval.lodgement_number) for approval in approvals_aup]) + ) + + def validate_vessel_length(self, request): + min_vessel_size_str = GlobalSettings.objects.get(key=GlobalSettings.KEY_MINIMUM_VESSEL_LENGTH).value + min_vessel_size = float(min_vessel_size_str) + + if self.vessel_details.vessel_applicable_length < min_vessel_size: + logger.error("Proposal {}: Vessel must be at least {}m in length".format(self, min_vessel_size_str)) + raise serializers.ValidationError("Vessel must be at least {}m in length".format(min_vessel_size_str)) + + # check new site licensee mooring + proposal_data = request.data.get('proposal') if request.data.get('proposal') else {} + mooring_id = proposal_data.get('mooring_id') + if mooring_id and proposal_data.get('site_licensee_email'): + mooring = Mooring.objects.get(id=mooring_id) + if (self.vessel_details.vessel_applicable_length > mooring.vessel_size_limit or + self.vessel_details.vessel_draft > mooring.vessel_draft_limit): + logger.error("Proposal {}: Vessel unsuitable for mooring".format(self)) + raise serializers.ValidationError("Vessel unsuitable for mooring") + if self.approval: + # Amend / Renewal + if proposal_data.get('keep_existing_mooring'): + # check existing moorings against current vessel dimensions + for moa in self.approval.mooringonapproval_set.filter(end_date__isnull=True): + if self.vessel_details.vessel_applicable_length > moa.mooring.vessel_size_limit: + logger.error(f"Vessel applicable lentgh: [{self.vessel_details.vessel_applicable_length}] is not suitable for the mooring: [{moa.mooring}]") + raise serializers.ValidationError(f"Vessel length: {self.vessel_details.vessel_applicable_length}[m] is not suitable for the vessel size limit: {moa.mooring.vessel_size_limit} [m] of the mooring: [{moa.mooring}]") + if self.vessel_details.vessel_draft > moa.mooring.vessel_draft_limit: + logger.error(f"Vessel draft: [{self.vessel_details.vessel_draft}] is not suitable for the mooring: [{moa.mooring}]") + raise serializers.ValidationError(f"Vessel draft: {self.vessel_details.vessel_draft} [m] is not suitable for the vessel draft limit: {moa.mooring.vessel_draft_limit} [m] of the mooring: [{moa.mooring}]") + + def process_after_discarded(self): + logger.debug(f'called in [{self}]') + + def process_after_withdrawn(self): + logger.debug(f'called in [{self}]') + + class Meta: + app_label = 'mooringlicensing' + + @property + def child_obj(self): + raise NotImplementedError('This method cannot be called on a child_obj') + + def create_fee_lines(self): + """ Create the ledger lines - line item for application fee sent to payment system """ + logger.info(f'Creating fee lines for the AuthorisedUserApplication: [{self}]...') + + from mooringlicensing.components.payments_ml.models import FeeConstructor + from mooringlicensing.components.payments_ml.utils import generate_line_item + + current_datetime = datetime.datetime.now(pytz.timezone(TIME_ZONE)) + target_date = self.get_target_date(current_datetime.date()) + annual_admission_type = ApplicationType.objects.get(code=AnnualAdmissionApplication.code) # Used for AUA / MLA + accept_null_vessel = False + + logger.info('Creating fee lines for the proposal: [{}], target date: {}'.format(self, target_date)) + + if self.vessel_details: + vessel_length = self.vessel_details.vessel_applicable_length + else: + # No vessel specified in the application + if self.does_accept_null_vessel: + # For the amendment application or the renewal application, vessel field can be blank when submit. + vessel_length = -1 + accept_null_vessel = True + else: + # msg = 'No vessel specified for the application {}'.format(self.lodgement_number) + msg = 'The application fee admin data has not been set up correctly for the Authorised User Permit application type. Please contact the Rottnest Island Authority.' + logger.error(msg) + raise Exception(msg) + + logger.info(f'vessel_length: {vessel_length}') + + # Retrieve FeeItem object from FeeConstructor object + fee_constructor = FeeConstructor.get_fee_constructor_by_application_type_and_date(self.application_type, target_date) + fee_constructor_for_aa = FeeConstructor.get_fee_constructor_by_application_type_and_date(annual_admission_type, target_date) + + logger.info(f'FeeConstructor (for main component(AU)): {fee_constructor}') + logger.info(f'FeeConstructor (for AA component): {fee_constructor_for_aa}') + + # There is also annual admission fee component for the AUA/MLA if needed. + ml_exists_for_this_vessel = False + application_has_vessel = True if self.vessel_details else False + + if application_has_vessel: + # When there is a vessel in this application + current_approvals_dict = self.vessel_details.vessel.get_current_approvals(target_date) + for key, approvals in current_approvals_dict.items(): + if key == 'mls' and approvals.count(): + ml_exists_for_this_vessel = True + + if ml_exists_for_this_vessel: + logger.info(f'ML for the vessel: {self.vessel_details.vessel} exists. No charges for the AUP: {self}') + + # When there is 'current' ML, no charge for the AUP + # But before leaving here, we want to store the fee_season under this application the user is applying for. + self.fee_season = fee_constructor.fee_season + self.save() + + logger.info(f'FeeSeason: {fee_constructor.fee_season} is saved under the proposal: {self}') + fee_lines = [generate_line_item(self.application_type, 0, fee_constructor, self, current_datetime),] + + return fee_lines, {} # no line items, no db process + else: + logger.info(f'ML for the vessel: {self.vessel_details.vessel} does not exist.') + + else: + # Null vessel application + logger.info(f'This is null vessel application') + + fee_items_to_store = [] + line_items = [] + + # Retrieve amounts paid + max_amount_paid = self.get_max_amount_paid_for_main_component() + logger.info(f'Max amount paid so far (for main component(AU)): ${max_amount_paid}') + fee_item = fee_constructor.get_fee_item(vessel_length, self.proposal_type, target_date, accept_null_vessel=accept_null_vessel) + logger.info(f'FeeItem (for main component(AU)): [{fee_item}] has been retrieved for calculation.') + fee_amount_adjusted = self.get_fee_amount_adjusted(fee_item, vessel_length, max_amount_paid) + logger.info(f'Fee amount adjusted (for main component(AU)) to be paid: ${fee_amount_adjusted}') + + fee_items_to_store.append({ + 'fee_item_id': fee_item.id, + 'vessel_details_id': self.vessel_details.id if self.vessel_details else '', + 'fee_amount_adjusted': str(fee_amount_adjusted), + }) + line_items.append(generate_line_item(self.application_type, fee_amount_adjusted, fee_constructor, self, current_datetime)) + + if application_has_vessel: + # When the application has a vessel, user have to pay for the AA component, too. + max_amount_paid = self.get_max_amount_paid_for_aa_component(target_date, self.vessel_details.vessel) + logger.info(f'Max amount paid so far (for AA component): ${max_amount_paid}') + fee_item_for_aa = fee_constructor_for_aa.get_fee_item(vessel_length, self.proposal_type, target_date) if fee_constructor_for_aa else None + logger.info(f'FeeItem (for AA component): [{fee_item_for_aa}] has been retrieved for calculation.') + fee_amount_adjusted_additional = self.get_fee_amount_adjusted(fee_item_for_aa, vessel_length, max_amount_paid) + logger.info(f'Fee amount adjusted (for AA component) to be paid: ${fee_amount_adjusted_additional}') + + fee_items_to_store.append({ + 'fee_item_id': fee_item_for_aa.id, + 'vessel_details_id': self.vessel_details.id if self.vessel_details else '', + 'fee_amount_adjusted': str(fee_amount_adjusted_additional), + }) + line_items.append(generate_line_item(annual_admission_type, fee_amount_adjusted_additional, fee_constructor_for_aa, self, current_datetime)) + + logger.info(f'line_items calculated: {line_items}') + + return line_items, fee_items_to_store + + def get_due_date_for_endorsement_by_target_date(self, target_date=timezone.localtime(timezone.now()).date()): + days_type = NumberOfDaysType.objects.get(code=CODE_DAYS_FOR_ENDORSER_AUA) + days_setting = NumberOfDaysSetting.get_setting_by_date(days_type, target_date) + if not days_setting: + # No number of days found + raise ImproperlyConfigured("NumberOfDays: {} is not defined for the date: {}".format(days_type.name, target_date)) + due_date = self.lodgement_date + datetime.timedelta(days=days_setting.number_of_days) + return due_date + + @property + def assessor_group(self): + return ledger_api_client.managed_models.SystemGroup.objects.get(name="Mooring Licensing - Assessors: Authorised User") + + @property + def approver_group(self): + return ledger_api_client.managed_models.SystemGroup.objects.get(name="Mooring Licensing - Approvers: Authorised User") + + @property + def assessor_recipients(self): + return [retrieve_email_userro(i).email for i in self.assessor_group.get_system_group_member_ids()] + + @property + def approver_recipients(self): + return [retrieve_email_userro(i).email for i in self.approver_group.get_system_group_member_ids()] + + def is_assessor(self, user): + # return user in self.assessor_group.user_set.all() + if isinstance(user, EmailUserRO): + user = user.id + return user in self.assessor_group.get_system_group_member_ids() + + def is_approver(self, user): + if isinstance(user, EmailUserRO): + user = user.id + return user in self.approver_group.get_system_group_member_ids() + + def save(self, *args, **kwargs): + super(AuthorisedUserApplication, self).save(*args, **kwargs) + if self.lodgement_number == '': + new_lodgment_id = '{1}{0:06d}'.format(self.proposal_id, self.prefix) + self.lodgement_number = new_lodgment_id + self.save() + self.proposal.refresh_from_db() + + def send_emails_after_payment_success(self, request): + # ret_value = send_submit_email_notification(request, self) + # TODO: Send payment success email to the submitter (applicant) + return True + + def get_mooring_authorisation_preference(self): + if self.keep_existing_mooring and self.previous_application: + return self.previous_application.child_obj.get_mooring_authorisation_preference() + else: + return self.mooring_authorisation_preference + + def process_after_submit(self, request): + self.lodgement_date = datetime.datetime.now(pytz.timezone(TIME_ZONE)) + self.save() + self.log_user_action(ProposalUserAction.ACTION_LODGE_APPLICATION.format(self.lodgement_number), request) + mooring_preference = self.get_mooring_authorisation_preference() + + # if mooring_preference.lower() != 'ria' and self.proposal_type.code in [PROPOSAL_TYPE_NEW,]: + if ((mooring_preference.lower() != 'ria' and self.proposal_type.code == PROPOSAL_TYPE_NEW) or + (mooring_preference.lower() != 'ria' and self.proposal_type.code != PROPOSAL_TYPE_NEW and not self.keep_existing_mooring)): + # Mooring preference is 'site_licensee' and which is new mooring applying for. + self.processing_status = Proposal.PROCESSING_STATUS_AWAITING_ENDORSEMENT + self.save() + # Email to endorser + send_endorsement_of_authorised_user_application_email(request, self) + send_confirmation_email_upon_submit(request, self, False) + else: + self.processing_status = Proposal.PROCESSING_STATUS_WITH_ASSESSOR + self.save() + send_confirmation_email_upon_submit(request, self, False) + if not self.auto_approve: + send_notification_email_upon_submit_to_assessor(request, self) + + def update_or_create_approval(self, current_datetime, request=None): + logger.info(f'Updating/Creating Authorised User Permit from the application: [{self}]...') + # This function is called after payment success for new/amendment/renewal application + + created = None + + # Manage approval + approval_created = False + if self.proposal_type.code == PROPOSAL_TYPE_NEW: + # When new application + approval, approval_created = self.approval_class.objects.update_or_create( + current_proposal=self, + defaults={ + 'issue_date': current_datetime, + 'start_date': current_datetime.date(), + 'expiry_date': self.end_date, + 'submitter': self.submitter, + } + ) + if approval_created: + from mooringlicensing.components.approvals.models import Approval + logger.info(f'Approval: [{approval}] has been created.') + approval.cancel_existing_annual_admission_permit(current_datetime.date()) + + self.approval = approval + self.save() + + elif self.proposal_type.code == PROPOSAL_TYPE_AMENDMENT: + # When amendment application + approval = self.approval.child_obj + approval.current_proposal = self + approval.issue_date = current_datetime + approval.start_date = current_datetime.date() + # We don't need to update expiry_date when amendment. Also self.end_date can be None. + approval.submitter = self.submitter + approval.save() + elif self.proposal_type.code == PROPOSAL_TYPE_RENEWAL: + # When renewal application + approval = self.approval.child_obj + approval.current_proposal = self + approval.issue_date = current_datetime + approval.start_date = current_datetime.date() + approval.expiry_date = self.end_date + approval.submitter = self.submitter + approval.renewal_sent = False + approval.expiry_notice_sent = False + approval.renewal_count += 1 + approval.save() + + # update proposed_issuance_approval and MooringOnApproval if not system reissue (no request) or auto_approve + existing_mooring_count = None + if request and not self.auto_approve: + # Create MooringOnApproval records + ## also see logic in approval.add_mooring() + mooring_id_pk = self.proposed_issuance_approval.get('mooring_id') + ria_selected_mooring = None + if mooring_id_pk: + ria_selected_mooring = Mooring.objects.get(id=mooring_id_pk) + + if ria_selected_mooring: + approval.add_mooring(mooring=ria_selected_mooring, site_licensee=False) + else: + if approval.current_proposal.mooring: + approval.add_mooring(mooring=approval.current_proposal.mooring, site_licensee=True) + # updating checkboxes + for moa1 in self.proposed_issuance_approval.get('mooring_on_approval'): + for moa2 in self.approval.mooringonapproval_set.filter(mooring__mooring_licence__status='current'): + # convert proposed_issuance_approval to an end_date + if moa1.get("id") == moa2.id and not moa1.get("checked") and not moa2.end_date: + moa2.end_date = current_datetime.date() + moa2.save() + elif moa1.get("id") == moa2.id and moa1.get("checked") and moa2.end_date: + moa2.end_date = None + moa2.save() + # set auto_approve renewal application ProposalRequirement due dates to those from previous application + 12 months + if self.auto_approve and self.proposal_type.code == PROPOSAL_TYPE_RENEWAL: + for req in self.requirements.filter(is_deleted=False): + if req.copied_from and req.copied_from.due_date: + req.due_date = req.copied_from.due_date + relativedelta(months=+12) + req.save() + # do not process compliances for system reissue + if request: + # Generate compliances + from mooringlicensing.components.compliances.models import Compliance, ComplianceUserAction + target_proposal = self.previous_application if self.proposal_type.code == PROPOSAL_TYPE_AMENDMENT else self.proposal + for compliance in Compliance.objects.filter( + approval=approval.approval, + proposal=target_proposal, + processing_status='future', + ): + #approval_compliances.delete() + compliance.processing_status='discarded' + compliance.customer_status = 'discarded' + compliance.reminder_sent=True + compliance.post_reminder_sent=True + compliance.save() + self.generate_compliances(approval, request) + # always reset this flag approval.renewal_sent = False approval.export_to_mooring_booking = True diff --git a/mooringlicensing/components/proposals/utils.py b/mooringlicensing/components/proposals/utils.py index fc8832cc5..16549392e 100644 --- a/mooringlicensing/components/proposals/utils.py +++ b/mooringlicensing/components/proposals/utils.py @@ -1,3 +1,4 @@ +import datetime import re from decimal import Decimal @@ -377,7 +378,7 @@ def save_proponent_data_aaa(instance, request, viewset): logger.info(f'Update the Proposal: [{instance}] with the data: [{proposal_data}].') if viewset.action == 'submit': - create_proposal_applicant_if_not_exist(instance.child_obj, request) + update_proposal_applicant(instance.child_obj, request) # if instance.invoice and instance.invoice.payment_status in ['paid', 'over_paid']: if instance.invoice and get_invoice_payment_status(instance.id) in ['paid', 'over_paid']: @@ -412,7 +413,7 @@ def save_proponent_data_wla(instance, request, viewset): logger.info(f'Update the Proposal: [{instance}] with the data: [{proposal_data}].') if viewset.action == 'submit': - create_proposal_applicant_if_not_exist(instance.child_obj, request) + update_proposal_applicant(instance.child_obj, request) # if instance.invoice and instance.invoice.payment_status in ['paid', 'over_paid']: if instance.invoice and get_invoice_payment_status(instance.invoice.id) in ['paid', 'over_paid']: @@ -422,7 +423,6 @@ def save_proponent_data_wla(instance, request, viewset): instance.processing_status = Proposal.PROCESSING_STATUS_WITH_ASSESSOR instance.save() - def save_proponent_data_mla(instance, request, viewset): logger.info(f'Saving proponent data of the proposal: [{instance}]') @@ -450,7 +450,7 @@ def save_proponent_data_mla(instance, request, viewset): logger.info(f'Update the Proposal: [{instance}] with the data: [{proposal_data}].') if viewset.action == 'submit': - create_proposal_applicant_if_not_exist(instance.child_obj, request) + update_proposal_applicant(instance.child_obj, request) instance.child_obj.process_after_submit(request) instance.refresh_from_db() @@ -482,7 +482,7 @@ def save_proponent_data_aua(instance, request, viewset): logger.info(f'Update the Proposal: [{instance}] with the data: [{proposal_data}].') if viewset.action == 'submit': - create_proposal_applicant_if_not_exist(instance.child_obj, request) + update_proposal_applicant(instance.child_obj, request) instance.child_obj.process_after_submit(request) instance.refresh_from_db() @@ -1007,10 +1007,44 @@ def get_fee_amount_adjusted(proposal, fee_item_being_applied, vessel_length): return fee_amount_adjusted -def create_proposal_applicant_if_not_exist(proposal, request): +def update_proposal_applicant(proposal, request): proposal_applicant, created = ProposalApplicant.objects.get_or_create(proposal=proposal) if created: - # Copy data from the EmailUserRO only when a new proposal_applicant obj is created + logger.info(f'ProposalApplicant: [{proposal_applicant}] has been created for the proposal: [{proposal}].') + + # Retrieve proposal applicant data from the application + proposal_applicant_data = request.data.get('profile') if request.data.get('profile') else {} + + # Copy data from the application + if proposal_applicant_data: + proposal_applicant.first_name = proposal_applicant_data['first_name'] + proposal_applicant.last_name = proposal_applicant_data['last_name'] + # correct_date = datetime.datetime.strptime(proposal_applicant_data['dob'], "%d/%m/%Y").strftime("%Y-%m-%d") + correct_date = datetime.datetime.strptime(proposal_applicant_data['dob'], '%d/%m/%Y').date() + proposal_applicant.dob = correct_date + + proposal_applicant.residential_line1 = proposal_applicant_data['residential_line1'] + proposal_applicant.residential_line2 = proposal_applicant_data['residential_line2'] + proposal_applicant.residential_line3 = proposal_applicant_data['residential_line3'] + proposal_applicant.residential_locality = proposal_applicant_data['residential_locality'] + proposal_applicant.residential_state = proposal_applicant_data['residential_state'] + proposal_applicant.residential_country = proposal_applicant_data['residential_country'] + proposal_applicant.residential_postcode = proposal_applicant_data['residential_postcode'] + + proposal_applicant.postal_same_as_residential = proposal_applicant_data['postal_same_as_residential'] + proposal_applicant.postal_line1 = proposal_applicant_data['postal_line1'] + proposal_applicant.postal_line2 = proposal_applicant_data['postal_line2'] + proposal_applicant.postal_line3 = proposal_applicant_data['postal_line3'] + proposal_applicant.postal_locality = proposal_applicant_data['postal_locality'] + proposal_applicant.postal_state = proposal_applicant_data['postal_state'] + proposal_applicant.postal_country = proposal_applicant_data['postal_country'] + proposal_applicant.postal_postcode = proposal_applicant_data['postal_postcode'] + + proposal_applicant.email = proposal_applicant_data['email'] + proposal_applicant.phone_number = proposal_applicant_data['phone_number'] + proposal_applicant.mobile_number = proposal_applicant_data['mobile_number'] + else: + # Copy data from the EmailUserRO proposal_applicant.first_name = request.user.first_name proposal_applicant.last_name = request.user.last_name proposal_applicant.dob = request.user.dob @@ -1036,8 +1070,8 @@ def create_proposal_applicant_if_not_exist(proposal, request): proposal_applicant.phone_number = request.user.phone_number proposal_applicant.mobile_number = request.user.mobile_number - proposal_applicant.save() - logger.info(f'ProposalApplicant: [{proposal_applicant}] has been created.') + proposal_applicant.save() + logger.info(f'ProposalApplicant: [{proposal_applicant}] has been updated.') def make_ownership_ready(proposal, request): diff --git a/mooringlicensing/components/users/api.py b/mooringlicensing/components/users/api.py index 311c4a57d..8efc45bd4 100755 --- a/mooringlicensing/components/users/api.py +++ b/mooringlicensing/components/users/api.py @@ -30,7 +30,7 @@ # from ledger.accounts.models import EmailUser,Address, Profile, EmailIdentity, EmailUserAction from ledger_api_client.ledger_models import EmailUserRO as EmailUser, Address from mooringlicensing.components.approvals.models import Approval - +from django.core.paginator import Paginator, EmptyPage from mooringlicensing.components.main.decorators import basic_exception_handler # from ledger.address.models import Country # from datetime import datetime,timedelta, date @@ -90,25 +90,24 @@ def get(self, request, format=None): class GetProposalApplicant(views.APIView): renderer_classes = [JSONRenderer,] - DISPLAY_PROPOSAL_APPLICANT = False def get(self, request, proposal_pk, format=None): from mooringlicensing.components.proposals.models import Proposal, ProposalApplicant proposal = Proposal.objects.get(id=proposal_pk) if (is_customer(self.request) and proposal.submitter == request.user.id) or is_internal(self.request): # Holder of this proposal is accessing OR internal user is accessing. - if self.DISPLAY_PROPOSAL_APPLICANT: - proposal_applicant = ProposalApplicant.objects.get(proposal=proposal) - serializer = ProposalApplicantSerializer(proposal_applicant, context={'request': request}) + if proposal.proposal_applicant: + # When proposal has a proposal_applicant + serializer = ProposalApplicantSerializer(proposal.proposal_applicant, context={'request': request}) else: submitter = retrieve_email_userro(proposal.submitter) serializer = EmailUserRoSerializer(submitter) return Response(serializer.data) elif is_customer(self.request) and proposal.site_licensee_email == request.user.email: # ML holder is accessing the proposal as an endorser - if self.DISPLAY_PROPOSAL_APPLICANT: - proposal_applicant = ProposalApplicant.objects.get(proposal=proposal) - serializer = ProposalApplicantForEndorserSerializer(proposal_applicant, context={'request': request}) + if proposal.proposal_applicant: + # When proposal has a proposal_applicant + serializer = ProposalApplicantForEndorserSerializer(proposal.proposal_applicant, context={'request': request}) else: submitter = retrieve_email_userro(proposal.submitter) serializer = EmailUserRoForEndorserSerializer(submitter) @@ -128,37 +127,30 @@ class GetPerson(views.APIView): renderer_classes = [JSONRenderer,] def get(self, request, format=None): - search_term = request.GET.get('term', '') - # a space in the search term is interpreted as first name, last name + search_term = request.GET.get('search_term', '') + page_number = request.GET.get('page_number', 1) + items_per_page = 10 + if search_term: - #if ' ' in search_term: - # first_name_part, last_name_part = search_term.split(' ') - # data = EmailUser.objects.filter( - # (Q(first_name__icontains=first_name_part) & - # Q(last_name__icontains=last_name_part)) | - # Q(first_name__icontains=search_term) | - # Q(last_name__icontains=search_term) - # )[:10] - #else: - # data = EmailUser.objects.filter( - # Q(first_name__icontains=search_term) | - # Q(last_name__icontains=search_term) | - # Q(email__icontains=search_term) - # )[:10] - data = EmailUser.objects.annotate( - search_term=Concat( - "first_name", - Value(" "), - "last_name", - Value(" "), - "email", - output_field=CharField(), - ) - ).filter(search_term__icontains=search_term)[:10] - print(data[0].__dict__) - print(len(data)) + my_queryset = EmailUser.objects.annotate( + search_term=Concat( + "first_name", + Value(" "), + "last_name", + Value(" "), + "email", + output_field=CharField(), + ) + ).filter(search_term__icontains=search_term) + paginator = Paginator(my_queryset, items_per_page) + try: + current_page = paginator.page(page_number) + my_objects = current_page.object_list + except EmptyPage: + my_objects = [] + data_transform = [] - for email_user in data: + for email_user in my_objects: if email_user.dob: text = '{} {} (DOB: {})'.format(email_user.first_name, email_user.last_name, email_user.dob) else: @@ -168,7 +160,12 @@ def get(self, request, format=None): email_user_data = serializer.data email_user_data['text'] = text data_transform.append(email_user_data) - return Response({"results": data_transform}) + return Response({ + "results": data_transform, + "pagination": { + "more": current_page.has_next() + } + }) return Response() @@ -374,23 +371,40 @@ def add_comms_log(self, request, *args, **kwargs): try: with transaction.atomic(): instance = self.get_object() - mutable=request.data._mutable - request.data._mutable=True - request.data['emailuser'] = u'{}'.format(instance.id) - request.data['staff'] = u'{}'.format(request.user.id) - request.data._mutable=mutable - serializer = EmailUserLogEntrySerializer(data=request.data) - serializer.is_valid(raise_exception=True) - comms = serializer.save() - # Save the files + # mutable=request.data._mutable + # request.data._mutable=True + # request.data['emailuser'] = u'{}'.format(instance.id) + # request.data['staff'] = u'{}'.format(request.user.id) + # request.data._mutable=mutable + # serializer = EmailUserLogEntrySerializer(data=request.data) + # serializer.is_valid(raise_exception=True) + # comms = serializer.save() + ### Save the files + # for f in request.FILES: + # document = comms.documents.create() + # document.name = str(request.FILES[f]) + # document._file = request.FILES[f] + # document.save() + ### End Save Documents + kwargs = { + 'subject': request.data.get('subject', ''), + 'text': request.data.get('text', ''), + 'email_user_id': instance.id, + 'customer': instance.id, + 'staff': request.data.get('staff', request.user.id), + 'to': request.data.get('to', ''), + 'fromm': request.data.get('fromm', ''), + 'cc': '', + } + eu_entry = EmailUserLogEntry.objects.create(**kwargs) + + # for attachment in attachments: for f in request.FILES: - document = comms.documents.create() + document = eu_entry.documents.create() document.name = str(request.FILES[f]) document._file = request.FILES[f] document.save() - # End Save Documents - - return Response(serializer.data) + return Response({}) except serializers.ValidationError: print(traceback.print_exc()) raise diff --git a/mooringlicensing/components/users/models.py b/mooringlicensing/components/users/models.py index e2fd87e3b..41f78bfdc 100644 --- a/mooringlicensing/components/users/models.py +++ b/mooringlicensing/components/users/models.py @@ -32,7 +32,7 @@ class Meta: def update_emailuser_comms_log_filename(instance, filename): - return '{}/emailusers/{}/communications/{}/{}'.format(settings.MEDIA_APP_DIR, instance.log_entry.emailuser.id, instance.id, filename) + return '{}/emailusers/{}/communications/{}/{}'.format(settings.MEDIA_APP_DIR, instance.log_entry.email_user_id, instance.id, filename) class EmailUserLogDocument(Document): diff --git a/mooringlicensing/components/users/serializers.py b/mooringlicensing/components/users/serializers.py index c892bb62c..7954ed97c 100755 --- a/mooringlicensing/components/users/serializers.py +++ b/mooringlicensing/components/users/serializers.py @@ -422,11 +422,10 @@ class CommunicationLogEntrySerializer(serializers.ModelSerializer): class EmailUserLogEntrySerializer(CommunicationLogEntrySerializer): # TODO: implement - pass # documents = serializers.SerializerMethodField() -# class Meta: -# model = EmailUserLogEntry -# fields = '__all__' + class Meta: + model = EmailUserLogEntry + fields = '__all__' # read_only_fields = ( # 'customer', # ) diff --git a/mooringlicensing/frontend/mooringlicensing/src/components/common/bookings_permits.vue b/mooringlicensing/frontend/mooringlicensing/src/components/common/bookings_permits.vue index bb0eb6b82..a614f94d1 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/common/bookings_permits.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/common/bookings_permits.vue @@ -153,14 +153,17 @@ from '@/utils/hooks' } } // Bookings - // TODO: separate vue component may be required if (this.entity.type === "vessel") { + console.log('vessel') const res = await this.$http.post(`/api/vessel/${this.entity.id}/find_related_bookings.json`, payload); this.bookings = []; + console.log('res.body: ') + console.log(res.body) for (let booking of res.body) { this.bookings.push(booking); } } else if (this.entity.type === "mooring") { + console.log('mooring') const res = await this.$http.post(`/api/mooring/${this.entity.id}/find_related_bookings.json`, payload); this.bookings = []; for (let booking of res.body) { diff --git a/mooringlicensing/frontend/mooringlicensing/src/components/common/comms_logs.vue b/mooringlicensing/frontend/mooringlicensing/src/components/common/comms_logs.vue index 2911aac14..816032850 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/common/comms_logs.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/common/comms_logs.vue @@ -22,7 +22,7 @@ -
+
Actions
Show
@@ -56,6 +56,10 @@ export default { disable_add_entry: { type: Boolean, default: true + }, + enable_actions_section: { + type: Boolean, + default: true } }, data() { diff --git a/mooringlicensing/frontend/mooringlicensing/src/components/common/table_compliances.vue b/mooringlicensing/frontend/mooringlicensing/src/components/common/table_compliances.vue index 6c470ab55..e67eb0335 100644 --- a/mooringlicensing/frontend/mooringlicensing/src/components/common/table_compliances.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/common/table_compliances.vue @@ -200,12 +200,13 @@ export default { approvalSubmitterColumn: function() { return { data: "id", - orderable: true, - searchable: true, + orderable: false, + searchable: false, visible: true, 'render': function(row, type, full){ return full.approval_submitter; - } + }, + // name: 'proposal__proposalapplicant__first_name' } }, approvalTypeColumn: function() { @@ -266,7 +267,7 @@ export default { // 5. Due Date data: "id", orderable: true, - searchable: true, + searchable: false, visible: true, 'render': function(row, type, full){ console.log(full) @@ -331,8 +332,8 @@ export default { return { // 7. Action data: "id", - orderable: true, - searchable: true, + orderable: false, + searchable: false, visible: true, 'render': function(row, type, full){ return full.assigned_to_name; diff --git a/mooringlicensing/frontend/mooringlicensing/src/components/external/proposal.vue b/mooringlicensing/frontend/mooringlicensing/src/components/external/proposal.vue index 1f53e6300..f8500e1a8 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/external/proposal.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/external/proposal.vue @@ -45,6 +45,7 @@ @mooringPreferenceChanged="updateMooringPreference" @updateVesselOwnershipChanged="updateVesselOwnershipChanged" @noVessel="noVessel" + @profile-fetched="populateProfile" />
@@ -190,6 +194,8 @@ export default { autoApprove: false, missingVessel: false, // add_vessel: false, + profile_original: {}, + profile: {}, } }, components: { @@ -198,33 +204,37 @@ export default { AuthorisedUserApplication, MooringLicenceApplication, }, - // watch: { - // disableSubmit() { - // //console.log("disableSubmit") - // }, - // }, computed: { + profileHasChanged: function(){ + let originalHash = JSON.stringify(this.profile_original) + let currentHash = JSON.stringify(this.profile) + if (originalHash !== currentHash){ + return true + } else { + return false + } + }, disableSubmit: function() { let disable = false if (this.proposal){ if (this.proposal.proposal_type.code ==='amendment'){ if (this.missingVessel && ['aaa', 'aua'].includes(this.proposal.application_type_code)){ - disable = true; + disable = true } else { if (['aaa', 'mla'].includes(this.proposal.application_type_code)){ - if (!this.vesselChanged && !this.vesselOwnershipChanged) { - disable = true; + if (!this.vesselChanged && !this.vesselOwnershipChanged && !this.profileHasChanged) { + disable = true console.log('%cSubmit button is disabled 1', 'color: #FF0000') } } else if (this.proposal.application_type_code === 'wla'){ - if (!this.vesselChanged && !this.mooringPreferenceChanged && !this.vesselOwnershipChanged) { - disable = true; + if (!this.vesselChanged && !this.mooringPreferenceChanged && !this.vesselOwnershipChanged && !this.profileHasChanged) { + disable = true console.log('%cSubmit button is disabled 2', 'color: #FF0000') } } else if (this.proposal.application_type_code === 'aua'){ - if (!this.vesselChanged && !this.mooringOptionsChanged && !this.vesselOwnershipChanged) { - disable = true; + if (!this.vesselChanged && !this.mooringOptionsChanged && !this.vesselOwnershipChanged && !this.profileHasChanged) { + disable = true console.log('%cSubmit button is disabled 3', 'color: #FF0000') } } @@ -311,6 +321,10 @@ export default { }, }, methods: { + populateProfile: function(profile) { + this.profile_original = Object.assign({}, profile) // This is shallow copy but it's enough + this.profile = profile + }, noVessel: function(noVessel) { this.missingVessel = noVessel; }, @@ -374,6 +388,7 @@ export default { let payload = { proposal: {}, vessel: {}, + profile: {}, } // WLA if (this.$refs.waiting_list_application) { @@ -472,6 +487,7 @@ export default { } */ } + payload.profile = this.profile //vm.$http.post(vm.proposal_form_url,payload).then(res=>{ const res = await vm.$http.post(url, payload); @@ -751,9 +767,6 @@ export default { }, submit: async function(){ - //console.log('in submit()') - //let vm = this; - // remove the confirm prompt when navigating away from window (on button 'Submit' click) this.submitting = true; this.paySubmitting=true; diff --git a/mooringlicensing/frontend/mooringlicensing/src/components/form_aaa.vue b/mooringlicensing/frontend/mooringlicensing/src/components/form_aaa.vue index af1627a86..c80e06a95 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/form_aaa.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/form_aaa.vue @@ -308,7 +308,9 @@ } }, populateProfile: function(profile) { - this.profile = Object.assign({}, profile); + // this.profile = Object.assign({}, profile); + this.profile = profile + this.$emit('profile-fetched', this.profile); }, set_tabs:function(){ let vm = this; diff --git a/mooringlicensing/frontend/mooringlicensing/src/components/form_aua.vue b/mooringlicensing/frontend/mooringlicensing/src/components/form_aua.vue index 21d2d6333..d28f70236 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/form_aua.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/form_aua.vue @@ -406,7 +406,8 @@ } }, populateProfile: function(profile) { - this.profile = Object.assign({}, profile); + this.profile = profile + this.$emit('profile-fetched', this.profile); }, set_tabs:function(){ let vm = this; diff --git a/mooringlicensing/frontend/mooringlicensing/src/components/form_mla.vue b/mooringlicensing/frontend/mooringlicensing/src/components/form_mla.vue index 90193c614..c0881bc88 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/form_mla.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/form_mla.vue @@ -413,7 +413,9 @@ } }, populateProfile: function(profile) { - this.profile = Object.assign({}, profile); + // this.profile = Object.assign({}, profile); + this.profile = profile + this.$emit('profile-fetched', this.profile); }, set_tabs:function(){ let vm = this; diff --git a/mooringlicensing/frontend/mooringlicensing/src/components/form_wla.vue b/mooringlicensing/frontend/mooringlicensing/src/components/form_wla.vue index f58c06a6d..0c208fb69 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/form_wla.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/form_wla.vue @@ -335,7 +335,9 @@ export default { }, populateProfile: function (profile) { - this.profile = Object.assign({}, profile); + // this.profile = Object.assign({}, profile); + this.profile = profile + this.$emit('profile-fetched', this.profile); }, set_tabs: function () { let vm = this; diff --git a/mooringlicensing/frontend/mooringlicensing/src/components/internal/person/person_detail.vue b/mooringlicensing/frontend/mooringlicensing/src/components/internal/person/person_detail.vue index 0ed801624..d67b961a0 100644 --- a/mooringlicensing/frontend/mooringlicensing/src/components/internal/person/person_detail.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/internal/person/person_detail.vue @@ -8,6 +8,7 @@ :logs_url="logs_url" :comms_add_url="comms_add_url" :disable_add_entry="false" + :enable_actions_section="false" />
diff --git a/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_mooring.vue b/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_mooring.vue index 977fd7e32..8b67f8f94 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_mooring.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_mooring.vue @@ -65,16 +65,24 @@ from '@/utils/hooks' "theme": "bootstrap", allowClear: true, placeholder:"Select Mooring", + pagination: true, ajax: { url: api_endpoints.mooring_lookup, - //url: api_endpoints.vessel_rego_nos, dataType: 'json', data: function(params) { - var query = { - term: params.term, + return { + search_term: params.term, + page: params.page || 1, type: 'public', } - return query; + }, + processResults: function(data){ + return { + 'results': data.results, + 'pagination': { + 'more': data.pagination.more + } + } }, }, }). diff --git a/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_person.vue b/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_person.vue index 445722142..53f76a72d 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_person.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_person.vue @@ -61,17 +61,25 @@ from '@/utils/hooks' "theme": "bootstrap", allowClear: true, placeholder:"Select Person", + pagination: true, ajax: { url: api_endpoints.person_lookup, - //url: api_endpoints.vessel_rego_nos, dataType: 'json', data: function(params) { - console.log(params) - var query = { - term: params.term, + return { + search_term: params.term, + page: params.page || 1, type: 'public', } - return query; + }, + processResults: function(data){ + console.log({data}) + return { + 'results': data.results, + 'pagination': { + 'more': data.pagination.more + } + } }, }, }). diff --git a/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_sticker.vue b/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_sticker.vue index fa6634a5d..0ecf5d87d 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_sticker.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_sticker.vue @@ -65,16 +65,24 @@ from '@/utils/hooks' "theme": "bootstrap", allowClear: true, placeholder:"Select Sticker", + pagination: true, ajax: { url: api_endpoints.sticker_lookup, - //url: api_endpoints.vessel_rego_nos, dataType: 'json', data: function(params) { - var query = { - term: params.term, + return { + search_term: params.term, + page: params.page || 1, type: 'public', } - return query; + }, + processResults: function(data){ + return { + 'results': data.results, + 'pagination': { + 'more': data.pagination.more + } + } }, }, }). diff --git a/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_vessel.vue b/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_vessel.vue index 2705936ce..97cb152fb 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_vessel.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_vessel.vue @@ -72,16 +72,24 @@ from '@/utils/hooks' "theme": "bootstrap", allowClear: true, placeholder:"Select Vessel", + pagination: true, ajax: { url: api_endpoints.vessel_lookup, - //url: api_endpoints.vessel_rego_nos, dataType: 'json', data: function(params) { - var query = { - term: params.term, + return { + search_term: params.term, + page: params.page || 1, type: 'public', } - return query; + }, + processResults: function(data){ + return { + 'results': data.results, + 'pagination': { + 'more': data.pagination.more + } + } }, }, }). diff --git a/mooringlicensing/frontend/mooringlicensing/src/components/user/profile.vue b/mooringlicensing/frontend/mooringlicensing/src/components/user/profile.vue index c39aa083e..4c8002bd7 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/user/profile.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/user/profile.vue @@ -35,30 +35,30 @@
- +
- +
- +
-
-
+
@@ -84,29 +84,29 @@
- +
- +
- +
- +
- @@ -120,36 +120,36 @@
- +
- +
- +
- +
- +
- @@ -158,12 +158,12 @@
-
-
+
@@ -190,7 +190,7 @@
- +
@@ -199,21 +199,21 @@
- +
- +
-
-
+
@@ -357,8 +357,6 @@ export default { role : null, phoneNumberReadonly: false, mobileNumberReadonly: false, - - readonly2: true, // We don't allow customer to edit the persona details on the application page } }, components: { @@ -969,13 +967,15 @@ export default { }); }, fetchProfile: async function(){ + console.log('in fetchProfile') let response = null; - if (this.submitterId) { - response = await Vue.http.get(`${api_endpoints.submitter_profile}?submitter_id=${this.submitterId}`); - } else { - response = await Vue.http.get(api_endpoints.profile + '/' + this.proposalId); - } - this.profile = Object.assign(response.body); + // if (this.submitterId) { + // response = await Vue.http.get(`${api_endpoints.submitter_profile}?submitter_id=${this.submitterId}`); + // } else { + // response = await Vue.http.get(api_endpoints.profile + '/' + this.proposalId); + // } + response = await Vue.http.get(api_endpoints.profile + '/' + this.proposalId) + this.profile = Object.assign(response.body) if (this.profile.residential_address == null){ this.profile.residential_address = Object.assign({country:'AU'}) } @@ -985,14 +985,14 @@ export default { if (this.profile.dob) { this.profile.dob = moment(this.profile.dob).format('DD/MM/YYYY') } - this.phoneNumberReadonly = this.profile.phone_number === '' || this.profile.phone_number === null || this.profile.phone_number === 0 ? false : true; - this.mobileNumberReadonly = this.profile.mobile_number === '' || this.profile.mobile_number === null || this.profile.mobile_number === 0 ? false : true; + this.phoneNumberReadonly = this.profile.phone_number === '' || this.profile.phone_number === null || this.profile.phone_number === 0 ? false : true + this.mobileNumberReadonly = this.profile.mobile_number === '' || this.profile.mobile_number === null || this.profile.mobile_number === 0 ? false : true }, }, beforeRouteEnter: function(to,from,next){ Vue.http.get(api_endpoints.profile).then((response) => { if (response.body.address_details && response.body.personal_details && response.body.contact_details && to.name == 'first-time'){ - window.location.href='/'; + window.location.href='/' } else{ next(vm => { diff --git a/requirements.txt b/requirements.txt index 05b730e3c..707e64260 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Django==1.11.29 -Django==3.2.20 +Django==3.2.23 ipython==7.19.0 psycopg2==2.8.6 jedi==0.17.2