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 # 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'

View File

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

View File

@ -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(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')
class Transaction:
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

View File

@ -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): class StatementLineReconciliation(db.Model):
"""Delete existing StatementLineTransactions with postings for this StatementLine""" __tablename__ = 'statement_line_reconciliations'
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'
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 {# 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>

View File

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