Compare commits

...

39 Commits

Author SHA1 Message Date
cfc7e6490f Merge branch 'master'
Converting the historical mercurial repository and merging into the new Gitea repository.
2025-09-11 12:50:47 -04:00
john
b5645e2492 Updated files to produce current daily interest accrual information. Fixed calculation bug. Added current balance to loan information screen. 2024-05-20 18:15:36 -04:00
john
d6bd844d5d Updated files to produce current daily interest accrual information. 2024-05-20 17:54:42 -04:00
john
d9d62a09b5 Updated files to produce accounting information. 2023-09-04 15:42:32 -04:00
john
cf2748c007 Updated files to produce accounting information. 2023-09-04 15:30:32 -04:00
john
9d74519c3e Updated files to produce accounting information. 2023-09-04 14:56:34 -04:00
john
3757989fcc Updated Dockerfile and removed web_ng.py
user: john
branch 'default'
changed Dockerfile
removed mortgage/web_ng.py
2023-05-21 14:20:19 +00:00
john
a761126148 Working issues with couchdb access in docker container. Set container to listen on all interfaces, not just localhost. 2023-05-20 14:34:13 -04:00
john
afa799479e Working issues with couchdb access in docker container. 2023-05-16 20:26:43 -04:00
john
94b1e97f4d Updated program to take database connection information from environment variables. 2023-05-14 11:39:19 -04:00
john
5eb42e2030 Found out the requirements were out of date. Recreated from pip freeze.
user: john
branch 'default'
changed .hgignore
2023-05-14 10:55:15 -04:00
john
5f5d025d72 Cleaning up what is kept in repository so that its not copied to server.
user: john
branch 'default'
changed .hgignore
2023-05-14 10:37:32 -04:00
john
c76cec1a73 Updated ignore file to include __pycache__/ directory.
user: john
branch 'default'
changed .hgignore
2023-05-14 10:25:35 -04:00
john
5930ad5af1 Additional changes to move application to CouchDB database backend. Removed loan files from repository (now in selfhosted couchdb instance). 2023-05-14 10:19:04 -04:00
john
c6291c3ab9 Starting the process of migrating to a Docker container. 2023-04-05 13:57:55 -04:00
john
6261ff7895 Creating new baseline. This will be the starting point for the updates. 2023-04-02 23:45:26 -04:00
john
bf3e789a25 Cleaning up... 2022-10-03 00:09:12 -04:00
john
466d2dce8b :G: Enter commit message. Lines beginning with 'HG:' are removed. 2022-10-03 00:02:12 -04:00
john
3ccd81cf76 Added a requirements.txt to support dockerfile deployment. 2021-01-16 14:19:14 -05:00
JohnKent
e5830536a1 Added late fee logic to calculation logic and the templates.
user: JohnKent
branch 'default'
added .hgignore
changed mortgage/templates/main.html
changed mortgage/templates/statement.html.jinja
changed mortgage/templates/statement.pdf.jinja
changed mortgage/templates/statement.text.jinja
changed mortgage/web.py
2020-06-23 22:03:38 -04:00
JohnKent
219c1ada2f Data file updates. 2020-06-23 22:00:23 -04:00
JohnKent
3ffbd5ab3d Created a source directly for purposes of deployment as zip file.
Updated to Python 3.
2020-01-26 14:02:44 -05:00
JohnKent
8bc23dc8b1 Fixed a problem in email sending when attach/embed flag was removed. 2019-07-10 23:12:21 -04:00
JohnKent
af033464ad The tool can now update payment history. 2019-07-10 22:57:55 -04:00
JohnKent
717210636d The tool can now update the payment history. 2019-07-10 22:57:23 -04:00
JohnKent
e9fefb1a1a Web version updated to allow sending emails. 2019-07-08 00:27:38 -04:00
JohnKent
a304869708 At this state, it produces a nice web-based viewer of the loan files including the payment history and future amortization. 2019-07-07 14:59:33 -04:00
JohnKent
27cf64aced Updated Brenda's contact information which has been wrong. 2019-07-07 14:58:20 -04:00
JohnKent
6462ae5628 Added options for the new 9K loan from Rivanna Graphite Investments, LLC. Removed the personal 10K loan I made to Bear Houses, LLC 2019-07-06 14:58:43 -04:00
JohnKent
8b2af86db9 Updated the payment history. 2019-07-06 14:50:59 -04:00
JohnKent
56c0f333f6 Updated the payment history. 2019-06-02 18:35:16 -04:00
JohnKent
8cb4be3248 Updated the payment history for April statements. 2019-04-03 14:06:02 -04:00
JohnKent
42232903c5 Updated the payment history. 2019-03-05 00:10:22 -05:00
JohnKent
4dae3256c5 Updated the template to print the correct interest information. Fixed logic in script to send correct interest information to the template. Added ability to do a test run to a different email address with a real data file (the debug flag replaces the email address with a hard coded one). Added an aborting checking that the payments are listed in order. 2019-03-05 00:09:56 -05:00
JohnKent
f5c78ca7dc Added this file for testing of email function. 2019-01-13 14:54:30 -05:00
JohnKent
7612f854d7 Fixed the program to print the annual interest paid. 2019-01-13 14:53:53 -05:00
JohnKent
45d6e32149 Updated all of the payment files to reflect new format. Updated the PDF generation template. Updated the email sending code. Fixed a few off by one errors when the last payment also is the last payment of the year. 2019-01-06 16:39:54 -05:00
JohnKent
e274b96672 Fixing references. 2018-12-23 01:05:36 -05:00
JohnKent
53327a9e09 Initial commit. Things dont work yet.
--
user: JohnKent
branch 'default'
added greenfield_mortgage.txt
added mortgage.py
added mortgage_template.py
added statement.pdf.jinja
added statement.txt.jinja
2018-12-19 23:09:04 -05:00
11 changed files with 1060 additions and 0 deletions

