Skip to content

Commit

Permalink
Merge pull request #490 from awf-dbca/invoice-due-date
Browse files Browse the repository at this point in the history
Invoice due date
  • Loading branch information
xzzy authored Nov 29, 2024
2 parents 9d7fdfb + d7487fa commit 26c7f8d
Show file tree
Hide file tree
Showing 24 changed files with 348 additions and 16 deletions.
4 changes: 2 additions & 2 deletions mooringlicensing/components/proposals/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2067,13 +2067,13 @@ def record_sale(self, request, *args, **kwargs):
if not instance.end_date:
# proposals with instance copied to listed_vessels
for proposal in instance.listed_on_proposals.all():
if proposal.processing_status not in [Proposal.PROCESSING_STATUS_DISCARDED, Proposal.PROCESSING_STATUS_APPROVED, Proposal.PROCESSING_STATUS_DECLINED,]:
if proposal.processing_status not in [Proposal.PROCESSING_STATUS_DISCARDED, Proposal.PROCESSING_STATUS_APPROVED, Proposal.PROCESSING_STATUS_DECLINED, Proposal.PROCESSING_STATUS_EXPIRED]:
raise serializers.ValidationError(
"You cannot record the sale of this vessel at this time as application {} that lists this vessel is still in progress.".format(proposal.lodgement_number)
)
# submitted proposals with instance == proposal.vessel_ownership
for proposal in instance.proposal_set.all():
if proposal.processing_status not in [Proposal.PROCESSING_STATUS_DISCARDED, Proposal.PROCESSING_STATUS_APPROVED, Proposal.PROCESSING_STATUS_DECLINED,]:
if proposal.processing_status not in [Proposal.PROCESSING_STATUS_DISCARDED, Proposal.PROCESSING_STATUS_APPROVED, Proposal.PROCESSING_STATUS_DECLINED, Proposal.PROCESSING_STATUS_EXPIRED]:
raise serializers.ValidationError(
"You cannot record the sale of this vessel at this time as application {} that lists this vessel is still in progress.".format(proposal.lodgement_number)
)
Expand Down
89 changes: 89 additions & 0 deletions mooringlicensing/components/proposals/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,95 @@ def send_invitee_reminder_email(approval, due_date, request=None):
_log_approval_email(msg, approval, sender=sender_user)
_log_user_email(msg, approval.applicant_obj, proposal.applicant_obj, sender=sender_user)

def send_expire_application_email(proposal, due_date,):

html_template = 'mooringlicensing/emails_2/application_expire_notification.html'
txt_template = 'mooringlicensing/emails_2/application_expire_notification.txt'

email = TemplateEmailBase(
subject='Application {} Expired - Rottnest Island Authority'.format(proposal.lodgement_number),
html_template=html_template,
txt_template=txt_template,
)
url = settings.SITE_URL if settings.SITE_URL else ''
dashboard_url = url + reverse('external')

# Configure recipients, contents, etc
context = {
'url': url,
'proposal': proposal,
'recipient': proposal.applicant_obj,
'dashboard_url': make_http_https(dashboard_url),
}
to_address = proposal.applicant_obj.email
cc = []
bcc = []

# Send email
msg = email.send(to_address, context=context, attachments=[], cc=cc, bcc=bcc,)
if msg:
sender = get_user_as_email_user(msg.from_email)
log_proposal_email(msg, proposal, sender)
return msg

def send_expire_notification_to_assessor(proposal, due_date):
email = TemplateEmailBase(
subject='Expired application - not paid on time',
html_template='mooringlicensing/emails_2/assessor_expiry_notification.html',
txt_template='mooringlicensing/emails_2/assessor_expiry_notification.txt',
)

context = {
'public_url': get_public_url(),
'applicant': proposal.applicant_obj,
'due_date': due_date,
'recipient': proposal.applicant_obj,
'proposal': proposal
}

