Updated Dockerfile and removed web_ng.py
user: john branch 'default' changed Dockerfile removed mortgage/web_ng.py
This commit is contained in:
@@ -1,8 +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"]
|
||||
CMD [ "python3", "-m" , "web", "run", "--host=0.0.0.0"]
|
||||
|
||||
@@ -1,544 +0,0 @@
|
||||
import json
|
||||
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
|
||||
|
||||
module_directory = os.path.dirname(__file__)
|
||||
app_directory = os.path.normpath(os.path.join(module_directory, ".."))
|
||||
loader = jinja2.FileSystemLoader([os.path.join(module_directory, "templates")])
|
||||
environment = jinja2.Environment(loader=loader)
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
######
|
||||
# Flask Call Backs
|
||||
######
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def hello():
|
||||
loans = getLoanFiles(app_directory)
|
||||
|
||||
#if a loan was not specified, choose the first loan and reload page with it
|
||||
if 'loan' in request.args:
|
||||
filename = request.args["loan"]
|
||||
else:
|
||||
return redirect('/?loan=' + loans[0]['filename'])
|
||||
|
||||
loan = loadLoanInformation(getFullPathofLoanFile(filename))
|
||||
amortizeLoan(loan)
|
||||
|
||||
return render_template('main.html', filename=filename, loans=loans, model=loan)
|
||||
|
||||
|
||||
@app.route('/update_file', methods=['POST'])
|
||||
def update_file():
|
||||
messages = []
|
||||
|
||||
loanFile = request.form["loan"]
|
||||
data = getDatastore(getFullPathofLoanFile(loanFile))
|
||||
|
||||
late_fee = Decimal('0.00').quantize(Decimal('1.00'))
|
||||
extra_payment = False
|
||||
payment_amount = Decimal('0.00').quantize(Decimal('1.00'))
|
||||
payment_history = data["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:
|
||||
try:
|
||||
backup_filename = loanFile + ".backup-" + datetime.now().strftime("%Y-%m-%d %H-%M-%S") + ".json"
|
||||
backup_file = open(getFullPathofLoanFile(backup_filename), 'w+')
|
||||
json.dump(data, backup_file, indent=2)
|
||||
backup_file.close()
|
||||
except:
|
||||
messages.append("A backup file could not be created. Your payment was not recorded.")
|
||||
proceed_flag = False
|
||||
else:
|
||||
messages.append("A backup of your file was created: '" + backup_filename + "'")
|
||||
|
||||
if proceed_flag is True:
|
||||
try:
|
||||
payment_history.append([payment_date, str(payment_amount), str(late_fee), str(extra_payment)])
|
||||
file = open(getFullPathofLoanFile(loanFile), 'w+')
|
||||
json.dump(data, file, indent=2)
|
||||
file.close()
|
||||
except:
|
||||
messages.append("An error occurred writing to the file. Your payment file may be corrupt, " +
|
||||
"please consider rolling back to the backup created above.")
|
||||
else:
|
||||
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', filename=loanFile, messages=messages)
|
||||
|
||||
messages.append("Payment notification email requested.")
|
||||
# send email
|
||||
emailParameters = data["email"]
|
||||
result = {}
|
||||
payment = {}
|
||||
result['payment'] = payment
|
||||
payment['today'] = todays_date
|
||||
payment['date'] = payment_date
|
||||
payment['payer'] = data['borrower']['name']
|
||||
payment['payee'] = data['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', filename=loanFile, messages=messages)
|
||||
|
||||
|
||||
@app.route('/send_statement', methods=['POST'])
|
||||
def send_statement():
|
||||
loanFile = request.form["loan"]
|
||||
subject = request.form["subject"]
|
||||
message = request.form["message"]
|
||||
|
||||
loan = loadLoanInformation(getFullPathofLoanFile(loanFile))
|
||||
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', filename=loanFile)
|
||||
|
||||
|
||||
######
|
||||
# Loan File Functions
|
||||
######
|
||||
|
||||
def getLoanFiles(directory):
|
||||
loans = []
|
||||
count = 0
|
||||
|
||||
directory = os.path.normpath(directory)
|
||||
print("Directory {}".format(directory))
|
||||
for filename in os.listdir(directory):
|
||||
if filename.endswith(".loan"):
|
||||
count=count+1
|
||||
loans.append(createLoanEntryMenuItem("{0} {1}".format(count, filename.removesuffix(".loan")), filename))
|
||||
|
||||
return loans
|
||||
|
||||
|
||||
def createLoanEntryMenuItem(loanName, fileName):
|
||||
x = {}
|
||||
x['name'] = loanName
|
||||
x['filename'] = fileName
|
||||
return x
|
||||
|
||||
|
||||
def getFullPathofLoanFile(filename):
|
||||
app_path = os.path.dirname(__file__)
|
||||
file_path = os.path.normpath(os.path.join(os.path.dirname(__file__),'..'))
|
||||
print("App Path: {0}\nFile Path: {1}".format(app_path, file_path))
|
||||
return os.path.join(file_path, filename)
|
||||
|
||||
|
||||
######
|
||||
# Datastore Manipulation Functions
|
||||
######
|
||||
|
||||
|
||||
def getStatementHeader(datastore):
|
||||
return datastore['header']
|
||||
|
||||
|
||||
def getEmailInformation(datastore):
|
||||
return datastore['email']
|
||||
|
||||
|
||||
def loadLoanInformation(filename):
|
||||
datastore = getDatastore(filename)
|
||||
|
||||
loanModel = {}
|
||||
loanModel['datastore'] = datastore
|
||||
loanModel['email'] = getEmailInformation(datastore)
|
||||
loanModel['parameters'] = getLoanParameters(datastore)
|
||||
loanModel['lender'] = getLender(datastore)
|
||||
loanModel['borrower'] = getBorrower(datastore)
|
||||
loanModel['header'] = getStatementHeader(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(filename=None):
|
||||
try:
|
||||
if filename:
|
||||
with open(filename, 'r') as f:
|
||||
datastore = json.load(f)
|
||||
|
||||
except Exception as e:
|
||||
print("An error occurred opening your loan file '%s'. " % filename)
|
||||
print("The Exception:")
|
||||
print(e.__repr__())
|
||||
quit()
|
||||
|
||||
return datastore
|
||||
|
||||
######
|
||||
# Loan Calculation Functions
|
||||
######
|
||||
|
||||
def new_amortizeLoan(loan):
|
||||
#loop over the historical data and re-calculate the actual amortization
|
||||
monthly_payment = loan["parameters"]["monthly_payment"]
|
||||
first_day_interest_accrues = 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()
|
||||
|
||||
|
||||
|
||||
def amortizeLoan(loan):
|
||||
# loop over the payments and calculate the actual amortization
|
||||
monthly_payment = loan["parameters"]["monthly_payment"]
|
||||
|
||||
actual_payments = loan["datastore"]["payments"]
|
||||
#invoices = loan["datastore"]["invoices"]
|
||||
#payments = loan["datastore"]["payments"]
|
||||
#allocations = loan["datastore"]["allocations"]
|
||||
|
||||
|
||||
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_interest = (days_since_last_payment * remaining_principal * daily_interest_rate).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['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
|
||||
past_payments.append(payment_record)
|
||||
|
||||
payment_number = payment_number + 1
|
||||
|
||||
print("Payment record length: " + str(len(payment)))
|
||||
#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
|
||||
|
||||
# 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_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['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
|
||||
|
||||
#msg.attach(MIMEText(body))
|
||||
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 MyFPDF(FPDF, HTMLMixin):
|
||||
pass
|
||||
|
||||
pdf = MyFPDF()
|
||||
pdf.set_font(family='Arial', size=12)
|
||||
pdf.add_page()
|
||||
pdf.write_html(report)
|
||||
return pdf.output(dest='S')
|
||||
|
||||
|
||||
def selectTemplate(format):
|
||||
if format == 'paymentNotification':
|
||||
return 'payment_received_email.html.jinja'
|
||||
|
||||
if format == 'html':
|
||||
return 'statement.html.jinja'
|
||||
|
||||
if format == 'pdf':
|
||||
return 'statement.pdf.jinja'
|
||||
|
||||
if format == 'text':
|
||||
return 'statement.text.jinja'
|
||||
|
||||
return 'statement.html.jinja'
|
||||
|
||||
|
||||
def main():
|
||||
app.debug = True
|
||||
app.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user