commit 8eef63bb08548c400c31787779dd080744a8b4e7 Author: John Kent Date: Fri Sep 12 21:25:28 2025 -0400 Converted from mecurial diff --git a/cli.py b/cli.py new file mode 100644 index 0000000..49511aa --- /dev/null +++ b/cli.py @@ -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() diff --git a/templates/main.html b/templates/main.html new file mode 100644 index 0000000..489dce7 --- /dev/null +++ b/templates/main.html @@ -0,0 +1,56 @@ + + + + + Line of Credit Interest Calculator + + + +

Line of Credit Interest Calculator

+
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Interest Postings

+ +
+

Transcript

+ + + {% for item in model.transcript %}{% endfor %} +
DateTypeAmountBalance
{{ item.date }}{{ item.type }}{{ item.amount }}{{ item.balance }}
+
+

BQL Query to Execute in Fava to Get the CSV File

+

select date, number where year=2022 And account~".*:CreditLine"

+ + \ No newline at end of file diff --git a/web.py b/web.py new file mode 100644 index 0000000..70443dc --- /dev/null +++ b/web.py @@ -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()