diff --git a/.gitignore b/.gitignore index 7c3b16d..84920f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ __pycache__ +/drcr/config.py /mydata /scripts /venv diff --git a/drcr/models.py b/drcr/models.py index e70e989..ae8db3e 100644 --- a/drcr/models.py +++ b/drcr/models.py @@ -57,3 +57,32 @@ class Amount: def quantity_string(self): return '{:.{dps}f}'.format(self.quantity / (10**AMOUNT_DPS), dps=AMOUNT_DPS) + +class TrialBalancer: + """ + Applies transactions to generate a trial balance + """ + + def __init__(self): + self.accounts = {} + + def apply_transactions(self, transactions): + for transaction in transactions: + for posting in transaction.postings: + if posting.account not in self.accounts: + self.accounts[posting.account] = Amount(0, '$') # FIXME: Other commodities + + # FIXME: Handle commodities + self.accounts[posting.account].quantity += posting.quantity + + def transfer_balance(self, source_account, destination_account, description=None): + """Transfer the balance of the source account to the destination account""" + + # TODO: Keep a record of internal transactions? + + if destination_account not in self.accounts: + self.accounts[destination_account] = Amount(0, '$') # FIXME: Other commodities + + # FIXME: Handle commodities + self.accounts[destination_account].quantity += self.accounts[source_account].quantity + del self.accounts[source_account] diff --git a/drcr/reports/__init__.py b/drcr/reports/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/drcr/reports/views.py b/drcr/reports/views.py new file mode 100644 index 0000000..efd0a2f --- /dev/null +++ b/drcr/reports/views.py @@ -0,0 +1,39 @@ +# 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 . + +from flask import render_template + +from ..config import COA_MAPPING +from ..models import Amount, TrialBalancer +from ..webapp import all_transactions, app + +@app.route('/reports/balance-sheet') +def balance_sheet(): + # Extract trial balance + 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) + + balancer.transfer_balance('Income', 'Equity') + balancer.transfer_balance('Expenses', 'Equity') + + 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) diff --git a/drcr/statements/views.py b/drcr/statements/views.py index 1019580..301bd4c 100644 --- a/drcr/statements/views.py +++ b/drcr/statements/views.py @@ -23,7 +23,12 @@ from .models import StatementLine, StatementLinePosting, StatementLineTransactio @app.route('/statement-lines') def statement_lines(): - return render_template('statement_lines.html', statement_lines=StatementLine.query.all()) + if 'account' in request.args: + statement_lines = StatementLine.query.filter_by(source_account=request.args['account']).all() + else: + statement_lines = StatementLine.query.all() + + return render_template('statement_lines.html', statement_lines=sorted(statement_lines, key=lambda l: l.dt)) @app.route('/statement-lines/charge', methods=['POST']) def statement_line_charge(): diff --git a/drcr/webapp.py b/drcr/webapp.py index e2afe63..637e34d 100644 --- a/drcr/webapp.py +++ b/drcr/webapp.py @@ -18,7 +18,7 @@ from flask import Flask, render_template from .database import db_session, init_db from .general_journal.models import GeneralJournalTransaction -from .models import Amount +from .models import Amount, TrialBalancer from .statements.models import StatementLine, StatementLineTransaction app = Flask(__name__) @@ -40,22 +40,16 @@ def general_ledger(): @app.route('/trial-balance') def trial_balance(): - # Get balances - accounts = {} + balancer = TrialBalancer() + balancer.apply_transactions(all_transactions()) - for transaction in all_transactions(): - for posting in transaction.postings: - if posting.account not in accounts: - accounts[posting.account] = Amount(0, '$') - - accounts[posting.account].quantity += posting.quantity + 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), '$') - total_dr = Amount(sum(v.quantity for v in accounts.values() if v.quantity > 0), '$') - total_cr = Amount(sum(v.quantity for v in accounts.values() if v.quantity < 0), '$') - - return render_template('trial_balance.html', accounts=dict(sorted(accounts.items())), total_dr=total_dr, total_cr=total_cr) + return render_template('trial_balance.html', accounts=dict(sorted(balancer.accounts.items())), total_dr=total_dr, total_cr=total_cr) from .general_journal import views +from .reports import views from .statements import views @app.cli.command('initdb')