7
.hgignore Normal file
View File

@@ -0,0 +1,7 @@
.Python
.DS_Store
bin/
lib/
.idea/
__pycache__/
pyvenv.cfg

10
Dockerfile Normal file
View File

@@ -0,0 +1,10 @@
FROM python:3.8-slim-buster
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
COPY mortgage/ .
CMD [ "python3", "-m" , "web", "run", "--host=0.0.0.0"]

View File

@@ -0,0 +1,22 @@
<html>
<head>
<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/ui/1.12.1/jquery-ui.js"></script>
<script>
$( function() { $( "#tabs" ).tabs(); } );
</script>
<title>Loan Management</title>
</head>
<body>
<p id="header" align="center"><font face="Arial" size=+2 >Web Mortgage Manager</font></p>
<table border="1px">
{% for message in messages %}
<tr><td>{{message}}</td></tr>
{% endfor %}
</table>
<a href="/?loan={{document_id}}">Return to Main Screen</a>.
</body>
</html>

View File

@@ -0,0 +1,19 @@
<html>
<head>
<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/ui/1.12.1/jquery-ui.js"></script>
<script>
$( function() { $( "#tabs" ).tabs(); } );
</script>
<title>Loan Management</title>
</head>
<body>
<p id="header" align="center"><font face="Arial" size=+2 >Web Mortgage Manager</font></p>
<p> The email has been sent.</p>
<a href="/?loan={{document_id}}">Return to Main Screen</a>.
</body>
</html>

View File

