From 5cb6db89d03df481a23bc89d352bb9e6565e0f47 Mon Sep 17 00:00:00 2001 From: Mohamed Fazrin Date: Mon, 18 May 2026 09:40:06 +0000 Subject: [PATCH 1/4] Fix ledger action security and validation issues --- django_ledger/forms/transactions.py | 16 ++++- django_ledger/models/journal_entry.py | 23 +++--- django_ledger/models/mixins.py | 39 +++++++++- django_ledger/models/transactions.py | 2 +- django_ledger/models/vendor.py | 6 +- django_ledger/settings.py | 4 +- .../account/tags/accounts_table.html | 48 +++++++------ .../tags/bank_accounts_table.html | 24 ++++--- .../django_ledger/bills/bill_update.html | 18 +++-- .../bills/includes/card_bill.html | 4 +- .../django_ledger/components/modals_v2.html | 14 +++- .../invoice/includes/card_invoice.html | 2 +- .../django_ledger/invoice/invoice_update.html | 18 +++-- .../includes/card_journal_entry.html | 26 ++++--- .../journal_entry/je_detail.html | 10 +-- .../journal_entry/je_detail_txs.html | 34 ++++----- .../django_ledger/journal_entry/je_list.html | 16 +++-- .../journal_entry/tags/je_table.html | 48 +++++++------ .../ledger/tags/ledgers_table.html | 72 +++++++++++-------- django_ledger/templatetags/django_ledger.py | 2 +- django_ledger/views/account.py | 8 +-- django_ledger/views/bank_account.py | 9 +-- django_ledger/views/bill.py | 8 +-- django_ledger/views/chart_of_accounts.py | 9 +-- django_ledger/views/closing_entry.py | 8 +-- django_ledger/views/djl_api.py | 4 +- django_ledger/views/estimate.py | 8 +-- django_ledger/views/invoice.py | 8 +-- django_ledger/views/journal_entry.py | 12 ++-- django_ledger/views/ledger.py | 9 +-- django_ledger/views/mixins.py | 27 +++++-- django_ledger/views/purchase_order.py | 8 +-- pyproject.toml | 3 + uv.lock | 52 ++++++++++++++ 34 files changed, 396 insertions(+), 203 deletions(-) diff --git a/django_ledger/forms/transactions.py b/django_ledger/forms/transactions.py index 7b42776d..532a406c 100644 --- a/django_ledger/forms/transactions.py +++ b/django_ledger/forms/transactions.py @@ -46,8 +46,22 @@ class Meta: class TransactionModelFormSet(BaseModelFormSet): - def __init__(self, *args, entity_model: EntityModel, je_model: JournalEntryModel, **kwargs): + def __init__(self, *args, entity_model: EntityModel = None, je_model: JournalEntryModel, **kwargs): + entity_slug = kwargs.pop('entity_slug', None) + user_model = kwargs.pop('user_model', None) + kwargs.pop('ledger_pk', None) super().__init__(*args, **kwargs) + + if entity_model is None: + if getattr(je_model, 'entity_model_id', None): + entity_model = je_model.entity_model + elif entity_slug and user_model: + entity_model = EntityModel.objects.for_user( + user_model=user_model + ).get(slug__exact=entity_slug) + else: + raise ValidationError(message=_('Must provide entity_model or entity_slug and user_model.')) + je_model.validate_for_entity(entity_model) self.JE_MODEL: JournalEntryModel = je_model self.ENTITY_MODEL = entity_model diff --git a/django_ledger/models/journal_entry.py b/django_ledger/models/journal_entry.py index 9c85a6c4..0f056b7f 100644 --- a/django_ledger/models/journal_entry.py +++ b/django_ledger/models/journal_entry.py @@ -670,18 +670,20 @@ def is_balance_valid(self, txs_qs: TransactionModelQuerySet, raise_exception: bo """ if len(txs_qs) > 0: balances = self.get_txs_balances(txs_qs=txs_qs, as_dict=True) - is_valid = balances[CREDIT] == balances[DEBIT] + is_valid = balances.get(CREDIT, Decimal('0.00')) == balances.get(DEBIT, Decimal('0.00')) if not is_valid: if raise_exception: raise JournalEntryValidationError( message='Balance of {0} CREDITs are {1} does not match DEBITs {2}.'.format( self, - balances[CREDIT], - balances[DEBIT] + balances.get(CREDIT, Decimal('0.00')), + balances.get(DEBIT, Decimal('0.00')) ) ) return is_valid - return True + if raise_exception: + raise JournalEntryValidationError('Journal entry must have transactions.') + return False def is_txs_qs_coa_valid(self, txs_qs: TransactionModelQuerySet, raise_exception: bool = True) -> bool: """ @@ -1363,7 +1365,7 @@ def generate_je_number(self, commit: bool = False) -> str: self.je_number = f'{DJANGO_LEDGER_JE_NUMBER_PREFIX}-{state_model.fiscal_year}-{unit_prefix}-{seq}' if commit: - self.save(update_fields=['je_number']) + self.save(update_fields=['je_number'], verify=False) return self.je_number @@ -1425,13 +1427,10 @@ def verify(self, except JournalEntryValidationError as e: raise e - # if not len(txs_qs): - # if raise_exception: - # raise JournalEntryValidationError('Journal entry has no transactions.') - - # if len(txs_qs) < 2: - # if raise_exception: - # raise JournalEntryValidationError('At least two transactions required.') + if len(txs_qs) < 2: + if raise_exception: + raise JournalEntryValidationError('At least two transactions required.') + return txs_qs, self.is_verified() if all([ is_balance_valid, diff --git a/django_ledger/models/mixins.py b/django_ledger/models/mixins.py index bd533470..ecf138d1 100644 --- a/django_ledger/models/mixins.py +++ b/django_ledger/models/mixins.py @@ -14,6 +14,7 @@ from typing import Dict, Optional, Union from uuid import UUID +import bleach from django.conf import settings from django.core.exceptions import ValidationError from django.core.validators import ( @@ -830,7 +831,9 @@ def migrate_state( JournalEntryModel = lazy_loader.get_journal_entry_model() TransactionModel = lazy_loader.get_txs_model() - unit_uuids = list(set(k[1] for k in idx_keys)) + unit_uuids = list(set(unit_uuid for (_, unit_uuid, _), amt in diff_idx.items() if amt)) + if not unit_uuids: + return item_data, io_data if je_timestamp: je_timestamp = validate_io_timestamp(dt=je_timestamp) @@ -1221,7 +1224,39 @@ def notes_html(self): """ if not self.markdown_notes: return '' - return markdown(force_str(self.markdown_notes)) + html = markdown(force_str(self.markdown_notes)) + return bleach.clean( + html, + tags=[ + 'a', + 'abbr', + 'acronym', + 'blockquote', + 'br', + 'code', + 'em', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'hr', + 'li', + 'ol', + 'p', + 'pre', + 'strong', + 'ul', + ], + attributes={ + 'a': ['href', 'title'], + 'abbr': ['title'], + 'acronym': ['title'], + }, + protocols=['http', 'https', 'mailto'], + strip=True, + ) def clean(self): super().clean() diff --git a/django_ledger/models/transactions.py b/django_ledger/models/transactions.py index 556ac2f4..2696a3b6 100644 --- a/django_ledger/models/transactions.py +++ b/django_ledger/models/transactions.py @@ -707,7 +707,7 @@ def transactionmodel_presave(instance: TransactionModel, **kwargs): f'Cannot create or modify transactions on account model {instance.account}.' ) ) - if instance.journal_entry.is_locked(): + if instance.journal_entry_id and instance.journal_entry.is_locked(): raise TransactionModelValidationError( message=_('Cannot modify transactions on locked journal entries.') ) diff --git a/django_ledger/models/vendor.py b/django_ledger/models/vendor.py index 316570a4..eb8b1ba5 100644 --- a/django_ledger/models/vendor.py +++ b/django_ledger/models/vendor.py @@ -38,9 +38,9 @@ def vendor_picture_upload_to(instance, filename): - if not instance.customer_number: - instance.generate_customer_number(commit=False) - vendor_number = instance.customer_number + if not instance.vendor_number: + instance.generate_vendor_number(commit=False) + vendor_number = instance.vendor_number name, ext = os.path.splitext(filename) safe_name = slugify(name) return f'vendor_pictures/{vendor_number}/{safe_name}{ext.lower()}' diff --git a/django_ledger/settings.py b/django_ledger/settings.py index 7521a90b..8dedeb68 100644 --- a/django_ledger/settings.py +++ b/django_ledger/settings.py @@ -28,13 +28,13 @@ DJANGO_LEDGER_TRANSACTION_CORRECTION = getattr(settings, 'DJANGO_LEDGER_TRANSACTION_CORRECTION', Decimal('0.01')) DJANGO_LEDGER_ACCOUNT_CODE_GENERATE = getattr(settings, 'DJANGO_LEDGER_ACCOUNT_CODE_GENERATE', True) DJANGO_LEDGER_ACCOUNT_CODE_GENERATE_LENGTH = getattr(settings, 'DJANGO_LEDGER_ACCOUNT_CODE_GENERATE_LENGTH', 5) -DJANGO_LEDGER_ACCOUNT_CODE_USE_PREFIX = getattr(settings, 'DJANGO_LEDGER_ACCOUNT_CODE_GENERATE_LENGTH', True) +DJANGO_LEDGER_ACCOUNT_CODE_USE_PREFIX = getattr(settings, 'DJANGO_LEDGER_ACCOUNT_CODE_USE_PREFIX', True) DJANGO_LEDGER_JE_NUMBER_PREFIX = getattr(settings, 'DJANGO_LEDGER_JE_NUMBER_PREFIX', 'JE') DJANGO_LEDGER_PO_NUMBER_PREFIX = getattr(settings, 'DJANGO_LEDGER_PO_NUMBER_PREFIX', 'PO') DJANGO_LEDGER_ESTIMATE_NUMBER_PREFIX = getattr(settings, 'DJANGO_LEDGER_ESTIMATE_NUMBER_PREFIX', 'E') DJANGO_LEDGER_INVOICE_NUMBER_PREFIX = getattr(settings, 'DJANGO_LEDGER_INVOICE_NUMBER_PREFIX', 'I') DJANGO_LEDGER_BILL_NUMBER_PREFIX = getattr(settings, 'DJANGO_LEDGER_BILL_NUMBER_PREFIX', 'B') -DJANGO_LEDGER_RECEIPT_NUMBER_PREFIX = getattr(settings, 'DJANGO_LEDGER_BILL_NUMBER_PREFIX', 'R') +DJANGO_LEDGER_RECEIPT_NUMBER_PREFIX = getattr(settings, 'DJANGO_LEDGER_RECEIPT_NUMBER_PREFIX', 'R') DJANGO_LEDGER_VENDOR_NUMBER_PREFIX = getattr(settings, 'DJANGO_LEDGER_VENDOR_NUMBER_PREFIX', 'V') DJANGO_LEDGER_CUSTOMER_NUMBER_PREFIX = getattr(settings, 'DJANGO_LEDGER_CUSTOMER_NUMBER_PREFIX', 'C') DJANGO_LEDGER_EXPENSE_NUMBER_PREFIX = getattr(settings, 'DJANGO_LEDGER_EXPENSE_NUMBER_PREFIX', 'IEX') diff --git a/django_ledger/templates/django_ledger/account/tags/accounts_table.html b/django_ledger/templates/django_ledger/account/tags/accounts_table.html index 77e722ab..33c31901 100644 --- a/django_ledger/templates/django_ledger/account/tags/accounts_table.html +++ b/django_ledger/templates/django_ledger/account/tags/accounts_table.html @@ -97,32 +97,40 @@

{{ root_role | upper }}

{% if account.can_activate %} - - {% icon 'bi:play-fill' 16 %} - {% trans 'Activate' %} - +
+ {% csrf_token %} + +
{% endif %} {% if account.can_deactivate %} - - {% icon 'bi:stop-fill' 16 %} - {% trans 'Deactivate' %} - +
+ {% csrf_token %} + +
{% endif %} {% if account.can_lock %} - - {% icon 'bi:lock-fill' 16 %} - {% trans 'Lock' %} - +
+ {% csrf_token %} + +
{% endif %} {% if account.can_unlock %} - - {% icon 'bi:unlock-fill' 16 %} - {% trans 'Unlock' %} - +
+ {% csrf_token %} + +
{% endif %} diff --git a/django_ledger/templates/django_ledger/bank_account/tags/bank_accounts_table.html b/django_ledger/templates/django_ledger/bank_account/tags/bank_accounts_table.html index a42af466..894db41b 100644 --- a/django_ledger/templates/django_ledger/bank_account/tags/bank_accounts_table.html +++ b/django_ledger/templates/django_ledger/bank_account/tags/bank_accounts_table.html @@ -51,17 +51,21 @@ {% trans 'Update' %} {% if bank_acc.can_activate %} - - {% icon 'bi:play-fill' 16 %} - {% trans 'Activate' %} - +
+ {% csrf_token %} + +
{% elif bank_acc.can_inactivate %} - - {% icon 'bi:stop-fill' 16 %} - {% trans 'Inactivate' %} - +
+ {% csrf_token %} + +
{% endif %} diff --git a/django_ledger/templates/django_ledger/bills/bill_update.html b/django_ledger/templates/django_ledger/bills/bill_update.html index b6483bb9..14c4fb75 100644 --- a/django_ledger/templates/django_ledger/bills/bill_update.html +++ b/django_ledger/templates/django_ledger/bills/bill_update.html @@ -88,12 +88,18 @@

{% trans 'Ledger State' %}

href="{% url 'django_ledger:je-list' ledger_pk=bill_model.ledger.uuid entity_slug=view.kwargs.entity_slug %}"> {% trans 'Ledger Journal Entries' %} - {% trans 'Lock Ledger' %} - {% trans 'Unlock Ledger' %} - {% trans 'Force Migrate' %} +
+ {% csrf_token %} + +
+
+ {% csrf_token %} + +
+
+ {% csrf_token %} + +
diff --git a/django_ledger/templates/django_ledger/bills/includes/card_bill.html b/django_ledger/templates/django_ledger/bills/includes/card_bill.html index 338d85d1..b386828e 100644 --- a/django_ledger/templates/django_ledger/bills/includes/card_bill.html +++ b/django_ledger/templates/django_ledger/bills/includes/card_bill.html @@ -36,7 +36,7 @@

{{ bill.vendor.address_1 }}

{# MARK AS PAID MODAL #} - {% modal_action bill 'get' entity_slug %} + {% modal_action bill 'post' entity_slug %}