:G: Enter commit message. Lines beginning with 'HG:' are removed.

This commit is contained in:
john
2022-10-03 00:02:12 -04:00
parent 3ccd81cf76
commit 466d2dce8b
20 changed files with 1630 additions and 729 deletions

View File

@@ -1,3 +1,5 @@
./venv/
.hgignore
.Python .Python
.DS_Store
./bin/
./lib/
./.idea/

View File

@@ -23,7 +23,7 @@
"date": "Today", "date": "Today",
"title": "Installment Loan Test Statement" "title": "Installment Loan Test Statement"
}, },
"payments": [ "payments": [
[ [
"2019-07-15", "2019-07-15",
"475.00" "475.00"
@@ -52,6 +52,36 @@
"2020-01-15", "2020-01-15",
"0", "0",
"50" "50"
],
[
"2020-09-27",
"278.15",
"20"
],
[
"2020-09-29",
"296.12",
"25"
],
[
"2020-10-16",
"133",
"0.00"
],
[
"2020-10-16",
"133",
"0.00"
],
[
"2020-10-16",
"133",
"0.00"
],
[
"2021-05-03",
"1",
"1"
] ]
], ],
"borrower": { "borrower": {
@@ -67,6 +97,6 @@
"subject": "Your test loan statement...", "subject": "Your test loan statement...",
"body": "Your test loan statement is attached.", "body": "Your test loan statement is attached.",
"server": "smtp.gmail.com", "server": "smtp.gmail.com",
"password": "pvyrbcnzrjoizprn" "password": "builcuouzobxroow"
} }
} }

View File

@@ -1,69 +0,0 @@
{
"htmlTemplate": "statement.pdf.jinja",
"txtTemplate": "statement.txt.jinja",
"format": "html",
"email": {
"from_address": "jkent3rd@gmail.com",
"to_address": "grady@gradystreet.com",
"server": "smtp.gmail.com",
"password": "pvyrbcnzrjoizprn",
"template": "./template.txt",
"send_pdf": "true",
"send_text": "true",
"subject": "Your loan statement...",
"body": "Your loan statement is attached.",
"text": "Please see your most recent account statement."
},
"parameters": {
"account_number": "100100",
"principal": 10020.00,
"interest_rate": 6.5,
"periods_per_year": 12,
"periods": 18,
"start_date": "2017-12-05",
"start_interest_date": "2017-12-05",
"first_payment_month": "2018-01-15",
"monthly_payment": 589.00,
"payment_day_of_month": "15"
},
"payments": [
["2018-01-16", "589.00"],
["2018-03-30", "589.00"],
["2018-04-06", "589.00"],
["2018-04-12", "589.00"],
["2018-05-28", "589.00"],
["2018-06-12", "589.00"],
["2018-07-13", "589.00"],
["2018-08-14", "589.00"],
["2018-09-26", "589.00"],
["2018-10-15", "0"],
["2018-11-29", "589.00"],
["2018-12-31", "589.00"],
["2019-01-15", "0.00"],
["2019-02-13", "589.00"],
["2019-03-15", "0"],
["2019-04-15", "0"],
["2019-05-15", "589.00"],
["2019-06-21", "2985.00"]
],
"borrower": {
"name": "Bear Houses, LLC",
"address": "301 N Beauregard St Apt 203",
"city": "Alexandria",
"state": "VA",
"zip": "22312"
},
"lender": {
"name": "John Kent",
"phone": "703.343.0782",
"address": "743 Madison St NW",
"city": "Washington",
"state": "DC",
"zip": "20011"
},
"header": {
"title": "Installment Loan Statement",
"date": "Today"
}
}

View File

@@ -22,7 +22,7 @@
"send_text": "true", "send_text": "true",
"template": "./template.txt", "template": "./template.txt",
"send_pdf": "true", "send_pdf": "true",
"password": "pvyrbcnzrjoizprn", "password": "builcuouzobxroow",
"subject": "Your loan statement..." "subject": "Your loan statement..."
}, },
"lender": { "lender": {
@@ -129,14 +129,164 @@
[ [
"2020-06-01", "2020-06-01",
"278.15" "278.15"
],
[
"2020-07-01",
"278.15"
],
[
"2020-08-01",
"278.15"
],
[
"2020-09-01",
"278.15"
],
[
"2020-10-01",
"278.15"
],
[
"2020-11-01",
"278.15",
"0.00"
],
[
"2020-12-01",
"278.15",
"0.00"
],
[
"2021-01-01",
"278.15",
"0"
],
[
"2021-02-01",
"278.15",
"0"
],
[
"2021-03-01",
"278.15",
"0.00"
],
[
"2021-04-01",
"278.15",
"0.00"
],
[
"2021-04-30",
"278.15",
"0"
],
[
"2021-06-01",
"278.15",
"0.00"
],
[
"2021-07-01",
"278.15",
"0"
],
[
"2021-07-31",
"278.15",
"0"
],
[
"2021-09-01",
"278.15",
"0.00",
"False"
],
[
"2021-10-01",
"278.15",
"0.00",
"False"
],
[
"2021-11-01",
"278.15",
"0",
"False"
],
[
"2021-12-01",
"278.15",
"0.00",
"False"
],
[
"2022-01-01",
"278.15",
"0.00",
"False"
],
[
"2022-02-01",
"278.15",
"0.00",
"False"
],
[
"2022-03-01",
"278.15",
"0.00",
"False"
],
[
"2022-04-01",
"278.15",
"0.00",
"False"
],
[
"2022-05-01",
"278.15",
"0.00",
"False"
],
[
"2022-06-01",
"278.15",
"0.00",
"False"
],
[
"2022-07-01",
"278.15",
"0.00",
"False"
],
[
"2022-08-01",
"278.15",
"0.00",
"False"
],
[
"2022-09-01",
"278.15",
"0.00",
"False"
],
[
"2022-10-01",
"278.15",
"0.00",
"False"
] ]
], ],
"borrower": { "borrower": {
"city": "Washington", "city": "Washington",
"state": "DC", "state": "DC",
"name": "Grandma Tina's Properties, LLC", "name": "Grandma Tina's Properties, LLC",
"zip": "20008", "zip": "20001",
"address": "3100 Connecticut Ave NW Apt 144" "address": "1720 New Jersey Ave NW Unit 401"
}, },
"txtTemplate": "statement.txt.jinja" "txtTemplate": "statement.txt.jinja"
} }

View File

