Implement API-based generation of reports

This commit is contained in:
RunasSudo 2023-01-04 00:27:44 +11:00
parent 7df955e5b6
commit c527442ac7
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
11 changed files with 323 additions and 354 deletions

View File

@ -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 = { TAX_MAPPING = {
'Government allowances': [], 'Government allowances': [],
'Other work-related expenses': [], 'Other work-related expenses': [],

View File

@ -19,6 +19,8 @@ from markupsafe import Markup
from . import AMOUNT_DPS from . import AMOUNT_DPS
from .database import db from .database import db
from itertools import groupby
class Transaction(db.Model): class Transaction(db.Model):
__tablename__ = 'transactions' __tablename__ = 'transactions'
@ -98,6 +100,14 @@ class Amount:
def __neg__(self): def __neg__(self):
return Amount(-self.quantity, self.commodity) 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): def format(self, force_commodity=False):
if self.commodity == '$' and not force_commodity: if self.commodity == '$' and not force_commodity:
return Markup('{:,.{dps}f}'.format(self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS).replace(',', ' ')) return Markup('{:,.{dps}f}'.format(self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS).replace(',', ' '))
@ -106,6 +116,12 @@ class Amount:
else: else:
return Markup('{1:,.{dps}f} {0}'.format(self.commodity, self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS).replace(',', ' ')) 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): def quantity_string(self):
if self.commodity == '$': if self.commodity == '$':
return '{:.{dps}f}'.format(self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS) 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) account = db.Column(db.String)
kind = db.Column(db.String) kind = db.Column(db.String)
data = db.Column(db.JSON) 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

212
drcr/reports.py Normal file
View File

@ -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 <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, 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

View File

@ -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 <https://www.gnu.org/licenses/>.
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)

View File

@ -31,12 +31,12 @@
<ul> <ul>
<li><a href="/general-ledger">General ledger</a></li> <li><a href="/general-ledger">General ledger</a></li>
<li><a href="/trial-balance">Trial balance</a></li> <li><a href="/trial-balance">Trial balance</a></li>
<li><a href="/balance-sheet">Balance sheet</a></li>
<li><a href="/income-statement">Income statement</a></li>
</ul> </ul>
<h1 class="h2 my-4">Advanced reports</h1> <h1 class="h2 my-4">Advanced reports</h1>
<ul> <ul>
<li><a href="/reports/balance-sheet">Balance sheet</a></li>
<li><a href="/reports/income-statement">Income statement</a></li>
<li><a href="/tax/summary">Tax summary</a></li> <li><a href="/tax/summary">Tax summary</a></li>
</ul> </ul>
{% endblock %} {% endblock %}

View File

