Unify all transactions in single model

This commit is contained in:
RunasSudo 2023-01-02 18:08:30 +11:00
parent dd96e7721b
commit dbc2bca8be
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
15 changed files with 93 additions and 341 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
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'

View File

@ -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':

View File

@ -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

View File

@ -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')

View File

@ -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')

View File

@ -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 @@
<a class="navbar-brand" href="/">DrCr</a>
<div class="collapse navbar-collapse">
<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="/trial-balance">Trial balance</a></li>
</ul>

View File

@ -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
@ -21,7 +21,7 @@
{% block content %}
<h1 class="h2 my-4">Data sources</h1>
<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="/balance-assertions">Balance assertions</a></li>
</ul>

View File

@ -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

View File

@ -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

View File

@ -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
@ -16,13 +16,13 @@
#}
{% extends 'base.html' %}
{% block title %}General journal{% endblock %}
{% block title %}Journal{% endblock %}
{% block content %}
<h1 class="h2 my-4">General journal</h1>
<h1 class="h2 my-4">Journal</h1>
<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 %}
<a href="?commodity-detail=0" class="btn btn-outline-secondary">Hide commodity detail</a>
{% else %}
@ -43,7 +43,7 @@
{% for transaction in transactions %}
<tr>
<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>
</tr>

View File

@ -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
@ -16,10 +16,10 @@
#}
{% 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 %}
<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">
<table class="table">

View File

@ -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 %}

View File

@ -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
@ -22,9 +22,9 @@
<h1 class="h2 my-4">Statement lines</h1>
<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>
</div>
</div>#}
<table class="table">
<thead>
@ -47,14 +47,18 @@
<td>{{ line.dt.strftime('%Y-%m-%d') }}</td>
<td>{{ line.description }}</td>
<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>
{# TODO #}
{% elif line.is_complex() %}
<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 %}
<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 %}
<a href="/statement-lines/edit-transaction?line-id={{ line.id }}" class="text-muted"><i class="bi bi-pencil"></i></a>
</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>

View File

@ -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,8 +18,8 @@ from flask import Flask, g
from flask_sqlalchemy.record_queries import get_recorded_queries
from .database import db
from .general_journal.models import GeneralJournalTransaction
from .statements.models import StatementLine, StatementLineTransaction
from .models import Transaction
from .statements.models import StatementLine
import time
@ -31,13 +31,12 @@ db.init_app(app)
def all_transactions():
return (
GeneralJournalTransaction.query.all() +
StatementLineTransaction.query.all() +
[line.into_transaction() for line in StatementLine.query.all() if len(line.postings) == 0] # TODO: Filter in SQL
Transaction.query.all() +
[line.into_transaction() for line in StatementLine.query.filter(StatementLine.reconciliation == None)]
)
from . import views
from .general_journal import views
from .journal import views
from .reports import views
from .statements import views