# DrCr: Web-based double-entry bookkeeping framework # Copyright (C) 2022–2024 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 abort, redirect, render_template, request, url_for from .. import AMOUNT_DPS from ..database import db from ..models import Amount, Posting, Transaction, TrialBalancer, reporting_commodity from ..webapp import all_accounts, all_transactions, app from .models import BalanceAssertion from ..statements.models import StatementLineReconciliation from datetime import datetime @app.route('/journal') def journal(): transactions = db.session.scalars(db.select(Transaction).options(db.selectinload(Transaction.postings)).order_by(Transaction.dt.desc(), Transaction.id.desc())).all() return render_template( 'journal/journal.html', commodity_detail=request.args.get('commodity_detail', '0') == '1', transactions=transactions ) @app.route('/journal/new-transaction', methods=['GET', 'POST']) def journal_new_transaction(): if request.method == 'GET': return render_template('journal/journal_edit_transaction.html', transaction=None, all_accounts=all_accounts()) # New transaction transaction = Transaction( dt=datetime.strptime(request.form['dt'], '%Y-%m-%d'), description=request.form['description'], postings=[] ) for account, sign, amount_str in zip(request.form.getlist('account'), request.form.getlist('sign'), request.form.getlist('amount')): amount = Amount.parse(amount_str) if sign == 'cr': amount = -amount posting = Posting( account=account, quantity=amount.quantity, commodity=amount.commodity ) transaction.postings.append(posting) transaction.assert_valid() db.session.add(transaction) db.session.commit() return redirect(request.form.get('referrer', '') or url_for('journal')) @app.route('/journal/edit-transaction', methods=['GET', 'POST']) def journal_edit_transaction(): transaction = db.session.get(Transaction, request.args['id']) if not transaction: abort(404) if request.method == 'GET': return render_template('journal/journal_edit_transaction.html', transaction=transaction, all_accounts=all_accounts()) if request.form.get('action', None) == 'delete': # Delete transaction db.session.delete(transaction) # Delete reconciliations if required for posting in transaction.postings: for reconciliation in StatementLineReconciliation.query.filter(StatementLineReconciliation.posting == posting): db.session.delete(reconciliation) db.session.commit() return redirect(request.form.get('referrer', '') or url_for('journal')) # Edit transaction transaction.dt = datetime.strptime(request.form['dt'], '%Y-%m-%d') transaction.description = request.form['description'] new_postings = [] for account, sign, amount_str in zip(request.form.getlist('account'), request.form.getlist('sign'), request.form.getlist('amount')): amount = Amount.parse(amount_str) if sign == 'cr': amount = -amount posting = Posting( account=account, quantity=amount.quantity, commodity=amount.commodity ) new_postings.append(posting) # Fix up reconciliations for old_posting in transaction.postings: for reconciliation in StatementLineReconciliation.query.filter(StatementLineReconciliation.posting == old_posting): # See if there is a corresponding new posting new_posting = next((p for p in new_postings if p.account == old_posting.account and p.quantity == old_posting.quantity and p.commodity == old_posting.commodity), None) if new_posting is not None: # Match up reconciliation reconciliation.posting = new_posting else: # No matching reconciliation db.session.delete(reconciliation) transaction.postings = new_postings # This queues the old postings for deletion transaction.assert_valid() db.session.commit() return redirect(request.form.get('referrer', '') or url_for('journal')) @app.route('/balance-assertions') def balance_assertions(): assertions = db.session.scalars(db.select(BalanceAssertion).order_by(BalanceAssertion.dt.desc(), BalanceAssertion.id.desc())).all() # Check assertion status transactions = all_transactions() assertion_status = {} for assertion in assertions: # FIXME: This is very inefficient balancer = TrialBalancer() balancer.apply_transactions([t for t in transactions if t.dt <= assertion.dt]) # TODO: Commodities if assertion.account in balancer.accounts and balancer.accounts[assertion.account].quantity == assertion.quantity: assertion_status[assertion] = True else: assertion_status[assertion] = False return render_template('journal/balance_assertions.html', assertions=assertions, assertion_status=assertion_status) @app.route('/balance-assertions/new', methods=['GET', 'POST']) def balance_assertions_new(): if request.method == 'GET': return render_template('journal/balance_assertions_edit.html', assertion=None, all_accounts=all_accounts()) quantity = round(float(request.form['amount']) * (10**AMOUNT_DPS)) if request.form['sign'] == 'cr': quantity = -quantity # New balance assertion assertion = BalanceAssertion( dt=datetime.strptime(request.form['dt'], '%Y-%m-%d'), description=request.form['description'], account=request.form['account'], quantity=quantity, commodity=reporting_commodity() ) db.session.add(assertion) db.session.commit() return redirect(url_for('balance_assertions')) @app.route('/balance-assertions/edit', methods=['GET', 'POST']) def balance_assertions_edit(): assertion = db.session.get(BalanceAssertion, request.args['id']) if not assertion: abort(404) if request.method == 'GET': return render_template('journal/balance_assertions_edit.html', assertion=assertion, all_accounts=all_accounts()) if request.form.get('action', None) == 'delete': # Delete balance assertion db.session.delete(assertion) db.session.commit() return redirect(request.form.get('referrer', '') or url_for('balance_assertions')) quantity = round(float(request.form['amount']) * (10**AMOUNT_DPS)) if request.form['sign'] == 'cr': quantity = -quantity # Edit balance assertion assertion.dt = datetime.strptime(request.form['dt'], '%Y-%m-%d') assertion.description = request.form['description'] assertion.account = request.form['account'] assertion.quantity = quantity db.session.commit() return redirect(url_for('balance_assertions'))