diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..503f669 --- /dev/null +++ b/.hgignore @@ -0,0 +1,7 @@ +.Python +.DS_Store +bin/ +lib/ +.idea/ +__pycache__/ +pyvenv.cfg diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b60507b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.8-slim-buster + +WORKDIR /app + +COPY requirements.txt requirements.txt +RUN pip3 install -r requirements.txt + +COPY mortgage/ . + +CMD [ "python3", "-m" , "web", "run", "--host=0.0.0.0"] diff --git a/mortgage/templates/add.html b/mortgage/templates/add.html new file mode 100644 index 0000000..a797f44 --- /dev/null +++ b/mortgage/templates/add.html @@ -0,0 +1,22 @@ + + + + + + + + Loan Management + + + + + +{% for message in messages %} + +{% endfor %} +
{{message}}
+Return to Main Screen. + + \ No newline at end of file diff --git a/mortgage/templates/email.html b/mortgage/templates/email.html new file mode 100644 index 0000000..609fde3 --- /dev/null +++ b/mortgage/templates/email.html @@ -0,0 +1,19 @@ + + + + + + + + Loan Management + + + + +

The email has been sent.

+Return to Main Screen. + + + \ No newline at end of file diff --git a/mortgage/templates/main.html b/mortgage/templates/main.html new file mode 100644 index 0000000..0e457da --- /dev/null +++ b/mortgage/templates/main.html @@ -0,0 +1,218 @@ + + + + + + + Loan Management + + + +
+ + + +
Loan: + + + +
+
+ +

+
+ +
+ + + + + + + + + + + + + + + + + +
Loan Information
Lender:{{ model.lender.name }}
{{ model.lender.address }}
+ {{ model.lender.city }} {{model.lender.state }} {{ model.lender.zip }}
+ {{ model.lender.phone }} +
Borrower:{{ model.borrower.name }} 
{{ model.borrower.address }}  +
{{ model.borrower.city }}, {{model.borrower.state }} {{ model.borrower.zip }} +
Account Number:{{ model.parameters.account_number }}
Origination Date: {{ model.parameters.start_date }}
Original Principal:{{ "$%.2f"|format(model.parameters.principal) }}
Rate:{{model.parameters.interest_rate }}%
Term: {{model.parameters.periods }} months
Next Payment Due Date: {{model.parameters.next_due_date}}
Payment Due: {{ "$%.2f"|format(model.parameters.next_payment_amt) }}
Current Balance {{ "$%.2f"|format(model.current_balance) }}
Daily Interest Accrual: {{ "$%.2f"|format(model.current_daily_interest_accrual) }}
+

+
+
+ + + + + + + + + + + + + + + + + + + {% for item in model.past_payments %} + + + + + + + + + + + + {% if item.month == 12 or loop.last %} + + {% endif %} + {% endfor %} + + +
Loan History
#Due DateDate PaidDays InterestPayment AmtPrincipal PmtInterest PmtLate FeeNew Balance
{{ item.payment_number }} {{ item.bill_date }} {{ item.payment_date }} {{ item.days_of_interest }} {{ "$%.2f"|format(item.payment_amount) }} {{ "$%.2f"|format(item.principal_payment) }} {{ "$%.2f"|format(item.interest_payment) }} {{ "$%.2f"|format(item.late_fee) }} {{ "$%.2f"|format(item.new_balance) }}
Total interest paid in {{item.year}} is {{ "$%.2f"|format(item.annual_interest_to_date) }}.
Total interest paid to date is {{ "$%.2f"|format(model.total_interest_paid_to_date) }}.
+
+
+

Loan Accounting

