From a5f86bad0ddfb5b7306488deb371b13402602a36 Mon Sep 17 00:00:00 2001 From: Miguel Sanda Date: Mon, 27 Mar 2023 14:23:24 -0400 Subject: [PATCH] V0.5.2.17 (#140) * Balance Sheet Statement Template Bugfixes io_context refactoring IncomeStatementContextManager develop * Income Statement IO Context Income Statement HTML format improvements. Refactoring * income_statement.html Income Statement IO Context Update --- django_ledger/__init__.py | 2 +- django_ledger/io/data_generator.py | 4 +- django_ledger/io/financial_statements.py | 126 ----------- django_ledger/io/io_context.py | 209 +++++++++++++++++- django_ledger/io/io_mixin.py | 35 +-- django_ledger/io/roles.py | 34 ++- django_ledger/models/coa_default.py | 80 +++---- .../tags/balance_sheet_statement.html | 14 +- .../tags/income_statement.html | 203 +++++++++++++++-- django_ledger/templatetags/django_ledger.py | 7 +- pyproject.toml | 2 +- 11 files changed, 498 insertions(+), 218 deletions(-) delete mode 100644 django_ledger/io/financial_statements.py diff --git a/django_ledger/__init__.py b/django_ledger/__init__.py index 2cc5ef71..9a0ec960 100644 --- a/django_ledger/__init__.py +++ b/django_ledger/__init__.py @@ -9,7 +9,7 @@ default_app_config = 'django_ledger.apps.DjangoLedgerConfig' """Django Ledger""" -__version__ = '0.5.2.16' +__version__ = '0.5.2.17' __license__ = 'GPLv3 License' __author__ = 'Miguel Sanda' diff --git a/django_ledger/io/data_generator.py b/django_ledger/io/data_generator.py index 01f2359d..8906ba49 100644 --- a/django_ledger/io/data_generator.py +++ b/django_ledger/io/data_generator.py @@ -17,7 +17,7 @@ from django.utils.timezone import localtime, localdate from django_ledger.io.roles import (INCOME_OPERATIONAL, ASSET_CA_INVENTORY, COGS, ASSET_CA_CASH, ASSET_CA_PREPAID, - LIABILITY_CL_DEFERRED_REVENUE, EXPENSE_REGULAR, EQUITY_CAPITAL, + LIABILITY_CL_DEFERRED_REVENUE, EXPENSE_OPERATIONAL, EQUITY_CAPITAL, ASSET_CA_RECEIVABLES, LIABILITY_CL_ACC_PAYABLE) from django_ledger.models import (EntityModel, TransactionModel, AccountModel, VendorModel, CustomerModel, EntityUnitModel, BankAccountModel, LedgerModel, UnitOfMeasureModel, ItemModel, @@ -370,7 +370,7 @@ def create_expenses(self): entity=self.entity_model, is_product_or_service=False, for_inventory=False, - expense_account=choice(self.accounts_by_role[EXPENSE_REGULAR]), + expense_account=choice(self.accounts_by_role[EXPENSE_OPERATIONAL]), ) for _ in range(expense_count) ] diff --git a/django_ledger/io/financial_statements.py b/django_ledger/io/financial_statements.py deleted file mode 100644 index 5447b266..00000000 --- a/django_ledger/io/financial_statements.py +++ /dev/null @@ -1,126 +0,0 @@ -from django.core.exceptions import ValidationError - -from django_ledger.io.io_context import GroupManager -from django_ledger.io.roles import ASSET_CA_CASH -from django_ledger.models.utils import lazy_loader - - -class CashFlowStatementError(ValidationError): - pass - - -class CashFlowStatement: - CFS_DIGEST_KEY = 'cash_flow_statement' - - def __init__(self, - io_digest: dict, - by_period: bool = False, - by_unit: bool = False): - self.IO_DIGEST = io_digest - self.CASH_ACCOUNTS = [a for a in self.IO_DIGEST['accounts'] if a['role'] == ASSET_CA_CASH] - self.JE_MODEL = lazy_loader.get_journal_entry_model() - - def check_io_digest(self): - if GroupManager.GROUP_BALANCE_KEY not in self.IO_DIGEST: - raise CashFlowStatementError( - 'IO Digest must have groups for Cash Flow Statement' - ) - - def operating(self): - group_balances = self.IO_DIGEST[GroupManager.GROUP_BALANCE_KEY] - operating_activities = dict() - operating_activities['GROUP_CFS_NET_INCOME'] = { - 'description': 'Net Income', - 'balance': group_balances['GROUP_CFS_NET_INCOME'] - } - operating_activities['GROUP_CFS_OP_DEPRECIATION_AMORTIZATION'] = { - 'description': 'Depreciation & Amortization of Assets', - 'balance': -group_balances['GROUP_CFS_OP_DEPRECIATION_AMORTIZATION'] - } - operating_activities['GROUP_CFS_OP_INVESTMENT_GAINS'] = { - 'description': 'Gain/Loss Sale of Assets', - 'balance': group_balances['GROUP_CFS_OP_INVESTMENT_GAINS'] - } - operating_activities['GROUP_CFS_OP_ACCOUNTS_RECEIVABLE'] = { - 'description': 'Accounts Receivable', - 'balance': -group_balances['GROUP_CFS_OP_ACCOUNTS_RECEIVABLE'] - } - operating_activities['GROUP_CFS_OP_INVENTORY'] = { - 'description': 'Inventories', - 'balance': -group_balances['GROUP_CFS_OP_INVENTORY'] - } - - operating_activities['GROUP_CFS_OP_ACCOUNTS_PAYABLE'] = { - 'description': 'Accounts Payable', - 'balance': group_balances['GROUP_CFS_OP_ACCOUNTS_PAYABLE'] - } - operating_activities['GROUP_CFS_OP_OTHER_CURRENT_ASSETS_ADJUSTMENT'] = { - 'description': 'Other Current Assets', - 'balance': -group_balances['GROUP_CFS_OP_OTHER_CURRENT_ASSETS_ADJUSTMENT'] - } - operating_activities['GROUP_CFS_OP_OTHER_CURRENT_LIABILITIES_ADJUSTMENT'] = { - 'description': 'Other Current Liabilities', - 'balance': group_balances['GROUP_CFS_OP_OTHER_CURRENT_LIABILITIES_ADJUSTMENT'] - } - - net_cash_by_op_activities = sum(i['balance'] for g, i in operating_activities.items()) - self.IO_DIGEST[self.CFS_DIGEST_KEY]['operating'] = operating_activities - self.IO_DIGEST[self.CFS_DIGEST_KEY]['net_cash_by_activity'] = dict( - OPERATING=net_cash_by_op_activities - ) - - def financing(self): - group_balances = self.IO_DIGEST[GroupManager.GROUP_BALANCE_KEY] - financing_activities = dict() - financing_activities['GROUP_CFS_FIN_ISSUING_EQUITY'] = { - 'description': 'Common Stock, Preferred Stock and Capital Raised', - 'balance': sum(a['balance'] for a in self.CASH_ACCOUNTS if a['activity'] == self.JE_MODEL.FINANCING_EQUITY) - } - financing_activities['GROUP_CFS_FIN_DIVIDENDS'] = { - 'description': 'Dividends Payed Out to Shareholders', - 'balance': sum( - a['balance'] for a in self.CASH_ACCOUNTS if a['activity'] == self.JE_MODEL.FINANCING_DIVIDENDS) - } - financing_activities['GROUP_CFS_FIN_ST_DEBT_PAYMENTS'] = { - 'description': 'Increase/Reduction of Short-Term Debt Principal', - 'balance': sum(a['balance'] for a in self.CASH_ACCOUNTS if a['activity'] == self.JE_MODEL.FINANCING_STD) - } - financing_activities['GROUP_CFS_FIN_LT_DEBT_PAYMENTS'] = { - 'description': 'Increase/Reduction of Long-Term Debt Principal', - 'balance': sum(a['balance'] for a in self.CASH_ACCOUNTS if a['activity'] == self.JE_MODEL.FINANCING_LTD) - } - - net_cash = sum(i['balance'] for g, i in financing_activities.items()) - self.IO_DIGEST[self.CFS_DIGEST_KEY]['financing'] = financing_activities - self.IO_DIGEST[self.CFS_DIGEST_KEY]['net_cash_by_activity']['FINANCING'] = net_cash - - def investing(self): - group_balances = self.IO_DIGEST[GroupManager.GROUP_BALANCE_KEY] - investing_activities = dict() - investing_activities['GROUP_CFS_INVESTING_SECURITIES'] = { - 'description': 'Purchase, Maturity and Sales of Investments & Securities', - 'balance': sum( - a['balance'] for a in self.CASH_ACCOUNTS if a['activity'] == self.JE_MODEL.INVESTING_SECURITIES) - } - investing_activities['GROUP_CFS_INVESTING_PPE'] = { - 'description': 'Addition and Disposition of Property, Plant & Equipment', - 'balance': sum( - a['balance'] for a in self.CASH_ACCOUNTS if a['activity'] == self.JE_MODEL.INVESTING_PPE) - } - - net_cash = sum(i['balance'] for g, i in investing_activities.items()) - self.IO_DIGEST[self.CFS_DIGEST_KEY]['investing'] = investing_activities - self.IO_DIGEST[self.CFS_DIGEST_KEY]['net_cash_by_activity']['INVESTING'] = net_cash - - def net_cash(self): - self.IO_DIGEST[self.CFS_DIGEST_KEY]['net_cash'] = sum([ - bal for act, bal in self.IO_DIGEST[self.CFS_DIGEST_KEY]['net_cash_by_activity'].items() - ]) - - def digest(self): - self.check_io_digest() - self.operating() - self.financing() - self.investing() - self.net_cash() - return self.IO_DIGEST diff --git a/django_ledger/io/io_context.py b/django_ledger/io/io_context.py index ab88dbc7..12726195 100644 --- a/django_ledger/io/io_context.py +++ b/django_ledger/io/io_context.py @@ -1,13 +1,15 @@ from collections import defaultdict from itertools import groupby, chain +from django.core.exceptions import ValidationError + from django_ledger.io import roles as roles_module -from django_ledger.models.utils import LazyLoader +from django_ledger.models.utils import LazyLoader, lazy_loader lazy_importer = LazyLoader() -class RoleManager: +class RoleContextManager: def __init__(self, tx_digest: dict, @@ -73,7 +75,7 @@ def process_roles(self): acc['balance'] for acc in acc_list if acc['unit_uuid'] == key[0]) -class GroupManager: +class GroupContextManager: GROUP_ACCOUNTS_KEY = 'group_account' GROUP_BALANCE_KEY = 'group_balance' GROUP_BALANCE_BY_UNIT_KEY = 'group_balance_by_unit' @@ -146,7 +148,7 @@ def process_groups(self): ) -class ActivityManager: +class ActivityContextManager: def __init__(self, tx_digest: dict, @@ -208,7 +210,7 @@ def process_activity(self): acc['balance'] for acc in acc_list if acc['unit_uuid'] == key[0]) -class BalanceSheetManager: +class BalanceSheetStatementContextManager: def __init__(self, tx_digest: dict): self.DIGEST = tx_digest @@ -240,3 +242,200 @@ def digest(self): role_data['total_balance'] = sum(a['balance'] for a in role_data['accounts']) role_data['role_name'] = roles_module.ACCOUNT_LIST_ROLE_VERBOSE[acc_role] return self.DIGEST + + +class IncomeStatementContextManager: + + def __init__(self, tx_digest: dict): + self.DIGEST = tx_digest + + def digest(self): + if 'group_account' in self.DIGEST: + self.DIGEST['income_statement'] = { + 'operating': { + 'revenues': [ + acc for acc in self.DIGEST['group_account']['GROUP_INCOME'] if + acc['role'] in roles_module.GROUP_IC_OPERATING_REVENUES + ], + 'cogs': [ + acc for acc in self.DIGEST['group_account']['GROUP_COGS'] if + acc['role'] in roles_module.GROUP_IC_OPERATING_COGS + ], + 'expenses': [ + acc for acc in self.DIGEST['group_account']['GROUP_EXPENSES'] if + acc['role'] in roles_module.GROUP_IC_OPERATING_EXPENSES + ] + }, + 'other': { + 'revenues': [acc for acc in self.DIGEST['group_account']['GROUP_INCOME'] if + acc['role'] in roles_module.GROUP_IC_OTHER_REVENUES], + 'expenses': [acc for acc in self.DIGEST['group_account']['GROUP_INCOME'] if + acc['role'] in roles_module.GROUP_IC_OTHER_EXPENSES], + } + } + + # OPERATING INCOME... + self.DIGEST['income_statement']['operating']['gross_profit'] = sum( + acc['balance'] for acc in chain.from_iterable( + [ + self.DIGEST['income_statement']['operating']['revenues'], + self.DIGEST['income_statement']['operating']['cogs'] + ] + )) + self.DIGEST['income_statement']['operating']['net_operating_income'] = sum( + acc['balance'] for acc in chain.from_iterable( + [ + self.DIGEST['income_statement']['operating']['revenues'], + self.DIGEST['income_statement']['operating']['cogs'], + self.DIGEST['income_statement']['operating']['expenses'], + ] + )) + self.DIGEST['income_statement']['operating']['net_operating_revenue'] = sum( + acc['balance'] for acc in self.DIGEST['income_statement']['operating']['revenues'] + ) + self.DIGEST['income_statement']['operating']['net_cogs'] = sum( + acc['balance'] for acc in self.DIGEST['income_statement']['operating']['cogs'] + ) + self.DIGEST['income_statement']['operating']['net_operating_expenses'] = sum( + acc['balance'] for acc in self.DIGEST['income_statement']['operating']['expenses'] + ) + + + # OTHER INCOME.... + self.DIGEST['income_statement']['other']['net_other_revenues'] = sum( + acc['balance'] for acc in self.DIGEST['income_statement']['other']['revenues'] + ) + self.DIGEST['income_statement']['other']['net_other_expenses'] = sum( + acc['balance'] for acc in self.DIGEST['income_statement']['other']['expenses'] + ) + self.DIGEST['income_statement']['other']['net_other_income'] = sum( + acc['balance'] for acc in chain.from_iterable( + [ + self.DIGEST['income_statement']['other']['revenues'], + self.DIGEST['income_statement']['other']['expenses'] + ] + )) + + # NET INCOME... + self.DIGEST['income_statement']['net_income'] = self.DIGEST['income_statement']['operating'][ + 'net_operating_income'] + self.DIGEST['income_statement']['net_income'] += self.DIGEST['income_statement']['other'][ + 'net_other_income'] + return self.DIGEST + + +class CashFlowStatementContextManager: + CFS_DIGEST_KEY = 'cash_flow_statement' + + def __init__(self, + io_digest: dict, + by_period: bool = False, + by_unit: bool = False): + self.IO_DIGEST = io_digest + self.CASH_ACCOUNTS = [a for a in self.IO_DIGEST['accounts'] if a['role'] == roles_module.ASSET_CA_CASH] + self.JE_MODEL = lazy_loader.get_journal_entry_model() + + def check_io_digest(self): + if GroupContextManager.GROUP_BALANCE_KEY not in self.IO_DIGEST: + raise ValidationError( + 'IO Digest must have groups for Cash Flow Statement' + ) + + def operating(self): + group_balances = self.IO_DIGEST[GroupContextManager.GROUP_BALANCE_KEY] + operating_activities = dict() + operating_activities['GROUP_CFS_NET_INCOME'] = { + 'description': 'Net Income', + 'balance': group_balances['GROUP_CFS_NET_INCOME'] + } + operating_activities['GROUP_CFS_OP_DEPRECIATION_AMORTIZATION'] = { + 'description': 'Depreciation & Amortization of Assets', + 'balance': -group_balances['GROUP_CFS_OP_DEPRECIATION_AMORTIZATION'] + } + operating_activities['GROUP_CFS_OP_INVESTMENT_GAINS'] = { + 'description': 'Gain/Loss Sale of Assets', + 'balance': group_balances['GROUP_CFS_OP_INVESTMENT_GAINS'] + } + operating_activities['GROUP_CFS_OP_ACCOUNTS_RECEIVABLE'] = { + 'description': 'Accounts Receivable', + 'balance': -group_balances['GROUP_CFS_OP_ACCOUNTS_RECEIVABLE'] + } + operating_activities['GROUP_CFS_OP_INVENTORY'] = { + 'description': 'Inventories', + 'balance': -group_balances['GROUP_CFS_OP_INVENTORY'] + } + + operating_activities['GROUP_CFS_OP_ACCOUNTS_PAYABLE'] = { + 'description': 'Accounts Payable', + 'balance': group_balances['GROUP_CFS_OP_ACCOUNTS_PAYABLE'] + } + operating_activities['GROUP_CFS_OP_OTHER_CURRENT_ASSETS_ADJUSTMENT'] = { + 'description': 'Other Current Assets', + 'balance': -group_balances['GROUP_CFS_OP_OTHER_CURRENT_ASSETS_ADJUSTMENT'] + } + operating_activities['GROUP_CFS_OP_OTHER_CURRENT_LIABILITIES_ADJUSTMENT'] = { + 'description': 'Other Current Liabilities', + 'balance': group_balances['GROUP_CFS_OP_OTHER_CURRENT_LIABILITIES_ADJUSTMENT'] + } + + net_cash_by_op_activities = sum(i['balance'] for g, i in operating_activities.items()) + self.IO_DIGEST[self.CFS_DIGEST_KEY]['operating'] = operating_activities + self.IO_DIGEST[self.CFS_DIGEST_KEY]['net_cash_by_activity'] = dict( + OPERATING=net_cash_by_op_activities + ) + + def financing(self): + group_balances = self.IO_DIGEST[GroupContextManager.GROUP_BALANCE_KEY] + financing_activities = dict() + financing_activities['GROUP_CFS_FIN_ISSUING_EQUITY'] = { + 'description': 'Common Stock, Preferred Stock and Capital Raised', + 'balance': sum(a['balance'] for a in self.CASH_ACCOUNTS if a['activity'] == self.JE_MODEL.FINANCING_EQUITY) + } + financing_activities['GROUP_CFS_FIN_DIVIDENDS'] = { + 'description': 'Dividends Payed Out to Shareholders', + 'balance': sum( + a['balance'] for a in self.CASH_ACCOUNTS if a['activity'] == self.JE_MODEL.FINANCING_DIVIDENDS) + } + financing_activities['GROUP_CFS_FIN_ST_DEBT_PAYMENTS'] = { + 'description': 'Increase/Reduction of Short-Term Debt Principal', + 'balance': sum(a['balance'] for a in self.CASH_ACCOUNTS if a['activity'] == self.JE_MODEL.FINANCING_STD) + } + financing_activities['GROUP_CFS_FIN_LT_DEBT_PAYMENTS'] = { + 'description': 'Increase/Reduction of Long-Term Debt Principal', + 'balance': sum(a['balance'] for a in self.CASH_ACCOUNTS if a['activity'] == self.JE_MODEL.FINANCING_LTD) + } + + net_cash = sum(i['balance'] for g, i in financing_activities.items()) + self.IO_DIGEST[self.CFS_DIGEST_KEY]['financing'] = financing_activities + self.IO_DIGEST[self.CFS_DIGEST_KEY]['net_cash_by_activity']['FINANCING'] = net_cash + + def investing(self): + group_balances = self.IO_DIGEST[GroupContextManager.GROUP_BALANCE_KEY] + investing_activities = dict() + investing_activities['GROUP_CFS_INVESTING_SECURITIES'] = { + 'description': 'Purchase, Maturity and Sales of Investments & Securities', + 'balance': sum( + a['balance'] for a in self.CASH_ACCOUNTS if a['activity'] == self.JE_MODEL.INVESTING_SECURITIES) + } + investing_activities['GROUP_CFS_INVESTING_PPE'] = { + 'description': 'Addition and Disposition of Property, Plant & Equipment', + 'balance': sum( + a['balance'] for a in self.CASH_ACCOUNTS if a['activity'] == self.JE_MODEL.INVESTING_PPE) + } + + net_cash = sum(i['balance'] for g, i in investing_activities.items()) + self.IO_DIGEST[self.CFS_DIGEST_KEY]['investing'] = investing_activities + self.IO_DIGEST[self.CFS_DIGEST_KEY]['net_cash_by_activity']['INVESTING'] = net_cash + + def net_cash(self): + self.IO_DIGEST[self.CFS_DIGEST_KEY]['net_cash'] = sum([ + bal for act, bal in self.IO_DIGEST[self.CFS_DIGEST_KEY]['net_cash_by_activity'].items() + ]) + + def digest(self): + self.check_io_digest() + self.operating() + self.financing() + self.investing() + self.net_cash() + return self.IO_DIGEST diff --git a/django_ledger/io/io_mixin.py b/django_ledger/io/io_mixin.py index d44fc115..99ece008 100644 --- a/django_ledger/io/io_mixin.py +++ b/django_ledger/io/io_mixin.py @@ -7,7 +7,7 @@ """ from collections import defaultdict from datetime import datetime, date -from itertools import groupby, chain +from itertools import groupby from random import choice from typing import List, Set, Union, Tuple, Optional @@ -21,8 +21,9 @@ from django_ledger.exceptions import InvalidDateInputError, TransactionNotInBalanceError from django_ledger.io import roles as roles_module -from django_ledger.io.financial_statements import CashFlowStatement -from django_ledger.io.io_context import RoleManager, GroupManager, ActivityManager, BalanceSheetManager +from django_ledger.io.io_context import (RoleContextManager, GroupContextManager, ActivityContextManager, + BalanceSheetStatementContextManager, IncomeStatementContextManager, + CashFlowStatementContextManager) from django_ledger.io.ratios import FinancialRatioManager from django_ledger.models.utils import LazyLoader from django_ledger.settings import (DJANGO_LEDGER_TRANSACTION_MAX_TOLERANCE, @@ -316,6 +317,9 @@ def python_digest(self, self.aggregate_balances(k, g) for k, g in accounts_gb_code ] + for acc in gb_digest: + acc['balance_abs'] = abs(acc['balance']) + if signs: TransactionModel = lazy_importer.get_txs_model() for acc in gb_digest: @@ -367,14 +371,15 @@ def digest(self, process_groups: bool = False, process_ratios: bool = False, process_activity: bool = False, - process_balance_sheet: bool = False, equity_only: bool = False, by_period: bool = False, by_unit: bool = False, by_activity: bool = False, by_tx_type: bool = False, - cash_flow_statement: bool = False, digest_name: str = None, + balance_sheet_statement: bool = False, + income_statement: bool = False, + cash_flow_statement: bool = False, ) -> dict or tuple: if activity: @@ -408,7 +413,7 @@ def digest(self, io_digest['to_date'] = to_date if process_roles: - roles_mgr = RoleManager( + roles_mgr = RoleContextManager( tx_digest=io_digest, by_period=by_period, by_unit=by_unit @@ -418,7 +423,7 @@ def digest(self, io_digest = roles_mgr.digest() if process_groups: - group_mgr = GroupManager( + group_mgr = GroupContextManager( io_digest=io_digest, by_period=by_period, by_unit=by_unit @@ -433,20 +438,24 @@ def digest(self, io_digest['group_account']['GROUP_CAPITAL'].sort( key=lambda acc: roles_module.ROLES_ORDER_CAPITAL.index(acc['role'])) - if process_balance_sheet: - balance_sheet_mgr = BalanceSheetManager(tx_digest=io_digest) - io_digest = balance_sheet_mgr.digest() - if process_ratios: ratio_gen = FinancialRatioManager(tx_digest=io_digest) io_digest = ratio_gen.digest() if process_activity: - activity_manager = ActivityManager(tx_digest=io_digest, by_unit=by_unit, by_period=by_period) + activity_manager = ActivityContextManager(tx_digest=io_digest, by_unit=by_unit, by_period=by_period) activity_manager.digest() + if balance_sheet_statement: + balance_sheet_mgr = BalanceSheetStatementContextManager(tx_digest=io_digest) + io_digest = balance_sheet_mgr.digest() + + if income_statement: + income_statement_mgr = IncomeStatementContextManager(tx_digest=io_digest) + io_digest = income_statement_mgr.digest() + if cash_flow_statement: - cfs = CashFlowStatement(io_digest=io_digest) + cfs = CashFlowStatementContextManager(io_digest=io_digest) io_digest = cfs.digest() if not digest_name: diff --git a/django_ledger/io/roles.py b/django_ledger/io/roles.py index ce3972b0..1eed8c86 100644 --- a/django_ledger/io/roles.py +++ b/django_ledger/io/roles.py @@ -81,7 +81,7 @@ COGS = 'cogs_regular' -EXPENSE_REGULAR = 'ex_regular' +EXPENSE_OPERATIONAL = 'ex_regular' EXPENSE_CAPITAL = 'ex_capital' EXPENSE_DEPRECIATION = 'ex_depreciation' EXPENSE_AMORTIZATION = 'ex_amortization' @@ -222,7 +222,7 @@ INCOME_INVESTING, INCOME_INTEREST, INCOME_CAPITAL_GAIN_LOSS, - INCOME_OTHER, + INCOME_OTHER ] GROUP_COGS = [ @@ -230,7 +230,7 @@ ] GROUP_EXPENSES = [ - EXPENSE_REGULAR, + EXPENSE_OPERATIONAL, EXPENSE_INTEREST, EXPENSE_TAXES, EXPENSE_CAPITAL, @@ -276,7 +276,31 @@ GROUP_INVOICE = [ASSET_CA_CASH, ASSET_CA_RECEIVABLES, LIABILITY_CL_DEFERRED_REVENUE] GROUP_BILL = [ASSET_CA_CASH, ASSET_CA_PREPAID, LIABILITY_CL_ACC_PAYABLE] -# ############# CASH FLOW STATEMENT GROUPS... +# ############# INCOME STATEMENT GROUPS ############### + +# ---> OPERATING REV/EXP (usual & frequent) <---- # +GROUP_IC_OPERATING_REVENUES = [INCOME_OPERATIONAL] +GROUP_IC_OPERATING_COGS = [COGS] +GROUP_IC_OPERATING_EXPENSES = [EXPENSE_OPERATIONAL] + +# ---> OTHER REV/EXP (unusual OR infrequent) <---- # +GROUP_IC_OTHER_REVENUES = [ + INCOME_INVESTING, + INCOME_INTEREST, + INCOME_CAPITAL_GAIN_LOSS, + INCOME_OTHER +] +GROUP_IC_OTHER_EXPENSES = [ + EXPENSE_INTEREST, + EXPENSE_TAXES, + EXPENSE_CAPITAL, + EXPENSE_DEPRECIATION, + EXPENSE_AMORTIZATION, + EXPENSE_OTHER +] + + +# ############# CASH FLOW STATEMENT GROUPS ############ GROUP_CFS_NET_INCOME = GROUP_EARNINGS # ---> OPERATING ACTIVITIES (INDIRECT) <---- # @@ -438,7 +462,7 @@ (COGS, _('Cost of Goods Sold')), # EXPENSES ---- - (EXPENSE_REGULAR, _('Regular Expense')), + (EXPENSE_OPERATIONAL, _('Regular Expense')), (EXPENSE_INTEREST, _('Interest Expense')), (EXPENSE_TAXES, _('Tax Expense')), (EXPENSE_CAPITAL, _('Capital Expense')), diff --git a/django_ledger/models/coa_default.py b/django_ledger/models/coa_default.py index 584a3514..702e61e4 100644 --- a/django_ledger/models/coa_default.py +++ b/django_ledger/models/coa_default.py @@ -142,46 +142,46 @@ {'code': '5010', 'role': roles.COGS, 'balance_type': 'debit', 'name': 'Cost of Goods Sold', 'parent': None}, # EXPENSE ACCOUNTS ------ - {'code': '6010', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Advertising', 'parent': None}, - {'code': '6020', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Amortization', 'parent': None}, - {'code': '6030', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Auto Expense', 'parent': None}, - {'code': '6040', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Bad Debt', 'parent': None}, - {'code': '6050', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Bank Charges', 'parent': None}, - {'code': '6060', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Commission Expense', - 'parent': None}, - {'code': '6080', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Employee Benefits', - 'parent': None}, - {'code': '6090', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Freight', 'parent': None}, - {'code': '6110', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Gifts', 'parent': None}, - {'code': '6120', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Insurance', 'parent': None}, - {'code': '6140', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Professional Fees', - 'parent': None}, - {'code': '6150', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'License Expense', 'parent': None}, - {'code': '6170', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Maintenance Expense', - 'parent': None}, - {'code': '6180', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Meals & Entertainment', - 'parent': None}, - {'code': '6190', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Office Expense', 'parent': None}, - {'code': '6220', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Printing', 'parent': None}, - {'code': '6230', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Postage', 'parent': None}, - {'code': '6240', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Rent', 'parent': None}, - {'code': '6250', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Maintenance & Repairs', - 'parent': None}, - {'code': '6251', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Maintenance', 'parent': None}, - {'code': '6252', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Repairs', 'parent': None}, - {'code': '6253', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'HOA', 'parent': None}, - {'code': '6254', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Snow Removal', 'parent': None}, - {'code': '6255', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Lawn Care', 'parent': None}, - {'code': '6260', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Salaries', 'parent': None}, - {'code': '6270', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Supplies', 'parent': None}, - {'code': '6290', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Utilities', 'parent': None}, - {'code': '6292', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Sewer', 'parent': None}, - {'code': '6293', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Gas', 'parent': None}, - {'code': '6294', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Garbage', 'parent': None}, - {'code': '6295', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Electricity', 'parent': None}, - {'code': '6300', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Property Management', - 'parent': None}, - {'code': '6400', 'role': roles.EXPENSE_REGULAR, 'balance_type': 'debit', 'name': 'Vacancy', 'parent': None}, + {'code': '6010', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Advertising', 'parent': None}, + {'code': '6020', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Amortization', 'parent': None}, + {'code': '6030', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Auto Expense', 'parent': None}, + {'code': '6040', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Bad Debt', 'parent': None}, + {'code': '6050', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Bank Charges', 'parent': None}, + {'code': '6060', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Commission Expense', + 'parent': None}, + {'code': '6080', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Employee Benefits', + 'parent': None}, + {'code': '6090', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Freight', 'parent': None}, + {'code': '6110', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Gifts', 'parent': None}, + {'code': '6120', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Insurance', 'parent': None}, + {'code': '6140', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Professional Fees', + 'parent': None}, + {'code': '6150', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'License Expense', 'parent': None}, + {'code': '6170', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Maintenance Expense', + 'parent': None}, + {'code': '6180', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Meals & Entertainment', + 'parent': None}, + {'code': '6190', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Office Expense', 'parent': None}, + {'code': '6220', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Printing', 'parent': None}, + {'code': '6230', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Postage', 'parent': None}, + {'code': '6240', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Rent', 'parent': None}, + {'code': '6250', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Maintenance & Repairs', + 'parent': None}, + {'code': '6251', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Maintenance', 'parent': None}, + {'code': '6252', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Repairs', 'parent': None}, + {'code': '6253', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'HOA', 'parent': None}, + {'code': '6254', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Snow Removal', 'parent': None}, + {'code': '6255', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Lawn Care', 'parent': None}, + {'code': '6260', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Salaries', 'parent': None}, + {'code': '6270', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Supplies', 'parent': None}, + {'code': '6290', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Utilities', 'parent': None}, + {'code': '6292', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Sewer', 'parent': None}, + {'code': '6293', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Gas', 'parent': None}, + {'code': '6294', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Garbage', 'parent': None}, + {'code': '6295', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Electricity', 'parent': None}, + {'code': '6300', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Property Management', + 'parent': None}, + {'code': '6400', 'role': roles.EXPENSE_OPERATIONAL, 'balance_type': 'debit', 'name': 'Vacancy', 'parent': None}, {'code': '6070', 'role': roles.EXPENSE_DEPRECIATION, 'balance_type': 'debit', 'name': 'Depreciation Expense', 'parent': None}, diff --git a/django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html b/django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html index 9a7238e4..f4a0aac1 100644 --- a/django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +++ b/django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html @@ -31,6 +31,9 @@ {{ acc_data.role_name | upper }} + {% if by_unit %} + + {% endif %} @@ -38,7 +41,7 @@ {% for acc in acc_data.accounts %} {{ acc.code }} - {{ acc.name }} + {{ acc.name }} {% if by_unit %} {% if acc.unit_name %}{{ acc.unit_name }}{% endif %} {% endif %} @@ -70,9 +73,12 @@ {{ acc_data.role_name | upper }} {% trans 'Total:' %} + {% if by_unit %} + + {% endif %} {% currency_symbol %}{{ acc_data.total_balance | currency_format }} - + {% endfor %} @@ -83,6 +89,7 @@ {% endif %} + {% currency_symbol %}{{ bs_role_data.total_balance | currency_format }} {% endfor %} @@ -95,6 +102,7 @@ {% endif %} + {% currency_symbol %}{{ tx_digest.group_balance.GROUP_EARNINGS | currency_format }} @@ -104,6 +112,7 @@ {% endif %} + {% currency_symbol %}{{ tx_digest.group_balance.GROUP_EQUITY | currency_format }} @@ -113,6 +122,7 @@ {% endif %} + {% currency_symbol %}{{ tx_digest.group_balance.GROUP_LIABILITIES_EQUITY | currency_format }} diff --git a/django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html b/django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html index 3d56de6a..5e6db810 100644 --- a/django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +++ b/django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html @@ -1,19 +1,22 @@ {% load django_ledger %} {% load i18n %} +
- - + + {% if by_unit %} - + {% endif %} - - + + + + {# OPERATING INCOME #} - + {% if by_unit %} @@ -21,9 +24,9 @@ - {% for acc in tx_digest.group_account.GROUP_INCOME %} + {% for acc in tx_digest.income_statement.operating.revenues %} - + {% if by_unit %} @@ -38,29 +41,80 @@ {% endfor %} - - + {% if by_unit %} {% endif %} - + + + {# COGS #} + + + + {% if by_unit %} + + {% endif %} + + + + {% for acc in tx_digest.income_statement.operating.cogs %} + + + + {% if by_unit %} + + {% endif %} + + + + {% endfor %} + + + {% if by_unit %} + + {% endif %} + + + + + {# GROSS PROFIT #} - + {% if by_unit %} + + {% endif %} + + + + + + {# OPERATING EXPENSES #} + + {% if by_unit %} {% endif %} + - {% for acc in tx_digest.group_account.GROUP_EXPENSES %} + {% for acc in tx_digest.income_statement.operating.expenses %} - + {% if by_unit %} @@ -72,29 +126,136 @@ {% icon 'bi:arrow-bar-up' 24 %} {% endif %} - + {% endfor %} - - + {% if by_unit %} {% endif %} - + + - - + + {# NET OPERATING INCOME #} + {% if by_unit %} {% endif %} - + + + {# OTHER REVENUES #} + + + + {% if by_unit %} + + {% endif %} + + + + {% for acc in tx_digest.income_statement.other.revenues %} + + + + {% if by_unit %} + + {% endif %} + + + + {% endfor %} + + + {% if by_unit %} + + {% endif %} + + + + + + + {# OTHER EXPENSES #} + + + + {% if by_unit %} + + {% endif %} + + + + {% for acc in tx_digest.income_statement.other.expenses %} + + + + {% if by_unit %} + + {% endif %} + + + + {% endfor %} + + + {% if by_unit %} + + {% endif %} + + + + + + {# NET OTHER INCOME/LOSS #} + + + {% if by_unit %} + + {% endif %} + + + + + + + {# NET INCOME #} + + + {% if by_unit %} + + {% endif %} + + + +
Account NumberDescription{% trans 'Account Number' %}{% trans 'Description' %}Unit{% trans 'Unit' %}Balance TypeBalance{% trans 'Balance Type' %}{% trans 'Balance' %}

{% trans 'Income' %}

{% trans 'Operating Revenues' %}

{{ acc.code }}{{ acc.code }} {{ acc.name }}{% if acc.unit_name %}{{ acc.unit_name }}{% endif %}{% currency_symbol %}{{ acc.balance | currency_format }}
Total Income
{% currency_symbol %}{{ tx_digest.group_balance.GROUP_INCOME | currency_format }}

{% trans 'Net Operating Revenues' %}

+ {% currency_symbol %}{{ tx_digest.income_statement.operating.net_operating_revenue | currency_format }}

{% trans 'Less: Cost of Goods Sold' %}

{{ acc.code }}{{ acc.name }}{% if acc.unit_name %}{{ acc.unit_name }}{% endif %} + {% if acc.balance_type == 'debit' %} + {% icon 'bi:arrow-bar-down' 24 %} + {% elif acc.balance_type == 'credit' %} + {% icon 'bi:arrow-bar-up' 24 %} + {% endif %} + {% currency_symbol %}{{ acc.balance_abs | currency_format }}

{% trans 'Net COGS' %}

+ {% currency_symbol %}{{ tx_digest.income_statement.operating.net_cogs | currency_format }}

{% trans 'Expenses' %}

{% trans 'Gross Profit' %}

+ {% currency_symbol %}{{ tx_digest.income_statement.operating.gross_profit | currency_format }}

{% trans 'Operating Expenses' %}

{{ acc.code }}{{ acc.code }} {{ acc.name }}{% if acc.unit_name %}{{ acc.unit_name }}{% endif %}{% currency_symbol %}{{ acc.balance | reverse_sing | currency_format }}{% currency_symbol %}{{ acc.balance_abs | currency_format }}
Total Expenses
{% currency_symbol %}{{ tx_digest.group_balance.GROUP_EXPENSES | reverse_sing | currency_format }}

{% trans 'Net Operating Expenses' %}

+ {% currency_symbol %}{{ tx_digest.income_statement.operating.net_operating_expenses | currency_format }}
Total Income (Loss)
{% currency_symbol %}{{ tx_digest.group_balance.GROUP_EARNINGS | currency_format }}

{% trans 'Net Operating Income (Loss)' %}

+ {% currency_symbol %}{{ tx_digest.income_statement.operating.net_operating_income| currency_format }}

{% trans 'Other Revenues' %}

{{ acc.code }}{{ acc.name }}{% if acc.unit_name %}{{ acc.unit_name }}{% endif %} + {% if acc.balance_type == 'debit' %} + {% icon 'bi:arrow-bar-down' 24 %} + {% elif acc.balance_type == 'credit' %} + {% icon 'bi:arrow-bar-up' 24 %} + {% endif %} + {% currency_symbol %}{{ acc.balance_abs | currency_format }}

{% trans 'Net Other Income' %}

+ {% currency_symbol %}{{ tx_digest.income_statement.other.net_other_revenues | currency_format }}

{% trans 'Other Expenses' %}

{{ acc.code }}{{ acc.name }}{% if acc.unit_name %}{{ acc.unit_name }}{% endif %} + {% if acc.balance_type == 'debit' %} + {% icon 'bi:arrow-bar-down' 24 %} + {% elif acc.balance_type == 'credit' %} + {% icon 'bi:arrow-bar-up' 24 %} + {% endif %} + {% currency_symbol %}{{ acc.balance_abs | currency_format }}

{% trans 'Net Other Expenses' %}

+ {% currency_symbol %}{{ tx_digest.income_statement.other.net_other_expenses | currency_format }}

{% trans 'Net Other Income (Loss)' %}

+ {% currency_symbol %}{{ tx_digest.income_statement.other.net_other_income| currency_format }}
+

{{ tx_digest.from_date | date }} {% trans 'through' %} {{ tx_digest.to_date | date }}

+

{% trans 'Net Income' %}

+ {% currency_symbol %}{{ tx_digest.income_statement.net_income| currency_format }}
diff --git a/django_ledger/templatetags/django_ledger.py b/django_ledger/templatetags/django_ledger.py index 31de7cbd..a1e918b5 100644 --- a/django_ledger/templatetags/django_ledger.py +++ b/django_ledger/templatetags/django_ledger.py @@ -107,7 +107,7 @@ def balance_sheet_statement(context, io_model, to_date=None): to_date=to_date, signs=True, process_groups=True, - process_balance_sheet=True) + balance_sheet_statement=True) # todo: this can be moved to the digest function... digest['by_unit'] = context['by_unit'] @@ -169,7 +169,10 @@ def income_statement_table(context, io_model, from_date=None, to_date=None): from_date=from_date, to_date=to_date, equity_only=True, - process_groups=True) + process_groups=True, + income_statement=True, + signs=True + ) digest['by_unit'] = context['by_unit'] digest['unit_model'] = context['unit_model'] diff --git a/pyproject.toml b/pyproject.toml index 38456601..f58c22ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "django-ledger" -version = "0.5.2.16" +version = "0.5.2.17" readme = "README.md" requires-python = ">=3.7" description = "Bookkeeping & Financial analysis backend for Django. Balance Sheet, Income Statements, Chart of Accounts, Entities"