to_address = proposal.assessor_recipients
cc = []
bcc = []

# Send email
msg = email.send(to_address, context=context, attachments=[], cc=cc, bcc=bcc,)
if msg:
sender = get_user_as_email_user(msg.from_email)
log_proposal_email(msg, proposal, sender)
return msg

def send_payment_reminder_email(proposal, request=None):

email = TemplateEmailBase(
subject='Payment reminder: Application {} - Rottnest Island Authority'.format(proposal.lodgement_number),
html_template='mooringlicensing/emails_2/application_payment_reminder.html',
txt_template='mooringlicensing/emails_2/application_payment_reminder.txt',
)

url = settings.SITE_URL if settings.SITE_URL else ''

due_date = proposal.payment_due_date

# Configure recipients, contents, etc
context = {
'url': url,
'proposal': proposal,
'recipient': proposal.applicant_obj,
'applicant': proposal.applicant_obj,
'due_date': due_date,
}
to_address = proposal.applicant_obj.email
cc = []
bcc = []

# Send email
msg = email.send(to_address, context=context, attachments=[], cc=cc, bcc=bcc,)
if msg:
sender = get_user_as_email_user(msg.from_email)
log_proposal_email(msg, proposal, sender)

return msg


def send_expire_mooring_licence_application_email(proposal, reason, due_date,):
# 12 email to mooring licence applicant when mooring licence application is not submitted within configurable
Expand Down
37 changes: 31 additions & 6 deletions mooringlicensing/components/proposals/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,8 @@ class Proposal(DirtyFieldsMixin, RevisionedMixin):
# To avoid that, this fee_season field is used in order to store those data.
auto_approve = models.BooleanField(default=False)
null_vessel_on_create = models.BooleanField(default=True)
payment_reminder_sent = models.BooleanField(default=False)
payment_due_date = models.DateField(blank=True, null=True) #date when payment is due for future invoices

class Meta:
app_label = 'mooringlicensing'
Expand Down Expand Up @@ -1053,6 +1055,7 @@ def can_officer_process(self):
Proposal.PROCESSING_STATUS_AWAITING_DOCUMENTS,
Proposal.PROCESSING_STATUS_PRINTING_STICKER,
Proposal.PROCESSING_STATUS_STICKER_TO_BE_RETURNED,
Proposal.PROCESSING_STATUS_EXPIRED,
]
return False if self.processing_status in officer_view_state else True

Expand Down Expand Up @@ -1113,7 +1116,8 @@ def has_approver_mode(self,user):
Proposal.PROCESSING_STATUS_AWAITING_PAYMENT,
Proposal.PROCESSING_STATUS_DECLINED,
Proposal.PROCESSING_STATUS_DRAFT,
Proposal.PROCESSING_STATUS_PRINTING_STICKER
Proposal.PROCESSING_STATUS_PRINTING_STICKER,
Proposal.PROCESSING_STATUS_EXPIRED,
]
if self.processing_status in status_without_approver:
return False
Expand All @@ -1135,7 +1139,8 @@ def has_assessor_mode(self,user):
Proposal.PROCESSING_STATUS_APPROVED,
Proposal.PROCESSING_STATUS_AWAITING_PAYMENT,
Proposal.PROCESSING_STATUS_DECLINED,
Proposal.PROCESSING_STATUS_PRINTING_STICKER
Proposal.PROCESSING_STATUS_PRINTING_STICKER,
Proposal.PROCESSING_STATUS_EXPIRED,
]
if self.processing_status in status_without_assessor:
return False
Expand Down Expand Up @@ -1758,7 +1763,15 @@ def final_approval_for_AUA_MLA(self, request=None):
return_preload_url = settings.MOORING_LICENSING_EXTERNAL_URL + reverse("ledger-api-success-callback", kwargs={"uuid": application_fee.uuid})

basket_hash_split = basket_hash.split("|")
pcfi = process_create_future_invoice(basket_hash_split[0], invoice_text, return_preload_url)

