DrCr/drcr/reports.py

199 lines
5.4 KiB
Python

# DrCr: Web-based double-entry bookkeeping framework
# Copyright (C) 2022–2023 Lee Yingtong Li (RunasSudo)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from .models import AccountConfiguration, Amount, TrialBalancer
from .webapp import all_transactions
class Report:
def __init__(self, title=None, entries=None):
self.title = title
self.entries = entries or []
def calculate(self):
"""Calculate all subtotals, etc."""
for entry in self.entries:
entry.calculate(self)
def by_id(self, id):
# TODO: Make more efficient?
for entry in self.entries:
if entry.id == id:
return entry
if isinstance(entry, Section):
result = entry.by_id(id)
if result is not None:
return result
return None
class Section:
def __init__(self, title=None, entries=None, *, id=None):
self.title = title
self.entries = entries or []
self.id = id
def calculate(self, parent):
for entry in self.entries:
entry.calculate(self)
def by_id(self, id):
# TODO: Make more efficient?
for entry in self.entries:
if entry.id == id:
return entry
if isinstance(entry, Section):
result = entry.by_id(id)
if result is not None:
return result
return None
class Entry:
def __init__(self, text=None, amount=None, *, id=None, link=None, heading=False, bordered=False):
self.text = text
self.amount = amount
self.id = id
self.link = link
self.heading = heading
self.bordered = bordered
def calculate(self, parent):
pass
class Subtotal:
def __init__(self, text=None, *, id=None, bordered=False):
self.text = text
self.id = id
self.bordered = bordered
self.amount = None
def calculate(self, parent):
self.amount = Amount(
sum(e.amount.quantity for e in parent.entries if isinstance(e, Entry)),
'$'
)
class Calculated(Entry):
def __init__(self, text=None, calc=None, **kwargs):
super().__init__(text=text, **kwargs)
self.calc = calc
self.amount = None
def calculate(self, parent):
self.amount = self.calc(parent)
class Spacer:
id = None
def calculate(self, parent):
pass
def validate_accounts(accounts, account_configurations):
for account in accounts:
n = sum(1 for c in account_configurations.get(account, []) if c in ('drcr.asset', 'drcr.liability', 'drcr.equity', 'drcr.income', 'drcr.expense'))
if n != 1:
raise Exception('Account "{}" mapped to {} account types (expected 1)'.format(account, n))
def entries_for_kind(account_configurations, accounts, kind, neg=False):
return [
Entry(text=account_name, amount=-amount if neg else amount, link='/account-transactions?account=' + account_name)
for account_name, amount in accounts.items()
if kind in account_configurations.get(account_name, []) and amount.quantity != 0
]
def balance_sheet_report():
# Get trial balance
balancer = TrialBalancer()
balancer.apply_transactions(all_transactions())
accounts = dict(sorted(balancer.accounts.items()))
# Get account configurations
account_configurations = AccountConfiguration.get_all_kinds()
validate_accounts(accounts, account_configurations)
report = Report(
title='Balance sheet',
entries=[
Section(
title='Assets',
entries=entries_for_kind(account_configurations, accounts, 'drcr.asset') + [Subtotal('Total assets', bordered=True)]
),
Spacer(),
Section(
title='Liabilities',
entries=entries_for_kind(account_configurations, accounts, 'drcr.liability', True) + [Subtotal('Total liabilities', bordered=True)]
),
Spacer(),
Section(
title='Equity',
entries=entries_for_kind(account_configurations, accounts, 'drcr.equity', True) + [
Calculated(
'Current year surplus (deficit)',
lambda _: income_statement_report().by_id('net_surplus').amount,
link='/income-statement'
),
Subtotal('Total equity', bordered=True)
]
),
]
)
report.calculate()
return report
def income_statement_report():
# Get trial balance
balancer = TrialBalancer()
balancer.apply_transactions(all_transactions())
accounts = dict(sorted(balancer.accounts.items()))
# Get account configurations
account_configurations = AccountConfiguration.get_all_kinds()
validate_accounts(accounts, account_configurations)
report = Report(
title='Income statement',
entries=[
Section(
title='Income',
entries=entries_for_kind(account_configurations, accounts, 'drcr.income', True) + [Subtotal('Total income', id='total_income', bordered=True)]
),
Spacer(),
Section(
title='Expenses',
entries=entries_for_kind(account_configurations, accounts, 'drcr.expense') + [Subtotal('Total expenses', id='total_expenses', bordered=True)]
),
Spacer(),
Calculated(
'Net surplus (deficit)',
lambda r: r.by_id('total_income').amount - r.by_id('total_expenses').amount,
id='net_surplus',
heading=True,
bordered=True
)
]
)
report.calculate()
return report