@@ -0,0 +1,218 @@
<html>
<head>
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<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>
$( function() { $( "#tabs" ).tabs(); } );
</script>
<title>Loan Management</title>
</head>
<body>
<p id="header" align="center"><font face="Arial" size=+2 >Web Mortgage Manager</font></p>
<form>
<table><tr>
<td><font face="Arial">Loan:</font></td>
<td>
<select name="loan" id="loan">
{% for loan in loans %}
{% if loan.document_id==document_id %}
<option value="{{loan.document_id}}" selected>{{loan.name}}</option>
{% else %}
<option value="{{loan.document_id}}">{{loan.name}}</option>
{% endif %}
{% endfor %}
</select>
<button>Select</button>
<button id='manageLoans'>Manage</button>
</td>
</table>
</form>
<script type="text/javascript">
$(
function() {
$("#loan").change(
function() {
this.form.submit();
});
});
</script>
<p></p>
<div id="tabs">
<ul>
<li><a href="#loan_information">Loan Information</a></li>
<li><a href="#loan_history">Loan History</a></li>
<li><a href="#loan_accounting">Loan Accounting</a></li>
<li><a href="#loan_amortization">Future Amortization</a></li>
<li><a href="#email">Email Statement</a></li>
<li><a href="#add_payment">Add a Regular Payment</a></li>
<li><a href="#add_extra_payment">Add an Extra Payment</a></li>
</ul>
<div id="loan_information">
<table align="center" border="1px">
<thead><tr><th colspan='2' width='60%' align='center'>Loan Information</th></tr></thead>
<tbody>
<tr>
<td><b>Lender:</b></td>
<td>{{ model.lender.name }}<br/>{{ model.lender.address }}<br/>
{{ model.lender.city }} {{model.lender.state }} {{ model.lender.zip }}<br/>
{{ model.lender.phone }}
</td></tr>
<tr><td><b>Borrower:</b></td><td>{{ model.borrower.name }}&nbsp;<br/>{{ model.borrower.address }}&nbsp;
<br/>{{ model.borrower.city }}, {{model.borrower.state }} {{ model.borrower.zip }}
</td></tr>
<tr><td><b>Account Number:</b></td><td>{{ model.parameters.account_number }}</td></tr>
<tr><td><b>Origination Date:</b></td><td> {{ model.parameters.start_date }}</td></tr>
<tr><td><b>Original Principal:</b></td><td>{{ "$%.2f"|format(model.parameters.principal) }}</td></tr>
<tr><td><b>Rate:</b></td><td>{{model.parameters.interest_rate }}% </td></tr>
<tr><td><b>Term: </b></td><td>{{model.parameters.periods }} months </td></tr>
<tr><td><b>Next Payment Due Date:</b></td><td> {{model.parameters.next_due_date}} </td></tr>
<tr><td><b>Payment Due:</b></td><td> {{ "$%.2f"|format(model.parameters.next_payment_amt) }} </td></tr>
<tr><td><b>Current Balance</b></td><td> {{ "$%.2f"|format(model.current_balance) }}</td></tr>
<tr><td><b>Daily Interest Accrual:</b></td><td> {{ "$%.2f"|format(model.current_daily_interest_accrual) }}</td></tr>
</tbody>
</table>
<p></p>
</div>
<div id="loan_history">
<table border="1px" align="center">
<thead>
<tr>
<th colspan="9">Loan History</th>
</tr>
<tr>
<th width='5%'>#</th>
<th width='10%'>Due Date</th>
<th width='10%'>Date Paid</th>
<th width='10%'>Days Interest</th>
<th width='12%' align='right'>Payment Amt</th>
<th width='12%' align='right'>Principal Pmt</th>
<th width='12%' align='right'>Interest Pmt</th>
<th width='10%' align='right'>Late Fee</th>
<th width='24%' align='right'>New Balance</th>
</tr>
</thead>
<tbody>
{% for item in model.past_payments %}
<tr>
<td align='center'> {{ item.payment_number }} </td>
<td align='center'> {{ item.bill_date }} </td>
<td align='center'> {{ item.payment_date }} </td>
<td align='center'> {{ item.days_of_interest }} </td>
<td align='right'> {{ "$%.2f"|format(item.payment_amount) }} </td>
<td align='right'> {{ "$%.2f"|format(item.principal_payment) }} </td>
<td align='right'> {{ "$%.2f"|format(item.interest_payment) }} </td>
<td align='right'> {{ "$%.2f"|format(item.late_fee) }} </td>
<td align='right'> {{ "$%.2f"|format(item.new_balance) }} </td>
</tr>
{% if item.month == 12 or loop.last %}
<tr><td colspan='9'> Total interest paid in {{item.year}} is {{ "$%.2f"|format(item.annual_interest_to_date) }}.</td></tr>
{% endif %}
{% endfor %}
<tr><td colspan='9'> Total interest paid to date is {{ "$%.2f"|format(model.total_interest_paid_to_date) }}.</td></tr>
</tbody>
</table>
</div>
<div id="loan_accounting">
<h3>Loan Accounting</h3>
<textarea border="1px" width="70%" cols="120" rows="25">
{% for item in model.past_payments | reverse %}
{{item.payment_date }} * "{{item.payee}}" "{{item.payee}} Mortgage Payment {{ item.payment_number}}"
{{item.payment_account}} {{ "%.2f"|format(item.payment_amount * -1) }} USD
{{item.loan_account}} {{ "%.2f"|format(item.principal_payment) }} USD
{{item.interest_account}} {{ "%.2f"|format(item.interest_payment) }} USD
{% if item.late_fee != 0 %}
{{item.late_fee_account}} {{ "%.2f"|format(item.late_fee) }} USD
{% endif %}
{% endfor %}
</textarea>
</div>
<div id="loan_amortization">
<table border="1px">
<thead>
<tr>
<th colspan="7">Remaining Amortization</th>
</tr>
<tr>
<th width='8%'>#</th>
<th width='15%'>Due Date</th>
<th width='8%'>Days Interest</th>
<th width='15%' align='right'>Payment Amt</th>
<th width='15%' align='right'>Principal Pmt</th>
<th width='15%' align='right'>Interest Pmt</th>
<th width='20%' align='right'>Principal Balance</th>
</tr>
</thead>
<tbody>
{% for item in model.future_payments %}
<tr><td align='center'> {{ item.payment_number }} </td>
<td align='center'> {{ item.payment_date }} </td>
<td align='center'> {{ item.days_of_interest }} </td>
<td align='right'> {{ "$%.2f"|format(item.payment_amount) }} </td>
<td align='right'> {{ "$%.2f"|format(item.principal_payment) }} </td>
<td align='right'> {{ "$%.2f"|format(item.interest_payment) }} </td>
<td align='right'> {{ "$%.2f"|format(item.new_balance) }} </td>
</tr>
{% endfor %}
<tr colspan="7">Balloon Payment Due: {{ "$%.2f"|format(model.balloon_payment) }}</tr>
</tbody>
</table>
</div>
<div id="email">
<form name="Send Statement" method="post" action="/send_statement">
<table align="center" border='1px' width="75%">
<tr><td align="center" bgcolor="darkgrey">Generate and Send Statement</td></tr>
<tr><td>Send From: {{model.email.from_address}}<br/>Send To: {{model.email.to_address}}</td></tr>
<tr><td bgcolor="beige">Subject:</td></tr>
<tr><td align="center"><textarea cols="120" name="subject">{{model.email.subject}}</textarea></td></tr>
<tr><td bgcolor="beige">Message:</td></tr>
<tr><td align="center"><textarea rows="8" cols="120" name="message">{{model.email.body}}</textarea></td></tr>
<tr><td bgcolor="beige">Send Statement As:</td></tr>
<tr><td>
<input type="checkbox" name="html" value="html"/> HTML
<input type="checkbox" name="pdf" value="pdf"/> PDF
<input type="checkbox" name="text" value="text" /> Plain Text
</td></tr>
<tr><td bgcolor="beige">Include Future Amortization</td></tr>
<tr><td>
<input name="amortization" type="radio" value="yes" checked>Yes
<input name="amortization" type="radio" value="no">No
</td></tr>
<tr><td><button>Send Statement</button></td></tr>
</table>
<input type="hidden" name='loan' value="{{document_id}}" />
</form>
</div>
<div id="add_payment">
<form name="AddPayment" method="post" action="/update_file">
<input type="hidden" name="loan" value="{{document_id}}" />
<table align="center" border="1px" width="75%">
<tr><td align="center" bgcolor="darkgrey">Record a 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 bgcolor="beige">Late Fee Amount:</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>
</table>
</form>
</div>
<div id="add_extra_payment">
<form name="AddExtraPayment" method="post" action="/update_file">
<input type="hidden" name="loan" value="{{document_id}}" />
<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>
</body>
</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