@ -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 <https://www.gnu.org/licenses/>.
#}
{% extends 'base.html' %}
{% block title %}{{ report.title }}{% endblock %}
{% macro render_section(section) %}
<tr>
<th>{{ section.title }}</th>
<th></th>
</tr>
{% 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' %}
<tr{% if entry.bordered %} style="border-width:1px 0"{% endif %}>
<th>{{ entry.text }}</th>
<th class="text-end">{{ entry.amount.format_accounting() }}</th>
</tr>
{% elif entry.__class__.__name__ == 'Spacer' %}
<tr><td colspan="2">&nbsp;</td></tr>
{% else %}
<tr{% if entry.bordered %} style="border-width:1px 0"{% endif %}>
<{{ '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' }}>
</tr>
{% endif %}
{% endmacro %}
{% block content %}
<h1 class="h2 mt-4">{{ report.title }}</h1>
<table class="table table-borderless table-sm">
<thead>
<tr style="border-bottom-width:1px">
<th></th>
<th class="text-end">$&nbsp;</th>
</tr>
</thead>
<tbody>
{% for entry in report.entries %}
{{ render_entry(entry) }}
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@ -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 <https://www.gnu.org/licenses/>.
#}
{% 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) }}&nbsp;
{% else %}
({{ '{:,.2f}'.format(amount.quantity|abs / 100) }})
{% endif %}
{% endmacro %}
{% macro acctrow(name, mul=1) %}
<tr>
{% if name == 'Current year surplus (deficit)' %}
<td><a href="/reports/income-statement">{{ name }}</a></td>
{% else %}
<td>{{ name }}</td>
{% endif %}
<td class="text-end">{{ fmtbal(accounts[name], mul) }}</td>
</tr>
{% set _ = running_total.__setattr__('quantity', running_total.quantity + accounts[name].quantity) %}
{% endmacro %}
{% block content %}
<h1 class="h2 mt-4">Balance sheet</h1>
<table class="table table-borderless table-sm">
<thead>
<tr style="border-width:0 0 1px 0">
<th></th>
<th class="text-end">$&nbsp;</th>
</tr>
</thead>
<tbody>
<tr>
<th colspan="2">Current assets</th>
</tr>
{% set _ = running_total.__setattr__('quantity', 0) %}
{% for account in BALANCE_SHEET_MAPPING['Current assets'] %}
{{ acctrow(account) }}
{% endfor %}
<tr>
<th>Total current assets</th>
<th class="text-end">{{ fmtbal(running_total) }}</th>
</tr>
{% set assets = running_total.quantity %}
<tr><td colspan="2">&nbsp;</td></tr>
<tr>
<th colspan="2">Non-current assets</th>
</tr>
{% set _ = running_total.__setattr__('quantity', 0) %}
{% for account in BALANCE_SHEET_MAPPING['Non-current assets'] %}
{{ acctrow(account) }}
{% endfor %}
<tr>
<th>Total non-current assets</th>
<th class="text-end">{{ fmtbal(running_total) }}</th>
</tr>
{% set assets = assets + running_total.quantity %}
<tr><td colspan="2">&nbsp;</td></tr>
<tr style="border-width:1px 0">
<th>Total assets</th>
{% set _ = running_total.__setattr__('quantity', assets) %}
<th class="text-end">{{ fmtbal(running_total) }}</th>
</tr>
<tr><td colspan="2">&nbsp;</td></tr>
<tr>
<th colspan="2">Current liabilities</th>
</tr>
{% set _ = running_total.__setattr__('quantity', 0) %}
{% for account in BALANCE_SHEET_MAPPING['Current liabilities'] %}
{{ acctrow(account, -1) }}
{% endfor %}
<tr>
<th>Total current liabilities</th>
<th class="text-end">{{ fmtbal(running_total, -1) }}</th>
</tr>
{% set liabilities = running_total.quantity %}
<tr><td colspan="2">&nbsp;</td></tr>
<tr>
<th colspan="2">Non-current liabilities</th>
</tr>
{% set _ = running_total.__setattr__('quantity', 0) %}
{% for account in BALANCE_SHEET_MAPPING['Non-current liabilities'] %}
{{ acctrow(account, -1) }}
{% endfor %}
<tr>
<th>Total non-current liabilities</th>
<th class="text-end">{{ fmtbal(running_total, -1) }}</th>
</tr>
{% set liabilities = liabilities + running_total.quantity %}
<tr><td colspan="2">&nbsp;</td></tr>
<tr style="border-width:1px 0">
<th>Total liabilities</th>
{% set _ = running_total.__setattr__('quantity', liabilities) %}
<th class="text-end">{{ fmtbal(running_total, -1) }}</th>
</tr>
<tr><td colspan="2">&nbsp;</td></tr>
<tr style="border-width:1px 0">
<th>Net assets</th>
{% set _ = running_total.__setattr__('quantity', assets + liabilities) %}
<th class="text-end">{{ fmtbal(running_total) }}</th>
</tr>
<tr><td colspan="2">&nbsp;</td></tr>
<tr>
<th colspan="2">Equity</th>
</tr>
{% set _ = running_total.__setattr__('quantity', 0) %}
{{ acctrow('Accumulated surplus (deficit)', -1) }}
{{ acctrow('Current year surplus (deficit)', -1) }}
<tr style="border-width:1px 0">
<th>Total equity</th>
<th class="text-end">{{ fmtbal(running_total, -1) }}</th>
</tr>
</tbody>
</table>
{% endblock %}

View File