@@ -22,7 +22,7 @@
"send_text": "true", "send_text": "true",
"template": "./template.txt", "template": "./template.txt",
"send_pdf": "true", "send_pdf": "true",
"password": "pvyrbcnzrjoizprn", "password": "builcuouzobxroow",
"subject": "Your loan statement..." "subject": "Your loan statement..."
}, },
"lender": { "lender": {
@@ -129,14 +129,164 @@
[ [
"2020-06-01", "2020-06-01",
"278.15" "278.15"
],
[
"2020-07-01",
"278.15"
],
[
"2020-08-01",
"278.15"
],
[
"2020-09-01",
"278.15"
],
[
"2020-10-01",
"278.15"
],
[
"2020-11-01",
"278.15",
"0.00"
],
[
"2020-12-01",
"278.15",
"0.00"
],
[
"2021-01-01",
"278.15",
"0"
],
[
"2021-02-01",
"278.15",
"0"
],
[
"2021-03-01",
"278.15",
"0.00"
],
[
"2021-04-01",
"278.15",
"0.00"
],
[
"2021-04-30",
"278.15",
"0"
],
[
"2021-06-01",
"278.15",
"0.00"
],
[
"2021-07-01",
"278.15",
"0"
],
[
"2021-07-31",
"278.15",
"0"
],
[
"2021-09-01",
"278.15",
"0.00",
"False"
],
[
"2021-10-01",
"278.15",
"0.00",
"False"
],
[
"2021-11-01",
"278.15",
"0",
"False"
],
[
"2021-12-01",
"278.15",
"0.00",
"False"
],
[
"2022-01-01",
"278.15",
"0.00",
"False"
],
[
"2022-02-01",
"278.15",
"0",
"False"
],
[
"2022-03-01",
"278.15",
"0.00",
"False"
],
[
"2022-04-01",
"278.15",
"0.00",
"False"
],
[
"2022-05-01",
"278.15",
"0.00",
"False"
],
[
"2022-06-01",
"278.15",
"0.00",
"False"
],
[
"2022-07-01",
"278.15",
"0.00",
"False"
],
[
"2022-08-01",
"278.15",
"0.00",
"False"
],
[
"2022-09-01",
"278.15",
"0.00",
"False"
],
[
"2022-10-01",
"278.15",
"0.00",
"False"
] ]
], ],
"borrower": { "borrower": {
"city": "Washington", "city": "Washington",
"state": "DC", "state": "DC",
"name": "Grandma Tina's Properties, LLC", "name": "Grandma Tina's Properties, LLC",
"zip": "20008", "zip": "20001",
"address": "3100 Connecticut Ave NW Apt 144" "address": "1720 New Jersey Ave NW Unit 401"
}, },
"txtTemplate": "statement.txt.jinja" "txtTemplate": "statement.txt.jinja"
} }

View File