@@ -0,0 +1,86 @@
<html>
<body>
<font face='arial' size='14'><p align='center'>{{ model.header.title }}</p></font>
<font face='arial' size='10'><p align='center'>{{ model.lender.name }}</p></font>
<font face='arial' size='8'>
<p align='center'>{{ model.lender.phone }} - {{ model.lender.address }} -
{{ model.lender.city }} {{model.lender.state }} {{ model.lender.zip }}</p>
<p align='right'>Statement Date: {{ model.header.date }}</p>
<p/>
<table>
<thead><tr><th width='45%' align='left'>Loan Information</th><th width='45%'>&nbsp;</th></tr></thead>
<tbody>
<tr><td>Borrower: {{ model.borrower.name }}&nbsp;</td> <td>Account Number: {{ model.parameters.account_number }}</td></tr>
<tr><td>{{ model.borrower.address }}&nbsp;</td> <td>Origination Date: {{ model.parameters.start_date }}</td></tr>
<tr><td>{{ model.borrower.city }}, {{model.borrower.state }} {{ model.borrower.zip }}</td>
<td>Original Principal: {{ "$%.2f"|format(model.parameters.principal) }}</td></tr>
<tr><td>Rate: {{model.parameters.interest_rate }}% </td> <td>Term: {{model.parameters.periods }} months </td></tr>
<tr><td>Next Payment Due Date: {{model.parameters.next_due_date}} </td> <td>Payment Due: {{ "$%.2f"|format(model.parameters.next_payment_amt) }} </td></tr>
</tbody>
</table>
<p/>
<p class='section_header' color='red'><font face='arial' size='14'>Payment History</font></p>
<table>
<thead><tr>
<th width='5%'>#</td>
<th width='10%'>Due Date</th>
<th width='10%'>Date Paid</th>
<th width='10%'>Days Interest</th>
<th width='12%' align='right'>Payment Amt</th>
<th width='12%' align='right'>Principal Pmt</th>
<th width='12%' align='right'>Interest Pmt</th>
<th width='10%' align='right'>Late Fee</th>
<th width='19%' align='right'>New Balance</th>
</tr></thead>
<tbody>
{% for item in model.past_payments %}
<tr><td align='center'> {{ item.payment_number }} </td>
<td align='center'> {{ item.bill_date }} </td>
<td align='center'> {{ item.payment_date }} </td>
<td align='center'> {{ item.days_of_interest }} </td>
<td align='right'> {{ "$%.2f"|format(item.payment_amount) }} </td>
<td align='right'> {{ "$%.2f"|format(item.principal_payment) }} </td>
<td align='right'> {{ "$%.2f"|format(item.interest_payment) }} </td>
<td align='right'> {{ "$%.2f"|format(item.late_fee) }} </td>
<td align='right'> {{ "$%.2f"|format(item.new_balance) }} </td>
</tr>
{% if item.month == 12 or loop.last %}
<tr><td colspan='8'"> Total interest paid in {{item.year}} is {{ "$%.2f"|format(item.annual_interest_to_date) }}.</td></tr>
{% endif %}
{% endfor %}
<tr><td colspan='8'"> Total interest paid to date is {{ "$%.2f"|format(model.total_interest_paid_to_date) }}.</td></tr>
</tbody>
</table>
<p/> <p/>
<p class='section_header'><font face='arial' size='14'>Remaining Amortization</font></p>
<table>
<thead>
<tr>
<th width='8%'>#</th>
<th width='15%'>Due Date</th>
<th width='8%'>Days Interest</th>
<th width='15%' align='right'>Payment Amt</th>
<th width='15%' align='right'>Principal Pmt</th>
<th width='15%' align='right'>Interest Pmt</th>
<th width='20%' align='right'>Principal Balance</th>
</tr>
</thead>
<tbody>
{% for item in model.future_payments %}
<tr><td align='center'> {{ item.payment_number }} </td>
<td align='center'> {{ item.payment_date }} </td>
<td align='center'> {{ item.days_of_interest }} </td>
<td align='right'> {{ "$%.2f"|format(item.payment_amount) }} </td>
<td align='right'> {{ "$%.2f"|format(item.principal_payment) }} </td>
<td align='right'> {{ "$%.2f"|format(item.interest_payment) }} </td>
<td align='right'> {{ "$%.2f"|format(item.new_balance) }} </td>
</tr>
{% endfor %}
</tbody>
</table>
<p>Balloon Payment Due: {{ "$%.2f"|format(model.balloon_payment) }} </p>
</font>
</body>
</html>