@ -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 <https://www.gnu.org/licenses/>.
#}
{% 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) }}&nbsp;
{% else %}
({{ '{:,.2f}'.format(amount.quantity|abs / 100) }})
{% endif %}
{% endmacro %}
{% macro acctrow(name, mul=1) %}
<tr>
<td>{{ name }}</td>
<td class="text-end">{{ fmtbal(accounts[name], mul) }}</td>
</tr>
{% set _ = running_total.__setattr__('quantity', running_total.quantity + accounts[name].quantity) %}
{% endmacro %}
{% block content %}
<h1 class="h2 mt-4">Income statement</h1>
<table class="table table-borderless table-sm">
<thead>
<tr style="border-width:0 0 1px 0">
<th></th>
<th class="text-end">$&nbsp;</th>
</tr>
</thead>
<tbody>
<tr>
<th colspan="2">Income</th>
</tr>
{% set _ = running_total.__setattr__('quantity', 0) %}
{% for account in INCOME_STATEMENT_MAPPING['Income'] if account in accounts %}
{{ acctrow(account, -1) }}
{% endfor %}
<tr>
<th>Total income</th>
<th class="text-end">{{ fmtbal(running_total, -1) }}</th>
</tr>
{% set surplus = running_total.quantity %}
<tr><td colspan="2">&nbsp;</td></tr>
<tr>
<th colspan="2">Expenses</th>
</tr>
{% set _ = running_total.__setattr__('quantity', 0) %}
{% for account in INCOME_STATEMENT_MAPPING['Expenses'] if account in accounts %}
{{ acctrow(account) }}
{% endfor %}
<tr>
<th>Total expenses</th>
<th class="text-end">{{ fmtbal(running_total) }}</th>
</tr>
{% set surplus = surplus + running_total.quantity %}
<tr><td colspan="2">&nbsp;</td></tr>
<tr>
<th>Net surplus (deficit) before income tax</th>
{% set _ = running_total.__setattr__('quantity', surplus) %}
<th class="text-end">{{ fmtbal(running_total, -1) }}</th>
</tr>
<tr>
<td>Income tax expense</td>
<td class="text-end">{{ fmtbal(accounts['Income tax']) }}</td>
{% set surplus = surplus + accounts['Income tax'].quantity %}
</tr>
<tr style="border-width:1px 0">
<th>Net surplus (deficit) after income tax</th>
{% set _ = running_total.__setattr__('quantity', surplus) %}
<th class="text-end">{{ fmtbal(running_total, -1) }}</th>
</tr>
</tbody>
</table>
{% endblock %}

View File

@ -18,21 +18,20 @@ from flask import redirect, render_template, request
from .database import db from .database import db
from .models import AccountConfiguration, Amount, Balance, Posting, TrialBalancer from .models import AccountConfiguration, Amount, Balance, Posting, TrialBalancer
from .reports import balance_sheet_report, income_statement_report
from .webapp import all_transactions, app from .webapp import all_transactions, app
from itertools import groupby
@app.route('/') @app.route('/')
def index(): def index():
return render_template('index.html') return render_template('index.html')
@app.route('/chart-of-accounts') @app.route('/chart-of-accounts')
def 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 # Get existing AccountConfiguration's
account_configurations = db.session.execute(db.select(AccountConfiguration).order_by(AccountConfiguration.account)).scalars() account_configurations = AccountConfiguration.get_all()
account_configurations = {v: list(g) for v, g in groupby(account_configurations, lambda c: c.account)}
# TODO: Handle orphans # TODO: Handle orphans
return render_template('chart_of_accounts.html', accounts=accounts, account_configurations=account_configurations) return render_template('chart_of_accounts.html', accounts=accounts, account_configurations=account_configurations)
@ -84,3 +83,13 @@ def account_transactions():
running_total=Amount(0, '$'), running_total=Amount(0, '$'),
transactions=sorted(transactions, key=lambda t: t.dt) 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)

View File

@ -39,7 +39,6 @@ def all_transactions():
from . import views from . import views
from .journal import views from .journal import views
from .reports import views
from .statements import views from .statements import views
from .tax import views from .tax import views