diff --git a/gnucash_reports/collate/key_generator.py b/gnucash_reports/collate/key_generator.py index 129a2e5..5bf2f98 100644 --- a/gnucash_reports/collate/key_generator.py +++ b/gnucash_reports/collate/key_generator.py @@ -4,6 +4,7 @@ from dateutil.rrule import rrule, MONTHLY from gnucash_reports.configuration.expense_categories import get_category_for_account +from gnucash_reports.utilities import clean_account_name def monthly(data_key): @@ -33,7 +34,7 @@ def category_key_fetcher(data_key): :param data_key: split :return: """ - return get_category_for_account(data_key.account.fullname.replace(':', '.')) + return get_category_for_account(clean_account_name(data_key.account.fullname)) def account_key_fetcher(data_key): diff --git a/gnucash_reports/configuration/expense_categories.py b/gnucash_reports/configuration/expense_categories.py index 59d844a..02bdd0b 100644 --- a/gnucash_reports/configuration/expense_categories.py +++ b/gnucash_reports/configuration/expense_categories.py @@ -3,6 +3,7 @@ that it is stored in. """ from gnucash_reports.wrapper import account_walker, parse_walker_parameters +from gnucash_reports.utilities import clean_account_name _expense_categories = dict() _reverse = dict() @@ -24,7 +25,7 @@ def configure(json_dictionary): all_accounts = set() for account in account_walker(**accounts): # print 'loading account: %s' % account.fullname - all_accounts.add(account.fullname.replace(':', '.')) + all_accounts.add(clean_account_name(account.fullname)) for account in all_accounts: _reverse[account] = category @@ -39,7 +40,7 @@ def get_category_for_account(account_name): :return: """ # Translate the account name to use the common formatting. - account_name = account_name.replace(':', '.') + account_name = clean_account_name(account_name) value = _reverse.get(account_name, _default_category) if value == _default_category: diff --git a/gnucash_reports/reports/budget_level.py b/gnucash_reports/reports/budget_level.py index 2d581cb..58ede92 100644 --- a/gnucash_reports/reports/budget_level.py +++ b/gnucash_reports/reports/budget_level.py @@ -35,7 +35,7 @@ def budget_level(definition): yearly_balance = Decimal('0.0') for account in account_walker(**accounts): - split_list = get_splits(account, PeriodStart.this_year.date, PeriodEnd.this_year.date, debit=False) + split_list = get_splits(account, PeriodStart.this_year.date, PeriodEnd.today.date, debit=False) for split in split_list: yearly_balance += split.value diff --git a/gnucash_reports/reports/expenses_monthly.py b/gnucash_reports/reports/expenses_monthly.py index 29bb2eb..fe2e143 100644 --- a/gnucash_reports/reports/expenses_monthly.py +++ b/gnucash_reports/reports/expenses_monthly.py @@ -77,7 +77,7 @@ def expenses_box(definition): def expenses_categories(definition): expense_accounts = parse_walker_parameters(definition.get('expenses_base', [])) - start_period = PeriodStart(definition.get('period_start', PeriodStart.this_month_year_ago)) + start_period = PeriodStart(definition.get('period_start', PeriodStart.this_month)) end_period = PeriodEnd(definition.get('period_end', PeriodEnd.this_month)) bucket = CategoryCollate(decimal_generator, split_summation) diff --git a/gnucash_reports/reports/income_tax.py b/gnucash_reports/reports/income_tax.py index cb543be..30d7e3d 100644 --- a/gnucash_reports/reports/income_tax.py +++ b/gnucash_reports/reports/income_tax.py @@ -6,6 +6,7 @@ from gnucash_reports.configuration.tax_tables import calculate_tax from gnucash_reports.periods import PeriodStart, PeriodEnd from gnucash_reports.wrapper import get_splits, account_walker, parse_walker_parameters +from gnucash_reports.utilities import clean_account_name def income_tax(definition): @@ -15,20 +16,46 @@ def income_tax(definition): period_end = PeriodEnd(definition.get('period_end', PeriodEnd.this_year)) tax_name = definition.get('tax_name', 'federal') tax_status = definition.get('tax_status', 'single') + deductions = definition.get('deductions', []) + deduction_accounts = parse_walker_parameters(definition.get('deduction_accounts', [])) total_income = Decimal(0.0) total_taxes = Decimal(0.0) + pre_tax_deductions = Decimal(0.0) + # Find all of the deduction accounts, and store them in a list so that they can be walked when handling the + # income from the income accounts + deduction_account_names = set() + for account in account_walker(**deduction_accounts): + deduction_account_names.add(clean_account_name(account.fullname)) + + # Calculate all of the income that has been received, and calculate all of the contributions to the deduction + # accounts that will reduce tax burden. for account in account_walker(**income_accounts): for split in get_splits(account, period_start.date, period_end.date): value = split.value * -1 # negate the value because income is leaving these accounts total_income += value + # Go through the split's parent and find all of the values that are in the deduction accounts as well + transaction = split.transaction + for t_split in transaction.splits: + if clean_account_name(t_split.account.fullname) in deduction_account_names: + pre_tax_deductions += t_split.value + + # Calculate all of the taxes that have been currently paid for account in account_walker(**tax_accounts): for split in get_splits(account, period_start.date, period_end.date): value = split.value total_taxes += value + # Remove all of the deductions from the total income value + for deduction in deductions: + pre_tax_deductions += Decimal(deduction) + + # Remove all of the contributions from the income accounts that went into pre-tax accounts and any standard + # deductions + total_income -= pre_tax_deductions + tax_value = calculate_tax(tax_name, tax_status, total_income) return dict(income=total_income, tax_value=tax_value, taxes_paid=total_taxes) diff --git a/gnucash_reports/reports/retirement_401k.py b/gnucash_reports/reports/retirement_401k.py index e393d68..a24afb3 100644 --- a/gnucash_reports/reports/retirement_401k.py +++ b/gnucash_reports/reports/retirement_401k.py @@ -7,6 +7,7 @@ from gnucash_reports.periods import PeriodStart, PeriodEnd from gnucash_reports.wrapper import get_account, get_splits +from gnucash_reports.utilities import clean_account_name def retirement_401k_report(definition): @@ -23,7 +24,7 @@ def retirement_401k_report(definition): today = date.today() beginning_of_year = date(today.year, 1, 1) - retirement_accounts = [account_name.replace(':', '.') for account_name in retirement_accounts] + retirement_accounts = [clean_account_name(account_name) for account_name in retirement_accounts] for account_name in income_accounts: account = get_account(account_name) @@ -33,7 +34,7 @@ def retirement_401k_report(definition): for income_split in parent.splits: - account_full_name = income_split.account.fullname.replace(':', '.') + account_full_name = clean_account_name(income_split.account.fullname) if account_full_name in retirement_accounts: contribution_total += income_split.value diff --git a/gnucash_reports/utilities.py b/gnucash_reports/utilities.py index f024ea6..e2478bf 100644 --- a/gnucash_reports/utilities.py +++ b/gnucash_reports/utilities.py @@ -12,4 +12,9 @@ def load_plugins(): # Register the configuration for ep in pkg_resources.iter_entry_points(group='gnucash_reports_configuration'): loader = ep.load() - loader() \ No newline at end of file + loader() + + +def clean_account_name(account_name): + """Replace account names with colons as separators with periods as separators.""" + return account_name.replace(':', '.') diff --git a/gnucash_reports/wrapper.py b/gnucash_reports/wrapper.py index 513af2c..377c08a 100644 --- a/gnucash_reports/wrapper.py +++ b/gnucash_reports/wrapper.py @@ -1,6 +1,7 @@ from datetime import datetime from decimal import Decimal from gnucash_reports.configuration.current_date import get_today +from gnucash_reports.utilities import clean_account_name import enum import piecash @@ -29,6 +30,8 @@ class AccountTypes(enum.Enum): gnucash_session = None +_account_cache = dict() + def initialize(file_uri, read_only=True, do_backup=False): global gnucash_session @@ -41,14 +44,28 @@ def get_session(): def get_account(account_name): + global _account_cache current_account = gnucash_session.root_account + current_account_name = '' + for child_name in re.split('[:.]', account_name): - account = gnucash_session.session.query(piecash.Account).filter(piecash.Account.parent == current_account, - piecash.Account.name == child_name).one_or_none() + + if current_account_name: + current_account_name = current_account_name + '.' + child_name + else: + current_account_name = child_name + + account = _account_cache.get(current_account_name, None) if account is None: - raise RuntimeError('Account %s is not found in %s' % (account_name, current_account)) + account = gnucash_session.session.query(piecash.Account).filter(piecash.Account.parent == current_account, + piecash.Account.name == child_name).one_or_none() + + if account is None: + raise RuntimeError('Account %s is not found in %s' % (account_name, current_account)) + + _account_cache[current_account_name] = account current_account = account @@ -104,6 +121,8 @@ def account_walker(accounts, ignores=None, place_holders=False, recursive=True, _account_list = [a for a in accounts] + ignores = [clean_account_name(account_name) for account_name in ignores] + while _account_list: account_name = _account_list.pop() if account_name in ignores: @@ -114,7 +133,7 @@ def account_walker(accounts, ignores=None, place_holders=False, recursive=True, yield account if recursive: - _account_list += [a.fullname for a in account.children] + _account_list += [clean_account_name(a.fullname) for a in account.children] def parse_walker_parameters(definition):