175 lines
6.2 KiB
Python
175 lines
6.2 KiB
Python
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()
|