+ +
+
+ + + + + + + + + + + + + + + + + {% for item in model.future_payments %} + + + + + + + + + {% endfor %} + Balloon Payment Due: {{ "$%.2f"|format(model.balloon_payment) }} + +
Remaining Amortization
#Due DateDays InterestPayment AmtPrincipal PmtInterest PmtPrincipal Balance
{{ item.payment_number }} {{ item.payment_date }} {{ item.days_of_interest }} {{ "$%.2f"|format(item.payment_amount) }} {{ "$%.2f"|format(item.principal_payment) }} {{ "$%.2f"|format(item.interest_payment) }} {{ "$%.2f"|format(item.new_balance) }}
+
+
+
+ + + + + + + + + + + + +
Generate and Send Statement
Send From: {{model.email.from_address}}
Send To: {{model.email.to_address}}
Subject:
Message:
Send Statement As:
+ HTML + PDF + Plain Text +
Include Future Amortization
+ Yes + No +
+ +
+
+
+
+ + + + + + + + + + + +
Record a Payment
Payment Date:
Payment Amount:
Late Fee Amount:
Send Payment Recorded Notification
+
+
+
+
+ + + + + + + + + +
Record an Extra Payment
Payment Date:
Payment Amount:
Send Payment Recorded Notification
+
+
+
+ + \ No newline at end of file diff --git a/mortgage/templates/payment_received_email.html.jinja b/mortgage/templates/payment_received_email.html.jinja new file mode 100644 index 0000000..53a1a0c --- /dev/null +++ b/mortgage/templates/payment_received_email.html.jinja @@ -0,0 +1,23 @@ + + + + + Title + + +Date: {{model.payment.today}}
+From: {{model.payment.payee}}
+To: {{model.payment.payer}}
+RE: Payment Received
+
 
+You are receiving this email to confirm receipt of your recent loan payment.
+
 
+Amount Received: {{ "$%.2f"|format(model.payment.amount) }}
+Date Received: {{model.payment.date}}
+{% if model.payment.late_fee != 0 %} +Late Fee Assessed: {{model.payment.late_fee}}
+{% endif %} +
 
+Thank you!
+ + \ No newline at end of file diff --git a/mortgage/templates/statement.html.jinja b/mortgage/templates/statement.html.jinja new file mode 100644 index 0000000..72db61f --- /dev/null +++ b/mortgage/templates/statement.html.jinja @@ -0,0 +1,86 @@ + + +

{{ model.header.title }}

+

{{ model.lender.name }}

+ +

{{ model.lender.phone }} - {{ model.lender.address }} - +{{ model.lender.city }} {{model.lender.state }} {{ model.lender.zip }}

+

Statement Date: {{ model.header.date }}

+ +

+ + + + + + + + + + +
Loan Information 
Borrower: {{ model.borrower.name }}  Account Number: {{ model.parameters.account_number }}
{{ model.borrower.address }}  Origination Date: {{ model.parameters.start_date }}
{{ model.borrower.city }}, {{model.borrower.state }} {{ model.borrower.zip }}Original Principal: {{ "$%.2f"|format(model.parameters.principal) }}
Rate: {{model.parameters.interest_rate }}% Term: {{model.parameters.periods }} months
Next Payment Due Date: {{model.parameters.next_due_date}} Payment Due: {{ "$%.2f"|format(model.parameters.next_payment_amt) }}
+

+

Payment History

+ + + + + + + + + + + + +{% for item in model.past_payments %} + + + + + + + + + + + {% if item.month == 12 or loop.last %} + + {% endif %} +{% endfor %} + + +
# + Due DateDate PaidDays InterestPayment AmtPrincipal PmtInterest PmtLate FeeNew Balance
{{ item.payment_number }} {{ item.bill_date }} {{ item.payment_date }} {{ item.days_of_interest }} {{ "$%.2f"|format(item.payment_amount) }} {{ "$%.2f"|format(item.principal_payment) }} {{ "$%.2f"|format(item.interest_payment) }} {{ "$%.2f"|format(item.late_fee) }} {{ "$%.2f"|format(item.new_balance) }}
Total interest paid in {{item.year}} is {{ "$%.2f"|format(item.annual_interest_to_date) }}.
Total interest paid to date is {{ "$%.2f"|format(model.total_interest_paid_to_date) }}.
+

+ +

Remaining Amortization

