Unify all transactions in single model
This commit is contained in:
parent
dd96e7721b
commit
dbc2bca8be
@ -1,5 +1,5 @@
|
|||||||
# DrCr: Web-based double-entry bookkeeping framework
|
# 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
|
# 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
|
# 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 <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from ..database import db
|
from ..database import db
|
||||||
from ..models import Amount, Posting, Transaction
|
from ..models import Amount
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
class BalanceAssertion(db.Model):
|
class BalanceAssertion(db.Model):
|
||||||
__tablename__ = 'balance_assertions'
|
__tablename__ = 'balance_assertions'
|
@ -1,5 +1,5 @@
|
|||||||
# DrCr: Web-based double-entry bookkeeping framework
|
# 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
|
# 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
|
# 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 .. import AMOUNT_DPS
|
||||||
from ..database import db
|
from ..database import db
|
||||||
from ..models import TrialBalancer
|
from ..models import Amount, Posting, Transaction, TrialBalancer
|
||||||
from ..webapp import all_transactions, app
|
from ..webapp import all_transactions, app
|
||||||
from .models import Amount, BalanceAssertion, GeneralJournalPosting, GeneralJournalTransaction
|
from .models import BalanceAssertion
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@app.route('/general-journal')
|
@app.route('/journal')
|
||||||
def general_journal():
|
def journal():
|
||||||
return render_template(
|
return render_template(
|
||||||
'general_journal/general_journal.html',
|
'journal/journal.html',
|
||||||
commodity_detail=request.args.get('commodity-detail', '0') == '1',
|
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'])
|
@app.route('/journal/new-transaction', methods=['GET', 'POST'])
|
||||||
def general_journal_new():
|
def journal_new_transaction():
|
||||||
if request.method == 'GET':
|
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
|
# New transaction
|
||||||
transaction = GeneralJournalTransaction(
|
transaction = Transaction(
|
||||||
dt=datetime.strptime(request.form['dt'], '%Y-%m-%d'),
|
dt=datetime.strptime(request.form['dt'], '%Y-%m-%d'),
|
||||||
description=request.form['description'],
|
description=request.form['description'],
|
||||||
postings=[]
|
postings=[]
|
||||||
@ -49,7 +49,7 @@ def general_journal_new():
|
|||||||
if sign == 'cr':
|
if sign == 'cr':
|
||||||
amount = -amount
|
amount = -amount
|
||||||
|
|
||||||
posting = GeneralJournalPosting(
|
posting = Posting(
|
||||||
account=account,
|
account=account,
|
||||||
quantity=amount.quantity,
|
quantity=amount.quantity,
|
||||||
commodity=amount.commodity
|
commodity=amount.commodity
|
||||||
@ -61,28 +61,30 @@ def general_journal_new():
|
|||||||
db.session.add(transaction)
|
db.session.add(transaction)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return redirect('/general-journal')
|
return redirect('/journal')
|
||||||
|
|
||||||
@app.route('/general-journal/edit', methods=['GET', 'POST'])
|
@app.route('/journal/edit-transaction', methods=['GET', 'POST'])
|
||||||
def general_journal_edit():
|
def journal_edit_transaction():
|
||||||
transaction = db.session.get(GeneralJournalTransaction, request.args['id'])
|
transaction = db.session.get(Transaction, request.args['id'])
|
||||||
if not transaction:
|
if not transaction:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
if request.method == 'GET':
|
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
|
# Edit transaction
|
||||||
transaction.dt = datetime.strptime(request.form['dt'], '%Y-%m-%d')
|
transaction.dt = datetime.strptime(request.form['dt'], '%Y-%m-%d')
|
||||||
transaction.description = request.form['description']
|
transaction.description = request.form['description']
|
||||||
transaction.postings = []
|
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')):
|
for account, sign, amount_str in zip(request.form.getlist('account'), request.form.getlist('sign'), request.form.getlist('amount')):
|
||||||
amount = Amount.parse(amount_str)
|
amount = Amount.parse(amount_str)
|
||||||
if sign == 'cr':
|
if sign == 'cr':
|
||||||
amount = -amount
|
amount = -amount
|
||||||
|
|
||||||
posting = GeneralJournalPosting(
|
posting = Posting(
|
||||||
account=account,
|
account=account,
|
||||||
quantity=amount.quantity,
|
quantity=amount.quantity,
|
||||||
commodity=amount.commodity
|
commodity=amount.commodity
|
||||||
@ -93,7 +95,7 @@ def general_journal_edit():
|
|||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return redirect('/general-journal')
|
return redirect('/journal')
|
||||||
|
|
||||||
@app.route('/balance-assertions')
|
@app.route('/balance-assertions')
|
||||||
def balance_assertions():
|
def balance_assertions():
|
||||||
@ -113,12 +115,12 @@ def balance_assertions():
|
|||||||
else:
|
else:
|
||||||
assertion_status[assertion] = False
|
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'])
|
@app.route('/balance-assertions/new', methods=['GET', 'POST'])
|
||||||
def balance_assertions_new():
|
def balance_assertions_new():
|
||||||
if request.method == 'GET':
|
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))
|
quantity = round(float(request.form['amount']) * (10**AMOUNT_DPS))
|
||||||
if request.form['sign'] == 'cr':
|
if request.form['sign'] == 'cr':
|
||||||
@ -144,7 +146,7 @@ def balance_assertions_edit():
|
|||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
if request.method == 'GET':
|
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))
|
quantity = round(float(request.form['amount']) * (10**AMOUNT_DPS))
|
||||||
if request.form['sign'] == 'cr':
|
if request.form['sign'] == 'cr':
|
@ -1,5 +1,5 @@
|
|||||||
# DrCr: Web-based double-entry bookkeeping framework
|
# 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
|
# 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
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -17,8 +17,18 @@
|
|||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
|
|
||||||
from . import AMOUNT_DPS
|
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):
|
def __init__(self, dt=None, description=None, postings=None):
|
||||||
self.dt = dt
|
self.dt = dt
|
||||||
self.description = description
|
self.description = description
|
||||||
@ -40,7 +50,19 @@ class Transaction:
|
|||||||
if total_dr != total_cr:
|
if total_dr != total_cr:
|
||||||
raise AssertionError('Transaction debits ({}) and credits ({}) do not balance'.format(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):
|
def __init__(self, description=None, account=None, quantity=None, commodity=None):
|
||||||
self.description = description
|
self.description = description
|
||||||
self.account = account
|
self.account = account
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# DrCr: Web-based double-entry bookkeeping framework
|
# 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
|
# 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
|
# 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)
|
balance = db.Column(db.Integer)
|
||||||
commodity = db.Column(db.String)
|
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):
|
def amount(self):
|
||||||
return Amount(self.quantity, self.commodity)
|
return Amount(self.quantity, self.commodity)
|
||||||
|
|
||||||
def into_transaction(self):
|
def into_transaction(self):
|
||||||
if len(self.postings) > 0:
|
if len(self.reconciliations) > 0:
|
||||||
# Will already be accounted for in a StatementLineTransaction
|
# Will already be accounted for in a StatementLineTransaction
|
||||||
#return None
|
|
||||||
raise Exception('Should not call into_transaction on a StatementLine with associated StatementLinePosting')
|
raise Exception('Should not call into_transaction on a StatementLine with associated StatementLinePosting')
|
||||||
|
|
||||||
# Not classified
|
# Not classified
|
||||||
@ -53,53 +52,18 @@ class StatementLine(db.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def is_complex(self):
|
def is_complex(self):
|
||||||
if len(self.postings) > 1:
|
if self.reconciliation and len(self.reconciliation.posting.transaction.postings) > 2:
|
||||||
return True
|
|
||||||
|
|
||||||
if any(len(p.transaction.postings) > 2 for p in self.postings):
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
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):
|
class StatementLineReconciliation(db.Model):
|
||||||
__tablename__ = 'statement_line_transactions'
|
__tablename__ = 'statement_line_reconciliations'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
dt = db.Column(db.DateTime)
|
statement_line_id = db.Column(db.Integer, db.ForeignKey('statement_lines.id'))
|
||||||
description = db.Column(db.String)
|
posting_id = db.Column(db.Integer, db.ForeignKey('postings.id'))
|
||||||
|
|
||||||
postings = db.relationship('StatementLinePosting', back_populates='transaction', cascade='all, delete')
|
statement_line = db.relationship('StatementLine', back_populates='reconciliation')
|
||||||
|
posting = db.relationship('Posting')
|
||||||
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)
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# DrCr: Web-based double-entry bookkeeping framework
|
# 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
|
# 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
|
# 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 .. import AMOUNT_DPS
|
||||||
from ..database import db
|
from ..database import db
|
||||||
from ..webapp import app
|
from ..webapp import app
|
||||||
from .models import StatementLine, StatementLinePosting, StatementLineTransaction
|
from .models import StatementLine
|
||||||
|
|
||||||
@app.route('/statement-lines')
|
@app.route('/statement-lines')
|
||||||
def statement_lines():
|
def statement_lines():
|
||||||
@ -53,57 +53,6 @@ def statement_line_charge():
|
|||||||
|
|
||||||
return 'OK'
|
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'])
|
@app.route('/statement-lines/reconcile-transfer', methods=['POST'])
|
||||||
def statement_line_reconcile_transfer():
|
def statement_line_reconcile_transfer():
|
||||||
line_ids = request.form.getlist('sel-line-id')
|
line_ids = request.form.getlist('sel-line-id')
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{# DrCr: Web-based double-entry bookkeeping framework
|
{# 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
|
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
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -31,7 +31,7 @@
|
|||||||
<a class="navbar-brand" href="/">DrCr</a>
|
<a class="navbar-brand" href="/">DrCr</a>
|
||||||
<div class="collapse navbar-collapse">
|
<div class="collapse navbar-collapse">
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item"><a class="nav-link" href="/general-journal">General journal</a></li>
|
<li class="nav-item"><a class="nav-link" href="/journal">Journal</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="/statement-lines">Statement lines</a></li>
|
<li class="nav-item"><a class="nav-link" href="/statement-lines">Statement lines</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="/trial-balance">Trial balance</a></li>
|
<li class="nav-item"><a class="nav-link" href="/trial-balance">Trial balance</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{# DrCr: Web-based double-entry bookkeeping framework
|
{# 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
|
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
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -21,7 +21,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="h2 my-4">Data sources</h1>
|
<h1 class="h2 my-4">Data sources</h1>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/general-journal">General journal</a></li>
|
<li><a href="/journal">Journal</a></li>
|
||||||
<li><a href="/statement-lines">Statement lines</a></li>
|
<li><a href="/statement-lines">Statement lines</a></li>
|
||||||
<li><a href="/balance-assertions">Balance assertions</a></li>
|
<li><a href="/balance-assertions">Balance assertions</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{# DrCr: Web-based double-entry bookkeeping framework
|
{# 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
|
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
|
it under the terms of the GNU Affero General Public License as published by
|
@ -1,5 +1,5 @@
|
|||||||
{# DrCr: Web-based double-entry bookkeeping framework
|
{# 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
|
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
|
it under the terms of the GNU Affero General Public License as published by
|
@ -1,5 +1,5 @@
|
|||||||
{# DrCr: Web-based double-entry bookkeeping framework
|
{# 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
|
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
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -16,13 +16,13 @@
|
|||||||
#}
|
#}
|
||||||
|
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block title %}General journal{% endblock %}
|
{% block title %}Journal{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="h2 my-4">General journal</h1>
|
<h1 class="h2 my-4">Journal</h1>
|
||||||
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<a href="/general-journal/new" class="btn btn-primary"><i class="bi bi-plus-lg"></i> New transaction</a>
|
{#<a href="/journal/new-transaction" class="btn btn-primary"><i class="bi bi-plus-lg"></i> New transaction</a>#}
|
||||||
{% if commodity_detail %}
|
{% if commodity_detail %}
|
||||||
<a href="?commodity-detail=0" class="btn btn-outline-secondary">Hide commodity detail</a>
|
<a href="?commodity-detail=0" class="btn btn-outline-secondary">Hide commodity detail</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -43,7 +43,7 @@
|
|||||||
{% for transaction in transactions %}
|
{% for transaction in transactions %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ transaction.dt.strftime('%Y-%m-%d') }}</td>
|
<td>{{ transaction.dt.strftime('%Y-%m-%d') }}</td>
|
||||||
<td colspan="3">{{ transaction.description }} <a href="/general-journal/edit?id={{ transaction.id }}"><i class="bi bi-pencil text-muted"></i></a></td>
|
<td colspan="3">{{ transaction.description }} {#<a href="/journal/edit-transaction?id={{ transaction.id }}"><i class="bi bi-pencil text-muted"></i></a>#}</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
@ -1,5 +1,5 @@
|
|||||||
{# DrCr: Web-based double-entry bookkeeping framework
|
{# 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
|
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
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -16,10 +16,10 @@
|
|||||||
#}
|
#}
|
||||||
|
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block title %}{{ 'Edit' if transaction else 'New' }} general journal transaction{% endblock %}
|
{% block title %}{{ 'Edit' if transaction else 'New' }} transaction{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="h2 my-4">{{ 'Edit' if transaction else 'New' }} general journal transaction</h1>
|
<h1 class="h2 my-4">{{ 'Edit' if transaction else 'New' }} transaction</h1>
|
||||||
|
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<table class="table">
|
<table class="table">
|
@ -1,162 +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 %}Edit statement line charge{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h1 class="h2 my-4">Statement line</h1>
|
|
||||||
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Source account</th>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Dr</th>
|
|
||||||
<th>Cr</th>
|
|
||||||
<th>Balance</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr id="statement-line" data-quantity="{{ statement_line.amount().quantity_string() }}" data-commodity="{{ statement_line.commodity }}">
|
|
||||||
<td>{{ statement_line.source_account }}</td>
|
|
||||||
<td>{{ statement_line.dt.strftime('%Y-%m-%d') }}</td>
|
|
||||||
<td>{{ statement_line.description }}</td>
|
|
||||||
<td>{{ statement_line.amount().format() if statement_line.quantity >= 0 else '' }}</td>
|
|
||||||
<td>{{ (statement_line.amount()|abs).format() if statement_line.quantity < 0 else '' }}</td>
|
|
||||||
<td>{{ statement_line.balance or '' }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h1 class="h2 mt-4 mb-4">Transaction</h1>
|
|
||||||
|
|
||||||
<form method="POST">
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Date</th>
|
|
||||||
<th colspan="2">Description</th>
|
|
||||||
<th>Dr</th>
|
|
||||||
<th>Cr</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{# FIXME: Customise date, etc. #}
|
|
||||||
<tr>
|
|
||||||
<td>{{ statement_line.dt.strftime('%Y-%m-%d') }}</td>
|
|
||||||
<td colspan="2"><input type="text" name="description" value="{{ statement_line.description }}" class="form-control"></td>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
<tr data-side="source">
|
|
||||||
{# Source line #}
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
<td>
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-text">{{ 'Dr' if statement_line.quantity >= 0 else 'Cr' }}</div>
|
|
||||||
<input type="text" class="form-control" value="{{ statement_line.source_account }}" disabled>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
{% if statement_line.quantity >= 0 %}<td class="has-amount">{{ statement_line.amount().format() }}</td>{% else %}<td></td>{% endif %}
|
|
||||||
{% if statement_line.quantity < 0 %}<td class="has-amount">{{ (statement_line.amount()|abs).format() }}</td>{% else %}<td></td>{% endif %}
|
|
||||||
</tr>
|
|
||||||
{# Charge lines #}
|
|
||||||
{% if transaction == None or transaction.postings|length == 2 %}
|
|
||||||
<tr data-side="charge">
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
<td>
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-text">{{ 'Cr' if statement_line.quantity >= 0 else 'Dr' }}</div>
|
|
||||||
<input type="text" name="charge-account" class="form-control" value="{{ transaction.charge_account(statement_line.source_account) if transaction else '' }}">
|
|
||||||
<a class="btn btn-outline-primary" href="#" onclick="addPosting(this);return false;"><i class="bi bi-plus-circle-fill"></i></a>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
{% if statement_line.quantity < 0 %}<td class="has-amount">{{ (statement_line.amount()|abs).format() }}</td>{% else %}<td></td>{% endif %}
|
|
||||||
{% if statement_line.quantity >= 0 %}<td class="has-amount">{{ statement_line.amount().format() }}</td>{% else %}<td></td>{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% else %}
|
|
||||||
{% for posting in transaction.postings if posting.account != statement_line.source_account %}
|
|
||||||
<tr data-side="charge">
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
<td>
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-text">{{ 'Cr' if statement_line.quantity >= 0 else 'Dr' }}</div>
|
|
||||||
<input type="text" name="charge-account" class="form-control" value="{{ posting.account }}">
|
|
||||||
<a class="btn btn-outline-primary" href="#" onclick="addPosting(this);return false;"><i class="bi bi-plus-circle-fill"></i></a>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
{% if statement_line.quantity < 0 %}
|
|
||||||
<td class="has-amount">
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-text">{{ posting.commodity }}</div>
|
|
||||||
<input type="number" name="charge-amount" step="0.01" value="{{ posting.amount().quantity_string() }}" class="form-control">
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
{% else %}<td></td>{% endif %}
|
|
||||||
{% if statement_line.quantity >= 0 %}
|
|
||||||
<td class="has-amount">
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-text">{{ posting.commodity }}</div>
|
|
||||||
<input type="number" name="charge-amount" step="0.01" value="{{ (posting.amount()|abs).quantity_string() }}" class="form-control">
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
{% else %}<td></td>{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-end">
|
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script>
|
|
||||||
function addPosting(el) {
|
|
||||||
let trPosting = el.parentNode.parentNode.parentNode;
|
|
||||||
let trLine = document.getElementById('statement-line');
|
|
||||||
let qtyf = parseFloat(trLine.dataset.quantity);
|
|
||||||
|
|
||||||
if (trPosting.dataset['side'] === 'source') {
|
|
||||||
alert('NYI');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trPosting.dataset['side'] === 'charge') {
|
|
||||||
let inputAmount = '<td class="has-amount"><div class="input-group"><div class="input-group-text">' + trLine.dataset.commodity + '</div><input type="number" name="charge-amount" step="0.01" class="form-control"></div></td>';
|
|
||||||
|
|
||||||
// Add new posting row
|
|
||||||
let trNew = document.createElement('tr');
|
|
||||||
trNew.dataset['side'] = 'charge';
|
|
||||||
trNew.innerHTML = '<td></td><td></td><td><div class="input-group"><div class="input-group-text">' + (qtyf >= 0 ? 'Cr' : 'Dr') + '</div><input type="text" name="charge-account" class="form-control"><a class="btn btn-outline-primary" href="#" onclick="addPosting(this);return false;"><i class="bi bi-plus-circle-fill"></i></a></div></td>' + (qtyf < 0 ? inputAmount : '<td></td>') + (qtyf >= 0 ? inputAmount : '<td></td>');
|
|
||||||
trPosting.after(trNew);
|
|
||||||
|
|
||||||
// Put input box in existing row
|
|
||||||
trPosting.querySelector('.has-amount').outerHTML = inputAmount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
@ -1,5 +1,5 @@
|
|||||||
{# DrCr: Web-based double-entry bookkeeping framework
|
{# 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
|
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
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -22,9 +22,9 @@
|
|||||||
<h1 class="h2 my-4">Statement lines</h1>
|
<h1 class="h2 my-4">Statement lines</h1>
|
||||||
|
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<div class="mb-2">
|
{#<div class="mb-2">
|
||||||
<button type="submit" class="btn btn-outline-secondary" formaction="/statement-lines/reconcile-transfer">Reconcile selected as transfer</button>
|
<button type="submit" class="btn btn-outline-secondary" formaction="/statement-lines/reconcile-transfer">Reconcile selected as transfer</button>
|
||||||
</div>
|
</div>#}
|
||||||
|
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
@ -47,14 +47,18 @@
|
|||||||
<td>{{ line.dt.strftime('%Y-%m-%d') }}</td>
|
<td>{{ line.dt.strftime('%Y-%m-%d') }}</td>
|
||||||
<td>{{ line.description }}</td>
|
<td>{{ line.description }}</td>
|
||||||
<td class="charge-account">
|
<td class="charge-account">
|
||||||
{% if line.postings|length == 0 %}
|
{% if not line.reconciliation %}
|
||||||
<a href="#" class="text-danger" onclick="classifyLine({{ line.id }});return false;">Unclassified</a>
|
<a href="#" class="text-danger" onclick="classifyLine({{ line.id }});return false;">Unclassified</a>
|
||||||
|
{# TODO #}
|
||||||
{% elif line.is_complex() %}
|
{% elif line.is_complex() %}
|
||||||
<i>(Complex)</i>
|
<i>(Complex)</i>
|
||||||
|
{#<a href="/journal/edit-transaction?id={{ line.reconciliation.posting.transaction.id }}" class="text-muted"><i class="bi bi-pencil"></i></a>#}
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="#" class="text-body" onclick="classifyLine({{ line.id }});return false;">{{ line.postings[0].transaction.charge_account(line.source_account) }}</a>
|
{% for posting in line.reconciliation.posting.transaction.postings if posting.account != line.source_account %}
|
||||||
|
<a href="#" class="text-body" onclick="classifyLine({{ line.id }});return false;">{{ posting.account }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
{#<a href="/journal/edit-transaction?id={{ line.reconciliation.posting.transaction.id }}" class="text-muted"><i class="bi bi-pencil"></i></a>#}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="/statement-lines/edit-transaction?line-id={{ line.id }}" class="text-muted"><i class="bi bi-pencil"></i></a>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">{{ line.amount().format() if line.quantity >= 0 else '' }}</td>
|
<td class="text-end">{{ line.amount().format() if line.quantity >= 0 else '' }}</td>
|
||||||
<td class="text-end">{{ (line.amount()|abs).format() if line.quantity < 0 else '' }}</td>
|
<td class="text-end">{{ (line.amount()|abs).format() if line.quantity < 0 else '' }}</td>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# DrCr: Web-based double-entry bookkeeping framework
|
# 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
|
# 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
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -18,8 +18,8 @@ from flask import Flask, g
|
|||||||
from flask_sqlalchemy.record_queries import get_recorded_queries
|
from flask_sqlalchemy.record_queries import get_recorded_queries
|
||||||
|
|
||||||
from .database import db
|
from .database import db
|
||||||
from .general_journal.models import GeneralJournalTransaction
|
from .models import Transaction
|
||||||
from .statements.models import StatementLine, StatementLineTransaction
|
from .statements.models import StatementLine
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@ -31,13 +31,12 @@ db.init_app(app)
|
|||||||
|
|
||||||
def all_transactions():
|
def all_transactions():
|
||||||
return (
|
return (
|
||||||
GeneralJournalTransaction.query.all() +
|
Transaction.query.all() +
|
||||||
StatementLineTransaction.query.all() +
|
[line.into_transaction() for line in StatementLine.query.filter(StatementLine.reconciliation == None)]
|
||||||
[line.into_transaction() for line in StatementLine.query.all() if len(line.postings) == 0] # TODO: Filter in SQL
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
from .general_journal import views
|
from .journal import views
|
||||||
from .reports import views
|
from .reports import views
|
||||||
from .statements import views
|
from .statements import views
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user