From 01c2bf99e2a55d96ca1be51b65fac3204f64e1a7 Mon Sep 17 00:00:00 2001 From: katsufumi shibata Date: Wed, 6 Dec 2023 11:12:52 +0800 Subject: [PATCH 01/14] Passing the personal details data upto proposal.vue file from the descendant components --- .../mooringlicensing/src/components/external/proposal.vue | 5 +++++ .../frontend/mooringlicensing/src/components/form_aua.vue | 4 +++- .../mooringlicensing/src/components/user/profile.vue | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/mooringlicensing/frontend/mooringlicensing/src/components/external/proposal.vue b/mooringlicensing/frontend/mooringlicensing/src/components/external/proposal.vue index 1f53e6300..900ec9a63 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/external/proposal.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/external/proposal.vue @@ -74,6 +74,7 @@ @changeMooring="updateMooringAuth" @updateVesselOwnershipChanged="updateVesselOwnershipChanged" @noVessel="noVessel" + @profile-fetched="populateProfile" /> Date: Wed, 6 Dec 2023 16:08:01 +0800 Subject: [PATCH 02/14] Latest proposal_applicant details are retrieved for the postal details when sending stickers --- .../components/approvals/models.py | 72 +- mooringlicensing/components/proposals/api.py | 4 +- .../components/proposals/models.py | 1036 ++++++++++++++++- .../components/proposals/utils.py | 49 +- .../src/components/external/proposal.vue | 5 +- 5 files changed, 1133 insertions(+), 33 deletions(-) diff --git a/mooringlicensing/components/approvals/models.py b/mooringlicensing/components/approvals/models.py index 7169f5dd3..de252d133 100755 --- a/mooringlicensing/components/approvals/models.py +++ b/mooringlicensing/components/approvals/models.py @@ -344,10 +344,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 +394,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 +404,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 +417,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 +430,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 '' @@ -2968,15 +3002,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 +3042,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 +3061,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/proposals/api.py b/mooringlicensing/components/proposals/api.py index 5c1960bad..dafbe83c3 100755 --- a/mooringlicensing/components/proposals/api.py +++ b/mooringlicensing/components/proposals/api.py @@ -26,7 +26,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 @@ -1241,7 +1241,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) diff --git a/mooringlicensing/components/proposals/models.py b/mooringlicensing/components/proposals/models.py index b847edde6..4d9d42c68 100644 --- a/mooringlicensing/components/proposals/models.py +++ b/mooringlicensing/components/proposals/models.py @@ -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,1040 @@ 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_suburb + else: + return self.postal_suburb + + @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, + + first_name = self.first_name, + last_name = self.last_name, + dob = self.dob, + + residential_line1 = self.residential_line1, + residential_line2 = self.residential_line2, + residential_line3 = self.residential_line3, + residential_locality = self.residential_locality, + residential_state = self.residential_state, + residential_country = self.residential_country, + residential_postcode = self.residential_postcode, + + postal_same_as_residential = self.postal_same_as_residential, + postal_line1 = self.postal_line1, + postal_line2 = self.postal_line2, + postal_line3 = self.postal_line3, + postal_locality = self.postal_locality, + postal_state = self.postal_state, + postal_country = self.postal_country, + postal_postcode = self.postal_postcode, + + email = self.email, + phone_number = self.phone_number, + mobile_number = self.mobile_number, + ) + logger.info(f'ProposalApplicant: [{proposal_applicant}] has been created for the Proposal: [{target_proposal}] by copying the ProposalApplicant: [{self}].') + + +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) + def copy_self_to_proposal(self, target_proposal): proposal_applicant = ProposalApplicant.objects.create( proposal=target_proposal, diff --git a/mooringlicensing/components/proposals/utils.py b/mooringlicensing/components/proposals/utils.py index fc8832cc5..2afc2080f 100644 --- a/mooringlicensing/components/proposals/utils.py +++ b/mooringlicensing/components/proposals/utils.py @@ -377,7 +377,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 +412,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 +422,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 +449,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 +481,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 +1006,42 @@ 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 + proposal_applicant.dob = proposal_applicant_data.dob + + 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 +1067,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/frontend/mooringlicensing/src/components/external/proposal.vue b/mooringlicensing/frontend/mooringlicensing/src/components/external/proposal.vue index 900ec9a63..a7f664f2b 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/external/proposal.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/external/proposal.vue @@ -379,6 +379,7 @@ export default { let payload = { proposal: {}, vessel: {}, + profile: {}, } // WLA if (this.$refs.waiting_list_application) { @@ -477,6 +478,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); @@ -756,9 +758,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; From d909341c21d7705ebdd9405ddf422c7048046e6a Mon Sep 17 00:00:00 2001 From: katsufumi shibata Date: Thu, 7 Dec 2023 14:53:00 +0800 Subject: [PATCH 03/14] Display the latest proposal_applicant obj of the proposal page --- mooringlicensing/components/main/utils.py | 3 +- .../components/proposals/models.py | 6 +-- .../components/proposals/utils.py | 51 ++++++++++--------- mooringlicensing/components/users/api.py | 8 ++- .../src/components/user/profile.vue | 20 ++++---- 5 files changed, 45 insertions(+), 43 deletions(-) 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/proposals/models.py b/mooringlicensing/components/proposals/models.py index 4d9d42c68..01107d66b 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 @@ -2410,9 +2410,9 @@ def postal_address_state(self): @property def postal_address_suburb(self): if self.postal_same_as_residential: - return self.residential_suburb + return self.residential_locality else: - return self.postal_suburb + return self.postal_locality @property def postal_address_postcode(self): diff --git a/mooringlicensing/components/proposals/utils.py b/mooringlicensing/components/proposals/utils.py index 2afc2080f..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 @@ -1016,30 +1017,32 @@ def update_proposal_applicant(proposal, request): # 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 - proposal_applicant.dob = proposal_applicant_data.dob - - 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 + 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 diff --git a/mooringlicensing/components/users/api.py b/mooringlicensing/components/users/api.py index 311c4a57d..38a5e88e8 100755 --- a/mooringlicensing/components/users/api.py +++ b/mooringlicensing/components/users/api.py @@ -90,7 +90,7 @@ def get(self, request, format=None): class GetProposalApplicant(views.APIView): renderer_classes = [JSONRenderer,] - DISPLAY_PROPOSAL_APPLICANT = False + DISPLAY_PROPOSAL_APPLICANT = True def get(self, request, proposal_pk, format=None): from mooringlicensing.components.proposals.models import Proposal, ProposalApplicant @@ -98,8 +98,7 @@ def get(self, request, proposal_pk, format=None): 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}) + serializer = ProposalApplicantSerializer(proposal.proposal_applicant, context={'request': request}) else: submitter = retrieve_email_userro(proposal.submitter) serializer = EmailUserRoSerializer(submitter) @@ -107,8 +106,7 @@ def get(self, request, proposal_pk, format=None): 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}) + serializer = ProposalApplicantForEndorserSerializer(proposal.proposal_applicant, context={'request': request}) else: submitter = retrieve_email_userro(proposal.submitter) serializer = EmailUserRoForEndorserSerializer(submitter) diff --git a/mooringlicensing/frontend/mooringlicensing/src/components/user/profile.vue b/mooringlicensing/frontend/mooringlicensing/src/components/user/profile.vue index 8eaec975d..92165253e 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/user/profile.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/user/profile.vue @@ -969,13 +969,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 +987,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 => { From b89b368a9418a3319c50991ee6b2794844227cba Mon Sep 17 00:00:00 2001 From: katsufumi shibata Date: Thu, 7 Dec 2023 15:50:48 +0800 Subject: [PATCH 04/14] Enable the Submit button when profile has been changed --- .../src/components/external/proposal.vue | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/mooringlicensing/frontend/mooringlicensing/src/components/external/proposal.vue b/mooringlicensing/frontend/mooringlicensing/src/components/external/proposal.vue index a7f664f2b..dbabb159d 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/external/proposal.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/external/proposal.vue @@ -191,6 +191,7 @@ export default { autoApprove: false, missingVessel: false, // add_vessel: false, + profile_original: {}, profile: {}, } }, @@ -200,33 +201,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') } } @@ -314,6 +319,7 @@ 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) { From 4c620889e9466efaf5aeaa84f1efaec21fcd35f0 Mon Sep 17 00:00:00 2001 From: katsufumi shibata Date: Fri, 8 Dec 2023 11:43:50 +0800 Subject: [PATCH 05/14] Application for each permit/licence type has its own proposal_applicant details. --- mooringlicensing/components/approvals/api.py | 6 +-- mooringlicensing/components/proposals/api.py | 7 +-- .../components/proposals/models.py | 31 ----------- mooringlicensing/components/users/api.py | 7 +-- .../src/components/external/proposal.vue | 3 ++ .../src/components/form_aaa.vue | 4 +- .../src/components/form_aua.vue | 1 - .../src/components/form_mla.vue | 4 +- .../src/components/form_wla.vue | 4 +- .../src/components/user/profile.vue | 54 +++++++++---------- 10 files changed, 47 insertions(+), 74 deletions(-) diff --git a/mooringlicensing/components/approvals/api.py b/mooringlicensing/components/approvals/api.py index 023a7f9ec..389ebe8e9 100755 --- a/mooringlicensing/components/approvals/api.py +++ b/mooringlicensing/components/approvals/api.py @@ -1294,13 +1294,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/proposals/api.py b/mooringlicensing/components/proposals/api.py index dafbe83c3..89d30b346 100755 --- a/mooringlicensing/components/proposals/api.py +++ b/mooringlicensing/components/proposals/api.py @@ -814,8 +814,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 +827,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() diff --git a/mooringlicensing/components/proposals/models.py b/mooringlicensing/components/proposals/models.py index 01107d66b..d25d450b5 100644 --- a/mooringlicensing/components/proposals/models.py +++ b/mooringlicensing/components/proposals/models.py @@ -3420,37 +3420,6 @@ def update_or_create_approval(self, current_datetime, request=None): compliance.save() self.generate_compliances(approval, request) - def copy_self_to_proposal(self, target_proposal): - proposal_applicant = ProposalApplicant.objects.create( - proposal=target_proposal, - - first_name = self.first_name, - last_name = self.last_name, - dob = self.dob, - - residential_line1 = self.residential_line1, - residential_line2 = self.residential_line2, - residential_line3 = self.residential_line3, - residential_locality = self.residential_locality, - residential_state = self.residential_state, - residential_country = self.residential_country, - residential_postcode = self.residential_postcode, - - postal_same_as_residential = self.postal_same_as_residential, - postal_line1 = self.postal_line1, - postal_line2 = self.postal_line2, - postal_line3 = self.postal_line3, - postal_locality = self.postal_locality, - postal_state = self.postal_state, - postal_country = self.postal_country, - postal_postcode = self.postal_postcode, - - email = self.email, - phone_number = self.phone_number, - mobile_number = self.mobile_number, - ) - logger.info(f'ProposalApplicant: [{proposal_applicant}] has been created for the Proposal: [{target_proposal}] by copying the ProposalApplicant: [{self}].') - def update_sticker_doc_filename(instance, filename): return '{}/stickers/batch/{}'.format(settings.MEDIA_APP_DIR, filename) diff --git a/mooringlicensing/components/users/api.py b/mooringlicensing/components/users/api.py index 38a5e88e8..4b1c60003 100755 --- a/mooringlicensing/components/users/api.py +++ b/mooringlicensing/components/users/api.py @@ -90,14 +90,14 @@ def get(self, request, format=None): class GetProposalApplicant(views.APIView): renderer_classes = [JSONRenderer,] - DISPLAY_PROPOSAL_APPLICANT = True 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: + 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) @@ -105,7 +105,8 @@ def get(self, request, proposal_pk, format=None): 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: + 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) diff --git a/mooringlicensing/frontend/mooringlicensing/src/components/external/proposal.vue b/mooringlicensing/frontend/mooringlicensing/src/components/external/proposal.vue index dbabb159d..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" />
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 e1b73a409..d28f70236 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/form_aua.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/form_aua.vue @@ -406,7 +406,6 @@ } }, populateProfile: function(profile) { - // this.profile = Object.assign({}, profile); this.profile = profile this.$emit('profile-fetched', this.profile); }, 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/user/profile.vue b/mooringlicensing/frontend/mooringlicensing/src/components/user/profile.vue index 92165253e..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: false, // We don't allow customer to edit the persona details on the application page } }, components: { From ed2369e8e9d6e6c97b7d9a4c889fb6ed8a87690e Mon Sep 17 00:00:00 2001 From: katsufumi shibata Date: Fri, 8 Dec 2023 15:01:35 +0800 Subject: [PATCH 06/14] Avoid decimal number when DEBUG --- .../components/approvals/models.py | 22 ++++++++++++++++--- .../components/payments_ml/utils.py | 13 +++++++++-- .../components/payments_ml/views.py | 14 ++++++++++-- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/mooringlicensing/components/approvals/models.py b/mooringlicensing/components/approvals/models.py index de252d133..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 @@ -2367,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, @@ -2377,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) @@ -2556,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( @@ -2568,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, }, ] diff --git a/mooringlicensing/components/payments_ml/utils.py b/mooringlicensing/components/payments_ml/utils.py index 312d1f538..0775e611d 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,20 @@ 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': 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: From 8beaa6e7e01ee1f1aa3e8c58874e604568b73a0d Mon Sep 17 00:00:00 2001 From: katsufumi shibata Date: Mon, 11 Dec 2023 16:17:13 +0800 Subject: [PATCH 07/14] Avoid decimal number when DEBUG --- mooringlicensing/components/payments_ml/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mooringlicensing/components/payments_ml/utils.py b/mooringlicensing/components/payments_ml/utils.py index 0775e611d..59948f550 100644 --- a/mooringlicensing/components/payments_ml/utils.py +++ b/mooringlicensing/components/payments_ml/utils.py @@ -119,6 +119,8 @@ def generate_line_item(application_type, fee_amount_adjusted, fee_constructor, i 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': total_amount, 'price_excl_tax': total_amount_excl_tax, 'quantity': 1, From 8f58cf9e7984eb884600ba32af1065d1c771eab3 Mon Sep 17 00:00:00 2001 From: katsufumi shibata Date: Tue, 12 Dec 2023 09:59:18 +0800 Subject: [PATCH 08/14] Add infinite scroll functionality to the Person search --- mooringlicensing/components/users/api.py | 42 +++++++++---------- .../internal/search/search_person.vue | 19 ++++++--- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/mooringlicensing/components/users/api.py b/mooringlicensing/components/users/api.py index 4b1c60003..fdd740606 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 @@ -127,24 +127,12 @@ class GetPerson(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 # a space in the search term is interpreted as first name, last name 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( + my_queryset = EmailUser.objects.annotate( search_term=Concat( "first_name", Value(" "), @@ -153,11 +141,16 @@ def get(self, request, format=None): "email", output_field=CharField(), ) - ).filter(search_term__icontains=search_term)[:10] - print(data[0].__dict__) - print(len(data)) + ).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: @@ -167,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() 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..146fbbdff 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,26 @@ 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, + console.log({params}) + 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 + } + } }, }, }). From f2e356a8a215cc97fd438fb297993bf26b8e3536 Mon Sep 17 00:00:00 2001 From: katsufumi shibata Date: Tue, 12 Dec 2023 12:46:16 +0800 Subject: [PATCH 09/14] Fix several searching bugs including showing person details, resolve spinning wheel issue, adding comms log functionality, etc. --- mooringlicensing/components/approvals/api.py | 2 +- mooringlicensing/components/main/models.py | 2 - mooringlicensing/components/proposals/api.py | 5 ++- mooringlicensing/components/users/api.py | 43 +++++++++++++------ mooringlicensing/components/users/models.py | 2 +- .../components/users/serializers.py | 7 ++- requirements.txt | 2 +- 7 files changed, 39 insertions(+), 24 deletions(-) diff --git a/mooringlicensing/components/approvals/api.py b/mooringlicensing/components/approvals/api.py index 389ebe8e9..b1afadb07 100755 --- a/mooringlicensing/components/approvals/api.py +++ b/mooringlicensing/components/approvals/api.py @@ -349,7 +349,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: 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/proposals/api.py b/mooringlicensing/components/proposals/api.py index 89d30b346..dc24909a4 100755 --- a/mooringlicensing/components/proposals/api.py +++ b/mooringlicensing/components/proposals/api.py @@ -566,9 +566,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]) diff --git a/mooringlicensing/components/users/api.py b/mooringlicensing/components/users/api.py index fdd740606..7d838c806 100755 --- a/mooringlicensing/components/users/api.py +++ b/mooringlicensing/components/users/api.py @@ -371,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/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 From c0b148e9ed23c477b412a89e22f4dd650c5147bb Mon Sep 17 00:00:00 2001 From: katsufumi shibata Date: Tue, 12 Dec 2023 14:25:17 +0800 Subject: [PATCH 10/14] Remove show-Actions link --- .../mooringlicensing/src/components/common/comms_logs.vue | 6 +++++- .../src/components/internal/person/person_detail.vue | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) 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/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" />
From 18e0e0752a9eb7b30c2d446a365a45633fbc2286 Mon Sep 17 00:00:00 2001 From: katsufumi shibata Date: Tue, 12 Dec 2023 14:25:43 +0800 Subject: [PATCH 11/14] Add infinite scroll functionality to the vessel search --- mooringlicensing/components/proposals/api.py | 71 +++++++++++++------ mooringlicensing/components/users/api.py | 20 +++--- .../internal/search/search_person.vue | 1 - .../internal/search/search_vessel.vue | 17 +++-- 4 files changed, 73 insertions(+), 36 deletions(-) diff --git a/mooringlicensing/components/proposals/api.py b/mooringlicensing/components/proposals/api.py index dc24909a4..4e82fbecd 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 @@ -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() diff --git a/mooringlicensing/components/users/api.py b/mooringlicensing/components/users/api.py index 7d838c806..8efc45bd4 100755 --- a/mooringlicensing/components/users/api.py +++ b/mooringlicensing/components/users/api.py @@ -130,18 +130,18 @@ 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 - # a space in the search term is interpreted as first name, last name + if search_term: my_queryset = EmailUser.objects.annotate( - search_term=Concat( - "first_name", - Value(" "), - "last_name", - Value(" "), - "email", - output_field=CharField(), - ) - ).filter(search_term__icontains=search_term) + 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) 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 146fbbdff..53f76a72d 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_person.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_person.vue @@ -66,7 +66,6 @@ from '@/utils/hooks' url: api_endpoints.person_lookup, dataType: 'json', data: function(params) { - console.log({params}) return { search_term: params.term, page: params.page || 1, 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..ef71e6280 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,25 @@ 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){ + console.log({data}) + return { + 'results': data.results, + 'pagination': { + 'more': data.pagination.more + } + } }, }, }). From 0cc254c3c623284d73a81715da74c49fd2a6d218 Mon Sep 17 00:00:00 2001 From: katsufumi shibata Date: Tue, 12 Dec 2023 15:19:48 +0800 Subject: [PATCH 12/14] Add infinite scroll function to the SearchMooring --- mooringlicensing/components/proposals/api.py | 48 ++++++++++++------- .../components/common/bookings_permits.vue | 5 +- .../internal/search/search_mooring.vue | 8 ++-- .../internal/search/search_vessel.vue | 1 - 4 files changed, 39 insertions(+), 23 deletions(-) diff --git a/mooringlicensing/components/proposals/api.py b/mooringlicensing/components/proposals/api.py index 4e82fbecd..68c396891 100755 --- a/mooringlicensing/components/proposals/api.py +++ b/mooringlicensing/components/proposals/api.py @@ -219,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() @@ -1904,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 @@ -1931,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/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/internal/search/search_mooring.vue b/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_mooring.vue index 977fd7e32..b71685793 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,16 @@ 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; }, }, }). 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 ef71e6280..97cb152fb 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_vessel.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_vessel.vue @@ -84,7 +84,6 @@ from '@/utils/hooks' } }, processResults: function(data){ - console.log({data}) return { 'results': data.results, 'pagination': { From 96d149aa9404c4dddd7452a5ca3367be7fc926d1 Mon Sep 17 00:00:00 2001 From: katsufumi shibata Date: Tue, 12 Dec 2023 15:26:10 +0800 Subject: [PATCH 13/14] Add infinite scroll to the SearchSticker function --- mooringlicensing/components/approvals/api.py | 24 +++++++++++++++---- .../internal/search/search_sticker.vue | 16 +++++++++---- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/mooringlicensing/components/approvals/api.py b/mooringlicensing/components/approvals/api.py index b1afadb07..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() 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 + } + } }, }, }). From 98023d21d8a58cbd5a6476b6e1766e9e5204f259 Mon Sep 17 00:00:00 2001 From: katsufumi shibata Date: Wed, 13 Dec 2023 16:44:13 +0800 Subject: [PATCH 14/14] Add infinite scroll to the Mooring search --- mooringlicensing/components/compliances/api.py | 13 +++++++++---- .../src/components/common/table_compliances.vue | 13 +++++++------ .../components/internal/search/search_mooring.vue | 8 ++++++++ 3 files changed, 24 insertions(+), 10 deletions(-) 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/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/internal/search/search_mooring.vue b/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_mooring.vue index b71685793..8b67f8f94 100755 --- a/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_mooring.vue +++ b/mooringlicensing/frontend/mooringlicensing/src/components/internal/search/search_mooring.vue @@ -76,6 +76,14 @@ from '@/utils/hooks' type: 'public', } }, + processResults: function(data){ + return { + 'results': data.results, + 'pagination': { + 'more': data.pagination.more + } + } + }, }, }). on("select2:select", function (e) {