Converted from mecurial
This commit is contained in:
125
cli.py
Normal file
125
cli.py
Normal file
@@ -0,0 +1,125 @@
|
||||
import json
|
||||
from decimal import *
|
||||
from datetime import *
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# This is the BQL query to put into Fava to generate the CSV that can be used
|
||||
# to create the JSON file for this. Might consider reading in the CSV directly
|
||||
# and prompting for the other information?
|
||||
#
|
||||
# select date, number where year=2022 And account~".*:CreditLine"
|
||||
#
|
||||
##############################################################################
|
||||
#
|
||||
# Consider altering this program to generate the text to go into the beancount
|
||||
# files directly so that it can just be a cut and paste job.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
|
||||
def main():
|
||||
filename = '2023.gtploc.json'
|
||||
account = loadAccountInformation(getFullPathofFile(filename))
|
||||
|
||||
starting_balance = Decimal(account['starting_balance'])
|
||||
apr = Decimal(account['interest_rate']) / 100
|
||||
daily_interest_rate = apr / 360
|
||||
|
||||
fiscal_year = account['fiscal_year']
|
||||
first_day = datetime(fiscal_year, 1, 1)
|
||||
last_day = datetime(fiscal_year, 12, 31)
|
||||
days_in_year = (last_day - first_day).days + 2
|
||||
print("Line of Credit Report")
|
||||
print("Fiscal Year: {1} Input File Name: {0}".format(filename, fiscal_year))
|
||||
print("Starting Balance: {0} Interest Rate: {1}".format(starting_balance, account['interest_rate']))
|
||||
print("Report Run Date: {0}".format(datetime.now()))
|
||||
print("\n=========================================================================================\n")
|
||||
|
||||
day_nets = {}
|
||||
transactions = account['transactions']
|
||||
print("Summarizing {0} transactions:".format(len(transactions)))
|
||||
for entry in transactions:
|
||||
entry_date = entry[0]
|
||||
entry_amount = Decimal(entry[1])
|
||||
|
||||
if entry_date not in day_nets:
|
||||
day_nets[entry_date] = entry_amount
|
||||
else:
|
||||
day_nets[entry_date] = day_nets[entry_date] + entry_amount
|
||||
|
||||
for key in day_nets.keys():
|
||||
print("Date: {0} Net Change: {1}".format(key, day_nets[key]))
|
||||
|
||||
print("Summary complete.")
|
||||
|
||||
current_balance = starting_balance
|
||||
accummulated_interest = Decimal('0.00')
|
||||
daily_interest = calculateDailyInterest(current_balance, daily_interest_rate)
|
||||
|
||||
print("\n=========================================================================================\n")
|
||||
print("Calculating interest charges...")
|
||||
# print("Beginning Balance: {1} Daily Interest: {2}".format(first_day,starting_balance, daily_interest))
|
||||
for i in range(1, days_in_year):
|
||||
current_date = (first_day + timedelta(days=i-1)).date()
|
||||
|
||||
if current_date.day == 1:
|
||||
print("BEGIN MONTH: {0} Starting Balance: {1} Starting Daily Interest: {2}".format(current_date,
|
||||
current_balance,
|
||||
daily_interest))
|
||||
|
||||
if current_date.__str__() in day_nets:
|
||||
current_balance = current_balance + day_nets[current_date.__str__()]
|
||||
daily_interest = calculateDailyInterest(current_balance, daily_interest_rate)
|
||||
print("Balance Change: Date: {0} Net: {1} New Balance: {2}".format(
|
||||
current_date, day_nets[current_date.__str__()], current_balance))
|
||||
|
||||
accummulated_interest = accummulated_interest + daily_interest
|
||||
|
||||
if is_last_day_of_month(current_date):
|
||||
current_balance = current_balance + accummulated_interest
|
||||
daily_interest = calculateDailyInterest(current_balance, daily_interest_rate)
|
||||
print("END MONTH: Date: {0} Post Accumulated Interest: {1} New Balance: {2} New Daily Interest: {3}".format(
|
||||
current_date, accummulated_interest, current_balance, daily_interest))
|
||||
accummulated_interest = 0
|
||||
|
||||
|
||||
def is_last_day_of_month(some_date):
|
||||
if some_date.day < 28:
|
||||
return False
|
||||
|
||||
next_day = some_date + timedelta(days=1)
|
||||
if next_day.month == some_date.month:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def calculateDailyInterest(principal, rate):
|
||||
return (principal*rate).quantize(Decimal('.001'))
|
||||
|
||||
|
||||
def loadAccountInformation(filename):
|
||||
return getDatastore(filename)
|
||||
|
||||
|
||||
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 file '%s'. " % filename)
|
||||
print("The Exception:")
|
||||
print(e.__repr__())
|
||||
quit()
|
||||
|
||||
return datastore
|
||||
|
||||
|
||||
def getFullPathofFile(filename):
|
||||
return '/Users/john/PycharmProjects/LoCInterestCalculator/' + filename
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
56
templates/main.html
Normal file
56
templates/main.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Line of Credit Interest Calculator</title>
|
||||
<style>
|
||||
.date, .type {
|
||||
text-align: center
|
||||
}
|
||||
.amount, .balance {
|
||||
text-align: right
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="header" align="center">Line of Credit Interest Calculator</h1>
|
||||
<hr>
|
||||
<form method="POST" enctype="multipart/form-data" action="{{ url_for('hello') }}">
|
||||
<table>
|
||||
<tr>
|
||||
<td><label>Interest Year:</label></td><td><input type="text" name="year" value="{{model.year}}"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label>Interest Rate:</label></td><td><input type="text" name="rate" value="{{model.rate}}"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label>Starting Principal Balance:</label></td><td><input type="text" name="starting_balance" value="{{model.starting_balance}}"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label>Principal Account:</label></td><td><input type="text" name="principal_account" value="{{model.principal_account}}"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label>Interest Account:</label></td><td><input type="text" name="interest_account" value="{{model.interest_account}}"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label>Transaction CSV File:</label></td><td><input type="file" name="transactions" accept=".csv"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><input type="submit" value="Generate Interest Posts"/> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
<hr>
|
||||
<h2>Interest Postings</h2>
|
||||
<textarea rows="20" cols="80">{% for item in model.int_txns %}{{item}}{% endfor %}</textarea>
|
||||
<hr>
|
||||
<h2>Transcript</h2>
|
||||
<table width="75%">
|
||||
<thead><th>Date</th><th>Type</th><th>Amount</th><th>Balance</th></thead>
|
||||
{% for item in model.transcript %}<tr><td class="date">{{ item.date }}</td><td class="type">{{ item.type }}</td><td class="balance">{{ item.amount }}</td><td class="balance">{{ item.balance }}</td></tr>{% endfor %}
|
||||
</table>
|
||||
<hr>
|
||||
<h2>BQL Query to Execute in Fava to Get the CSV File</h2>
|
||||
<p>select date, number where year=2022 And account~".*:CreditLine"</p>
|
||||
</body>
|
||||
</html>
|
||||
174
web.py
Normal file
174
web.py
Normal file
@@ -0,0 +1,174 @@
|
||||
import jinja2
|
||||
import os
|
||||
from flask import Flask, render_template, request
|
||||
from decimal import *
|
||||
from datetime import *
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# This is the BQL query to put into Fava to generate the CSV that can be used
|
||||
# to create the JSON file for this. Might consider reading in the CSV directly
|
||||
# and prompting for the other information?
|
||||
#pip
|
||||
# select date, number where year=2022 And account~".*:CreditLine"
|
||||
#
|
||||
##############################################################################
|
||||
#
|
||||
# Consider altering this program to generate the text to go into the beancount
|
||||
# files directly so that it can just be a cut and paste job.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
app = Flask(__name__)
|
||||
trans_template = '''{0} * "Interest Due on LoC {1} - {2}"\n\t{3} {4} USD\n\t{5}\n\n'''
|
||||
loader = jinja2.FileSystemLoader(os.path.dirname(__file__))
|
||||
environment = jinja2.Environment(loader=loader)
|
||||
DZERO = Decimal("0.00")
|
||||
|
||||
|
||||
######
|
||||
# Flask Call Backs
|
||||
######
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
def hello():
|
||||
now = datetime.now()
|
||||
model = dict()
|
||||
model['year'] = now.date().strftime("%Y")
|
||||
model['rate'] = 8
|
||||
model['starting_balance'] = 0
|
||||
model['principal_account'] = "Liabilities:Current:CreditLine"
|
||||
model['interest_account'] = "Expenses:Interest:LoCInterest"
|
||||
model['int_txns'] = 'Not calculated.'
|
||||
model['txns'] = []
|
||||
model['net_balance_changes'] = {}
|
||||
model['transcript'] = []
|
||||
|
||||
if request.method == 'POST':
|
||||
model['year'] = request.form['year']
|
||||
model['rate'] = request.form['rate']
|
||||
starting_balance = Decimal(request.form['starting_balance'])
|
||||
model['starting_balance'] = (DZERO + starting_balance).quantize(Decimal("0.00"))
|
||||
model['principal_account'] = request.form['principal_account']
|
||||
model['interest_account'] = request.form['interest_account']
|
||||
files = request.files
|
||||
|
||||
if 'transactions' in files:
|
||||
result, model['message'], txns = validateUserInput(model, files['transactions'])
|
||||
|
||||
if result is True:
|
||||
model['txns'] = txns
|
||||
model['int_txns'] = generate_interest_postings(model, txns)
|
||||
|
||||
return render_template('main.html', model=model)
|
||||
|
||||
|
||||
def validateUserInput(model: object, csv_content: object) -> (bool, str, dict):
|
||||
#TODO: What validations should I put here?
|
||||
|
||||
#put the contents of the csv file into a list
|
||||
transactions = {}
|
||||
for line in csv_content:
|
||||
text = line.decode('utf-8').rstrip()
|
||||
if "date" in text:
|
||||
pass
|
||||
else:
|
||||
items = text.split(',')
|
||||
current_day = items[0]
|
||||
amount = Decimal("0.00") + Decimal(items[1])
|
||||
if current_day in transactions:
|
||||
transactions[current_day].append(amount)
|
||||
else:
|
||||
transactions[current_day] = [amount]
|
||||
return True, '', transactions
|
||||
|
||||
|
||||
def appendToTranscript(transcript: dict, date: str, type: str, amount: Decimal, balance: Decimal):
|
||||
transcript.append({"date": date, "type": type, "amount": amount, "balance": balance})
|
||||
|
||||
|
||||
def generate_interest_postings(model: dict, transactions: dict) -> list:
|
||||
postings = []
|
||||
transcript = []
|
||||
|
||||
starting_balance = Decimal(model['starting_balance'])
|
||||
apr = Decimal(model['rate']) / 100
|
||||
daily_interest_rate = apr / 360
|
||||
year = int(model['year'])
|
||||
first_day = datetime(year, 1, 1)
|
||||
last_day = datetime(year, 12, 31)
|
||||
days_in_year = (last_day - first_day).days + 2
|
||||
|
||||
current_balance = starting_balance
|
||||
accummulated_interest = DZERO
|
||||
daily_interest = calculateDailyInterest(current_balance, daily_interest_rate)
|
||||
|
||||
for i in range(1, days_in_year):
|
||||
current_date = (first_day + timedelta(days=i-1)).date()
|
||||
|
||||
if current_date.day == 1:
|
||||
appendToTranscript(transcript, current_date, "Balance", "", current_balance)
|
||||
|
||||
|
||||
#first add this day's transactions to the current balance
|
||||
if current_date.__str__() in transactions:
|
||||
for item in transactions[current_date.__str__()]:
|
||||
current_balance = current_balance - item
|
||||
appendToTranscript(transcript, current_date, "Transaction", item, current_balance)
|
||||
|
||||
daily_interest = calculateDailyInterest(current_balance, daily_interest_rate)
|
||||
appendToTranscript(transcript, current_date, "Daily Interest Change", daily_interest, "")
|
||||
|
||||
|
||||
#accumulate this day's interest
|
||||
accummulated_interest = accummulated_interest + daily_interest
|
||||
|
||||
#if this is the last day of a month, accrue the accumulated interest to the balance
|
||||
#then recalculate the new daily_interest accrual amount
|
||||
#then emit that month's interest accrual posting
|
||||
#then zero accumulated interest
|
||||
if is_last_day_of_month(current_date):
|
||||
current_balance = current_balance + accummulated_interest
|
||||
daily_interest = calculateDailyInterest(current_balance, daily_interest_rate)
|
||||
postings.append(trans_template.format(fava_date_format(current_date),
|
||||
first_day_of_month(current_date), current_date,
|
||||
model['principal_account'], accummulated_interest * -1,
|
||||
model['interest_account']))
|
||||
appendToTranscript(transcript, current_date, "Interest Posting", accummulated_interest * -1, current_balance)
|
||||
accummulated_interest = DZERO
|
||||
model['transcript'] = transcript
|
||||
return postings
|
||||
|
||||
|
||||
def is_last_day_of_month(some_date):
|
||||
if some_date.day < 28:
|
||||
return False
|
||||
|
||||
next_day = some_date + timedelta(days=1)
|
||||
if next_day.month == some_date.month:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def first_day_of_month(some_date):
|
||||
firstDayOfMonth = date(some_date.year, some_date.month, 1)
|
||||
return firstDayOfMonth
|
||||
|
||||
|
||||
def calculateDailyInterest(principal, rate):
|
||||
return (principal*rate).quantize(Decimal('.01'))
|
||||
|
||||
|
||||
def fava_date_format(some_date):
|
||||
day = some_date.day
|
||||
month = some_date.month
|
||||
year = some_date.year
|
||||
return "{0}-{1}-{2}".format(year, month, day)
|
||||
|
||||
|
||||
def main():
|
||||
app.debug = True
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user