diff --git a/financial_common.py b/financial_common.py new file mode 100644 index 0000000..3dad228 --- /dev/null +++ b/financial_common.py @@ -0,0 +1,41 @@ +from datetime import datetime +from dateutil.relativedelta import relativedelta + +DATE = 'Date' +ASSETS = 'Assets' +LIABILITIES = 'Liabilities' +CAPITAL = 'Capital' +DUES = 'Dues' +DONATIONS = 'Donations' +FOOD_DONATIONS = 'Food donations' +MEMBERS = 'Members' +DONATING_MEMBERS = 'Donating members' +EXPENSES = 'Expenses' +FOOD_EXPENSES = 'Food expenses' +PROJECTED_CAPITAL = 'Projected capital' +PROJECTED_DUES = 'Projected dues' +PROJECTED_DONATIONS = 'Projected donations' +PROJECTED_MEMBERS = 'Projected members' +PROJECTED_DONATING_MEMBERS = 'Projected donating members' +PROJECTED_FOOD_DONATIONS = 'Projected food donations' +PROJECTED_FOOD_EXPENSES = 'Projected food expenses' +CAPITAL_TARGET = 'Target balance (3 month buffer)' + +MONTH_START_DAY = 4 + +def figure_out_today_and_start_and_end_dates(): + today = datetime.now().replace(second=0, microsecond=0) + if today.day < MONTH_START_DAY: + today -= relativedelta(months=+1) + delta = relativedelta(months=+6) + start = today - delta + end = today + delta + + return [start, today, end] + +def report_days(start_date, end_date): + delta = relativedelta(months=+1) + d = start_date.replace(day=MONTH_START_DAY) + while d < end_date.replace(day=MONTH_START_DAY): + d += delta + yield d diff --git a/financial_csv.py b/financial_csv.py new file mode 100644 index 0000000..40d60de --- /dev/null +++ b/financial_csv.py @@ -0,0 +1,33 @@ +import csv + +import financial_common + +def write_to_csv_the(financial_records): + fieldnames = [ + DATE, + ASSETS, + LIABILITIES, + CAPITAL, + PROJECTED_CAPITAL, + DUES, + PROJECTED_DUES, + DONATIONS, + PROJECTED_DONATIONS, + MEMBERS, + PROJECTED_MEMBERS, + DONATING_MEMBERS, + PROJECTED_DONATING_MEMBERS, + EXPENSES, + FOOD_DONATIONS, + PROJECTED_FOOD_DONATIONS, + FOOD_EXPENSES, + PROJECTED_FOOD_EXPENSES, + CAPITAL_TARGET, + ] + + with open('foobar.csv', 'wb') as csvfile: + writer = csv.DictWriter(csvfile, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL, fieldnames=fieldnames) + + writer.writeheader() + for finances_for_a_month in financial_records: + writer.writerow(finances_for_a_month) diff --git a/financial_future.py b/financial_future.py new file mode 100644 index 0000000..f8b09e7 --- /dev/null +++ b/financial_future.py @@ -0,0 +1,68 @@ +from __future__ import print_function +from datetime import datetime + +import financial_common + +def get_future_projections_based_on( historical_records ): + future_projections = [] + + monthly_amount_projections = get_monthly_amount_projections_based_on( historical_records ) + + for month in months_to_project(): + starting_capital = future_projections[-1][PROJECTED_CAPITAL] if len(future_projections) else historical_records[-1][CAPITAL] + + future_projections.append( project_finances_for( month, starting_capital, finances_for_current_month, monthly_amount_projections ) ) + + return future_projections + +def months_to_project(): + start, today, end = figure_out_today_and_start_and_end_dates() + return report_days(today, end) + +def project_finances_for( month, starting_capital, finances_for_current_month, monthly_amount_projections ): + print(month) + + return { + # same for each projected month + DATE: month, + PROJECTED_DUES: finances_for_current_month[DUES], + PROJECTED_DONATIONS: finances_for_current_month[DONATIONS], + PROJECTED_MEMBERS: finances_for_current_month[MEMBERS], + PROJECTED_DONATING_MEMBERS: finances_for_current_month[DONATING_MEMBERS], + PROJECTED_FOOD_DONATIONS: monthly_amount_projections[ 'food income' ], + PROJECTED_FOOD_EXPENSES: monthly_amount_projections[ 'food expenses' ], + + # differs in each month + PROJECTED_CAPITAL: starting_capital + monthly_amount_projections['income'] + monthly_amount_projections['expenses'], + + # two different values - before & after rent increase + CAPITAL_TARGET: (monthly_amount_projections['expenses'] - rent_increase_for( month )) * -3, + } + +def get_monthly_amount_projections_based_on( historical_records ): + current_month_amounts = historical_records[-1] + + monthly_amount_projections['income'] = current_month_amounts[DUES] + current_month_amounts[DONATIONS] + monthly_amount_projections['expenses'] = get_average( EXPENSES, historical_records ) + monthly_amount_projections['food income'] = get_average( FOOD_DONATIONS, historical_records ) + monthly_amount_projections['food expenses'] = get_average( FOOD_EXPENSES, historical_records ) + + print( "Projected income: ", monthly_amount_projections['income'] ) + print( "Projected expenses: ", monthly_amount_projections['expenses'] ) + + return monthly_amount_projections + +def rent_increase_for( month ): + february_2015 = datetime(2015, 02, 01): + + if month >= february_2015 + rent_increase = 100 + else + rent_increase = 0 + + return rent_increase + +def get_average( datum_key, financial_records ): + total = sum( record[datum_key] for record in financial_records ) + + return total / len( financial_records ) diff --git a/financial_history.py b/financial_history.py new file mode 100644 index 0000000..e164778 --- /dev/null +++ b/financial_history.py @@ -0,0 +1,136 @@ +import calendar +import gnucashxml +import sys +from datetime import datetime +from __future__ import print_function + +import financial_common + +def get_historical_records(): + historical_records = [] + + book = get_requested_financial_book() + + for month in months_to_report_on(): + historical_records.append( book.finances_for( month ) ) + + return historical_records + +def get_requested_financial_book(): + provided_by_user = sys.argv[1] + return Book( gnucash_filename = provided_by_user ) + +def months_to_report_on(): + start, today, end = figure_out_today_and_start_and_end_dates() + return report_days( start, today ) + +class Book: + def __init__( self, gnucash_filename ): + self.book = gnucashxml.from_filename( gnucash_filename ) + + def finances_for( self, month, book ): + assets = self.get_assets_on( date = month ) + liabilities = self.get_liability_on( date = month ) + expenses = self.get_expenses_for( month ) + + monthly_finances = { + DATE: month, + ASSETS: assets, + LIABILITIES: liabilities, + CAPITAL: assets + liabilities, + DUES: self.get_amount_of( "Member Dues", month ), + DONATIONS: self.get_amount_of( "Regular donations", month ), + FOOD_DONATIONS: self.get_amount_of( "Food and Drink Donations", month ), + MEMBERS: self.get_number_of( "Member Dues", month ), + DONATING_MEMBERS: self.get_number_of( "Regular donations", month ), + EXPENSES: expenses, + FOOD_EXPENSES: self.get_amount_of( "Groceries", month ), + CAPITAL_TARGET: expenses * -3, + } + + display( monthly_finances ) + + return monthly_finances + + def get_assets_on( self, date ): + assets = self.book.find_account("Current Assets") + + asset_total = 0 + for account in assets.children: + if account.name != "Prepaid Rent": + asset_total += sum(split.value for split in account.splits if split.transaction.date.replace(tzinfo=None) <= date) + + return asset_total + + def get_liability_on( self, date ): + liability = self.get_total( "Active Members", date ) + self.get_total( "Former Members", date ) + + return liability + + def get_total( self, account_name, date ): + account = self.book.find_account( account_name ) + total = sum( split.value for split in account.get_all_splits() if split.transaction.date.replace(tzinfo=None) <= date) + + return total + + def get_expenses_for( self, month_end ): + end_date = month_end + start_date = subtract_month(month_end) + + expense_accounts = self.book.find_account("Expenses") + expenses = 0 + for account in expense_accounts.children: + if account.name != "Anti-social 10-04" and account.name != "Groceries": + for split in account.splits: + if start_date < split.transaction.date.replace(tzinfo=None) <= end_date: + expenses += split.value + + for subaccount in account.children: + for split in subaccount.splits: + if start_date < split.transaction.date.replace(tzinfo=None) <= end_date: + expenses += split.value + + + return expenses * -1 + + def get_number_of( self, account_name, month_end ): + return sum( len( entry.transaction.splits ) - 1 for entry in self.get_entries_for( account_name, month_end ) ) + + def get_amount_of( self, account_name, month_end ): + return sum( entry.value for entry in self.get_entries_for( account_name, month_end ) ) * -1 + + def get_entries_for( self, account_name, month_end ): + end_date = month_end + start_date = subtract_month(month_end) + + account = self.book.find_account( account_name ) + + entries = [] + + for split in account.get_all_splits(): + if start_date < split.transaction.date.replace(tzinfo=None) <= end_date: + entries.add( split ) + + return entries + +def subtract_month(date): + month = date.month - 2 + month = month % 12 + 1 + year = date.year - 1/12 + day = min(date.day, calendar.monthrange(year, month)[1]) + return datetime(year, month, day) + +def display( monthly_finances ): + print(monthly_finances[ DATE ]) + print("Total assets: ", monthly_finances[ ASSETS ]) + print("Total liability: ", monthly_finances[ LIABILITIES ]) + print("Available capital: ", monthly_finances[ CAPITAL ]) + print("Dues collected last month: ", monthly_finances[ DUES ]) + print("Dues paying members: ", monthly_finances[ MEMBERS ]) + print("Regular donations collected last month: ", monthly_finances[ DONATIONS ]) + print("Regularly donating members: ", monthly_finances[ DONATING_MEMBERS ]) + print("Food donations: ", monthly_finances[ FOOD_DONATIONS ]) + print("Total expected income: ", (monthly_finances[ DUES ] + monthly_finances[ DONATIONS ] + monthly_finances[ FOOD_DONATIONS ])) + print("Expenses: ", monthly_finances[ EXPENSES ]) + print("Food expenses: ", monthly_finances[ FOOD_EXPENSES ]) + print() diff --git a/forecast.py b/forecast.py index f83bfab..46cc466 100755 --- a/forecast.py +++ b/forecast.py @@ -1,329 +1,14 @@ #!/usr/bin/env python -from __future__ import print_function -import sys -from datetime import datetime -from dateutil.relativedelta import relativedelta -import calendar -#import getpass -import gnucashxml -import csv - - -DATE = 'Date' -ASSETS = 'Assets' -LIABILITIES = 'Liabilities' -CAPITAL = 'Capital' -DUES = 'Dues' -DONATIONS = 'Donations' -FOOD_DONATIONS = 'Food donations' -MEMBERS = 'Members' -DONATING_MEMBERS = 'Donating members' -EXPENSES = 'Expenses' -FOOD_EXPENSES = 'Food expenses' -PROJECTED_CAPITAL = 'Projected capital' -PROJECTED_DUES = 'Projected dues' -PROJECTED_DONATIONS = 'Projected donations' -PROJECTED_MEMBERS = 'Projected members' -PROJECTED_DONATING_MEMBERS = 'Projected donating members' -PROJECTED_FOOD_DONATIONS = 'Projected food donations' -PROJECTED_FOOD_EXPENSES = 'Projected food expenses' -CAPITAL_TARGET = 'Target balance (3 month buffer)' - -MONTH_START_DAY = 4 - +from financial_history import get_historical_records +from financial_future import get_future_projections_based_on +from financial_csv import write_to_csv_the def main(): - filename = sys.argv[1] - book = gnucashxml.from_filename(filename) - - today = datetime.now().replace(second=0, microsecond=0) - if today.day < MONTH_START_DAY: - today -= relativedelta(months=+1) - delta = relativedelta(months=+6) - start = today - delta - end = today + delta - - history = [] - - for month in report_days(start, today): - print(month) - assets = get_assets_on_date(book, month) - liabilities = get_liability_on_date(book, month) - capital = assets + liabilities - dues = get_dues_for_month(book, month) - donations = get_donations_for_month(book, month) - food_donations = get_food_donations_for_month(book, month) - members = get_paying_members(book, month) - donating_members = get_donating_members(book, month) - expenses = get_expenses_for_month(book, month) - food_expenses = get_food_expenses_for_month(book, month) - - print("Total assets: ", assets) - print("Total liability: ", liabilities) - print("Available capital: ", capital) - print("Dues collected last month: ", dues) - print("Dues paying members: ", members) - print("Regular donations collected last month: ", donations) - print("Regularly donating members: ", donating_members) - print("Food donations: ", food_donations) - print("Total expected income: ", (dues + donations + food_donations)) - print("Expenses: ", expenses) - print("Food expenses: ", food_expenses) - - history.append({ - DATE: month, - ASSETS: assets, - LIABILITIES: liabilities, - CAPITAL: capital, - DUES: dues, - DONATIONS: donations, - FOOD_DONATIONS: food_donations, - MEMBERS: members, - DONATING_MEMBERS: donating_members, - EXPENSES: expenses, - FOOD_EXPENSES: food_expenses, - CAPITAL_TARGET: expenses * -3, - }) - - print() - - income = get_projected_income(history) - expenses = get_projected_expenses(history) - print("Projected income: ", income) - print("Projected expenses: ", expenses) - - food_income = get_projected_food_income(history) - food_expenses = get_projected_food_expenses(history) - - history[-1][PROJECTED_CAPITAL] = history[-1][CAPITAL] - history[-1][PROJECTED_DUES] = history[-1][DUES] - history[-1][PROJECTED_DONATIONS] = history[-1][DONATIONS] - history[-1][PROJECTED_MEMBERS] = history[-1][MEMBERS] - history[-1][PROJECTED_DONATING_MEMBERS] = history[-1][DONATING_MEMBERS] - #history[-1][PROJECTED_FOOD_DONATIONS] = history[-1][FOOD_DONATIONS] - #history[-1][PROJECTED_FOOD_EXPENSES] = history[-1][FOOD_EXPENSES] - - - for month in report_days(today, end): - print(month) - - rent_increase = 0 - if month >= datetime(2015, 02, 01): - rent_increase = -100 - - history.append({ - DATE: month, - PROJECTED_CAPITAL: history[-1][PROJECTED_CAPITAL] + income + expenses, - PROJECTED_DUES: history[-1][PROJECTED_DUES], - PROJECTED_DONATIONS: history[-1][PROJECTED_DONATIONS], - PROJECTED_MEMBERS: history[-1][PROJECTED_MEMBERS], - PROJECTED_DONATING_MEMBERS: history[-1][PROJECTED_DONATING_MEMBERS], - PROJECTED_FOOD_DONATIONS: food_income, - PROJECTED_FOOD_EXPENSES: food_expenses, - CAPITAL_TARGET: (expenses + rent_increase) * -3, - }) - - with open('foobar.csv', 'wb') as csvfile: - fieldnames = [ - DATE, - ASSETS, - LIABILITIES, - CAPITAL, - PROJECTED_CAPITAL, - DUES, - PROJECTED_DUES, - DONATIONS, - PROJECTED_DONATIONS, - MEMBERS, - PROJECTED_MEMBERS, - DONATING_MEMBERS, - PROJECTED_DONATING_MEMBERS, - EXPENSES, - FOOD_DONATIONS, - PROJECTED_FOOD_DONATIONS, - FOOD_EXPENSES, - PROJECTED_FOOD_EXPENSES, - CAPITAL_TARGET, - ] - - - - writer = csv.DictWriter(csvfile, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL, fieldnames=fieldnames) - - writer.writeheader() - for history_item in history: - writer.writerow(history_item) - - -def get_assets_on_date(book, date): - assets = book.find_account("Current Assets") - - asset_total = 0 - for account in assets.children: - if account.name != "Prepaid Rent": - asset_total += sum(split.value for split in account.splits if split.transaction.date.replace(tzinfo=None) <= date) - - return asset_total - - -def get_liability_on_date(book, date): - liabilities = book.find_account("Active Members") - liability_total = sum(split.value for split in liabilities.get_all_splits() if split.transaction.date.replace(tzinfo=None) <= date) - - liabilities = book.find_account("Former Members") - liability_total += sum(split.value for split in liabilities.get_all_splits() if split.transaction.date.replace(tzinfo=None) <= date) - - return liability_total - - -def get_dues_for_month(book, month_end): - end_date = month_end - start_date = subtract_month(month_end) - - member_dues = book.find_account("Member Dues") - dues = 0 - for split in member_dues.get_all_splits(): - if start_date < split.transaction.date.replace(tzinfo=None) <= end_date: - dues += split.value - - return dues * -1 - - -def get_paying_members(book, month_end): - end_date = month_end - start_date = subtract_month(month_end) - - member_dues = book.find_account("Member Dues") - members = 0 - for split in member_dues.get_all_splits(): - if start_date < split.transaction.date.replace(tzinfo=None) <= end_date: - members += len(split.transaction.splits) - 1 - - return members - - -def get_donations_for_month(book, month_end): - end_date = month_end - start_date = subtract_month(month_end) - - member_donations = book.find_account("Regular donations") - donations = 0 - for split in member_donations.get_all_splits(): - if start_date < split.transaction.date.replace(tzinfo=None) <= end_date: - donations += split.value - - return donations * -1 - - -def get_expenses_for_month(book, month_end): - end_date = month_end - start_date = subtract_month(month_end) - - expense_accounts = book.find_account("Expenses") - expenses = 0 - for account in expense_accounts.children: - if account.name != "Anti-social 10-04" and account.name != "Groceries": - #print(account.name) - for split in account.splits: - if start_date < split.transaction.date.replace(tzinfo=None) <= end_date: - expenses += split.value - #print("\t", split.transaction.description, " \t", split.value) - - for subaccount in account.children: - #print("\t", subaccount.name) - for split in subaccount.splits: - if start_date < split.transaction.date.replace(tzinfo=None) <= end_date: - expenses += split.value - #print("\t\t", split.transaction.description, " \t", split.value) - - - return expenses * -1 - - -def get_food_expenses_for_month(book, month_end): - end_date = month_end - start_date = subtract_month(month_end) - - expense_accounts = book.find_account("Groceries") - expenses = 0 - for split in expense_accounts.get_all_splits(): - if start_date < split.transaction.date.replace(tzinfo=None) <= end_date: - expenses += split.value - #print("\t", split.transaction.description, " \t", split.value) - - return expenses * -1 - - -def get_food_donations_for_month(book, month_end): - end_date = month_end - start_date = subtract_month(month_end) - - food_donations = book.find_account("Food and Drink Donations") - donations = 0 - for split in food_donations.get_all_splits(): - if start_date < split.transaction.date.replace(tzinfo=None) <= end_date: - donations += split.value - - return donations * -1 - - -def get_donating_members(book, month_end): - end_date = month_end - start_date = subtract_month(month_end) - - member_dues = book.find_account("Regular donations") - members = 0 - for split in member_dues.get_all_splits(): - if start_date < split.transaction.date.replace(tzinfo=None) <= end_date: - members += len(split.transaction.splits) - 1 - - return members - - -def subtract_month(date): - month = date.month - 2 - month = month % 12 + 1 - year = date.year - 1/12 - day = min(date.day, calendar.monthrange(year, month)[1]) - return datetime(year, month, day) - - -def report_days(start_date, end_date): - delta = relativedelta(months=+1) - d = start_date.replace(day=MONTH_START_DAY) - while d < end_date.replace(day=MONTH_START_DAY): - d += delta - yield d - - -def get_projected_income(history): - return history[-1][DUES] + history[-1][DONATIONS] - - -def get_projected_expenses(history): - expenses = 0 - for data_point in history: - expenses += data_point[EXPENSES] - - return expenses / len(history) - - -def get_projected_food_income(history): - income = 0 - for data_point in history: - income += data_point[FOOD_DONATIONS] - - return income / len(history) - - -def get_projected_food_expenses(history): - expenses = 0 - for data_point in history: - expenses += data_point[FOOD_EXPENSES] - - return expenses / len(history) - + historical_records = get_historical_records() + future_projections = get_future_projections_based_on( historical_records ) + write_to_csv_the( financial_records = historical_records + future_projections ) +# should it always just call main() if __name__ == "__main__": main()