Converted from mecurial

This commit is contained in:
2025-09-12 21:25:28 -04:00
commit 8eef63bb08
3 changed files with 355 additions and 0 deletions

125
cli.py Normal file
View 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
View 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
View 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()