View File

@@ -0,0 +1,86 @@
<html>
<body>
<font face='arial' size='14'><p align='center'>{{ model.header.title }}</p></font>
<font face='arial' size='10'><p align='center'>{{ model.lender.name }}</p></font>
<font face='arial' size='8'>
<p align='center'>{{ model.lender.phone }} - {{ model.lender.address }} -
{{ model.lender.city }} {{model.lender.state }} {{ model.lender.zip }}</p>
<p align='right'>Statement Date: {{ model.header.date }}</p>
<p/>
<table>
<thead><tr><th width='45%' align='left'>Loan Information</th><th width='45%'>&nbsp;</th></tr></thead>
<tbody>
<tr><td>Borrower: {{ model.borrower.name }}&nbsp;</td> <td>Account Number: {{ model.parameters.account_number }}</td></tr>
<tr><td>{{ model.borrower.address }}&nbsp;</td> <td>Origination Date: {{ model.parameters.start_date }}</td></tr>
<tr><td>{{ model.borrower.city }}, {{model.borrower.state }} {{ model.borrower.zip }}</td>
<td>Original Principal: {{ "$%.2f"|format(model.parameters.principal) }}</td></tr>
<tr><td>Rate: {{model.parameters.interest_rate }}% </td> <td>Term: {{model.parameters.periods }} months </td></tr>
<tr><td>Next Payment Due Date: {{model.parameters.next_due_date}} </td> <td>Payment Due: {{ "$%.2f"|format(model.parameters.next_payment_amt) }} </td></tr>
</tbody>
</table>
<p/>
<p class='section_header' color='red'><font face='arial' size='14'>Payment History</font></p>
<table>
<thead><tr>
<th width='5%'>#</td>
<th width='10%'>Due Date</th>
<th width='10%'>Date Paid</th>
<th width='10%'>Days Interest</th>
<th width='12%' align='right'>Payment Amt</th>
<th width='12%' align='right'>Principal Pmt</th>
<th width='12%' align='right'>Interest Pmt</th>
<th width='10%' align='right'>Late Fee</th>
<th width='24%' align='right'>New Balance</th>
</tr></thead>
<tbody>
{% for item in model.past_payments %}
<tr><td align='center'> {{ item.payment_number }} </td>
<td align='center'> {{ item.bill_date }} </td>
<td align='center'> {{ item.payment_date }} </td>
<td align='center'> {{ item.days_of_interest }} </td>
<td align='right'> {{ "$%.2f"|format(item.payment_amount) }} </td>
<td align='right'> {{ "$%.2f"|format(item.principal_payment) }} </td>
<td align='right'> {{ "$%.2f"|format(item.interest_payment) }} </td>
<td align='right'> {{ "$%.2f"|format(item.late_fee) }} </td>
<td align='right'> {{ "$%.2f"|format(item.new_balance) }} </td>
</tr>
{% if item.month == 12 or loop.last %}
<tr><td colspan='8'"> Total interest paid in {{item.year}} is {{ "$%.2f"|format(item.annual_interest_to_date) }}.</td></tr>
{% endif %}
{% endfor %}
<tr><td colspan='8'"> Total interest paid to date is {{ "$%.2f"|format(model.total_interest_paid_to_date) }}.</td></tr>
</tbody>
</table>
<p/> <p/>
<p class='section_header'><font face='arial' size='14'>Remaining Amortization</font></p>
<table>
<thead>
<tr>
<th width='10%'>#</th>
<th width='15%'>Due Date</th>
<th width='10%'>Days Interest</th>
<th width='15%' align='right'>Payment Amt</th>
<th width='15%' align='right'>Principal Pmt</th>
<th width='15%' align='right'>Interest Pmt</th>
<th width='20%' align='right'>Principal Balance</th>
</tr>
</thead>
<tbody>
{% for item in model.future_payments %}
<tr><td align='center'> {{ item.payment_number }} </td>
<td align='center'> {{ item.payment_date }} </td>
<td align='center'> {{ item.days_of_interest }} </td>
<td align='right'> {{ "$%.2f"|format(item.payment_amount) }} </td>
<td align='right'> {{ "$%.2f"|format(item.principal_payment) }} </td>
<td align='right'> {{ "$%.2f"|format(item.interest_payment) }} </td>
<td align='right'> {{ "$%.2f"|format(item.new_balance) }} </td>
</tr>
{% endfor %}
</tbody>
</table>
<p>Balloon Payment Due: {{ "$%.2f"|format(model.balloon_payment) }} </p>
</font>
</body>
</html>