+ + + + + + + + + + + + + +{% for item in model.future_payments %} + + + + + + + + +{% endfor %} + +
#Due DateDays InterestPayment AmtPrincipal PmtInterest PmtPrincipal Balance
{{ item.payment_number }} {{ item.payment_date }} {{ item.days_of_interest }} {{ "$%.2f"|format(item.payment_amount) }} {{ "$%.2f"|format(item.principal_payment) }} {{ "$%.2f"|format(item.interest_payment) }} {{ "$%.2f"|format(item.new_balance) }}
+

Balloon Payment Due: {{ "$%.2f"|format(model.balloon_payment) }}

+
+ + \ No newline at end of file diff --git a/mortgage/templates/statement.pdf.jinja b/mortgage/templates/statement.pdf.jinja new file mode 100644 index 0000000..944d6f3 --- /dev/null +++ b/mortgage/templates/statement.pdf.jinja @@ -0,0 +1,86 @@ + + +

{{ model.header.title }}

+

{{ model.lender.name }}

+ +

{{ model.lender.phone }} - {{ model.lender.address }} - +{{ model.lender.city }} {{model.lender.state }} {{ model.lender.zip }}

+

Statement Date: {{ model.header.date }}

+ +

+ + + + + + + + + + +
Loan Information 
Borrower: {{ model.borrower.name }}  Account Number: {{ model.parameters.account_number }}
{{ model.borrower.address }}  Origination Date: {{ model.parameters.start_date }}
{{ model.borrower.city }}, {{model.borrower.state }} {{ model.borrower.zip }}Original Principal: {{ "$%.2f"|format(model.parameters.principal) }}
Rate: {{model.parameters.interest_rate }}% Term: {{model.parameters.periods }} months
Next Payment Due Date: {{model.parameters.next_due_date}} Payment Due: {{ "$%.2f"|format(model.parameters.next_payment_amt) }}
+

+

Payment History

+ + + + + + + + + + + + +{% for item in model.past_payments %} + + + + + + + + + + + {% if item.month == 12 or loop.last %} + + {% endif %} +{% endfor %} + + +
# + Due DateDate PaidDays InterestPayment AmtPrincipal PmtInterest PmtLate FeeNew Balance
{{ item.payment_number }} {{ item.bill_date }} {{ item.payment_date }} {{ item.days_of_interest }} {{ "$%.2f"|format(item.payment_amount) }} {{ "$%.2f"|format(item.principal_payment) }} {{ "$%.2f"|format(item.interest_payment) }} {{ "$%.2f"|format(item.late_fee) }} {{ "$%.2f"|format(item.new_balance) }}
Total interest paid in {{item.year}} is {{ "$%.2f"|format(item.annual_interest_to_date) }}.
Total interest paid to date is {{ "$%.2f"|format(model.total_interest_paid_to_date) }}.
+

+ +

Remaining Amortization

+ + + + + + + + + + + + + +{% for item in model.future_payments %} + + + + + + + + +{% endfor %} + +
#Due DateDays InterestPayment AmtPrincipal PmtInterest PmtPrincipal Balance
{{ item.payment_number }} {{ item.payment_date }} {{ item.days_of_interest }} {{ "$%.2f"|format(item.payment_amount) }} {{ "$%.2f"|format(item.principal_payment) }} {{ "$%.2f"|format(item.interest_payment) }} {{ "$%.2f"|format(item.new_balance) }}
+

Balloon Payment Due: {{ "$%.2f"|format(model.balloon_payment) }}