invoice_name = self.proposal_applicant.get_full_name()
today = timezone.localtime(timezone.now()).date()
days_type = NumberOfDaysType.objects.get(code=settings.CODE_DAYS_BEFORE_DUE_PAYMENT)
days_setting = NumberOfDaysSetting.get_setting_by_date(days_type, today)
self.payment_due_date = today + datetime.timedelta(days=days_setting.number_of_days)
self.save()

pcfi = process_create_future_invoice(basket_hash_split[0], invoice_text, return_preload_url, invoice_name, self.payment_due_date.strftime("%d/%m/%Y"))

application_fee.invoice_reference = pcfi['data']['invoice']
application_fee.save()
Expand All @@ -1778,9 +1791,10 @@ def final_approval_for_AUA_MLA(self, request=None):
amount_to_be_paid=amount_to_be_paid,
)
logger.info(f'FeeItemApplicationFee: [{fiaf}] has been created.')

if not self.payment_required():
self.approval.generate_doc()

send_application_approved_or_declined_email(self, 'approved', request)
self.log_user_action(ProposalUserAction.ACTION_APPROVE_APPLICATION.format(self.lodgement_number), request)

Expand Down Expand Up @@ -2614,6 +2628,7 @@ def get_intermediate_proposals(email_user_id):
Proposal.PROCESSING_STATUS_APPROVED,
Proposal.PROCESSING_STATUS_DECLINED,
Proposal.PROCESSING_STATUS_DISCARDED,
Proposal.PROCESSING_STATUS_EXPIRED,
])
return proposals

Expand Down Expand Up @@ -2788,6 +2803,7 @@ def validate_against_existing_proposals_and_approvals(self):
Proposal.PROCESSING_STATUS_APPROVED,
Proposal.PROCESSING_STATUS_DECLINED,
Proposal.PROCESSING_STATUS_DISCARDED,
Proposal.PROCESSING_STATUS_EXPIRED,
]:
if type(proposal) == MooringLicenceApplication:
proposals_mla.append(proposal)
Expand Down Expand Up @@ -3008,6 +3024,7 @@ def validate_against_existing_proposals_and_approvals(self):
Proposal.PROCESSING_STATUS_APPROVED,
Proposal.PROCESSING_STATUS_DECLINED,
Proposal.PROCESSING_STATUS_DISCARDED,
Proposal.PROCESSING_STATUS_EXPIRED,
]:
if type(proposal) == AuthorisedUserApplication:
proposals_aua.append(proposal)
Expand Down Expand Up @@ -3510,6 +3527,7 @@ def validate_against_existing_proposals_and_approvals(self):
Proposal.PROCESSING_STATUS_APPROVED,
Proposal.PROCESSING_STATUS_DECLINED,
Proposal.PROCESSING_STATUS_DISCARDED,
Proposal.PROCESSING_STATUS_EXPIRED,
]:
if type(proposal) == MooringLicenceApplication:
proposals_mla.append(proposal)
Expand Down Expand Up @@ -3582,7 +3600,9 @@ def get_intermediate_proposals(email_user_id):
proposals = MooringLicenceApplication.objects.filter(proposal_applicant__email_user_id=email_user_id).exclude(processing_status__in=[
Proposal.PROCESSING_STATUS_APPROVED,
Proposal.PROCESSING_STATUS_DECLINED,
Proposal.PROCESSING_STATUS_DISCARDED,])
Proposal.PROCESSING_STATUS_DISCARDED,
Proposal.PROCESSING_STATUS_EXPIRED,
])
return proposals