View File

@@ -0,0 +1,36 @@
{{ model.header.title }}
{{ model.lender.name }}
{{ model.lender.address }}
{{ model.lender.city }}, {{model.lender.state }} {{ model.lender.zip }}
{{ model.lender.phone }}
Statement Date: {{ model.header.date }}
Loan Information
Borrower: {{ model.borrower.name }}
Account Number: {{ model.parameters.account_number }}
Origination Date: {{ model.parameters.start_date }}
Address:
{{ model.borrower.address }}
{{ model.borrower.city }}, {{model.borrower.state }} {{ model.borrower.zip }}
Original Principal: {{ "$%.2f"|format(model.parameters.principal) }}
Rate: {{model.parameters.interest_rate }}%
Term: {{model.parameters.periods }} months
Next Payment Due Date: {{model.parameters.next_due_date}}
Payment Due: {{ "$%.2f"|format(model.parameters.next_payment_amt) }}
Payment History
#,Due Date,Date Paid,Days Interest,Payment Amt,Principal Pmt,Interest Pmt,Late Fee,New Balance
{% for item in model.past_payments %}
{{ item.payment_number }},{{ item.bill_date }},{{ item.payment_date }},{{ item.days_of_interest }},{{ "$%.2f"|format(item.payment_amount) }},{{ "$%.2f"|format(item.principal_payment) }},{{ "$%.2f"|format(item.interest_payment) }},{{ "$%.2f"|format(item.late_fee) }},{{ "$%.2f"|format(item.new_balance) }}
{% if item.month == 12 or loop.last %}
Total interest paid in {{item.year}} is {{ "$%.2f"|format(item.annual_interest_to_date) }}.
{% endif %}
{% endfor %}
Total interest paid to date is {{ "$%.2f"|format(model.total_interest_paid_to_date) }}.
Remaining Amortization
#,Due Date,Days Interest,Payment Amt,Principal Pmt,Interest Pmt,Principal Balance<
{% for item in model.future_payments %}
{{ item.payment_number }},{{ item.payment_date }},{{ item.days_of_interest }},{{ "$%.2f"|format(item.payment_amount) }},{{ "$%.2f"|format(item.principal_payment) }},{{ "$%.2f"|format(item.interest_payment) }},{{ "$%.2f"|format(item.new_balance) }}
{% endfor %}
Balloon Payment Due: {{ "$%.2f"|format(model.balloon_payment) }}

541
mortgage/web.py Normal file
View File

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

12
requirements.txt Normal file
View File

@@ -0,0 +1,12 @@
click==8.1.3
CouchDB==1.2
defusedxml==0.7.1
Flask==2.2.2
fonttools==4.37.4
fpdf2==2.5.7
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.1
Pillow==9.2.0
svg.path==6.2
Werkzeug==2.2.2