Implement API-based generation of reports
This commit is contained in:
parent
7df955e5b6
commit
c527442ac7
@ -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': [],
|
||||
|
@ -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
|
||||
|
212
drcr/reports.py
Normal file
212
drcr/reports.py
Normal 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
|
@ -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)
|
@ -31,12 +31,12 @@
|
||||
<ul>
|
||||
<li><a href="/general-ledger">General ledger</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>
|
||||
|
||||
<h1 class="h2 my-4">Advanced reports</h1>
|
||||
<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>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
65
drcr/templates/report.html
Normal file
65
drcr/templates/report.html
Normal 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"> </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">$ </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in report.entries %}
|
||||
{{ render_entry(entry) }}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
@ -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) }}
|
||||
{% 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">$ </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"> </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"> </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"> </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"> </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"> </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"> </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"> </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 %}
|
@ -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) }}
|
||||
{% 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">$ </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"> </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"> </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 %}
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user