diff --git a/drcr/general_journal/__init__.py b/drcr/journal/__init__.py similarity index 100% rename from drcr/general_journal/__init__.py rename to drcr/journal/__init__.py diff --git a/drcr/general_journal/models.py b/drcr/journal/models.py similarity index 53% rename from drcr/general_journal/models.py rename to drcr/journal/models.py index 33fb528..6dee0d5 100644 --- a/drcr/general_journal/models.py +++ b/drcr/journal/models.py @@ -1,5 +1,5 @@ # DrCr: Web-based double-entry bookkeeping framework -# Copyright (C) 2022 Lee Yingtong Li (RunasSudo) +# 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 @@ -15,33 +15,7 @@ # along with this program. If not, see . from ..database import db -from ..models import Amount, Posting, Transaction - -class GeneralJournalTransaction(db.Model, Transaction): - __tablename__ = 'general_journal_transactions' - - id = db.Column(db.Integer, primary_key=True) - - dt = db.Column(db.DateTime) - description = db.Column(db.String) - - postings = db.relationship('GeneralJournalPosting', back_populates='transaction', cascade='all, delete-orphan') - -class GeneralJournalPosting(db.Model, Posting): - __tablename__ = 'general_journal_postings' - - id = db.Column(db.Integer, primary_key=True) - transaction_id = db.Column(db.Integer, db.ForeignKey('general_journal_transactions.id')) - - description = db.Column(db.String) - account = db.Column(db.String) - quantity = db.Column(db.Integer) - commodity = db.Column(db.String) - - transaction = db.relationship('GeneralJournalTransaction', back_populates='postings') - - def amount(self): - return Amount(self.quantity, self.commodity) +from ..models import Amount class BalanceAssertion(db.Model): __tablename__ = 'balance_assertions' diff --git a/drcr/general_journal/views.py b/drcr/journal/views.py similarity index 75% rename from drcr/general_journal/views.py rename to drcr/journal/views.py index 748f1ed..87934ea 100644 --- a/drcr/general_journal/views.py +++ b/drcr/journal/views.py @@ -1,5 +1,5 @@ # DrCr: Web-based double-entry bookkeeping framework -# Copyright (C) 2022 Lee Yingtong Li (RunasSudo) +# 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 @@ -18,27 +18,27 @@ from flask import abort, redirect, render_template, request from .. import AMOUNT_DPS from ..database import db -from ..models import TrialBalancer +from ..models import Amount, Posting, Transaction, TrialBalancer from ..webapp import all_transactions, app -from .models import Amount, BalanceAssertion, GeneralJournalPosting, GeneralJournalTransaction +from .models import BalanceAssertion from datetime import datetime -@app.route('/general-journal') -def general_journal(): +@app.route('/journal') +def journal(): return render_template( - 'general_journal/general_journal.html', + 'journal/journal.html', commodity_detail=request.args.get('commodity-detail', '0') == '1', - transactions=sorted(GeneralJournalTransaction.query.all(), key=lambda t: t.dt) + transactions=sorted(Transaction.query.all(), key=lambda t: t.dt) ) -@app.route('/general-journal/new', methods=['GET', 'POST']) -def general_journal_new(): +@app.route('/journal/new-transaction', methods=['GET', 'POST']) +def journal_new_transaction(): if request.method == 'GET': - return render_template('general_journal/general_journal_edit.html', transaction=None) + return render_template('journal/journal_edit_transaction.html', transaction=None) # New transaction - transaction = GeneralJournalTransaction( + transaction = Transaction( dt=datetime.strptime(request.form['dt'], '%Y-%m-%d'), description=request.form['description'], postings=[] @@ -49,7 +49,7 @@ def general_journal_new(): if sign == 'cr': amount = -amount - posting = GeneralJournalPosting( + posting = Posting( account=account, quantity=amount.quantity, commodity=amount.commodity @@ -61,28 +61,30 @@ def general_journal_new(): db.session.add(transaction) db.session.commit() - return redirect('/general-journal') + return redirect('/journal') -@app.route('/general-journal/edit', methods=['GET', 'POST']) -def general_journal_edit(): - transaction = db.session.get(GeneralJournalTransaction, request.args['id']) +@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('general_journal/general_journal_edit.html', transaction=transaction) + return render_template('journal/journal_edit_transaction.html', transaction=transaction) # Edit transaction transaction.dt = datetime.strptime(request.form['dt'], '%Y-%m-%d') transaction.description = request.form['description'] transaction.postings = [] + # FIXME: This will orphan StatementLineReconciliations + 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 = GeneralJournalPosting( + posting = Posting( account=account, quantity=amount.quantity, commodity=amount.commodity @@ -93,7 +95,7 @@ def general_journal_edit(): db.session.commit() - return redirect('/general-journal') + return redirect('/journal') @app.route('/balance-assertions') def balance_assertions(): @@ -113,12 +115,12 @@ def balance_assertions(): else: assertion_status[assertion] = False - return render_template('general_journal/balance_assertions.html', assertions=assertions, assertion_status=assertion_status) + 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('general_journal/balance_assertions_edit.html', assertion=None) + return render_template('journal/balance_assertions_edit.html', assertion=None) quantity = round(float(request.form['amount']) * (10**AMOUNT_DPS)) if request.form['sign'] == 'cr': @@ -144,7 +146,7 @@ def balance_assertions_edit(): abort(404) if request.method == 'GET': - return render_template('general_journal/balance_assertions_edit.html', assertion=assertion) + return render_template('journal/balance_assertions_edit.html', assertion=assertion) quantity = round(float(request.form['amount']) * (10**AMOUNT_DPS)) if request.form['sign'] == 'cr': diff --git a/drcr/models.py b/drcr/models.py index 9bd1473..6761c11 100644 --- a/drcr/models.py +++ b/drcr/models.py @@ -1,5 +1,5 @@ # DrCr: Web-based double-entry bookkeeping framework -# Copyright (C) 2022 Lee Yingtong Li (RunasSudo) +# 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 @@ -17,8 +17,18 @@ from markupsafe import Markup from . import AMOUNT_DPS +from .database import db -class Transaction: +class Transaction(db.Model): + __tablename__ = 'transactions' + + id = db.Column(db.Integer, primary_key=True) + + dt = db.Column(db.DateTime) + description = db.Column(db.String) + + postings = db.relationship('Posting', back_populates='transaction', cascade='all, delete-orphan') + def __init__(self, dt=None, description=None, postings=None): self.dt = dt self.description = description @@ -40,7 +50,19 @@ class Transaction: if total_dr != total_cr: raise AssertionError('Transaction debits ({}) and credits ({}) do not balance'.format(total_dr, total_cr)) -class Posting: +class Posting(db.Model): + __tablename__ = 'postings' + + id = db.Column(db.Integer, primary_key=True) + transaction_id = db.Column(db.Integer, db.ForeignKey('transactions.id')) + + description = db.Column(db.String) + account = db.Column(db.String) + quantity = db.Column(db.Integer) + commodity = db.Column(db.String) + + transaction = db.relationship('Transaction', back_populates='postings') + def __init__(self, description=None, account=None, quantity=None, commodity=None): self.description = description self.account = account diff --git a/drcr/statements/models.py b/drcr/statements/models.py index 951865b..06c8ccc 100644 --- a/drcr/statements/models.py +++ b/drcr/statements/models.py @@ -1,5 +1,5 @@ # DrCr: Web-based double-entry bookkeeping framework -# Copyright (C) 2022 Lee Yingtong Li (RunasSudo) +# 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 @@ -29,15 +29,14 @@ class StatementLine(db.Model): balance = db.Column(db.Integer) commodity = db.Column(db.String) - postings = db.relationship('StatementLinePosting', back_populates='statement_line') + reconciliation = db.relationship('StatementLineReconciliation', back_populates='statement_line', uselist=False) def amount(self): return Amount(self.quantity, self.commodity) def into_transaction(self): - if len(self.postings) > 0: + if len(self.reconciliations) > 0: # Will already be accounted for in a StatementLineTransaction - #return None raise Exception('Should not call into_transaction on a StatementLine with associated StatementLinePosting') # Not classified @@ -53,53 +52,18 @@ class StatementLine(db.Model): ) def is_complex(self): - if len(self.postings) > 1: - return True - - if any(len(p.transaction.postings) > 2 for p in self.postings): + if self.reconciliation and len(self.reconciliation.posting.transaction.postings) > 2: return True return False - - def delete_postings(self): - """Delete existing StatementLineTransactions with postings for this StatementLine""" - - for posting in self.postings: - # TODO: Will be wonky if transaction covers multiple StatementLines - db.session.delete(posting.transaction) -class StatementLineTransaction(db.Model, Transaction): - __tablename__ = 'statement_line_transactions' +class StatementLineReconciliation(db.Model): + __tablename__ = 'statement_line_reconciliations' id = db.Column(db.Integer, primary_key=True) - dt = db.Column(db.DateTime) - description = db.Column(db.String) + statement_line_id = db.Column(db.Integer, db.ForeignKey('statement_lines.id')) + posting_id = db.Column(db.Integer, db.ForeignKey('postings.id')) - postings = db.relationship('StatementLinePosting', back_populates='transaction', cascade='all, delete') - - def charge_account(self, source_account): - if len(self.postings) > 2: - raise Exception('Cannot call charge_account on StatementLineTransaction with more than 2 postings') - - for posting in self.postings: - if posting.account != source_account: - return posting.account - -class StatementLinePosting(db.Model, Posting): - __tablename__ = 'statement_line_postings' - - id = db.Column(db.Integer, primary_key=True) - transaction_id = db.Column(db.Integer, db.ForeignKey('statement_line_transactions.id')) - line_id = db.Column(db.Integer, db.ForeignKey('statement_lines.id')) - - description = db.Column(db.String) - account = db.Column(db.String) - quantity = db.Column(db.Integer) - commodity = db.Column(db.String) - - transaction = db.relationship('StatementLineTransaction', back_populates='postings') - statement_line = db.relationship('StatementLine', back_populates='postings') - - def amount(self): - return Amount(self.quantity, self.commodity) + statement_line = db.relationship('StatementLine', back_populates='reconciliation') + posting = db.relationship('Posting') diff --git a/drcr/statements/views.py b/drcr/statements/views.py index 8b3b288..fbfce33 100644 --- a/drcr/statements/views.py +++ b/drcr/statements/views.py @@ -1,5 +1,5 @@ # DrCr: Web-based double-entry bookkeeping framework -# Copyright (C) 2022 Lee Yingtong Li (RunasSudo) +# 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 @@ -19,7 +19,7 @@ from flask import abort, redirect, render_template, request from .. import AMOUNT_DPS from ..database import db from ..webapp import app -from .models import StatementLine, StatementLinePosting, StatementLineTransaction +from .models import StatementLine @app.route('/statement-lines') def statement_lines(): @@ -53,57 +53,6 @@ def statement_line_charge(): return 'OK' -@app.route('/statement-lines/edit-transaction', methods=['GET', 'POST']) -def statement_line_edit_transaction(): - statement_line = db.session.get(StatementLine, request.args['line-id']) - if not statement_line: - abort(404) - - if request.method == 'GET': - if len(statement_line.postings) == 0: - return render_template('statements/statement_line_edit_transaction.html', statement_line=statement_line, transaction=None) - - if len(statement_line.postings) > 1: - # NYI - raise Exception('Cannot display a StatementLine with >2 postings') - - # Get existing transaction - return render_template('statements/statement_line_edit_transaction.html', statement_line=statement_line, transaction=statement_line.postings[0].transaction) - - # Delete existing postings - statement_line.delete_postings() - - if len(request.form.getlist('charge-account')) == 1: - # Simple transaction - postings = [ - StatementLinePosting(statement_line=statement_line, account=statement_line.source_account, quantity=statement_line.quantity, commodity=statement_line.commodity), - StatementLinePosting(account=request.form['charge-account'], quantity=-statement_line.quantity, commodity=statement_line.commodity) - ] - else: - # Complex transaction, multiple postings - postings = [ - StatementLinePosting(statement_line=statement_line, account=statement_line.source_account, quantity=statement_line.quantity, commodity=statement_line.commodity) - ] - - for charge_account, charge_amount_str in zip(request.form.getlist('charge-account'), request.form.getlist('charge-amount')): - charge_quantity = round(float(charge_amount_str) * (10**AMOUNT_DPS)) - if statement_line.quantity >= 0: - # If source is debit, charge is credit - charge_quantity = -charge_quantity - postings.append(StatementLinePosting(account=charge_account, quantity=charge_quantity, commodity=statement_line.commodity)) - - transaction = StatementLineTransaction( - dt=statement_line.dt, - description=request.form['description'], - postings=postings - ) - transaction.assert_valid() - - db.session.add(transaction) - db.session.commit() - - return redirect('/statement-lines') - @app.route('/statement-lines/reconcile-transfer', methods=['POST']) def statement_line_reconcile_transfer(): line_ids = request.form.getlist('sel-line-id') diff --git a/drcr/templates/base.html b/drcr/templates/base.html index 6736f15..3720aa4 100644 --- a/drcr/templates/base.html +++ b/drcr/templates/base.html @@ -1,5 +1,5 @@ {# DrCr: Web-based double-entry bookkeeping framework - Copyright (C) 2022 Lee Yingtong Li (RunasSudo) + 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 @@ -31,7 +31,7 @@ DrCr