def create_fee_lines(self):
Expand Down Expand Up @@ -4054,7 +4074,12 @@ def get_queryset(self):
# now check whether there are any blocking proposals
blocking_proposal = False
for proposal in mooring.ria_generated_proposal.all():
if proposal.processing_status not in [Proposal.PROCESSING_STATUS_APPROVED, Proposal.PROCESSING_STATUS_DECLINED, Proposal.PROCESSING_STATUS_DISCARDED,]:
if proposal.processing_status not in [
Proposal.PROCESSING_STATUS_APPROVED,
Proposal.PROCESSING_STATUS_DECLINED,
Proposal.PROCESSING_STATUS_DISCARDED,
Proposal.PROCESSING_STATUS_EXPIRED,
]:
blocking_proposal = True
if not blocking_proposal:
available_ids.append(mooring.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,6 @@ <H4>Hover over button to see help text.</H4>

<div class="row">
<button type="submit" name="script" value="update_approval_status" {% if update_approval_status == 'true' %}style="color:green" {% endif %} title="Update Approval Status">Run Script 'update_approval_status'</button>
<!-- {% if debug %}-->
<!-- <div class="debug-box">-->
<!-- <div><span>Enter the approval ID and click the button above</span></div>-->
<!-- <div>-->
<!-- Approval ID to update: <input type="text" name="update_approval_status_id" />-->
<!-- </div>-->
<!-- </div>-->
<!-- {% endif %}-->
</div>

<div class="row">
Expand All @@ -59,6 +51,14 @@ <H4>Hover over button to see help text.</H4>
<button type="submit" name="script" value="send_compliance_reminder" {% if send_compliance_reminder == 'true' %}style="color:green" {% endif %} title="Send Compliance Reminder">Run Script 'send_compliance_reminder'</button>
</div>

<div class="row">
<button type="submit" name="script" value="expire_application_due_to_no_payment" {% if expire_application_due_to_no_payment == 'true' %}style="color:green" {% endif %} title="Expire Applications due to no payment">Run Script 'expire_application_due_to_no_payment'</button>
</div>

<div class="row">
<button type="submit" name="script" value="send_application_payment_due_reminder" {% if send_application_payment_due_reminder == 'true' %}style="color:green" {% endif %} title="Send Application Payment Due Reminder">Run Script 'send_application_payment_due_reminder'</button>
</div>

<div class="row">
<button type="submit" name="script" value="send_endorser_reminder" {% if send_endorser_reminder == 'true' %}style="color:green" {% endif %} title="Send Endorsement Reminder">Run Script 'send_endorser_reminder'</button>
{% if debug %}
Expand Down
2 changes: 2 additions & 0 deletions mooringlicensing/management/commands/cron_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ def handle(self, *args, **options):
subprocess.call('python manage_ml.py remove_unpaid_dcv_submissions', shell=True)
subprocess.call('python manage_ml.py expire_dcv_permits_out_of_season', shell=True)
subprocess.call('python manage_ml.py check_proposal_endorsements', shell=True)
subprocess.call('python manage_ml.py expire_application_due_to_no_payment', shell=True)
subprocess.call('python manage_ml.py send_application_payment_due_reminder', shell=True)

logger.info('===== Completed command: {} ====='.format(__name__))
cron_email.info('</div>')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from datetime import timedelta

from django.utils import timezone
from django.core.management.base import BaseCommand
from django.core.exceptions import ImproperlyConfigured
from django.db.models import Q

import logging

from mooringlicensing.components.proposals.email import send_expire_application_email, send_expire_notification_to_assessor
from mooringlicensing.components.main.models import NumberOfDaysType, NumberOfDaysSetting
from mooringlicensing.components.proposals.models import Proposal
from mooringlicensing.management.commands.utils import construct_email_message
from mooringlicensing.settings import CODE_DAYS_BEFORE_DUE_PAYMENT

logger = logging.getLogger('cron_tasks')
cron_email = logging.getLogger('cron_email')

class Command(BaseCommand):
help = 'expire application if not paid within configurable number of days after being approved and send email to inform applicant'

def handle(self, *args, **options):
errors = []
updates = []
today = timezone.localtime(timezone.now()).date()

# Retrieve the number of days before expiry date of the proposals to email
days_type = NumberOfDaysType.objects.get(code=CODE_DAYS_BEFORE_DUE_PAYMENT)
days_setting = NumberOfDaysSetting.get_setting_by_date(days_type, today)
if not days_setting:
# No number of days found
raise ImproperlyConfigured("NumberOfDays: {} is not defined for the date: {}".format(days_type.name, today))

logger.info('Running command {}'.format(__name__))

# Construct queries
queries = Q()
queries &= Q(processing_status=Proposal.PROCESSING_STATUS_AWAITING_PAYMENT)
queries &= Q(payment_due_date__lt=today)

for p in Proposal.objects.filter(queries):
try:
p.processing_status = Proposal.PROCESSING_STATUS_EXPIRED
p.save()

send_expire_application_email(p, p.payment_due_date)
send_expire_notification_to_assessor(p, p.payment_due_date)
logger.info('Expired notification sent for Proposal {}'.format(p.lodgement_number))
updates.append(p.lodgement_number)
except Exception as e:
err_msg = 'Error sending expired notification for Proposal {}'.format(p.lodgement_number)
logger.error('{}\n{}'.format(err_msg, str(e)))
errors.append(err_msg)

cmd_name = __name__.split('.')[-1].replace('_', ' ').upper()
msg = construct_email_message(cmd_name, errors, updates)
logger.info(msg)
cron_email.info(msg)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from datetime import timedelta

from django.utils import timezone
from django.core.management.base import BaseCommand
from django.core.exceptions import ImproperlyConfigured
from django.db.models import Q

import logging

from mooringlicensing.components.proposals.email import send_payment_reminder_email
from mooringlicensing.components.main.models import NumberOfDaysType, NumberOfDaysSetting
from mooringlicensing.components.proposals.models import Proposal
from mooringlicensing.management.commands.utils import construct_email_message
from mooringlicensing.settings import CODE_DAYS_FOR_DUE_PAYMENT_REMINDER

logger = logging.getLogger('cron_tasks')
cron_email = logging.getLogger('cron_email')

class Command(BaseCommand):
help = 'send email reminder to applicant to submit payment for an application if not paid within configurable number of days after being approved'

def handle(self, *args, **options):
errors = []
updates = []
today = timezone.localtime(timezone.now()).date()

# Retrieve the number of days before expiry date of the proposals to email
days_type = NumberOfDaysType.objects.get(code=CODE_DAYS_FOR_DUE_PAYMENT_REMINDER)
days_setting = NumberOfDaysSetting.get_setting_by_date(days_type, today)
if not days_setting:
# No number of days found
raise ImproperlyConfigured("NumberOfDays: {} is not defined for the date: {}".format(days_type.name, today))
boundary_date = today + timedelta(days=days_setting.number_of_days)

logger.info('Running command {}'.format(__name__))

# Construct queries
queries = Q()
queries &= Q(processing_status=Proposal.PROCESSING_STATUS_AWAITING_PAYMENT)
queries &= Q(payment_due_date__lt=boundary_date)
queries &= Q(payment_reminder_sent=False)

for p in Proposal.objects.filter(queries):
try:
p.payment_reminder_sent = True
p.save()
send_payment_reminder_email(p)
logger.info('Payment reminder sent for Proposal {}'.format(p.lodgement_number))
updates.append(p.lodgement_number)
except Exception as e:
err_msg = 'Error sending payment reminder for Proposal {}'.format(p.lodgement_number)
logger.error('{}\n{}'.format(err_msg, str(e)))
errors.append(err_msg)

cmd_name = __name__.split('.')[-1].replace('_', ' ').upper()
msg = construct_email_message(cmd_name, errors, updates)
logger.info(msg)
cron_email.info(msg)
Loading

0 comments on commit 26c7f8d

Please sign in to comment.