@@ -1,16 +1,16 @@
{ {
"htmlTemplate": "statement.pdf.jinja", "htmlTemplate": "statement.pdf.jinja",
"parameters": { "parameters": {
"monthly_payment": 475.0, "monthly_payment": 842.93,
"interest_rate": 6.5, "interest_rate": 5.75,
"start_interest_date": "2019-06-17", "start_interest_date": "2022-08-12",
"payment_day_of_month": "15", "payment_day_of_month": "15",
"first_payment_month": "2019-07-15", "first_payment_month": "2022-09-15",
"account_number": "100002", "account_number": "22-0001",
"periods": 20, "periods": 180,
"start_date": "2019-06-15", "start_date": "2022-08-12",
"periods_per_year": 12, "periods_per_year": 12,
"principal": 9000.0 "principal": 101508.04
}, },
"format": "html", "format": "html",
"email": { "email": {
@@ -22,66 +22,33 @@
"send_text": "true", "send_text": "true",
"template": "./template.txt", "template": "./template.txt",
"send_pdf": "true", "send_pdf": "true",
"password": "pvyrbcnzrjoizprn", "password": "builcuouzobxroow",
"subject": "Your loan statement..." "subject": "Your loan statement..."
}, },
"lender": { "lender": {
"city": "Washington", "city": "Washington",
"name": "Rivanna Graphite Investments, LLC", "name": "Rivanna Graphite Investments, LLC",
"zip": "20008", "zip": "20001",
"phone": "703.343.0782", "phone": "703.343.0782",
"state": "DC", "state": "DC",
"address": "3100 Connecticut Ave NW Apt 144" "address": "1720 New Jersey Ave NW Unit 401"
}, },
"header": { "header": {
"date": "Today", "date": "Today",
"title": "Installment Loan Statement" "title": "Mortgage Loan Statement - Harbor Bend Loan 1"
}, },
"payments": [ "payments": [
[ [
"2019-07-15", "2022-08-15",
"475.00" "803",
],
[
"2019-08-14",
"475.00"
],
[
"2019-09-17",
"475"
],
[
"2019-10-15",
"0"
],
[
"2019-11-15",
"0"
],
[
"2019-12-5",
"475"
],
[
"2020-01-15",
"0", "0",
"0" "False"
], ],
[ [
"2020-02-17", "2022-09-09",
"0" "803",
], "0.00",
[ "False"
"2020-03-03",
"475"
],
[
"2020-04-15",
"0"
],
[
"2020-05-19",
"475"
] ]
], ],
"borrower": { "borrower": {

View File

@@ -22,16 +22,16 @@
"send_text": "true", "send_text": "true",
"template": "./template.txt", "template": "./template.txt",
"send_pdf": "true", "send_pdf": "true",
"password": "pvyrbcnzrjoizprn", "password": "builcuouzobxroow",
"subject": "Your loan statement..." "subject": "Your loan statement..."
}, },
"lender": { "lender": {
"city": "Washington", "city": "Washington",
"name": "Rivanna Graphite Investments, LLC", "name": "Rivanna Graphite Investments, LLC",
"zip": "20008", "zip": "20001",
"phone": "703.343.0782", "phone": "703.343.0782",
"state": "DC", "state": "DC",
"address": "3100 Connecticut Ave NW Apt 144" "address": "1720 New Jersey Ave NW Unit 401"
}, },
"header": { "header": {
"date": "Today", "date": "Today",
@@ -162,6 +162,86 @@
[ [
"2020-06-16", "2020-06-16",
"803.00" "803.00"
],
[
"2020-07-15",
"803"
],
[
"2020-08-13",
"803"
],
[
"2020-09-17",
"803"
],
[
"2020-10-15",
"803",
"0.00"
],
[
"2020-11-16",
"803.00",
"0"
],
[
"2020-12-15",
"803.00",
"0.00"
],
[
"2021-01-13",
"803.00",
"0"
],
[
"2021-02-16",
"803.00",
"0"
],
[
"2021-03-22",
"0",
"32.12"
],
[
"2021-04-14",
"803.00",
"0.00"
],
[
"2021-05-18",
"803.00",
"0"
],
[
"2021-06-15",
"803.00",
"0"
],
[
"2021-07-31",
"0",
"32.12"
],
[
"2021-08-18",
"803",
"0.00",
"False"
],
[
"2021-09-24",
"803",
"0.00",
"False"
],
[
"2021-09-25",
"0",
"0.00",
"False"
] ]
], ],
"borrower": { "borrower": {

View File

@@ -0,0 +1,128 @@
{
"htmlTemplate": "statement.pdf.jinja",
"parameters": {
"monthly_payment": 831.0,
"interest_rate": 5.5,
"start_interest_date": "2021-10-13",
"payment_day_of_month": "15",
"first_payment_month": "2021-11-15",
"account_number": "21-0004",
"periods": 180,
"start_date": "2021-10-13",
"periods_per_year": 12,
"principal": 101123.26
},
"format": "html",
"email": {
"body": "Your loan statement is attached.",
"to_address": "grady@gradystreet.com",
"from_address": "jkent3rd@gmail.com",
"text": "Please see your most recent account statement.",
"server": "smtp.gmail.com",
"send_text": "true",
"template": "./template.txt",
"send_pdf": "true",
"password": "builcuouzobxroow",
"subject": "Your loan statement..."
},
"lender": {
"city": "Washington",
"name": "Rivanna Graphite Investments, LLC",
"zip": "20001",
"phone": "703.343.0782",
"state": "DC",
"address": "1720 New Jersey Ave NW Unit 401"
},
"header": {
"date": "Today",
"title": "Mortgage Loan Statement - 195 Greenfield Lane, Pearl MS"
},
"payments": [
[
"2021-10-14",
"803",
"0.00",
"False"
],
[
"2021-10-14",
"28",
"0.00",
"False"
],
[
"2021-10-18",
"28",
"0.00",
"False"
],
[
"2021-11-16",
"803.00",
"0.00",
"False"
],
[
"2021-11-19",
"28",
"0.00",
"False"
],
[
"2022-02-10",
"831",
"0.00",
"False"
],
[
"2022-04-04",
"1000",
"55.00",
"False"
],
[
"2022-05-15",
"0",
"0.00",
"False"
],
[
"2022-06-15",
"0",
"0.00",
"False"
],
[
"2022-07-15",
"0",
"0.00",
"False"
],
[
"2022-07-26",
"803",
"0.00",
"False"
],
[
"2022-08-12",
"0",
"0.00",
"False"
],
[
"2022-08-12",
"101508.04",
"0.00",
"False"
]
],
"borrower": {
"city": "Alexandria",
"state": "VA",
"name": "Bear Houses, LLC",
"zip": "22312",
"address": "301 N Beauregard St Apt 203"
},
"txtTemplate": "statement.txt.jinja"
}

View File

@@ -0,0 +1,50 @@
{
"htmlTemplate": "statement.pdf.jinja",
"parameters": {
"monthly_payment": 848,
"interest_rate": 5.75,
"start_interest_date": "2022-08-12",
"payment_day_of_month": "15",
"first_payment_month": "2022-09-15",
"account_number": "22-0001",
"periods": 180,
"start_date": "2022-08-12",
"periods_per_year": 12,
"principal": 101508.04
},
"format": "html",
"email": {
"body": "Your loan statement is attached.",
"to_address": "grady@gradystreet.com",
"from_address": "jkent3rd@gmail.com",
"text": "Please see your most recent account statement.",
"server": "smtp.gmail.com",
"send_text": "true",
"template": "./template.txt",
"send_pdf": "true",
"password": "builcuouzobxroow",
"subject": "Your loan statement..."
},
"lender": {
"city": "Washington",
"name": "Rivanna Graphite Investments, LLC",
"zip": "20001",
"phone": "703.343.0782",
"state": "DC",
"address": "1720 New Jersey Ave NW Unit 401"
},
"header": {
"date": "Today",
"title": "Mortgage Loan Statement - Harbor Bend Loan 1"
},
"payments": [
],
"borrower": {
"city": "Alexandria",
"state": "VA",
"name": "Bear Houses, LLC",
"zip": "22312",
"address": "301 N Beauregard St Apt 203"
},
"txtTemplate": "statement.txt.jinja"
}

View File

@@ -0,0 +1,137 @@
{
"htmlTemplate": "statement.pdf.jinja",
"parameters": {
"monthly_payment": 175.55,
"interest_rate": 4.5,
"start_interest_date": "2020-07-15",
"payment_day_of_month": "15",
"first_payment_month": "2020-08-15",
"account_number": "100003",
"periods": 60,
"start_date": "2020-07-15",
"periods_per_year": 12,
"principal": 16884
},
"format": "html",
"email": {
"body": "Your loan statement is attached.",
"to_address": "grady@gradystreet.com",
"from_address": "jkent3rd@gmail.com",
"text": "Please see your most recent account statement.",
"server": "smtp.gmail.com",
"send_text": "true",
"template": "./template.txt",
"send_pdf": "true",
"password": "builcuouzobxroow",
"subject": "Your loan statement..."
},
"lender": {
"city": "Washington",
"name": "Rivanna Graphite Investments, LLC",
"zip": "20008",
"phone": "703.343.0782",
"state": "DC",
"address": "3100 Connecticut Ave NW Apt 144"
},
"header": {
"date": "Today",
"title": "Mortgage Loan Statement - 5025 Wayneland Drive Apt D11 Jackson MS 39211"
},
"payments": [
[
"2020-08-27",
"175.55"
],
[
"2020-09-30",
"175.55"
],
[
"2020-10-19",
"175.55",
"0.00"
],
[
"2020-12-01",
"0",
"17.55"
],
[
"2020-12-29",
"0",
"17.55"
],
[
"2021-01-20",
"175.55",
"0"
],
[
"2021-02-15",
"0.00",
"17.55"
],
[
"2021-03-12",
"175.55",
"0.00"
],
[
"2021-04-15",
"0",
"17.55"
],
[
"2021-05-17",
"0",
"17.55"
],
[
"2021-06-30",
"0",
"0.00"
],
[
"2021-07-29",
"175.55",
"17.55"
],
[
"2021-08-09",
"175.55",
"0"
],
[
"2021-08-10",
"175.55",
"0",
"extra"
],
[
"2021-09-03",
"175.55",
"0.00",
"False"
],
[
"2021-09-17",
"175.55",
"0.00",
"False"
],
[
"2021-09-18",
"0",
"0.00",
"False"
]
],
"borrower": {
"city": "Alexandria",
"state": "VA",
"name": "Bear Houses, LLC",
"zip": "22312",
"address": "301 N Beauregard St Apt 203"
},
"txtTemplate": "statement.txt.jinja"
}

View File

@@ -1,130 +0,0 @@
import json
from decimal import *
from datetime import *
# this program reads the json file describing a mortgage and calculate the actual and projected amortization
# the first payment in the file should be the interest paid at closing
# the interest at closing covers interest that would be incurred from the date of closing until the first day
# that will be covered by the first loan payment
# i.e., the first loan payment is expected a month and a half after closing, the first two weeks interest is due
# at closing. The first payment will incur interest from one month before the bill is due.
#read in the file
#filename = "./10Kloan.txt"
filename = "./dadmortgage.txt"
#filename = "./brendamortgage.txt"
#filename = "./greenfield_mortgage.txt"
if filename:
with open(filename, 'r') as f:
datastore = json.load(f)
#read in the loan profile information
annual_rate = Decimal(datastore['interest rate'])/100
daily_interest_rate = annual_rate/360
principal = Decimal(datastore["principal"]).quantize(Decimal("1.00"))
periods_per_year = Decimal(datastore["periods per year"])
total_periods = Decimal(datastore["periods"])
payment_day_of_month = int(datastore['payment day of month'])
if "monthly_payment" in datastore:
monthly_payment = Decimal(datastore["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"))
print "Principal: ", principal
print "Interest Rate: ", annual_rate
print "Payments Per Year: ", periods_per_year
print "Loan Term: ", total_periods
print "Monthly Payment", monthly_payment
print "Payments due day: ", payment_day_of_month
print
#loop over the payments and calculate the actual amortization
actual_payments = datastore["payments"]
remaining_principal = principal
annual_interest = 0
total_interest = 0
interest_paid_through_date = datetime.strptime(datastore["start_interest_date"], "%Y-%m-%d").date()
next_payment_date = datetime.strptime(datastore["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)
old_bill_date = next_bill_date
payment_number = 1
current_year = next_bill_date.year
print "Payment History:"
print "\tBill\t\t\tPayment\tDays of\tPayment\t\tRemaining\t\tPrincipal\tInterest\t\t\tNew"
print "#\tDate\t\t\tDate\tInterst\tAmount\t\tPrincipal\t\tPayment\t\tPayment\t\t\tBalance"
for payment in actual_payments:
payment_date = datetime.strptime((payment[0]), '%Y-%m-%d').date()
payment_amount = Decimal(payment[1]).quantize(Decimal("1.00"))
#print next_bill_date, "\t", next_payment_date, "\t",
days_since_last_payment = (payment_date-interest_paid_through_date).days
#print days_since_last_payment
new_interest = (days_since_last_payment * remaining_principal * daily_interest_rate).quantize(Decimal("0.00"))
# new_principal = monthly_payment - new_interest
new_principal = payment_amount - new_interest
print payment_number, " ", next_bill_date, "\t\t", payment_date, "\t", days_since_last_payment, "\t", payment_amount, "\t\t\t", remaining_principal, "\t\t", new_principal,\
"\t\t", new_interest,
interest_paid_through_date = payment_date
total_interest = total_interest + new_interest
annual_interest=annual_interest+new_interest
remaining_principal = remaining_principal - new_principal
print "\t\t\t", remaining_principal
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)
print "Total interest paid to date for", old_bill_date.year, " is", annual_interest
else:
next_bill_date = date(year=old_bill_date.year+1, month = 1, day=payment_day_of_month)
print "Total interest for ", old_bill_date.year, " was ", annual_interest
annual_interest = 0
print "Total interest paid to date on this loan is", total_interest
#loop over remaining scheduled payments and present estimated amortization
print "\nEstimated future amortization"
while (payment_number <= total_periods) and (remaining_principal > 0):
print payment_number, "\t",
payment_number = payment_number+1
print next_bill_date,
days_since_last_payment = (next_bill_date-interest_paid_through_date).days
print days_since_last_payment, "\t",
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
print monthly_payment, "\t\t\t", remaining_principal, "\t\t", new_principal,\
"\t\t", new_interest,
remaining_principal = remaining_principal - new_principal
print "\t", remaining_principal
interest_paid_through_date = next_bill_date
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)
if remaining_principal > 0:
print 'Balloon payment due:', remaining_principal

View File

@@ -0,0 +1,68 @@
'''''''take in the csv file of transactions'''
import datetime
from decimal import *
import csv
from calendar import monthrange
def main():
getcontext().prec=10
open_balance = Decimal("-24521.81")
interest_rate = Decimal("8.00")/Decimal("100.00")/Decimal("365.00")
year = 2021
start_date = datetime.date(year, 1, 1)
end_date = datetime.date(year, 12, 31)+datetime.timedelta(1)
days_in_year = (end_date - start_date).days
print ("Days in year: %d" % days_in_year)
balances = [Decimal('0.00')] * days_in_year
interest_invoices = [Decimal('0.00')] * 12
with open("/Users/john/Downloads/query_result.csv") as f:
reader = csv.DictReader(f)
for row in reader:
print(row)
year = row['year']
month = row['month']
day = row['day']
amount = row['position (USD)']
date_val = datetime.date(int(year), int(month), int(day))
day_of_year = date_val.toordinal() - datetime.date(date_val.year, 1, 1).toordinal() + 1
print("Record created for day of year %d" % day_of_year)
print("%s - %s - %s: %s " % (year, month, day, amount))
balances[day_of_year] = balances[day_of_year] + Decimal(amount)
current_balance = open_balance
day_being_processed = 1
accumlated_interest = Decimal('0.000000')
while (day_being_processed <= days_in_year):
print("Day of year: %d" % day_being_processed)
#calculate the day's balance
current_balance = current_balance + balances[day_being_processed-1]
balances[day_being_processed-1] = current_balance
#accummulate the day's interest
todays_interest = current_balance * interest_rate
accumlated_interest = accumlated_interest + todays_interest
#check if this is the last day of the month, if it is do the transition stuff
current_month = datetime.date.fromordinal(start_date.toordinal()+day_being_processed-1).month
next_month = datetime.date.fromordinal(start_date.toordinal()+day_being_processed).month
print("Current Month: %d Next_month: %d" % (current_month, next_month ))
if (current_month != next_month):
interest_invoices[current_month-1] = accumlated_interest.quantize(Decimal('.01'))
current_balance = current_balance + accumlated_interest
accumlated_interest = Decimal('0.00')
day_being_processed = day_being_processed + 1
i=0
while (i<12):
print ("%d: %s" %(i+1, interest_invoices[i]) )
i = i +1
if __name__ == '__main__':
main()

View File

@@ -1,345 +0,0 @@
import json
from decimal import *
from datetime import *
from jinja2 import Environment, FileSystemLoader
from fpdf import FPDF, HTMLMixin
import smtplib
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
from email.MIMEBase import MIMEBase
from email import Encoders
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
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
#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
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
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
past_payments.append(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:
annual_interest = Decimal("0.00")
next_bill_date = date(year=old_bill_date.year + 1, month=1, day = payment_day_of_month)
loan["total_interest_paid_to_date"] = total_interest
loan["parameters"]["next_due_date"] = next_bill_date
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
def transformTemplate(template_fileName, loanModel):
# template_filename = "statement.text.jinja"
# setup jinja for creating the statement
env = Environment(loader=FileSystemLoader('.'))
template = env.get_template(template_fileName)
print loanModel
report = template.render(model=loanModel)
return report
def generatePDFAttachment():
template_filename = "statement.pdf.jinja"
pass
def generateEmail(from_address, to_address, subject, body, pdfAttachment, txtAttachment):
msg = MIMEMultipart()
msg['Subject'] = subject
msg['From'] = from_address
msg['To'] = to_address
msg.attach(MIMEText(body))
if (pdfAttachment != None):
part = MIMEBase("application", "octet-stream")
part.set_payload(pdfAttachment)
Encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment; filename="statement.pdf"')
msg.attach(part)
if (txtAttachment != None):
part = MIMEBase("text", "html")
part.set_payload(txtAttachment)
Encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment; filename="statement.html"')
msg.attach(part)
return msg
def generateEmailWithAttachments(from_address, to_address, subject, body, pdfAttachment, htmlAttachment, txtAttachment):
msg = MIMEMultipart()
msg['Subject'] = subject
msg['From'] = from_address
msg['To'] = to_address
msg.attach(MIMEText(body))
if (pdfAttachment != None):
part = MIMEBase("application", "octet-stream")
part.set_payload(pdfAttachment)
Encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment; filename="statement.pdf"')
msg.attach(part)
if (txtAttachment != None):
part = MIMEBase("text", "html")
part.set_payload(txtAttachment)
Encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment; filename="statement.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)
# pdf.output(name='test.pdf', dest='F')
return pdf.output(dest='S')
def selectTemplate(loan):
templateKey = loan["datastore"]["format"] + "Template"
if templateKey in loan:
template = loan[templateKey]
else:
template = 'statement.pdf.jinja'
return template
def main():
test_flag = True
# this program reads the json file describing a mortgage and calculate the actual and projected amortization
# the first payment in the file should be the interest paid at closing
# the interest at closing covers interest that would be incurred from the date of closing until the first day
# that will be covered by the first loan payment
# i.e., the first loan payment is expected a month and a half after closing, the first two weeks interest is due
# at closing. The first payment will incur interest from one month before the bill is due.
# read in the file
test_flag = False
test_address = 'jkent3rd@yahoo.com'
#filename = "./testloan.json"
filename = "./9Kloan.json"
#filename = "./dadmortgage.json"
#filename = "./brendamortgage.json"
#filename = "./greenfield_mortgage.json"
template_filename = "statement.pdf.jinja"
loan = loadLoanInformation(filename)
amortizeLoan(loan)
report = transformTemplate(selectTemplate(loan), loan)
if loan["email"]["send_pdf"] == "true":
pdfAttachment = createPDF(report)
# send email
emailParameters = loan["email"]
msg = generateEmail(emailParameters["from_address"], emailParameters["to_address"], emailParameters["subject"],
emailParameters["body"], pdfAttachment, report)
if test_flag == False:
sendEmail(msg, emailParameters["from_address"], emailParameters["to_address"], emailParameters['password'])
else:
sendEmail(msg, emailParameters["from_address"], test_address, emailParameters['password'])
if __name__ == '__main__':
main()

View File

@@ -1,7 +1,6 @@
<html> <html>
<head> <head>
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css"> <link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<link rel="stylesheet" href="/resources/demos/style.css">
<script src="https://code.jquery.com/jquery-1.12.4.js"></script> <script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script> <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script> <script>
@@ -13,7 +12,7 @@
<p id="header" align="center"><font face="Arial" size=+2 >Web Mortgage Manager</font></p> <p id="header" align="center"><font face="Arial" size=+2 >Web Mortgage Manager</font></p>
<form> <form>
<table><tr> <table><tr>
<td>Loan:</td> <td><font face="Arial">Loan:</font></td>
<td> <td>
<select name="loan" id="loan"> <select name="loan" id="loan">
{% for loan in loans %} {% for loan in loans %}
@@ -25,6 +24,7 @@
{% endfor %} {% endfor %}
</select> </select>
<button>Select</button> <button>Select</button>
<button id='manageLoans'>Manage</button>
</td> </td>
</table> </table>
</form> </form>
@@ -45,6 +45,7 @@
<li><a href="#loan_amortization">Future Amortization</a></li> <li><a href="#loan_amortization">Future Amortization</a></li>
<li><a href="#email">Email Statement</a></li> <li><a href="#email">Email Statement</a></li>
<li><a href="#add_payment">Add Payment</a></li> <li><a href="#add_payment">Add Payment</a></li>
<li><a href="#add_extra_payment">Add Payment</a></li>
</ul> </ul>
<div id="loan_information"> <div id="loan_information">
<table align="center" border="1px"> <table align="center" border="1px">
@@ -176,10 +177,25 @@
<tr><td><input type="text" name="amount"></td></tr> <tr><td><input type="text" name="amount"></td></tr>
<tr><td bgcolor="beige">Late Fee Amount:</td></tr> <tr><td bgcolor="beige">Late Fee Amount:</td></tr>
<tr><td><input type="text" name="latefee"></td></tr> <tr><td><input type="text" name="latefee"></td></tr>
<tr><td><input name="notify" type="checkbox" value="notify"/>Send Payment Recorded Notification</td></tr>
<tr><td><button>Record Payment</button></td></tr> <tr><td><button>Record Payment</button></td></tr>
</table> </table>
</form> </form>
</div> </div>
<div id="add_extra_payment">
<form name="AddExtraPayment" method="post" action="/update_file">
<input type="hidden" name="loan" value="{{filename}}" />
<table align="center" border="1px" width="75%">
<tr><td align="center" bgcolor="darkgrey">Record an Extra Payment</td></tr>
<tr><td bgcolor="beige">Payment Date:</td></tr>
<tr><td><input type="date" name="date"</td></tr>
<tr><td bgcolor="beige">Payment Amount:</td></tr>
<tr><td><input type="text" name="amount"></td></tr>
<tr><td><input name="notify" type="checkbox" value="notify"/>Send Payment Recorded Notification</td></tr>
<tr><td><button>Record Extra Payment</button></td></tr>
</table>
</form>
</div>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
Date: {{model.payment.today}}<br/>
From: {{model.payment.payee}}<br/>
To: {{model.payment.payer}}<br/>
RE: Payment Received<br/>
<br/>&nbsp;<br/>
You are receiving this email to confirm receipt of your recent loan payment.<br/>
<br/>&nbsp;<br/>
Amount Received: {{ "$%.2f"|format(model.payment.amount) }}<br/>
Date Received: {{model.payment.date}}<br/>
{% if model.payment.late_fee != 0 %}
Late Fee Assessed: {{model.payment.late_fee}}<br/>
{% endif %}
<br/>&nbsp;<br/>
Thank you!<br/>
</body>
</html>

View File

@@ -1,51 +1,36 @@
Original Principal Balance: {{ original_principal_balance }} {{ 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 Payment History
#\tBill Date #,Due Date,Date Paid,Days Interest,Payment Amt,Principal Pmt,Interest Pmt,Late Fee,New Balance
Payment Date {% for item in model.past_payments %}
Days of Interest {{ 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) }}
Payment Amount {% if item.month == 12 or loop.last %}
Principal Payment Total interest paid in {{item.year}} is {{ "$%.2f"|format(item.annual_interest_to_date) }}.
Interest Payment {% endif %}
Late Fee
New Balance
{% for item in past_payments %}
{{ item.payment_number }}
{{ item.bill_date }}
{{ item.payment_date }}
{{ item.days_of_interest }}
{{ item.payment_amount }}
{{ item.principal_payment }}
{{ item.interest_payment }}
{{ item.late_fee }}
{{ item.new_balance }}
{% if item.print_interest_total %}
{{ item.interest_total_message }}
{% endif %}
{% endfor %} {% endfor %}
Total interest paid to date is {{ total_interest_paid_to_date }}. Total interest paid to date is {{ "$%.2f"|format(model.total_interest_paid_to_date) }}.
Remaining Amortization Remaining Amortization
# #,Due Date,Days Interest,Payment Amt,Principal Pmt,Interest Pmt,Principal Balance<
Days of Interest {% for item in model.future_payments %}
Due Date {{ 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) }}
Payment Amount
Scheduled Principal Payment
Scheduled Interest Payment
Expected New Balance
{% for item in future_payments %}
{{ item.payment_number }}
{{ item.days_of_interest }}
{{ item.payment_date }}
{{ item.payment_amount }}
{{ item.principal_payment }}
{{ item.interest_payment }}
{{ item.new_balance }}
{% endfor %} {% endfor %}
Balloon Payment Due: {{ "$%.2f"|format(model.balloon_payment) }}
Balloon Payment Due: {{ balloon_payment }}

View File

@@ -1,31 +1,32 @@
from flask import Flask, render_template, request, redirect
import json import json
from decimal import *
from datetime import *
import jinja2 import jinja2
from fpdf import FPDF, HTMLMixin
import smtplib import smtplib
import os import os
from flask import Flask, render_template, request, redirect
loader=jinja2.FileSystemLoader([os.path.join(os.path.dirname(__file__),"templates")]) from decimal import *
environment = jinja2.Environment(loader=loader) from datetime import *
from fpdf import FPDF, HTML2FPDF, HTMLMixin
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.mime.base import MIMEBase from email.mime.base import MIMEBase
from email import encoders 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__) app = Flask(__name__)
######
# Flask Call Backs
######
@app.route('/') @app.route('/')
def hello(): def hello():
loans = getLoanFiles(app_directory)
loans = [] #if a loan was not specified, choose the first loan and reload page with it
loans.append( addLoan("1 Greenfield Lane Mortgage", "greenfield_mortgage.json" ) )
loans.append( addLoan("2 Bear Houses LLC Loan", "9Kloan.json" ) )
loans.append( addLoan("3 Brenda Mortgage", "brendamortgage.json" ) )
loans.append( addLoan("4 Dad Mortgage", "dadmortgage.json") )
loans.append( addLoan("5 Test Loan", "testloan.json"))
if 'loan' in request.args: if 'loan' in request.args:
filename = request.args["loan"] filename = request.args["loan"]
else: else:
@@ -36,42 +37,56 @@ def hello():
return render_template('main.html', filename=filename, loans=loans, model=loan) return render_template('main.html', filename=filename, loans=loans, model=loan)
@app.route('/update_file', methods=['POST']) @app.route('/update_file', methods=['POST'])
def update_file(): def update_file():
messages = [] messages = []
loanFile = request.form["loan"] loanFile = request.form["loan"]
data = getDatastore(getFullPathofLoanFile(loanFile)) 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"] payment_history = data["payments"]
todays_date = str(datetime.now().strftime("%Y-%m-%d"))
if 'date' in request.form: if 'date' in request.form:
if (request.form['date'] == ''): if request.form['date'] == '':
now = datetime.now() payment_date = todays_date
payment_date = str(now.strftime("%Y-%m-%d")) messages.append("No date was provided. Assuming today's date of " + payment_date + ".")
messages.append("No date was provided. Assuming today's date of " + payment_date + ".") else:
else: payment_date = request.form['date']
payment_date = request.form['date'] messages.append("Date provided: " + payment_date + ".")
messages.append("Date provided: " + payment_date + ".")
else: else:
now = datetime.now() payment_date = todays_date
payment_date = str(now.strftime("%Y-%m-%d")) messages.append("No date was provided. Assuming today's date of " + payment_date + ".")
payment_amount = Decimal('0.00')
proceed_flag = True proceed_flag = True
if 'amount' in request.form: if 'amount' in request.form:
if (request.form['amount'] != ''): if request.form['amount'] != '':
try: try:
payment_amount = Decimal(request.form['amount']) payment_amount = Decimal(request.form['amount'])
messages.append("Amount provided: " + str(payment_amount) + ".") messages.append("Amount provided: " + str(payment_amount) + ".")
except: except:
payment_amount = Decimal('0.00')
messages.append("The amount provided could not be interpreted. Your payment was not recorded.") messages.append("The amount provided could not be interpreted. Your payment was not recorded.")
proceed_flag = False proceed_flag = False
else: else:
pass 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: if proceed_flag is True:
try: try:
backup_filename = loanFile + ".backup-" + datetime.now().strftime("%Y-%m-%d %H-%M-%S") + ".json" backup_filename = loanFile + ".backup-" + datetime.now().strftime("%Y-%m-%d %H-%M-%S") + ".json"
@@ -82,22 +97,47 @@ def update_file():
messages.append("A backup file could not be created. Your payment was not recorded.") messages.append("A backup file could not be created. Your payment was not recorded.")
proceed_flag = False proceed_flag = False
else: else:
messages.append("A backup of your file was created: '" + backup_filename +"'" ) messages.append("A backup of your file was created: '" + backup_filename + "'")
if proceed_flag is True: if proceed_flag is True:
try: try:
payment_history.append( [payment_date, str(payment_amount)]) payment_history.append([payment_date, str(payment_amount), str(late_fee), str(extra_payment)])
file = open(getFullPathofLoanFile(loanFile), 'w+') file = open(getFullPathofLoanFile(loanFile), 'w+')
json.dump(data, file, indent=2) json.dump(data, file, indent=2)
file.close() file.close()
except: except:
messages.append("An error occurred writing to the file. Your payment file may be corrupt, " + \ messages.append("An error occurred writing to the file. Your payment file may be corrupt, " +
"please consider rolling back to the backup created above.") "please consider rolling back to the backup created above.")
else: else:
messages.append("The payment was successfully written. ") 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) return render_template('add.html', filename=loanFile, messages=messages)
@app.route('/send_statement', methods=['POST']) @app.route('/send_statement', methods=['POST'])
def send_statement(): def send_statement():
loanFile = request.form["loan"] loanFile = request.form["loan"]
@@ -122,32 +162,67 @@ def send_statement():
if ('html' in request.form) or (reportCreated is False): if ('html' in request.form) or (reportCreated is False):
htmlReport = transformTemplate(selectTemplate('html'), loan) htmlReport = transformTemplate(selectTemplate('html'), loan)
# send email # send email
emailParameters = loan["email"] emailParameters = loan["email"]
msg = generateEmail( emailParameters["from_address"], msg = generateStatementEmail(emailParameters["from_address"],
emailParameters["to_address"], emailParameters["to_address"],
subject, subject,
message, pdfReport, htmlReport, textReport) message, pdfReport, htmlReport, textReport)
sendEmail(msg, emailParameters["from_address"], emailParameters["to_address"], emailParameters['password']) sendEmail(msg, emailParameters["from_address"], emailParameters["to_address"], emailParameters['password'])
return render_template('email.html', filename=loanFile) return render_template('email.html', filename=loanFile)
def addLoan(loanName, fileName):
######
# Loan File Functions
######
def getLoanFiles(directory):
loans = []
count = 0
directory = os.path.normpath(directory)
print("Directory {}".format(directory))
fileNames = os.listdir(directory)
fileNames.sort()
for filename in fileNames:
if filename.endswith(".loan"):
displayName = filename.removesuffix(".loan").replace("_"," ")
loans.append(createLoanEntryMenuItem("{0}".format(displayName), filename))
#loans.append(createLoanEntryMenuItem("1 Brenda Mortgage", "brendamortgage.json"))
#loans.append(createLoanEntryMenuItem("2 Dad Mortgage", "Dad-Mortgage.loan"))
#loans.append(createLoanEntryMenuItem("3 839 Harbor Bend Loan 1", "HarborBend839-2022.json"))
#loans.append(createLoanEntryMenuItem("--ARCHIVE-- Bear Houses LLC Loan", "9Kloan.loan"))
#loans.append(createLoanEntryMenuItem("--ARCHIVE-- Test Loan", "testloan.json"))
#loans.append(createLoanEntryMenuItem("--ARCHIVE-- Greenfield Lane Mortgage", "greenfield_mortgage.json"))
#loans.append(createLoanEntryMenuItem("--ARCHIVE-- Greenfield Lane Refinance", "greenfield_refinance.json"))
#loans.append(createLoanEntryMenuItem("--ARCHIVE-- Harbor Bend Loan 1", "BearHousesLLC-22-0001.json"))
#loans.append(createLoanEntryMenuItem("--ARCHIVE-- Wayneland Dr D11 Mortgage", "wayneland_mortgage.json"))
return loans
def createLoanEntryMenuItem(loanName, fileName):
x = {} x = {}
x['name'] = loanName x['name'] = loanName
x['filename'] = fileName x['filename'] = fileName
return x return x
def getFullPathofLoanFile(filename):
return '/Users/john/PycharmProjects/mortgage/' + filename
################### def getFullPathofLoanFile(filename):
# from old code # 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): def getStatementHeader(datastore):
return datastore['header'] return datastore['header']
@@ -179,7 +254,7 @@ def getLoanParameters(datastore):
principal = Decimal(loan["principal"]).quantize(Decimal("1.00")) principal = Decimal(loan["principal"]).quantize(Decimal("1.00"))
periods_per_year = Decimal(loan["periods_per_year"]) periods_per_year = Decimal(loan["periods_per_year"])
total_periods = Decimal(loan["periods"]) total_periods = Decimal(loan["periods"])
payment_day_of_month = int(loan['payment_day_of_month']) #payment_day_of_month = int(loan['payment_day_of_month'])
if "monthly_payment" in loan: if "monthly_payment" in loan:
monthly_payment = Decimal(loan["monthly_payment"]).quantize(Decimal("1.00")) monthly_payment = Decimal(loan["monthly_payment"]).quantize(Decimal("1.00"))
@@ -190,8 +265,8 @@ def getLoanParameters(datastore):
periodic_rate * ((1 + periodic_rate) ** total_periods)) periodic_rate * ((1 + periodic_rate) ** total_periods))
monthly_payment = (principal / discount_factor).quantize(Decimal("1.00")) monthly_payment = (principal / discount_factor).quantize(Decimal("1.00"))
loan['principal'] = principal # standardizes the format loan['principal'] = principal # standardizes the format
loan['annual_rate'] = annual_rate # standardizes the format loan['annual_rate'] = annual_rate # standardizes the format
loan['daily_interest_rate'] = daily_interest_rate loan['daily_interest_rate'] = daily_interest_rate
loan['rate'] = '' + (annual_rate * 100).__str__() + '%' loan['rate'] = '' + (annual_rate * 100).__str__() + '%'
loan['next_payment_amt'] = 0 loan['next_payment_amt'] = 0
@@ -224,6 +299,10 @@ def getDatastore(filename=None):
return datastore return datastore
######
# Loan Calculation Functions
######
def amortizeLoan(loan): def amortizeLoan(loan):
# loop over the payments and calculate the actual amortization # loop over the payments and calculate the actual amortization
@@ -242,8 +321,8 @@ def amortizeLoan(loan):
payment_number = 1 payment_number = 1
annual_interest = 0 annual_interest = 0
total_interest = 0 total_interest = 0
old_bill_date = next_bill_date #old_bill_date = next_bill_date
current_year = next_bill_date.year #current_year = next_bill_date.year
past_payments = [] past_payments = []
future_payments = [] future_payments = []
@@ -252,15 +331,16 @@ def amortizeLoan(loan):
payment_date = datetime.strptime((payment[0]), '%Y-%m-%d').date() payment_date = datetime.strptime((payment[0]), '%Y-%m-%d').date()
payment_amount = Decimal(payment[1]).quantize(Decimal("1.00")) payment_amount = Decimal(payment[1]).quantize(Decimal("1.00"))
days_since_last_payment = (payment_date - interest_paid_through_date).days days_since_last_payment = (payment_date - interest_paid_through_date).days
if len(payment)>2: if len(payment) > 2:
late_fee = Decimal(payment[2]).quantize(Decimal("1.00")) late_fee = Decimal(payment[2]).quantize(Decimal("1.00"))
else: else:
late_fee = Decimal("0.00") late_fee = Decimal("0.00")
#check for out of order payments, generally a sign of a data entry problem, especially years # check for out of order payments, generally a sign of a data entry problem, especially years
if days_since_last_payment < 0: 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'." \ print(
% (payment_number, payment_date, interest_paid_through_date)) "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() quit()
new_interest = (days_since_last_payment * remaining_principal * daily_interest_rate).quantize(Decimal("0.00")) new_interest = (days_since_last_payment * remaining_principal * daily_interest_rate).quantize(Decimal("0.00"))
@@ -270,11 +350,10 @@ def amortizeLoan(loan):
annual_interest = annual_interest + new_interest annual_interest = annual_interest + new_interest
remaining_principal = remaining_principal - new_principal remaining_principal = remaining_principal - new_principal
# create the payment record for the template to render # create the payment record for the template to render
payment_record = {} payment_record = {}
payment_record['year']=next_bill_date.year payment_record['year'] = next_bill_date.year
payment_record['month']=next_bill_date.month payment_record['month'] = next_bill_date.month
payment_record['payment_number'] = payment_number payment_record['payment_number'] = payment_number
payment_record['bill_date'] = next_bill_date payment_record['bill_date'] = next_bill_date
payment_record['payment_date'] = payment_date payment_record['payment_date'] = payment_date
@@ -289,16 +368,24 @@ def amortizeLoan(loan):
past_payments.append(payment_record) past_payments.append(payment_record)
payment_number = payment_number + 1 payment_number = payment_number + 1
old_bill_date = next_bill_date
if old_bill_date.month < 12: print("Payment record length: " + str(len(payment)))
next_bill_date = date(year=old_bill_date.year, month=old_bill_date.month + 1, day=payment_day_of_month) #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: else:
annual_interest = Decimal("0.00") old_bill_date = next_bill_date
next_bill_date = date(year=old_bill_date.year + 1, month=1, day = payment_day_of_month)
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 loan["total_interest_paid_to_date"] = total_interest
loan["parameters"]["next_due_date"] = next_bill_date
if (remaining_principal < monthly_payment): if (remaining_principal < monthly_payment):
loan["parameters"]["next_payment_amt"] = remaining_principal loan["parameters"]["next_payment_amt"] = remaining_principal
@@ -338,12 +425,17 @@ def amortizeLoan(loan):
else: else:
next_bill_date = date(year=old_bill_date.year + 1, month=1, day=payment_day_of_month) next_bill_date = date(year=old_bill_date.year + 1, month=1, day=payment_day_of_month)
loan["balloon_payment"] = remaining_principal loan["balloon_payment"] = remaining_principal
loan["past_payments"] = past_payments loan["past_payments"] = past_payments
loan["future_payments"] = future_payments loan["future_payments"] = future_payments
return return
######
# Report Generation Functions
######
def transformTemplate(template_fileName, loanModel): def transformTemplate(template_fileName, loanModel):
# template_filename = "statement.text.jinja" # template_filename = "statement.text.jinja"
# setup jinja for creating the statement # setup jinja for creating the statement
@@ -354,7 +446,7 @@ def transformTemplate(template_fileName, loanModel):
return report return report
def generateEmail(from_address, to_address, subject, body, pdf, html, txt): def generateStatementEmail(from_address, to_address, subject, body, pdf, html, txt):
msg = MIMEMultipart() msg = MIMEMultipart()
msg['Subject'] = subject msg['Subject'] = subject
msg['From'] = from_address msg['From'] = from_address
@@ -362,21 +454,21 @@ def generateEmail(from_address, to_address, subject, body, pdf, html, txt):
msg.attach(MIMEText(body)) msg.attach(MIMEText(body))
if (pdf != None): if pdf is not None:
part = MIMEBase("application", "octet-stream") part = MIMEBase("application", "octet-stream")
part.set_payload(pdf) part.set_payload(pdf)
encoders.encode_base64(part) encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment; filename="statement.pdf"') part.add_header('Content-Disposition', 'attachment; filename="statement.pdf"')
msg.attach(part) msg.attach(part)
if (html != None): if html is not None:
part = MIMEBase("text", "html") part = MIMEBase("text", "html")
part.set_payload(html) part.set_payload(html)
encoders.encode_base64(part) encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment; filename="statement.html"') part.add_header('Content-Disposition', 'attachment; filename="statement.html"')
msg.attach(part) msg.attach(part)
if (txt != None): if txt is not None:
part = MIMEBase("text", "plain") part = MIMEBase("text", "plain")
part.set_payload(txt) part.set_payload(txt)
encoders.encode_base64(part) encoders.encode_base64(part)
@@ -386,6 +478,22 @@ def generateEmail(from_address, to_address, subject, body, pdf, html, txt):
return msg 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): def sendEmail(msg, from_address, to_address, passwd):
try: try:
server = smtplib.SMTP('smtp.gmail.com', 587) server = smtplib.SMTP('smtp.gmail.com', 587)
@@ -401,18 +509,20 @@ def sendEmail(msg, from_address, to_address, passwd):
def createPDF(report): def createPDF(report):
# create pdf # create pdf
class MyFPDF(FPDF, HTMLMixin): class PDF(FPDF, HTMLMixin):
pass pass
pdf = MyFPDF() pdf = PDF()
pdf.set_font(family='Arial', size=12) pdf.set_font(family='Arial', size=12)
pdf.add_page() pdf.add_page()
pdf.write_html(report) pdf.write_html(report)
# pdf.output(name='test.pdf', dest='F')
return pdf.output(dest='S') return pdf.output(dest='S')
def selectTemplate(format): def selectTemplate(format):
if format == 'paymentNotification':
return 'payment_received_email.html.jinja'
if format == 'html': if format == 'html':
return 'statement.html.jinja' return 'statement.html.jinja'
@@ -424,9 +534,11 @@ def selectTemplate(format):
return 'statement.html.jinja' return 'statement.html.jinja'
def main(): def main():
app.debug = True app.debug = True
app.run() app.run()
if __name__ == '__main__': if __name__ == '__main__':
main() main()

544
mortgage/web_ng.py Normal file
View File

@@ -0,0 +1,544 @@
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()

3
pyvenv.cfg Normal file
View File

@@ -0,0 +1,3 @@
home = /opt/local/bin
include-system-site-packages = false
version = 3.10.7

View File

@@ -1,3 +1,3 @@
flask flask
fpdf fpdf2
jinja2 jinja2