2025-11-05 14:43:11 -05:00
from actual import Actual
import os
from dotenv import load_dotenv
from actual . queries import get_budgets , get_categories , get_category , get_transactions
from datetime import datetime , timedelta
from moneyed import Money , USD
from moneyed . l10n import format_money
from dateutil . relativedelta import relativedelta
import json
import re
import pprint
pp = pprint . PrettyPrinter ( indent = 4 , sort_dicts = False )
load_dotenv ( )
THISMONTH = datetime . now ( )
2025-11-11 09:47:47 -05:00
# datestring= "2025-08-06 13:57:57"
# format = '%Y-%m-%d %H:%M:%S'
# THISMONTH = datetime.strptime(datestring, format)
2025-11-05 14:43:11 -05:00
MONTHMINUSONE = THISMONTH - relativedelta ( months = 1 )
MONTHMINUSTWO = THISMONTH - relativedelta ( months = 2 )
2025-11-11 09:47:47 -05:00
LASTM_STRING = datetime . strftime ( MONTHMINUSONE , ' % B ' )
TWOM_STRING = datetime . strftime ( MONTHMINUSTWO , ' % B ' )
ONBUDGETACCOUNTS = list ( os . getenv ( ' ONBUDGETACCOUNTS ' ) )
2025-11-05 14:43:11 -05:00
def compare_months ( actual ) :
budget_one = main ( actual , MONTHMINUSONE )
budget_two = main ( actual , MONTHMINUSTWO )
x = find_percent_diff ( budget_one , budget_two )
first_sort = dict ( sorted ( budget_one . items ( ) , key = lambda item : item [ 1 ] [ ' amount_spent ' ] ) )
reduced_sort = dict ( list ( first_sort . items ( ) ) [ : 5 ] )
2025-11-11 09:47:47 -05:00
text_list = [ ]
for i , k in reduced_sort . items ( ) :
items = ( i , format_money ( k [ ' amount_spent ' ] ) )
text_list . append ( items )
formatted = str ( text_list ) [ 1 : - 1 ]
replacements = [ ( ' \\ ( ' , ' ' ) , ( ' \\ ) ' , ' ' ) , ( " ' " , ' ' ) , ( ' " ' , ' ' ) , ( ' ( \\ d) \\ , \\ s \\ b ' , ' \\ 1 \\ n ' ) , ( ' \\ b, ( \\ -) ' , ' : \\ 1 ' ) ]
for old , new in replacements :
formatted = re . sub ( fr " { old } " , fr " { new } " , formatted )
x = x . replace ( ' " ' , ' ' )
print ( f """ **This is an automated message!** \n Here ' s your budget status. Here are your top 5 spending categories from last month: \n \n { formatted } \n \n And here are the top 5 categories with the biggest increase in spending between last month and the previous month: \n \n { x } """ )
2025-11-05 14:43:11 -05:00
def find_percent_diff ( lastmonth , twomonths ) :
diffs = [ ]
for k , v in lastmonth . items ( ) :
for y , x in twomonths . items ( ) :
if k == y :
absolute = v [ ' amount_spent ' ] - x [ ' amount_spent ' ]
average = ( v [ ' amount_spent ' ] + x [ ' amount_spent ' ] ) / 2
diff = round ( absolute / average , 2 )
2025-11-11 09:47:47 -05:00
diffs . append ( ( k , diff , ( LASTM_STRING , v [ ' amount_spent ' ] ) , ( TWOM_STRING , x [ ' amount_spent ' ] ) ) )
2025-11-05 14:43:11 -05:00
sort = sorted ( diffs , reverse = True , key = lambda item : item [ 1 ] )
2025-11-11 09:47:47 -05:00
first = sort [ : 5 ]
second = [ ]
for t in first :
t1 = str ( f " { float ( t [ 1 ] ) } % " )
t2 = ( t [ 2 ] [ 0 ] , format_money ( t [ 2 ] [ 1 ] ) )
t3 = ( t [ 3 ] [ 0 ] , format_money ( t [ 3 ] [ 1 ] ) )
newlist = f " { t [ 0 ] } , { t1 } , { t2 } , { t3 } "
second . append ( newlist )
formatted = str ( second ) [ 1 : - 1 ]
replacements = [ ( ' \\ ( ' , ' ' ) , ( ' \\ ) ' , ' ' ) , ( " ' " , ' ' ) , ( ' " , ' , ' \\ n ' ) , ( ' ( \\ % ), ' , ' \\ 1 --> ' ) , ( ' ( \\ d) \\ , \\ s \\ b ' , ' \\ 1 --> ' ) , ( ' ([a-z]), ' , ' \\ 1: ' ) ]
for old , new in replacements :
formatted = re . sub ( fr " { old } " , fr " { new } " , formatted )
return formatted
2025-11-05 14:43:11 -05:00
def simple_track ( actual ) :
main_dict = { }
main_list = [ ]
budget = get_budgets ( actual . session )
for b in budget :
if b . month == int ( MONTH_DAY_ONE ) and b . amount > 0 :
cats = get_categories ( actual . session )
for cat in cats :
if cat . id == b . category_id :
if cat . name == " Income " :
formatted_amount = str ( b . amount ) [ : - 2 ] + " . " + str ( b . amount ) [ - 2 : ]
expected_income = Money ( formatted_amount , USD )
main_dict [ cat . name ] = { " Expected Income " : expected_income , " Actual Income " : Money ( 0 , USD ) , " Total Spent " : Money ( 0 , USD ) , " Amount Left Against Budget " : Money ( 0 , USD ) }
category_transcations ( sesh = actual . session , main_list = main_dict , origin = " simple " )
def main ( actual , month ) :
intmonth = month . strftime ( " % Y % m " )
main_dict = { }
main_list = [ ]
budget = get_budgets ( actual . session )
for b in budget :
if b . month == int ( intmonth ) and b . amount > 0 :
cats = get_categories ( actual . session )
for cat in cats :
if cat . id == b . category_id :
# if cat.name == "Kid's Activities":
formatted_amount = str ( b . amount ) [ : - 2 ] + " . " + str ( b . amount ) [ - 2 : ]
budgeted_amount = Money ( formatted_amount , USD )
main_dict [ cat . name ] = { " budgeted_amount " : budgeted_amount , " amount_spent " : " " , " amount_left " : " " }
returns = category_transcations ( sesh = actual . session , main_list = main_dict , origin = " all_cats " , month = month )
return returns
def category_transcations ( sesh , main_list , origin , month ) :
first_day_month = month . replace ( day = 1 )
first_day_next_month = ( month . replace ( day = 1 ) + timedelta ( days = 32 ) ) . replace ( day = 1 )
if origin == " simple " :
this_monhth_income = get_transactions ( sesh , category = ' Income ' , start_date = MONTHMINUSONE . replace ( day = 1 ) , end_date = THISMONTH . replace ( day = 1 ) )
income_sum = Money ( 0 , USD )
for tit in this_monhth_income :
print ( tit . amount , tit . payee . name , tit . date )
income = str ( tit . amount ) [ : - 2 ] + " . " + str ( tit . amount ) [ - 2 : ]
total_income = Money ( income , USD )
income_sum + = total_income
this_month_trans = get_transactions ( sesh , account = ' onbudget ' , start_date = first_day_month , end_date = first_day_next_month )
curr_sum = Money ( 0 , USD )
count = 0
for ta in this_month_trans :
if ta . category_id != ' 3c1699a5-522a-435e-86dc-93d900a14f0e ' and ta . account . id in ONBUDGETACCOUNTS :
tamount = str ( ta . amount ) [ : - 2 ] + " . " + str ( ta . amount ) [ - 2 : ]
total_sum = Money ( tamount , USD )
curr_sum + = total_sum
count + = 1
amount_left = curr_sum + main_list [ ' Income ' ] [ ' Expected Income ' ]
main_list [ ' Income ' ] [ ' Expected Income ' ] = main_list [ ' Income ' ] [ ' Expected Income ' ]
main_list [ ' Income ' ] [ ' Actual Income ' ] = income_sum
main_list [ ' Income ' ] [ ' Amount Left Against Budget ' ] = amount_left
main_list [ ' Income ' ] [ ' Total Spent ' ] = curr_sum
else :
for keys , vals in main_list . items ( ) :
this_month_trans = get_transactions ( sesh , category = keys , start_date = first_day_month , end_date = first_day_next_month )
curr_sum = Money ( 0 , USD )
for ta in this_month_trans :
tamount = str ( ta . amount ) [ : - 2 ] + " . " + str ( ta . amount ) [ - 2 : ]
total_sum = Money ( tamount , USD )
curr_sum + = total_sum
# print(f"curr_sum: {curr_sum} PLUS total_sum: {total_sum}")
amount_list = curr_sum + vals [ " budgeted_amount " ]
vals [ " budgeted_amount " ] = vals [ ' budgeted_amount ' ]
vals [ " amount_spent " ] = curr_sum
vals [ " amount_left " ] = amount_list
# sorted_items = sorted(main_list.items(), key=lambda item: item[1]['amount_left'])
# sorted_nested_dict = dict(sorted_items)
# pp.pprint(sorted_nested_dict)
# pp.pprint(sorted_key_val)
# sorted_data = sorted(main_list, key=lambda x: main_list[x]['amount_left'])
# pp.pprint(sorted_data)
# sort_budgets_for_notification(sorted_data)
if origin == " simple " :
x = format_dicts ( main_list )
simple_notifications ( x )
else :
return main_list
# all_budgets_for_notifications(main_list)
def simple_notifications ( x ) :
print ( f """ Here ' s where you stand last month! \n { x } """ )
def all_budgets_for_notifications ( sorted_data ) :
negative_budget = { }
no_budget_left = { }
some_budget_left = { }
final_tuple = ( )
for categories , values in sorted_data . items ( ) :
# for budget_type, amount in values.items():
intmoney = values [ ' amount_left ' ] . amount
if intmoney == 0 :
# no_budget_left.append((categories, values) )
no_budget_left [ categories ] = values
elif intmoney < 0 :
# negative_budget.append((categories, values))
negative_budget [ categories ] = values
else :
# some_budget_left.append(( categories, values) )
some_budget_left [ categories ] = values
final_tuple = ( format_dicts ( some_budget_left ) , format_dicts ( negative_budget ) , format_dicts ( no_budget_left ) )
print ( f """ **This is an automated message!** Here ' s your spending status so far for this month. \n First, here are the categories where you ' ve overspent: \n { final_tuple [ 1 ] } . \n \n Here is where you still have some budget left: \n { final_tuple [ 0 ] } . \n \n And finally, here is where you ' re exactly where you need to be. $0 left. \n { final_tuple [ 2 ] } """ )
def format_dicts ( budgets_sorted ) :
x = str ( budgets_sorted ) [ 10 : ]
# y = re.search(r"(\'\w*\':)|(\'\w{1,9}\s\w{1,9})|(\d*.\d{2})", x).group()
y = ( x
. replace ( " { ' " , " " )
. replace ( " } " , " " )
. replace ( " Money( ' " , " " )
. replace ( " ' " , " " )
. replace ( " , USD " , " " )
. replace ( " ) " , " " )
. replace ( " ( " , " " )
. replace ( ' " ' , ' ' )
)
z = re . sub ( r " ((([A-Z][a-z]*( | & )) { 1,2}[A-Z][a-z]*:)|([A-Z][a-z]*)|529 Contrib) " , ' \n \\ 1 ' , y )
t = re . sub ( r " , \ n " , ' \n ' , z )
h = re . sub ( r ' ([a-z]*_[a-z]*) ' , lambda x : x [ 1 ] . replace ( ' _ ' , ' ' ) . capitalize ( ) , t )
p = re . sub ( r ' ( \ d { 1,5}. \ d {2} ) ' , ' $ \\ 1 ' , h )
o = re . sub ( r ' ( \ d { 4,5}) ' , lambda x : x [ 1 ] [ : - 3 ] + ' , ' + x [ 1 ] [ - 3 : ] , p )
return o
# categies = str([x['category'] for x in negative_budget ])[1:-1].replace('"','').replace("'",'')
# categies2 = str([x['category'] for x in some_budget_left ])[1:-1].replace('"','').replace("'",'')
# categies3 = str([x['category'] for x in no_budget_left ])[1:-1].replace('"','').replace("'",'')
# print(f"""Here's your spending status so far for this month. First, here are the categories where you've overspent:\n- {str(categies)}.\n\nHere is where you still have some budget left:\n- {str(categies2)}.\n\nAnd finally, here is where you're exactly where you need to be. $0 left.\n- {str(categies3)}""")
if __name__ == " __main__ " :
with Actual (
base_url = os . getenv ( ' BASEURL ' ) ,
password = os . getenv ( ' PASSWORD ' ) ,
encryption_password = None , # Optional: Password for the file encryption. Will not use it if set to None.
file = os . getenv ( ' FILE ' )
) as actual :
# main(actual)
compare_months ( actual )
# simple_track(actual)