06/02 Modified transaction, transaction type switchable
This commit is contained in:
+37
-11
@@ -29,6 +29,15 @@ def _category_choices(cat_type):
|
|||||||
return [('', '— None —')] + [(str(c.id), c.name) for c in cats]
|
return [('', '— None —')] + [(str(c.id), c.name) for c in cats]
|
||||||
|
|
||||||
|
|
||||||
|
def _category_choices_json(cat_type):
|
||||||
|
cats = Category.query.filter(
|
||||||
|
Category.category_type.in_([cat_type, 'both']),
|
||||||
|
Category.is_active == True,
|
||||||
|
Category.parent_id == None
|
||||||
|
).order_by(Category.name).all()
|
||||||
|
return [{'id': str(c.id), 'name': c.name} for c in cats]
|
||||||
|
|
||||||
|
|
||||||
class TransactionForm(FlaskForm):
|
class TransactionForm(FlaskForm):
|
||||||
transaction_type = HiddenField(default='expense')
|
transaction_type = HiddenField(default='expense')
|
||||||
account_id = SelectField('Account', validators=[DataRequired()])
|
account_id = SelectField('Account', validators=[DataRequired()])
|
||||||
@@ -122,13 +131,18 @@ def new():
|
|||||||
txn_type = 'expense'
|
txn_type = 'expense'
|
||||||
|
|
||||||
form = TransactionForm()
|
form = TransactionForm()
|
||||||
form.transaction_type.data = txn_type
|
# On POST, honour the type the user selected in the toggle
|
||||||
|
if request.method == 'POST':
|
||||||
|
submitted = request.form.get('transaction_type', txn_type)
|
||||||
|
if submitted in ('income', 'expense'):
|
||||||
|
txn_type = submitted
|
||||||
form.account_id.choices = _account_choices()
|
form.account_id.choices = _account_choices()
|
||||||
form.category_id.choices = _category_choices(txn_type)
|
form.category_id.choices = _category_choices(txn_type)
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
|
saved_type = form.transaction_type.data
|
||||||
txn = Transaction(
|
txn = Transaction(
|
||||||
transaction_type=form.transaction_type.data,
|
transaction_type=saved_type,
|
||||||
account_id=int(form.account_id.data),
|
account_id=int(form.account_id.data),
|
||||||
category_id=int(form.category_id.data) if form.category_id.data else None,
|
category_id=int(form.category_id.data) if form.category_id.data else None,
|
||||||
amount=form.amount.data,
|
amount=form.amount.data,
|
||||||
@@ -139,15 +153,17 @@ def new():
|
|||||||
db.session.add(txn)
|
db.session.add(txn)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
calc_balance(txn.account_id)
|
calc_balance(txn.account_id)
|
||||||
flash(f'{"Income" if txn_type == "income" else "Expense"} added.', 'success')
|
flash(f'{"Income" if saved_type == "income" else "Expense"} added.', 'success')
|
||||||
if txn_type == 'expense':
|
if saved_type == 'expense':
|
||||||
from app.services.alert_service import check_and_flash_budget_alerts
|
from app.services.alert_service import check_and_flash_budget_alerts
|
||||||
check_and_flash_budget_alerts(flash)
|
check_and_flash_budget_alerts(flash)
|
||||||
return redirect(url_for('transactions.index', tab=txn_type))
|
return redirect(url_for('transactions.index', tab=saved_type))
|
||||||
|
|
||||||
return render_template('transactions/form.html',
|
return render_template('transactions/form.html',
|
||||||
form=form,
|
form=form,
|
||||||
txn_type=txn_type,
|
txn_type=txn_type,
|
||||||
|
income_cats=_category_choices_json('income'),
|
||||||
|
expense_cats=_category_choices_json('expense'),
|
||||||
title=f'New {"Income" if txn_type == "income" else "Expense"}')
|
title=f'New {"Income" if txn_type == "income" else "Expense"}')
|
||||||
|
|
||||||
|
|
||||||
@@ -155,18 +171,26 @@ def new():
|
|||||||
@login_required
|
@login_required
|
||||||
def edit(id):
|
def edit(id):
|
||||||
txn = db.get_or_404(Transaction, id)
|
txn = db.get_or_404(Transaction, id)
|
||||||
form = TransactionForm(obj=txn)
|
|
||||||
form.transaction_type.data = txn.transaction_type
|
|
||||||
form.account_id.choices = _account_choices()
|
|
||||||
form.category_id.choices = _category_choices(txn.transaction_type)
|
|
||||||
|
|
||||||
# Pre-populate foreign keys as strings for SelectField
|
# Determine active type: from POST toggle or existing record
|
||||||
|
if request.method == 'POST':
|
||||||
|
submitted_type = request.form.get('transaction_type', txn.transaction_type)
|
||||||
|
active_type = submitted_type if submitted_type in ('income', 'expense') else txn.transaction_type
|
||||||
|
else:
|
||||||
|
active_type = txn.transaction_type
|
||||||
|
|
||||||
|
form = TransactionForm(obj=txn)
|
||||||
|
form.account_id.choices = _account_choices()
|
||||||
|
form.category_id.choices = _category_choices(active_type)
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
|
form.transaction_type.data = txn.transaction_type
|
||||||
form.account_id.data = str(txn.account_id)
|
form.account_id.data = str(txn.account_id)
|
||||||
form.category_id.data = str(txn.category_id) if txn.category_id else ''
|
form.category_id.data = str(txn.category_id) if txn.category_id else ''
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
old_account_id = txn.account_id
|
old_account_id = txn.account_id
|
||||||
|
txn.transaction_type = active_type
|
||||||
txn.account_id = int(form.account_id.data)
|
txn.account_id = int(form.account_id.data)
|
||||||
txn.category_id = int(form.category_id.data) if form.category_id.data else None
|
txn.category_id = int(form.category_id.data) if form.category_id.data else None
|
||||||
txn.amount = form.amount.data
|
txn.amount = form.amount.data
|
||||||
@@ -185,7 +209,9 @@ def edit(id):
|
|||||||
return render_template('transactions/form.html',
|
return render_template('transactions/form.html',
|
||||||
form=form,
|
form=form,
|
||||||
txn=txn,
|
txn=txn,
|
||||||
txn_type=txn.transaction_type,
|
txn_type=active_type,
|
||||||
|
income_cats=_category_choices_json('income'),
|
||||||
|
expense_cats=_category_choices_json('expense'),
|
||||||
title='Edit Transaction')
|
title='Edit Transaction')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -57,11 +57,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Transaction Form -->
|
<!-- Transaction Form -->
|
||||||
<div class="pcard" style="border-top: 4px solid {% if txn_type=='income' %}var(--income){% else %}var(--expense){% endif %};">
|
<div class="pcard" id="txnCard" style="border-top: 4px solid {% if txn_type=='income' %}var(--income){% else %}var(--expense){% endif %};">
|
||||||
<form method="POST" novalidate id="txnForm">
|
<form method="POST" novalidate id="txnForm">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
{{ form.transaction_type() }}
|
{{ form.transaction_type() }}
|
||||||
|
|
||||||
|
<!-- Type toggle — only for income/expense (not transfer) -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-medium" style="font-size:13px;">Type</label>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="button" id="toggleIncome" onclick="setTxnType('income')"
|
||||||
|
class="btn btn-sm flex-fill {% if txn_type=='income' %}btn-success{% else %}btn-outline-secondary{% endif %}"
|
||||||
|
style="font-size:13px;">
|
||||||
|
<i class="bi bi-arrow-down-circle me-1"></i>Income
|
||||||
|
</button>
|
||||||
|
<button type="button" id="toggleExpense" onclick="setTxnType('expense')"
|
||||||
|
class="btn btn-sm flex-fill {% if txn_type=='expense' %}btn-danger{% else %}btn-outline-secondary{% endif %}"
|
||||||
|
style="font-size:13px;">
|
||||||
|
<i class="bi bi-arrow-up-circle me-1"></i>Expense
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{{ form.description.label(class="form-label fw-medium", style="font-size:13px;") }}
|
{{ form.description.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||||
{{ form.description(class="form-control" + (" is-invalid" if form.description.errors else ""),
|
{{ form.description(class="form-control" + (" is-invalid" if form.description.errors else ""),
|
||||||
@@ -103,10 +120,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="submit" class="btn {% if txn_type=='income' %}btn-success{% else %}btn-danger{% endif %}">
|
<button type="submit" id="submitBtn"
|
||||||
|
class="btn {% if txn_type=='income' %}btn-success{% else %}btn-danger{% endif %}">
|
||||||
Save {{ txn_type | title }}
|
Save {{ txn_type | title }}
|
||||||
</button>
|
</button>
|
||||||
<a href="{{ url_for('transactions.index', tab=txn_type) }}" class="btn btn-outline-secondary">Cancel</a>
|
<a id="cancelLink" href="{{ url_for('transactions.index', tab=txn_type) }}" class="btn btn-outline-secondary">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -370,5 +388,42 @@ function autoOcrOnUpload(input) {
|
|||||||
if (panel) { panel.style.display = 'block'; document.getElementById('ocrChevron').className = 'bi bi-chevron-up'; }
|
if (panel) { panel.style.display = 'block'; document.getElementById('ocrChevron').className = 'bi bi-chevron-up'; }
|
||||||
runOcr(file);
|
runOcr(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Transaction type toggle ───────────────────────────────────────────────────
|
||||||
|
const incomeCats = {{ income_cats | tojson }};
|
||||||
|
const expenseCats = {{ expense_cats | tojson }};
|
||||||
|
|
||||||
|
function setTxnType(type) {
|
||||||
|
// Update hidden field
|
||||||
|
document.querySelector('[name=transaction_type]').value = type;
|
||||||
|
|
||||||
|
// Toggle button styles
|
||||||
|
const incBtn = document.getElementById('toggleIncome');
|
||||||
|
const expBtn = document.getElementById('toggleExpense');
|
||||||
|
incBtn.className = 'btn btn-sm flex-fill ' + (type === 'income' ? 'btn-success' : 'btn-outline-secondary');
|
||||||
|
expBtn.className = 'btn btn-sm flex-fill ' + (type === 'expense' ? 'btn-danger' : 'btn-outline-secondary');
|
||||||
|
|
||||||
|
// Card border + submit button
|
||||||
|
document.getElementById('txnCard').style.borderTopColor = type === 'income' ? 'var(--income)' : 'var(--expense)';
|
||||||
|
const submitBtn = document.getElementById('submitBtn');
|
||||||
|
submitBtn.className = 'btn ' + (type === 'income' ? 'btn-success' : 'btn-danger');
|
||||||
|
submitBtn.textContent = 'Save ' + (type === 'income' ? 'Income' : 'Expense');
|
||||||
|
|
||||||
|
// Update cancel link tab param
|
||||||
|
const cancelLink = document.getElementById('cancelLink');
|
||||||
|
if (cancelLink) {
|
||||||
|
cancelLink.href = cancelLink.href.replace(/([\?&]tab=)[^&]*/, '$1' + type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap category options, preserving current selection if still valid
|
||||||
|
const sel = document.getElementById('field_category');
|
||||||
|
const prevVal = sel.value;
|
||||||
|
const cats = type === 'income' ? incomeCats : expenseCats;
|
||||||
|
sel.innerHTML = '<option value="">— None —</option>' +
|
||||||
|
cats.map(c => `<option value="${c.id}">${c.name}</option>`).join('');
|
||||||
|
if (prevVal && cats.some(c => c.id === prevVal)) {
|
||||||
|
sel.value = prevVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user