+
+ + \ No newline at end of file diff --git a/mortgage/templates/statement.text.jinja b/mortgage/templates/statement.text.jinja new file mode 100644 index 0000000..c96e31e --- /dev/null +++ b/mortgage/templates/statement.text.jinja @@ -0,0 +1,36 @@ +{{ model.header.title }} +{{ model.lender.name }} +{{ model.lender.address }} +{{ model.lender.city }}, {{model.lender.state }} {{ model.lender.zip }} +{{ model.lender.phone }} +Statement Date: {{ model.header.date }} + +Loan Information +Borrower: {{ model.borrower.name }} +Account Number: {{ model.parameters.account_number }} +Origination Date: {{ model.parameters.start_date }} +Address: +{{ model.borrower.address }} +{{ model.borrower.city }}, {{model.borrower.state }} {{ model.borrower.zip }} +Original Principal: {{ "$%.2f"|format(model.parameters.principal) }} +Rate: {{model.parameters.interest_rate }}% +Term: {{model.parameters.periods }} months +Next Payment Due Date: {{model.parameters.next_due_date}} +Payment Due: {{ "$%.2f"|format(model.parameters.next_payment_amt) }} + +Payment History +#,Due Date,Date Paid,Days Interest,Payment Amt,Principal Pmt,Interest Pmt,Late Fee,New Balance +{% for item in model.past_payments %} +{{ item.payment_number }},{{ item.bill_date }},{{ item.payment_date }},{{ item.days_of_interest }},{{ "$%.2f"|format(item.payment_amount) }},{{ "$%.2f"|format(item.principal_payment) }},{{ "$%.2f"|format(item.interest_payment) }},{{ "$%.2f"|format(item.late_fee) }},{{ "$%.2f"|format(item.new_balance) }} +{% if item.month == 12 or loop.last %} +Total interest paid in {{item.year}} is {{ "$%.2f"|format(item.annual_interest_to_date) }}. +{% endif %} +{% endfor %} +Total interest paid to date is {{ "$%.2f"|format(model.total_interest_paid_to_date) }}. + +Remaining Amortization +#,Due Date,Days Interest,Payment Amt,Principal Pmt,Interest Pmt,Principal Balance< +{% for item in model.future_payments %} +{{ item.payment_number }},{{ item.payment_date }},{{ item.days_of_interest }},{{ "$%.2f"|format(item.payment_amount) }},{{ "$%.2f"|format(item.principal_payment) }},{{ "$%.2f"|format(item.interest_payment) }},{{ "$%.2f"|format(item.new_balance) }} +{% endfor %} +Balloon Payment Due: {{ "$%.2f"|format(model.balloon_payment) }} diff --git a/mortgage/web.py b/mortgage/web.py new file mode 100644 index 0000000..e040033 --- /dev/null +++ b/mortgage/web.py @@ -0,0 +1,541 @@ +import jinja2 +import smtplib +import os +from flask import Flask, render_template, request, redirect +from decimal import * +from datetime import * +from fpdf import FPDF, HTMLMixin +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.mime.base import MIMEBase +from email import encoders +import couchdb + +try: + dbUserName = os.environ['COUCHDB_USERNAME'] +except: + dbUserName = 'admin' + +try: + dbPassword = os.environ['COUCHDB_PW'] +except: + dbPassword = 'ams19230' + +try: + dbHost = os.environ['COUCHDB_HOST'] +except: + dbHost = 'couch.jkent.org' + +connectString = f'https://{dbUserName}:{dbPassword}@{dbHost}/' +print(connectString) + +couch = couchdb.Server(connectString) + +database = couch['mortgage'] +print(f"Database: {database}") + +module_directory = os.path.dirname(__file__) +print(f"Module Directory: {module_directory}") +templates_directory = os.path.join(module_directory, "templates") +print(f"Templates Directory: {templates_directory}") +loader = jinja2.FileSystemLoader([templates_directory]) +environment = jinja2.Environment(loader=loader) +app = Flask(__name__) + +###### +# Flask Call Backs +###### + + +@app.route('/') +def hello(): + loans = getLoanFiles() + + #if a loan was not specified, choose the first loan and reload page with it + if 'loan' in request.args: + document_id = request.args["loan"] + print(f"Hello was access with document_id: '{request.args['loan']}'") + else: + print(f"Hello was accessed without document_id parameter. Using '{loans[0]['document_id']}'.") + return redirect('/?loan=' + loans[0]['document_id']) + + loan = loadLoanInformation(document_id) + amortizeLoan(loan) + + return render_template('main.html', document_id=document_id, loans=loans, model=loan) + + +@app.route('/update_file', methods=['POST']) +def update_file(): + messages = [] + + document_id = request.form["loan"] + document = getDatastore(document_id) + + late_fee = Decimal('0.00').quantize(Decimal('1.00')) + extra_payment = False + payment_amount = Decimal('0.00').quantize(Decimal('1.00')) + payment_history = document["payments"] + todays_date = str(datetime.now().strftime("%Y-%m-%d")) + if 'date' in request.form: + if request.form['date'] == '': + payment_date = todays_date + messages.append("No date was provided. Assuming today's date of " + payment_date + ".") + else: + payment_date = request.form['date'] + messages.append("Date provided: " + payment_date + ".") + else: + payment_date = todays_date + messages.append("No date was provided. Assuming today's date of " + payment_date + ".") + + proceed_flag = True + if 'amount' in request.form: + if request.form['amount'] != '': + try: + payment_amount = Decimal(request.form['amount']) + messages.append("Amount provided: " + str(payment_amount) + ".") + except: + messages.append("The amount provided could not be interpreted. Your payment was not recorded.") + proceed_flag = False + else: + pass + + if 'latefee' in request.form: + if request.form['latefee'] != '': + try: + late_fee = Decimal(request.form['latefee']) + messages.append("Late fee amount: " + str(late_fee) + ".") + except: + messages.append("The late fee amount was specified but could not be interpreted. " + + "Your payment was not recorded.") + proceed_flag = False + + if 'extra' in request.form: + if request.form['extra'] != '': + extra_payment = True + + if proceed_flag is True: + payment_history.append([payment_date, str(payment_amount), str(late_fee), str(extra_payment)]) + database[document_id] = document + messages.append("The payment was successfully written. ") + + # see if an email notification was requested, if not, exit now + if 'notify' not in request.form: + messages.append("Payment notification email not requested.") + return render_template('add.html', document_id=document_id, messages=messages) + + messages.append("Payment notification email requested.") + # send email + emailParameters = document["email"] + result = {} + payment = {} + result['payment'] = payment + payment['today'] = todays_date + payment['date'] = payment_date + payment['payer'] = document['borrower']['name'] + payment['payee'] = document['lender']['name'] + payment['amount'] = payment_amount + payment['late_fee'] = late_fee + payment['extra_payment'] = extra_payment + + subject = "Your loan payment has been received" + html = transformTemplate(selectTemplate('paymentNotification'), result) + msg = generatePaymentNotificationEmail(emailParameters["from_address"], emailParameters["to_address"], + subject, html) + sendEmail(msg, emailParameters["from_address"], emailParameters["to_address"], emailParameters['password']) + return render_template('add.html', document_id=document_id, messages=messages) + + +@app.route('/send_statement', methods=['POST']) +def send_statement(): + document_id = request.form["loan"] + subject = request.form["subject"] + message = request.form["message"] + + loan = loadLoanInformation(document_id) + amortizeLoan(loan) + + reportCreated = False + textReport = pdfReport = htmlReport = None + + if 'text' in request.form: + textReport = transformTemplate(selectTemplate('text'), loan) + reportCreated = True + + if 'pdf' in request.form: + pdfInterimReport = transformTemplate(selectTemplate('pdf'), loan) + pdfReport = createPDF(pdfInterimReport) + reportCreated = True + + if ('html' in request.form) or (reportCreated is False): + htmlReport = transformTemplate(selectTemplate('html'), loan) + + # send email + emailParameters = loan["email"] + + msg = generateStatementEmail(emailParameters["from_address"], + emailParameters["to_address"], + subject, + message, pdfReport, htmlReport, textReport) + + sendEmail(msg, emailParameters["from_address"], emailParameters["to_address"], emailParameters['password']) + + return render_template('email.html', document_id=document_id) + + +###### +# Loan File Functions +###### + + +def getLoanFiles(): + #iterate over each of the documents found in the mortgage couchdb + #database and pull their display name + loans = [] + + for document_id in database: + document = database[document_id] + displayName = document['displayName'] + loans.append(createLoanEntryMenuItem(displayName, document_id)) + + return loans + + +def createLoanEntryMenuItem(loanName, document_id): + x = {} + x['name'] = loanName + x['document_id'] = document_id + return x + + +###### +# Datastore Manipulation Functions +###### +def getStatementHeader(datastore): + return datastore['header'] + + +def getAccounting(datastore): + try: + return datastore['accounting'] + except: + return {"payment_account": "", "loan_account": "", "interest_account": ""} + + +def getEmailInformation(datastore): + return datastore['email'] + + +def loadLoanInformation(document_id): + datastore = getDatastore(document_id) + + loanModel = {} + loanModel['datastore'] = datastore + loanModel['email'] = getEmailInformation(datastore) + loanModel['parameters'] = getLoanParameters(datastore) + loanModel['lender'] = getLender(datastore) + loanModel['borrower'] = getBorrower(datastore) + loanModel['header'] = getStatementHeader(datastore) + loanModel['accounting'] = getAccounting(datastore) + return loanModel + + +def getLoanParameters(datastore): + # read in the loan profile information + loan = datastore['parameters'] + + annual_rate = Decimal(loan['interest_rate']) / 100 + daily_interest_rate = annual_rate / 360 + principal = Decimal(loan["principal"]).quantize(Decimal("1.00")) + periods_per_year = Decimal(loan["periods_per_year"]) + total_periods = Decimal(loan["periods"]) + #payment_day_of_month = int(loan['payment_day_of_month']) + + if "monthly_payment" in loan: + monthly_payment = Decimal(loan["monthly_payment"]).quantize(Decimal("1.00")) + else: + # calculate expected monthly payment + periodic_rate = annual_rate / periods_per_year + discount_factor = (((1 + periodic_rate) ** total_periods) - 1) / ( + periodic_rate * ((1 + periodic_rate) ** total_periods)) + monthly_payment = (principal / discount_factor).quantize(Decimal("1.00")) + + loan['principal'] = principal # standardizes the format + loan['annual_rate'] = annual_rate # standardizes the format + loan['daily_interest_rate'] = daily_interest_rate + loan['rate'] = '' + (annual_rate * 100).__str__() + '%' + loan['next_payment_amt'] = 0 + loan['next_payment_date'] = '12/12/12' + loan['total_periods'] = total_periods + loan['monthly_payment'] = monthly_payment + datastore['parameters'] = loan + return loan + + +def getLender(datastore): + return datastore['lender'] + + +def getBorrower(datastore): + return datastore['borrower'] + + +def getDatastore(document_id=None): + if document_id is None: + quit("Invalid document.") + + datastore = database[document_id] + return datastore + + +###### +# Loan Calculation Functions +###### +def amortizeLoan(loan): + # loop over the payments and calculate the actual amortization + monthly_payment = loan["parameters"]["monthly_payment"] + + actual_payments = loan["datastore"]["payments"] + + remaining_principal = loan["parameters"]["principal"] + payment_day_of_month = int(loan["parameters"]["payment_day_of_month"]) + daily_interest_rate = loan["parameters"]["daily_interest_rate"] + total_periods = loan["parameters"]["total_periods"] + interest_paid_through_date = datetime.strptime(loan["parameters"]["start_interest_date"], "%Y-%m-%d").date() + next_payment_date = datetime.strptime(loan["parameters"]["first_payment_month"], '%Y-%m-%d').date() + next_bill_date = date(year=next_payment_date.year, month=next_payment_date.month, day=payment_day_of_month) + + payment_number = 1 + annual_interest = 0 + total_interest = 0 + #old_bill_date = next_bill_date + #current_year = next_bill_date.year + + past_payments = [] + future_payments = [] + + for payment in actual_payments: + payment_date = datetime.strptime((payment[0]), '%Y-%m-%d').date() + payment_amount = Decimal(payment[1]).quantize(Decimal("1.00")) + days_since_last_payment = (payment_date - interest_paid_through_date).days + if len(payment) > 2: + late_fee = Decimal(payment[2]).quantize(Decimal("1.00")) + else: + late_fee = Decimal("0.00") + + # check for out of order payments, generally a sign of a data entry problem, especially years + if days_since_last_payment < 0: + print( + "Payment Number %s appears out of order. The payment date '%s' is before the previous payment on '%s'." + % (payment_number, payment_date, interest_paid_through_date)) + quit() + + new_daily_interest = (remaining_principal * daily_interest_rate).quantize(Decimal("0.00")) + new_interest = (days_since_last_payment * new_daily_interest).quantize(Decimal("0.00")) + new_principal = payment_amount - new_interest - late_fee + interest_paid_through_date = payment_date + total_interest = total_interest + new_interest + annual_interest = annual_interest + new_interest + remaining_principal = remaining_principal - new_principal + + # create the payment record for the template to render + payment_record = {} + payment_record['year'] = next_bill_date.year + payment_record['month'] = next_bill_date.month + payment_record['payment_number'] = payment_number + payment_record['bill_date'] = next_bill_date + payment_record['payment_date'] = payment_date + payment_record['days_of_interest'] = days_since_last_payment + payment_record['daily_interest'] = new_daily_interest + payment_record['payment_amount'] = payment_amount + payment_record['principal_payment'] = payment_amount - new_interest - late_fee + payment_record['interest_payment'] = new_interest + payment_record['new_balance'] = remaining_principal + payment_record['interest_to_date'] = total_interest + payment_record['annual_interest_to_date'] = annual_interest + payment_record['late_fee'] = late_fee + payment_record['payee'] = loan["lender"]["name"] + payment_record['payment_account'] = loan["accounting"]["payment_account"] + payment_record['loan_account'] = loan["accounting"]["loan_account"] + payment_record['interest_account'] = loan["accounting"]["interest_account"] + payment_record['late_fee_account'] = "Expenses:Interest:LateFees" + past_payments.append(payment_record) + + payment_number = payment_number + 1 + + #check for the extra payment flag, if its there, don't advance the next payment date + if len(payment) > 3 and payment[3] == "extra": + print("Extra payment flag: " + payment[3]) + #this is an extra payment don't advance the date + else: + old_bill_date = next_bill_date + + if old_bill_date.month < 12: + next_bill_date = date(year=old_bill_date.year, month=old_bill_date.month + 1, day=payment_day_of_month) + else: + annual_interest = Decimal("0.00") + next_bill_date = date(year=old_bill_date.year + 1, month=1, day=payment_day_of_month) + + loan["parameters"]["next_due_date"] = next_bill_date + + loan["total_interest_paid_to_date"] = total_interest + + if remaining_principal < monthly_payment: + loan["parameters"]["next_payment_amt"] = remaining_principal + else: + loan["parameters"]["next_payment_amt"] = monthly_payment + + loan["current_balance"] = remaining_principal + loan["current_daily_interest_accrual"] = (remaining_principal * daily_interest_rate).quantize(Decimal("0.00")) + + # loop over remaining scheduled payments and present estimated amortization + while (payment_number <= total_periods) and (remaining_principal > 0): + days_since_last_payment = (next_bill_date - interest_paid_through_date).days + new_daily_interest = (remaining_principal * daily_interest_rate).quantize(Decimal("0.00")) + new_interest = (days_since_last_payment * remaining_principal * daily_interest_rate).quantize(Decimal("0.00")) + + # make sure the last payment isn't too much + if new_interest + remaining_principal < monthly_payment: + monthly_payment = new_interest + remaining_principal + + new_principal = monthly_payment - new_interest + + remaining_principal = remaining_principal - new_principal + + interest_paid_through_date = next_bill_date + + # complete the future payment amortization record + future_payment_record = {} + future_payment_record['payment_number'] = payment_number + future_payment_record['payment_date'] = next_bill_date + future_payment_record['days_of_interest'] = days_since_last_payment + future_payment_record['daily_interest'] = new_daily_interest + future_payment_record['payment_amount'] = monthly_payment + future_payment_record['principal_payment'] = new_principal + future_payment_record['interest_payment'] = new_interest + future_payment_record['new_balance'] = remaining_principal + future_payments.append(future_payment_record) + + payment_number = payment_number + 1 + old_bill_date = next_bill_date + if old_bill_date.month < 12: + next_bill_date = date(year=old_bill_date.year, month=old_bill_date.month + 1, day=payment_day_of_month) + else: + next_bill_date = date(year=old_bill_date.year + 1, month=1, day=payment_day_of_month) + + loan["balloon_payment"] = remaining_principal + loan["past_payments"] = past_payments + loan["future_payments"] = future_payments + return + + +###### +# Report Generation Functions +###### + + +def transformTemplate(template_fileName, loanModel): + # template_filename = "statement.text.jinja" + # setup jinja for creating the statement + template = environment.get_template(template_fileName) + + print(loanModel) + report = template.render(model=loanModel) + return report + + +def generateStatementEmail(from_address, to_address, subject, body, pdf, html, txt): + msg = MIMEMultipart() + msg['Subject'] = subject + msg['From'] = from_address + msg['To'] = to_address + + msg.attach(MIMEText(body)) + + if pdf is not None: + part = MIMEBase("application", "octet-stream") + part.set_payload(pdf) + encoders.encode_base64(part) + part.add_header('Content-Disposition', 'attachment; filename="statement.pdf"') + msg.attach(part) + + if html is not None: + part = MIMEBase("text", "html") + part.set_payload(html) + encoders.encode_base64(part) + part.add_header('Content-Disposition', 'attachment; filename="statement.html"') + msg.attach(part) + + if txt is not None: + part = MIMEBase("text", "plain") + part.set_payload(txt) + encoders.encode_base64(part) + part.add_header('Content-Disposition', 'attachment; filename="statement.txt"') + msg.attach(part) + + return msg + + +def generatePaymentNotificationEmail(from_address, to_address, subject, html): + msg = MIMEMultipart() + msg['Subject'] = subject + msg['From'] = from_address + msg['To'] = to_address + + part = MIMEBase("text", "html") + part.set_payload(html) + encoders.encode_base64(part) + part.add_header('Content-Disposition', 'attachment; filename="notification.html"') + msg.attach(part) + + return msg + + +def sendEmail(msg, from_address, to_address, passwd): + try: + server = smtplib.SMTP('smtp.gmail.com', 587) + server.ehlo() + server.starttls() + server.login(from_address, passwd) + + server.sendmail(from_address, to_address, msg.as_string()) + server.close() + except: + print("Couldn't send email.") + + +def createPDF(report): + # create pdf + class PDF(FPDF, HTMLMixin): + pass + + pdf = PDF() + pdf.set_font(family='Arial', size=12) + pdf.add_page() + pdf.write_html(report) + return pdf.output(dest='S') + + +def selectTemplate(formatType): + if formatType == 'paymentNotification': + return 'payment_received_email.html.jinja' + + if formatType == 'html': + return 'statement.html.jinja' + + if formatType == 'pdf': + return 'statement.pdf.jinja' + + if formatType == 'text': + return 'statement.text.jinja' + + return 'statement.html.jinja' + + +def main(): + app.debug = True + app.run(host='0.0.0.0', port=8000) + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9ad34f9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +click==8.1.3 +CouchDB==1.2 +defusedxml==0.7.1 +Flask==2.2.2 +fonttools==4.37.4 +fpdf2==2.5.7 +itsdangerous==2.1.2 +Jinja2==3.1.2 +MarkupSafe==2.1.1 +Pillow==9.2.0 +svg.path==6.2 +Werkzeug==2.2.2