diff --git a/drcr/config.py.example b/drcr/config.py.example
index 9a104e6..fe7b711 100644
--- a/drcr/config.py.example
+++ b/drcr/config.py.example
@@ -1,26 +1,3 @@
-COA_MAPPING = {
- 'Accounts Payable': 'Accounts payable',
- 'Accounts Receivable': 'Accounts receivable',
- 'Cash at Bank': 'Cash and cash equivalents',
- 'Depreciation': 'Depreciation',
- 'Land': 'Property, plant and equipment',
- 'Opening Balances': 'Accumulated surplus (deficit)',
- 'Operating Costs': 'Operating costs',
- 'Purchases': 'Purchases'
-}
-
-BALANCE_SHEET_MAPPING = {
- 'Current assets': ['Accounts receivable', 'Cash and cash equivalents'],
- 'Non-current assets': ['Property, plant and equipment'],
- 'Current liabilities': ['Accounts payable'],
- 'Non-current liabilities': []
-}
-
-INCOME_STATEMENT_MAPPING = {
- 'Income': ['Sales'],
- 'Expenses': ['Operating costs', 'Purchases']
-}
-
TAX_MAPPING = {
'Government allowances': [],
'Other work-related expenses': [],
diff --git a/drcr/models.py b/drcr/models.py
index deabf2b..1a4736e 100644
--- a/drcr/models.py
+++ b/drcr/models.py
@@ -19,6 +19,8 @@ from markupsafe import Markup
from . import AMOUNT_DPS
from .database import db
+from itertools import groupby
+
class Transaction(db.Model):
__tablename__ = 'transactions'
@@ -98,6 +100,14 @@ class Amount:
def __neg__(self):
return Amount(-self.quantity, self.commodity)
+ def __add__(self, other):
+ if self.commodity != other.commodity:
+ raise ValueError('Cannot add incompatible commodities {} and {}'.format(self.commodity, other.commodity))
+ return Amount(self.quantity + other.quantity, self.commodity)
+
+ def __sub__(self, other):
+ return self + (-other)
+
def format(self, force_commodity=False):
if self.commodity == '$' and not force_commodity:
return Markup('{:,.{dps}f}'.format(self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS).replace(',', ' '))
@@ -106,6 +116,12 @@ class Amount:
else:
return Markup('{1:,.{dps}f} {0}'.format(self.commodity, self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS).replace(',', ' '))
+ def format_accounting(self):
+ if self.quantity >= 0:
+ return Markup('{:,.{dps}f} '.format(self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS).replace(',', ' '))
+ else:
+ return Markup('({:,.{dps}f})'.format(-self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS).replace(',', ' '))
+
def quantity_string(self):
if self.commodity == '$':
return '{:.{dps}f}'.format(self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS)
@@ -194,3 +210,17 @@ class AccountConfiguration(db.Model):
account = db.Column(db.String)
kind = db.Column(db.String)
data = db.Column(db.JSON)
+
+ @staticmethod
+ def get_all():
+ account_configurations = db.session.execute(db.select(AccountConfiguration).order_by(AccountConfiguration.account)).scalars()
+ account_configurations = {v: list(g) for v, g in groupby(account_configurations, lambda c: c.account)}
+
+ return account_configurations
+
+ @staticmethod
+ def get_all_kinds():
+ account_configurations = AccountConfiguration.get_all()
+ kinds = {k: [vv.kind for vv in v] for k, v in account_configurations.items()}
+
+ return kinds
diff --git a/drcr/reports.py b/drcr/reports.py
new file mode 100644
index 0000000..47546fc
--- /dev/null
+++ b/drcr/reports.py
@@ -0,0 +1,212 @@
+# 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 .
+
+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, heading=False, bordered=False):
+ self.text = text
+ self.amount = amount
+ self.id = id
+ 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, *, id=None, heading=False, bordered=False):
+ self.text = text
+ self.calc = calc
+ self.id = id
+ self.heading = heading
+ self.bordered = bordered
+
+ 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 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=[
+ Entry(text=account_name, amount=amount)
+ for account_name, amount in accounts.items()
+ if 'drcr.asset' in account_configurations.get(account_name, [])
+ ] + [Subtotal('Total assets', bordered=True)]
+ ),
+ Spacer(),
+ Section(
+ title='Liabilities',
+ entries=[
+ Entry(text=account_name, amount=-amount)
+ for account_name, amount in accounts.items()
+ if 'drcr.liability' in account_configurations.get(account_name, [])
+ ] + [Subtotal('Total liabilities', bordered=True)]
+ ),
+ Spacer(),
+ Section(
+ title='Equity',
+ entries=[
+ Entry(text=account_name, amount=-amount)
+ for account_name, amount in accounts.items()
+ if 'drcr.equity' in account_configurations.get(account_name, [])
+ ] + [
+ Calculated(
+ 'Current year surplus (deficit)',
+ lambda _: income_statement_report().by_id('net_surplus').amount
+ ),
+ 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=[
+ Entry(text=account_name, amount=-amount)
+ for account_name, amount in accounts.items()
+ if 'drcr.income' in account_configurations.get(account_name, [])
+ ] + [Subtotal('Total income', id='total_income', bordered=True)]
+ ),
+ Spacer(),
+ Section(
+ title='Expenses',
+ entries=[
+ Entry(text=account_name, amount=amount)
+ for account_name, amount in accounts.items()
+ if 'drcr.expense' in account_configurations.get(account_name, [])
+ ] + [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
diff --git a/drcr/reports/__init__.py b/drcr/reports/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/drcr/reports/views.py b/drcr/reports/views.py
deleted file mode 100644
index 5b8c8c6..0000000
--- a/drcr/reports/views.py
+++ /dev/null
@@ -1,78 +0,0 @@
-# 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 .
-
-from flask import render_template
-
-from ..config import BALANCE_SHEET_MAPPING, COA_MAPPING, INCOME_STATEMENT_MAPPING
-from ..models import Amount, TrialBalancer
-from ..webapp import all_transactions, app
-
-def get_trial_balance():
- # Get trial balance and validate COA
- balancer = TrialBalancer()
- balancer.apply_transactions(all_transactions())
-
- # Classify accounts
- for source_account, destination_account in COA_MAPPING.items():
- balancer.transfer_balance(source_account, destination_account)
-
- # Validate COA
- for account in balancer.accounts:
- if account in BALANCE_SHEET_MAPPING['Current assets']:
- continue
- if account in BALANCE_SHEET_MAPPING['Non-current assets']:
- continue
- if account in BALANCE_SHEET_MAPPING['Current liabilities']:
- continue
- if account in BALANCE_SHEET_MAPPING['Non-current liabilities']:
- continue
- if account in INCOME_STATEMENT_MAPPING['Income']:
- continue
- if account in INCOME_STATEMENT_MAPPING['Expenses']:
- continue
- if account == 'Accumulated surplus (deficit)':
- continue
- if account == 'Income tax':
- continue
-
- raise Exception('Account "{}" is not mapped to a report'.format(account))
-
- return balancer
-
-#@app.route('/reports/mapped-trial-balance')
-#def mapped_trial_balance():
-# balancer = get_trial_balance()
-#
-# total_dr = Amount(sum(v.quantity for v in balancer.accounts.values() if v.quantity > 0), '$')
-# total_cr = Amount(sum(v.quantity for v in balancer.accounts.values() if v.quantity < 0), '$')
-#
-# return render_template('trial_balance.html', accounts=dict(sorted(balancer.accounts.items())), total_dr=total_dr, total_cr=total_cr)
-
-@app.route('/reports/balance-sheet')
-def balance_sheet():
- balancer = get_trial_balance()
-
- # Transfer surplus to balance sheet
- for account in INCOME_STATEMENT_MAPPING['Income'] + INCOME_STATEMENT_MAPPING['Expenses'] + ['Income tax']:
- balancer.transfer_balance(account, 'Current year surplus (deficit)')
-
- return render_template('reports/balance_sheet.html', accounts=balancer.accounts, running_total=Amount(0, '$'), BALANCE_SHEET_MAPPING=BALANCE_SHEET_MAPPING)
-
-@app.route('/reports/income-statement')
-def income_statement():
- balancer = get_trial_balance()
-
- return render_template('reports/income_statement.html', accounts=balancer.accounts, running_total=Amount(0, '$'), INCOME_STATEMENT_MAPPING=INCOME_STATEMENT_MAPPING)
diff --git a/drcr/templates/index.html b/drcr/templates/index.html
index 374583a..0320bf9 100644
--- a/drcr/templates/index.html
+++ b/drcr/templates/index.html
@@ -31,12 +31,12 @@
Advanced reports
{% endblock %}
diff --git a/drcr/templates/report.html b/drcr/templates/report.html
new file mode 100644
index 0000000..79b24d0
--- /dev/null
+++ b/drcr/templates/report.html
@@ -0,0 +1,65 @@
+{# 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 .
+#}
+
+{% extends 'base.html' %}
+{% block title %}{{ report.title }}{% endblock %}
+
+{% macro render_section(section) %}
+
+ {{ section.title }} |
+ |
+
+ {% for entry in section.entries %}
+ {{ render_entry(entry) }}
+ {% endfor %}
+{% endmacro %}
+
+{% macro render_entry(entry) %}
+ {% if entry.__class__.__name__ == 'Section' %}
+ {{ render_section(entry) }}
+ {% elif entry.__class__.__name__ == 'Subtotal' %}
+
+ {{ entry.text }} |
+ {{ entry.amount.format_accounting() }} |
+
+ {% elif entry.__class__.__name__ == 'Spacer' %}
+ |
+ {% else %}
+
+ <{{ 'th' if entry.heading else 'td' }}>{{ entry.text }}{{ 'th' if entry.heading else 'td' }}>
+ <{{ 'th' if entry.heading else 'td' }} class="text-end">{{ entry.amount.format_accounting() }}{{ 'th' if entry.heading else 'td' }}>
+
+ {% endif %}
+{% endmacro %}
+
+{% block content %}
+ {{ report.title }}
+
+
+
+
+ |
+ $ |
+
+
+
+ {% for entry in report.entries %}
+ {{ render_entry(entry) }}
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/drcr/templates/reports/balance_sheet.html b/drcr/templates/reports/balance_sheet.html
deleted file mode 100644
index f4a5e1f..0000000
--- a/drcr/templates/reports/balance_sheet.html
+++ /dev/null
@@ -1,149 +0,0 @@
-{# DrCr: Web-based double-entry bookkeeping framework
- Copyright (C) 2022 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 .
-#}
-
-{% extends 'base.html' %}
-{% block title %}Balance sheet{% endblock %}
-
-{% macro fmtbal(amount, mul=1) %}
- {# FIXME: Honour AMOUNT_DPS #}
- {% if amount.quantity * mul >= 0 %}
- {{ '{:,.2f}'.format(amount.quantity|abs / 100) }}
- {% else %}
- ({{ '{:,.2f}'.format(amount.quantity|abs / 100) }})
- {% endif %}
-{% endmacro %}
-
-{% macro acctrow(name, mul=1) %}
-
- {% if name == 'Current year surplus (deficit)' %}
- {{ name }} |
- {% else %}
- {{ name }} |
- {% endif %}
- {{ fmtbal(accounts[name], mul) }} |
-
- {% set _ = running_total.__setattr__('quantity', running_total.quantity + accounts[name].quantity) %}
-{% endmacro %}
-
-{% block content %}
- Balance sheet
-
-
-
-
- |
- $ |
-
-
-
-
- Current assets |
-
- {% set _ = running_total.__setattr__('quantity', 0) %}
- {% for account in BALANCE_SHEET_MAPPING['Current assets'] %}
- {{ acctrow(account) }}
- {% endfor %}
-
- Total current assets |
- {{ fmtbal(running_total) }} |
-
- {% set assets = running_total.quantity %}
-
- |
-
-
- Non-current assets |
-
- {% set _ = running_total.__setattr__('quantity', 0) %}
- {% for account in BALANCE_SHEET_MAPPING['Non-current assets'] %}
- {{ acctrow(account) }}
- {% endfor %}
-
- Total non-current assets |
- {{ fmtbal(running_total) }} |
-
- {% set assets = assets + running_total.quantity %}
-
- |
-
-
- Total assets |
- {% set _ = running_total.__setattr__('quantity', assets) %}
- {{ fmtbal(running_total) }} |
-
-
- |
-
-
- Current liabilities |
-
- {% set _ = running_total.__setattr__('quantity', 0) %}
- {% for account in BALANCE_SHEET_MAPPING['Current liabilities'] %}
- {{ acctrow(account, -1) }}
- {% endfor %}
-
- Total current liabilities |
- {{ fmtbal(running_total, -1) }} |
-
- {% set liabilities = running_total.quantity %}
-
- |
-
-
- Non-current liabilities |
-
- {% set _ = running_total.__setattr__('quantity', 0) %}
- {% for account in BALANCE_SHEET_MAPPING['Non-current liabilities'] %}
- {{ acctrow(account, -1) }}
- {% endfor %}
-
- Total non-current liabilities |
- {{ fmtbal(running_total, -1) }} |
-
- {% set liabilities = liabilities + running_total.quantity %}
-
- |
-
-
- Total liabilities |
- {% set _ = running_total.__setattr__('quantity', liabilities) %}
- {{ fmtbal(running_total, -1) }} |
-
-
- |
-
-
- Net assets |
- {% set _ = running_total.__setattr__('quantity', assets + liabilities) %}
- {{ fmtbal(running_total) }} |
-
-
- |
-
-
- Equity |
-
- {% set _ = running_total.__setattr__('quantity', 0) %}
- {{ acctrow('Accumulated surplus (deficit)', -1) }}
- {{ acctrow('Current year surplus (deficit)', -1) }}
-
- Total equity |
- {{ fmtbal(running_total, -1) }} |
-
-
-
-{% endblock %}
diff --git a/drcr/templates/reports/income_statement.html b/drcr/templates/reports/income_statement.html
deleted file mode 100644
index 97a626b..0000000
--- a/drcr/templates/reports/income_statement.html
+++ /dev/null
@@ -1,96 +0,0 @@
-{# 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 .
-#}
-
-{% extends 'base.html' %}
-{% block title %}Income statement{% endblock %}
-
-{% macro fmtbal(amount, mul=1) %}
- {# FIXME: Honour AMOUNT_DPS #}
- {% if amount.quantity * mul >= 0 %}
- {{ '{:,.2f}'.format(amount.quantity|abs / 100) }}
- {% else %}
- ({{ '{:,.2f}'.format(amount.quantity|abs / 100) }})
- {% endif %}
-{% endmacro %}
-
-{% macro acctrow(name, mul=1) %}
-
- {{ name }} |
- {{ fmtbal(accounts[name], mul) }} |
-
- {% set _ = running_total.__setattr__('quantity', running_total.quantity + accounts[name].quantity) %}
-{% endmacro %}
-
-{% block content %}
- Income statement
-
-
-
-
- |
- $ |
-
-
-
-
- Income |
-
- {% set _ = running_total.__setattr__('quantity', 0) %}
- {% for account in INCOME_STATEMENT_MAPPING['Income'] if account in accounts %}
- {{ acctrow(account, -1) }}
- {% endfor %}
-
- Total income |
- {{ fmtbal(running_total, -1) }} |
-
- {% set surplus = running_total.quantity %}
-
- |
-
-
- Expenses |
-
- {% set _ = running_total.__setattr__('quantity', 0) %}
- {% for account in INCOME_STATEMENT_MAPPING['Expenses'] if account in accounts %}
- {{ acctrow(account) }}
- {% endfor %}
-
- Total expenses |
- {{ fmtbal(running_total) }} |
-
- {% set surplus = surplus + running_total.quantity %}
-
- |
-
-
- Net surplus (deficit) before income tax |
- {% set _ = running_total.__setattr__('quantity', surplus) %}
- {{ fmtbal(running_total, -1) }} |
-
-
- Income tax expense |
- {{ fmtbal(accounts['Income tax']) }} |
- {% set surplus = surplus + accounts['Income tax'].quantity %}
-
-
- Net surplus (deficit) after income tax |
- {% set _ = running_total.__setattr__('quantity', surplus) %}
- {{ fmtbal(running_total, -1) }} |
-
-
-
-{% endblock %}
diff --git a/drcr/views.py b/drcr/views.py
index d9fb282..2e981ac 100644
--- a/drcr/views.py
+++ b/drcr/views.py
@@ -18,21 +18,20 @@ from flask import redirect, render_template, request
from .database import db
from .models import AccountConfiguration, Amount, Balance, Posting, TrialBalancer
+from .reports import balance_sheet_report, income_statement_report
from .webapp import all_transactions, app
-from itertools import groupby
-
@app.route('/')
def index():
return render_template('index.html')
@app.route('/chart-of-accounts')
def chart_of_accounts():
- accounts = sorted(db.session.execute(db.select(Posting.account)).unique().scalars().all())
+ #accounts = sorted(db.session.execute(db.select(Posting.account)).unique().scalars().all())
+ accounts = sorted(list(set(p.account for t in all_transactions() for p in t.postings)))
# Get existing AccountConfiguration's
- account_configurations = db.session.execute(db.select(AccountConfiguration).order_by(AccountConfiguration.account)).scalars()
- account_configurations = {v: list(g) for v, g in groupby(account_configurations, lambda c: c.account)}
+ account_configurations = AccountConfiguration.get_all()
# TODO: Handle orphans
return render_template('chart_of_accounts.html', accounts=accounts, account_configurations=account_configurations)
@@ -84,3 +83,13 @@ def account_transactions():
running_total=Amount(0, '$'),
transactions=sorted(transactions, key=lambda t: t.dt)
)
+
+@app.route('/balance-sheet')
+def balance_sheet():
+ report = balance_sheet_report()
+ return render_template('report.html', report=report)
+
+@app.route('/income-statement')
+def income_statement():
+ report = income_statement_report()
+ return render_template('report.html', report=report)
diff --git a/drcr/webapp.py b/drcr/webapp.py
index 465f2b8..1cfb869 100644
--- a/drcr/webapp.py
+++ b/drcr/webapp.py
@@ -39,7 +39,6 @@ def all_transactions():
from . import views
from .journal import views
-from .reports import views
from .statements import views
from .tax import views