{# DrCr: Web-based double-entry bookkeeping framework Copyright (C) 2022–2024 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' if transaction and transaction.id else 'New' }} transaction{% endblock %} {# Macros for template posting rows as these are reused #} {% macro template_dr() %} <tr> <td></td> {#<td></td>#} <td class="py-1 px-1" colspan="2"> <div class="relative flex"> <div class="relative flex flex-grow items-stretch shadow-sm"> <div class="absolute inset-y-0 left-0 flex items-center z-10"> <select class="h-full border-0 bg-transparent py-0 pl-2 pr-8 text-gray-900 focus:ring-2 focus:ring-inset focus:ring-indigo-600" name="sign" onchange="changeDrCr(this)"> <option value="dr" selected>Dr</option> <option value="cr">Cr</option> </select> </div> <div class="relative combobox w-full"> <input type="text" class="bordered-field pl-16 peer" name="account"> {% include 'components/accounts_combobox_inner.html' %} </div> </div> <a class="relative -ml-px px-2 py-2 text-gray-500 hover:text-gray-700" href="#" onclick="addPosting(this);return false;"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> </svg> </a> </div> </td> <td class="amount-dr has-amount py-1 px-1"> <div class="relative shadow-sm"> <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> {# FIXME: Gracefully handle when the reporting commodity is not a single character #} <span class="text-gray-500">{{ reporting_commodity }}</span> </div> <input type="text" class="bordered-field pl-7" name="amount" oninput="changeAmount(this)"> </div> </td> <td class="amount-cr py-1 pl-1"></td> </tr> {% endmacro %} {% macro template_cr() %} <tr> <td></td> {#<td></td>#} <td class="py-1 px-1" colspan="2"> <div class="relative flex"> <div class="relative flex flex-grow items-stretch shadow-sm"> <div class="absolute inset-y-0 left-0 flex items-center z-10"> <select class="h-full border-0 bg-transparent py-0 pl-2 pr-8 text-gray-900 focus:ring-2 focus:ring-inset focus:ring-indigo-600" name="sign" onchange="changeDrCr(this)"> <option value="dr">Dr</option> <option value="cr" selected>Cr</option> </select> </div> <div class="relative combobox w-full"> <input type="text" class="bordered-field pl-16 peer" name="account"> {% include 'components/accounts_combobox_inner.html' %} </div> </div> <a class="relative -ml-px px-2 py-2 text-gray-500 hover:text-gray-700" href="#" onclick="addPosting(this);return false;"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> </svg> </a> </div> </td> <td class="amount-dr py-1 px-1"></td> <td class="amount-cr has-amount py-1 pl-1"> <div class="relative shadow-sm"> <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> <span class="text-gray-500">{{ reporting_commodity }}</span> </div> <input type="text" class="bordered-field pl-7" name="amount" oninput="changeAmount(this)"> </div> </td> </tr> {% endmacro %} {% block content %} <h1 class="page-heading mb-4"> {{ 'Edit' if transaction and transaction.id else 'New' }} transaction </h1> <form method="POST" id="edit-transaction-form"> <table class="min-w-full"> <thead> <tr class="border-b border-gray-300"> <th class="pt-0.5 pb-1 pr-1 text-gray-900 font-semibold text-start">Date</th> <th class="pt-0.5 pb-1 px-1 text-gray-900 font-semibold text-start" colspan="2">Description</th> <th class="pt-0.5 pb-1 px-1 text-gray-900 font-semibold text-start">Dr</th> <th class="pt-0.5 pb-1 pl-1 text-gray-900 font-semibold text-start">Cr</th> </tr> </thead> <tbody> <tr> <td class="pt-2 pb-1 pr-1"> <input type="date" class="bordered-field" name="dt" value="{{ transaction.dt.strftime('%Y-%m-%d') if transaction else '' }}"> </td> <td class="pt-2 pb-1 px-1" colspan="2"> <input type="text" class="bordered-field" name="description" value="{{ transaction.description if transaction else '' }}"> </td> <td></td> <td></td> </tr> {% if transaction %} {% for posting in transaction.postings %} <tr> <td></td> {#<td></td>#} <td class="py-1 px-1" colspan="2"> <div class="relative flex"> <div class="relative flex flex-grow items-stretch shadow-sm"> <div class="absolute inset-y-0 left-0 flex items-center z-10"> <select class="h-full border-0 bg-transparent py-0 pl-2 pr-8 text-gray-900 focus:ring-2 focus:ring-inset focus:ring-indigo-600" name="sign" onchange="changeDrCr(this)"> <option value="dr"{% if posting.quantity >= 0 %} selected{% endif %}>Dr</option> <option value="cr"{% if posting.quantity < 0 %} selected{% endif %}>Cr</option> </select> </div> <div class="relative combobox w-full"> <input type="text" class="bordered-field pl-16 peer" name="account" value="{{ posting.account }}"> {% include 'components/accounts_combobox_inner.html' %} </div> </div> <a class="relative -ml-px px-2 py-2 text-gray-500 hover:text-gray-700" href="#" onclick="addPosting(this);return false;"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> </svg> </a> </div> </td> {% if posting.quantity >= 0 %} <td class="amount-dr has-amount py-1 px-1"> <div class="relative shadow-sm"> <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> <span class="text-gray-500">{{ reporting_commodity }}</span> </div> <input type="text" class="bordered-field pl-7" name="amount" value="{{ posting.amount().quantity_string() }}" oninput="changeAmount(this)"> </div> </td> <td class="amount-cr py-1 pl-1"></td> {% else %} <td class="amount-dr py-1 px-1"></td> <td class="amount-cr has-amount py-1 pl-1"> <div class="relative shadow-sm"> <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> <span class="text-gray-500">{{ reporting_commodity }}</span> </div> <input type="text" class="bordered-field pl-7" name="amount" value="{{ (posting.amount()|abs).quantity_string() }}" oninput="changeAmount(this)"> </div> </td> {% endif %} </tr> {% endfor %} {% else %} {{ template_dr() }} {{ template_cr() }} {% endif %} </tbody> </table> <div class="flex justify-end mt-4 space-x-2"> {% if transaction and transaction.id %} <button type="submit" name="action" value="delete" class="btn-secondary text-red-600 ring-red-500" onclick="return confirm('Are you sure you want to delete this transaction? This operation is irreversible.');">Delete</button> {% endif %} <button type="submit" class="btn-primary">Save</button> </div> <input type="hidden" name="referrer" value="{{ request.referrer or '' }}"> </form> {# Save HTML for template posting rows so we can access this from JS when required #} <table style="display:none" id="template-dr">{{ template_dr() }}</table> <table style="display:none" id="template-cr">{{ template_cr() }}</table> {% endblock %} {% block scripts %} <script> function changeDrCr(el) { let trPosting = el.parentNode.parentNode.parentNode.parentNode.parentNode; let amountContent = trPosting.querySelector('.has-amount').innerHTML; let amountValue = trPosting.querySelector('.has-amount input').value; // Remove input boxes for (let td of trPosting.querySelectorAll('.amount-dr, .amount-cr')) { td.innerHTML = ''; td.classList.remove('has-amount'); } // Add correct input box let td = trPosting.querySelector(el.value === 'dr' ? '.amount-dr' : '.amount-cr'); td.innerHTML = amountContent; td.classList.add('has-amount'); td.querySelector('input').value = amountValue; } function addPosting(el) { let trPosting = el.parentNode.parentNode.parentNode; let sign = trPosting.querySelector('select').value; // Use same sign as row clicked // Add new posting row let trNew = document.createElement('tr'); trNew.innerHTML = document.getElementById(sign === 'dr' ? 'template-dr' : 'template-cr').querySelector('tr').innerHTML; trPosting.after(trNew); // Initialise new combobox initCombobox(trNew.querySelector('.combobox')); } function changeAmount(el) { // Update linked postings if there are only 2 and the first is edited // This allows changing the second posting independently by editing it (e.g. mixing commodities of equivalent total value) let amountInputs = document.querySelectorAll('#edit-transaction-form input[name="amount"]'); if (amountInputs.length === 2 && el === amountInputs[0]) { for (inp of amountInputs) { if (inp !== el) { // Update other input with amount inp.value = el.value; } } } } </script> <script src="{{ url_for('static', filename='js/combobox.js') }}"></script> {% endblock %}