From e162284638e6c8917a8cdaa3da3ab24e6201ddfc Mon Sep 17 00:00:00 2001 From: Miguel Sanda Date: Thu, 26 Dec 2024 18:48:15 -0500 Subject: [PATCH] v0.7.1 (#235) * Relaxing Dependency Constraints * Issue 208 ofx files (#209) * fix: initial attempt * Allow .qfx files in data_import form. The accept attribute in the file-input field now includes .qfx files. This permits users to import data from files in .qfx format in addition to the previously supported .ofx format. * remove debugging lines, remove debug code, add sample ofx for tests --------- Co-authored-by: Miguel Sanda * v0.6.3 (#214) * Add proxy functions to JournalEntry model The commit introduces proxy functions to the JournalEntry model in the Django Ledger code. Specifically, it adds 'post', 'unpost', 'lock', and 'unlock' methods, each serving as a proxy to their counterpart methods 'mark_as_posted', 'mark_as_unposted', 'mark_as_locked', and 'mark_as_unlocked'. This simplifies the interface for interacting with JournalEntry objects. * Minor code optimization & Django Ledger admin fields. * access the queryset instance using .all() which returns a queryset. (#213) * Correct urls for going back in entity and ledger balance sheet view (#215) * Add signal handling for various models' statuses Added signals for different status changes of Django Ledger models to enable real-time, event-driven system behavior. Signals are now sent each time an action is performed in the Ledger, Invoice, Bill, Journal Entry, Purchase Order, and Estimate. These changes will allow us to trigger specific actions depending on these changes. * Update Python version and package versions in Pipfile Updated the Python version from 3.11 to 3.12 in Pipfile and Pipfile.lock. Also, updated the package versions of 'django', 'faker' and 'pillow' in Pipfile.lock for improved functionality and security. * Update Django Ledger version to 0.6.3 This commit updates the version number of the Django Ledger project in both __init__.py and pyproject.toml files. The version has been incremented from 0.6.2 to 0.6.3. * Update signal comments in models Updated the comments in the signals.py file to clearly specify that the signals correspond to Journal Entry Models. Additional context was also included for the signals module to enhance clarity for developers in understanding the importance of events or states in the models. * Update documentation structure Rearrange sections in documentation, focusing on IO and models. For docs/source/models.rst, the automodule section for django_ledger.models.signals was added. Meanwhile, in docs/source/io.rst, sections were reshuffled and terms updated for better clarity. These steps aim to enhance documentation readability and accuracy. --------- Co-authored-by: Eric paul <65466957+25-do@users.noreply.github.com> Co-authored-by: Ubaid ur Rehman * Update dependencies versions in Pipfile.lock Upgraded several packages to their latest versions, including Faker, Alabaster, and SQLParse, among others. Added new dependencies appnope and backports.tarfile, and adjusted Python version markers for some packages. Removed cryptography and jeepney from the list of dependencies. * Updated Logos * comment out attribute that doesn't exist (#220) * Docker Fix (#218) * Update version to 0.6.4 Bump the version number in `__init__.py` and `pyproject.toml`. This update ensures consistency and readiness for the next release cycle. * API authentication (#217) * access the queryset instance using .all() which returns a queryset. * fixed invalid client * changes in API authentication usage * API Authentication usage * Add DjangoCon2024 Jupyter notebook example Introduced a detailed Jupyter notebook for DjangoCon 2024. This notebook demonstrates various functionalities including setting up Django, creating a UserModel, defining an EntityModel, and creating and populating a Chart of Accounts. * Update entity model CoA handling and streamline notebook Enhanced the entity model to handle various types for CoA (Chart of Accounts) input. Cleared execution counts and outputs from DjangoCon2024 notebook for better clarity. * Add support for validating parent roles in accounts Implemented `VALID_PARENTS` to allow filtering accounts based on hierarchical roles. This ensures that parent-child relationships between account roles are correctly validated and managed. * Pipfile Update * Refactor and improve docs for AccountModel and QuerySets Refactored and enhanced documentation for AccountModel and its related query sets to ensure clarity and consistency. Introduced detailed attribute descriptions and method explanations while maintaining code functionality. * Add net income computation to IO middleware Introduced a new `net_income` method to calculate and store net income as part of the IO_DATA digest process. Updated the `digest` method to include the net income calculation step. This enhances the financial reporting capabilities of the middleware. * v0.7.0 (#226) * v0.6.4 (#223) * Relaxing Dependency Constraints * Issue 208 ofx files (#209) * fix: initial attempt * Allow .qfx files in data_import form. The accept attribute in the file-input field now includes .qfx files. This permits users to import data from files in .qfx format in addition to the previously supported .ofx format. * remove debugging lines, remove debug code, add sample ofx for tests --------- Co-authored-by: Miguel Sanda * v0.6.3 (#214) * Add proxy functions to JournalEntry model The commit introduces proxy functions to the JournalEntry model in the Django Ledger code. Specifically, it adds 'post', 'unpost', 'lock', and 'unlock' methods, each serving as a proxy to their counterpart methods 'mark_as_posted', 'mark_as_unposted', 'mark_as_locked', and 'mark_as_unlocked'. This simplifies the interface for interacting with JournalEntry objects. * Minor code optimization & Django Ledger admin fields. * access the queryset instance using .all() which returns a queryset. (#213) * Correct urls for going back in entity and ledger balance sheet view (#215) * Add signal handling for various models' statuses Added signals for different status changes of Django Ledger models to enable real-time, event-driven system behavior. Signals are now sent each time an action is performed in the Ledger, Invoice, Bill, Journal Entry, Purchase Order, and Estimate. These changes will allow us to trigger specific actions depending on these changes. * Update Python version and package versions in Pipfile Updated the Python version from 3.11 to 3.12 in Pipfile and Pipfile.lock. Also, updated the package versions of 'django', 'faker' and 'pillow' in Pipfile.lock for improved functionality and security. * Update Django Ledger version to 0.6.3 This commit updates the version number of the Django Ledger project in both __init__.py and pyproject.toml files. The version has been incremented from 0.6.2 to 0.6.3. * Update signal comments in models Updated the comments in the signals.py file to clearly specify that the signals correspond to Journal Entry Models. Additional context was also included for the signals module to enhance clarity for developers in understanding the importance of events or states in the models. * Update documentation structure Rearrange sections in documentation, focusing on IO and models. For docs/source/models.rst, the automodule section for django_ledger.models.signals was added. Meanwhile, in docs/source/io.rst, sections were reshuffled and terms updated for better clarity. These steps aim to enhance documentation readability and accuracy. --------- Co-authored-by: Eric paul <65466957+25-do@users.noreply.github.com> Co-authored-by: Ubaid ur Rehman * Update dependencies versions in Pipfile.lock Upgraded several packages to their latest versions, including Faker, Alabaster, and SQLParse, among others. Added new dependencies appnope and backports.tarfile, and adjusted Python version markers for some packages. Removed cryptography and jeepney from the list of dependencies. * Updated Logos * comment out attribute that doesn't exist (#220) * Docker Fix (#218) * Update version to 0.6.4 Bump the version number in `__init__.py` and `pyproject.toml`. This update ensures consistency and readiness for the next release cycle. * API authentication (#217) * access the queryset instance using .all() which returns a queryset. * fixed invalid client * changes in API authentication usage * API Authentication usage * Add DjangoCon2024 Jupyter notebook example Introduced a detailed Jupyter notebook for DjangoCon 2024. This notebook demonstrates various functionalities including setting up Django, creating a UserModel, defining an EntityModel, and creating and populating a Chart of Accounts. * Update entity model CoA handling and streamline notebook Enhanced the entity model to handle various types for CoA (Chart of Accounts) input. Cleared execution counts and outputs from DjangoCon2024 notebook for better clarity. * Add support for validating parent roles in accounts Implemented `VALID_PARENTS` to allow filtering accounts based on hierarchical roles. This ensures that parent-child relationships between account roles are correctly validated and managed. * Pipfile Update * Refactor and improve docs for AccountModel and QuerySets Refactored and enhanced documentation for AccountModel and its related query sets to ensure clarity and consistency. Introduced detailed attribute descriptions and method explanations while maintaining code functionality. --------- Co-authored-by: Tom Hodder Co-authored-by: Eric paul <65466957+25-do@users.noreply.github.com> Co-authored-by: Ubaid ur Rehman Co-authored-by: Krzysztof Czapla <107809367+KrzysztofCzapla@users.noreply.github.com> * Add logo and update documentation for Django Ledger CoA This commit adds the Django Ledger logo to the Jupyter notebook and reworks the documentation for the Chart of Accounts (CoA) section. It also updates type imports, adjusts outputs, and includes explanations for handling multiple CoA and sample data population. * Ui Icons Fix Subscription ID on UI Wallet Create Mutation Sidebar Simplification * Update to version 0.6.5 and add account lock/unlock methods Bumped the project version to 0.6.5 and added Django 5.0 framework classifier. Enhanced AccountModel by introducing methods to lock and unlock accounts with validation. Simplified the get_queryset method for item views. * Update to version 0.6.5 and add account lock/unlock methods Bumped the project version to 0.6.5 and added Django 5.0 framework classifier. Enhanced AccountModel by introducing methods to lock and unlock accounts with validation. Simplified the get_queryset method for item views. * Add `allow_empty` attribute and fix sales tax rate help text Enable empty ledger list handling in LedgerModelListView by setting `allow_empty` to True. Additionally, update the help text for the sales tax rate field to correct the example conversion factor. * Refactor queryset initialization in item views Updated the get_queryset methods to use the AUTHORIZED_ENTITY_MODEL attribute for fetching related models instead of directly invoking model managers. This change improves code consistency and leverages the new entity model for querying related objects. Removed an outdated comment from the items.py model file. * Add 'active' filter to queries and reorganize docstrings Enhanced vendor and customer queries with an 'active' filter for better data accuracy. Also, standardized contribution docstrings across multiple files and updated the EstimateModel queryset to use the `AUTHORIZED_ENTITY_MODEL`. * Update view mixins and clean up docstrings Replaced security mixins with specific view mixins across multiple views to streamline authorization logic. Additionally, cleaned up and standardized docstrings in several modules. * Rename model view mixins for better consistency Replaced *ModelViewQuerySetMixIn class names with *ModelBaseView across multiple modules. This change improves naming clarity and consistency in the project's codebase. * Add entity-based filtering and queryset improvements Refactor multiple classes to enhance entity-based filtering and queryset handling. Streamline vendor and account queries, improve code readability, and consolidate security checks. - Optimize AccountModel forms and remove unnecessary arguments - Enhance AccountModel views to inherit from a base view and add select_related fields to view querysets, minimizing database queries during template rendering - Implement AccountModel navigation URLs as model methods and annotate default AccountModel queryset with necessary relevant information - Replace unique_together Meta option with constraints option on the AccountModel to conform with new API - Add coa_slug as mandatory kwarg on all AccountModel-related views. * Format code for readability and remove redundant methods Reformatted various parts of the code for better readability by breaking long lines and ensuring consistent indentation. Additionally, removed redundant methods `with_roles` and `with_roles_available` from `models/accounts.py` to clean up unnecessary code. * Simplify and format README.md for better readability Condensed the README by removing verbose descriptions and reformatting key sections. Enhanced navigation with clear links to Discord, documentation, and the QuickStart Notebook, and streamlined the list of features for clarity. * Update invoice detail URLs to include coa_slug parameter Added the 'coa_slug' parameter to account-detail URLs in the invoice detail template to ensure proper linkage to the chart of accounts model. Updated the view to prefetch related coa_model fields for cash, prepaid, and unearned accounts. * Reformat `with_roles` method calls for readability Improve readability by reformatting `with_roles` method calls to use multiline styling consistently. This change enhances code maintainability without affecting functionality. It should make the nested roles easier to read and modify in the future. * Refactor initialization logic in Bill forms Replace individual entity and user model parameters with a single entity model parameter in BillModelCreateForm and BaseBillItemTransactionFormset. This simplifies querysets and improves code readability by leveraging direct relationships. * Refactor bill views and forms with new base view Refactored multiple components in `bill.py` to streamline and simplify the code by introducing `BillModelModelBaseView`. Replaced repeated logic for querying BillModel instances and standardized the creation and updating of forms. * Add "Is Active" column to expense table and update account URLs Added an "Is Active" column to the expense item table to indicate the status of expense items. Updated account detail URLs in bill detail template to include the chart of accounts slug for accurate link resolution. * Account Model QuerySets optimization. Update entity_slug to entity_model in various files Replaces 'entity_slug' with 'entity_model' for better consistency and clarity across forms, views, and notebooks. This change also introduces the handling of ObjectDoesNotExist within notebooks and improves the use of queryset filtering. * Refactor account tests and form ID handling Added account management tests, including view protection and creation. Introduce form ID generation in account creation forms and implement utility methods for URL pattern resolution in tests. * Add account activation and locking tests Implemented tests for account activation and locking functionalities, including the ability to lock, unlock, activate, and deactivate accounts. Also updated the `get_coa_accounts` method to filter by locked status. * Update `get_random_account` to filter by active and locked status Add parameters to filter accounts based on their active and locked status. This enhances the function's flexibility and ensures more precise query results. * Update execution timestamps in DjangoCon2024 notebook Updated metadata execution timestamps throughout the DjangoCon2024 notebook to reflect the current date. Also added a note emphasizing a limitation in executing a particular code snippet within a Jupyter notebook. * Add lock/unlock actions and COA active check in accounts Introduced methods to generate lock/unlock URLs in the Account model, along with corresponding URL patterns. Added `is_coa_active` to check the COA active status, updating `can_activate` to utilize this check. * Add lock status and actions to accounts table Added a new column to display the lock status of each account and incorporated conditional icons to visually indicate if an account is locked or unlocked. Additionally, added actions in the dropdown menu to lock and unlock accounts based on the account's current state. * Documentation Update * Add new account model tests and refactor imports Introduce tests for account annotations and transaction capabilities to ensure accurate entity associations and transaction status checks. Refactor import statements to remove unused `DEBIT` import and improve code readability with minor reformatting. * Refactor AUTHORS.md for better organization Reorganize sections in AUTHORS.md to separate Developers, Contributors, and Documentation Contributors for better clarity. * Update README and contribution guidelines Expanded the contact section in README for better clarity. Revised the Contribute.md file to include detailed guidelines, ensuring clarity on contribution processes and standards. * v0.7.0 --------- Co-authored-by: Tom Hodder Co-authored-by: Eric paul <65466957+25-do@users.noreply.github.com> Co-authored-by: Ubaid ur Rehman Co-authored-by: Krzysztof Czapla <107809367+KrzysztofCzapla@users.noreply.github.com> * V0.7.1 (#234) * v0.7.1 Chart of Accounts Model create views and forms. New inactive Chart of Accounts views. Balance Sheet Report bugfixes and template organizations. Chart of Accounts Model default queryset optimization. Entity Model Chart of Accounts list/create URLs. Footer information update. DjangoLedgerSecurityMixIn context now incorporates Authorized Entity Model. * v0.7.1 Django Ledger Model extension capabilities and customization using django settings. * v0.7.1 Implementing Swappable Model base functionality. * Add COA validation and annotate `coa_id` for transactions Ensure transactions belong to a single Chart of Accounts (COA) in `is_txs_qs_coa_valid`. Extend queryset with `coa_id` annotation for better query performance and add a corresponding property to the transaction model. These changes enhance consistency and validation when handling journal entries and transactions. --------- Co-authored-by: Tom Hodder Co-authored-by: Eric paul <65466957+25-do@users.noreply.github.com> Co-authored-by: Ubaid ur Rehman Co-authored-by: Krzysztof Czapla <107809367+KrzysztofCzapla@users.noreply.github.com> --- django_ledger/__init__.py | 2 +- django_ledger/admin/__init__.py | 2 +- .../admin/{coa.py => chart_of_accounts.py} | 2 +- django_ledger/admin/entity.py | 2 +- django_ledger/forms/account.py | 2 +- django_ledger/forms/bank_account.py | 2 + django_ledger/forms/chart_of_accounts.py | 82 +++++++++ django_ledger/forms/coa.py | 47 ------ django_ledger/io/io_library.py | 2 +- ...ed_transactionmodel_reconciled_and_more.py | 37 ++++ django_ledger/models/__init__.py | 2 +- django_ledger/models/accounts.py | 4 + django_ledger/models/bank_account.py | 8 +- django_ledger/models/bill.py | 10 +- .../models/{coa.py => chart_of_accounts.py} | 158 +++++++++++++++--- django_ledger/models/closing_entry.py | 12 +- django_ledger/models/coa_default.py | 2 +- django_ledger/models/customer.py | 8 +- django_ledger/models/data_import.py | 17 +- django_ledger/models/entity.py | 30 +++- django_ledger/models/estimate.py | 7 +- django_ledger/models/invoice.py | 22 ++- django_ledger/models/items.py | 27 ++- django_ledger/models/journal_entry.py | 96 ++++++++--- django_ledger/models/ledger.py | 13 +- django_ledger/models/purchase_order.py | 14 +- django_ledger/models/transactions.py | 26 ++- django_ledger/models/unit.py | 4 + django_ledger/models/vendor.py | 4 + django_ledger/settings.py | 31 +++- .../account/tags/accounts_table.html | 5 +- .../chart_of_accounts/coa_create.html | 25 +++ .../chart_of_accounts/coa_list.html | 31 +++- .../chart_of_accounts/coa_update.html | 4 +- .../chart_of_accounts/includes/coa_card.html | 14 +- .../tags/balance_sheet_statement.html | 4 +- .../django_ledger/includes/footer.html | 4 +- django_ledger/urls/chart_of_accounts.py | 6 + django_ledger/utils.py | 37 +--- django_ledger/views/__init__.py | 2 +- django_ledger/views/account.py | 19 ++- .../views/{coa.py => chart_of_accounts.py} | 92 +++++----- django_ledger/views/mixins.py | 21 ++- pyproject.toml | 2 +- 44 files changed, 671 insertions(+), 270 deletions(-) rename django_ledger/admin/{coa.py => chart_of_accounts.py} (97%) create mode 100644 django_ledger/forms/chart_of_accounts.py delete mode 100644 django_ledger/forms/coa.py create mode 100644 django_ledger/migrations/0018_transactionmodel_cleared_transactionmodel_reconciled_and_more.py rename django_ledger/models/{coa.py => chart_of_accounts.py} (84%) create mode 100644 django_ledger/templates/django_ledger/chart_of_accounts/coa_create.html rename django_ledger/views/{coa.py => chart_of_accounts.py} (59%) diff --git a/django_ledger/__init__.py b/django_ledger/__init__.py index d5cecb37..893c01a1 100644 --- a/django_ledger/__init__.py +++ b/django_ledger/__init__.py @@ -6,7 +6,7 @@ default_app_config = 'django_ledger.apps.DjangoLedgerConfig' """Django Ledger""" -__version__ = '0.7.0' +__version__ = '0.7.1' __license__ = 'GPLv3 License' __author__ = 'Miguel Sanda' diff --git a/django_ledger/admin/__init__.py b/django_ledger/admin/__init__.py index b50b2d76..ddccc8ef 100644 --- a/django_ledger/admin/__init__.py +++ b/django_ledger/admin/__init__.py @@ -1,6 +1,6 @@ from django.contrib import admin -from django_ledger.admin.coa import ChartOfAccountsModelAdmin +from django_ledger.admin.chart_of_accounts import ChartOfAccountsModelAdmin from django_ledger.admin.entity import EntityModelAdmin from django_ledger.admin.ledger import LedgerModelAdmin from django_ledger.models import EntityModel, ChartOfAccountModel, LedgerModel diff --git a/django_ledger/admin/coa.py b/django_ledger/admin/chart_of_accounts.py similarity index 97% rename from django_ledger/admin/coa.py rename to django_ledger/admin/chart_of_accounts.py index 50d2108b..60fc24fd 100644 --- a/django_ledger/admin/coa.py +++ b/django_ledger/admin/chart_of_accounts.py @@ -3,7 +3,7 @@ from django.forms import ModelForm, BooleanField, BaseInlineFormSet from django_ledger.models.accounts import AccountModel -from django_ledger.models.coa import ChartOfAccountModel +from django_ledger.models.chart_of_accounts import ChartOfAccountModel from django_ledger.models.entity import EntityModel diff --git a/django_ledger/admin/entity.py b/django_ledger/admin/entity.py index 788d8c03..226c4c22 100644 --- a/django_ledger/admin/entity.py +++ b/django_ledger/admin/entity.py @@ -7,7 +7,7 @@ from django.urls import reverse from django.utils.html import format_html -from django_ledger.admin.coa import ChartOfAccountsInLine +from django_ledger.admin.chart_of_accounts import ChartOfAccountsInLine from django_ledger.io.io_core import get_localtime from django_ledger.models import EntityUnitModel from django_ledger.models.entity import EntityModel, EntityManagementModel diff --git a/django_ledger/forms/account.py b/django_ledger/forms/account.py index e8a00f63..8c833671 100644 --- a/django_ledger/forms/account.py +++ b/django_ledger/forms/account.py @@ -38,7 +38,7 @@ def __init__(self, coa_model: ChartOfAccountModel, *args, **kwargs): self.fields['role'].choices = ACCOUNT_CHOICES_NO_ROOT self.fields['code'].required = False self.fields['coa_model'].disabled = True - self.fields['coa_model'].required = False + self.fields['coa_model'].required = True self.form_id: str = self.get_form_id() diff --git a/django_ledger/forms/bank_account.py b/django_ledger/forms/bank_account.py index e314b1bc..b6c3ac28 100644 --- a/django_ledger/forms/bank_account.py +++ b/django_ledger/forms/bank_account.py @@ -13,6 +13,8 @@ def __init__(self, *args, entity_slug, user_model, **kwargs): super().__init__(*args, **kwargs) self.ENTITY_SLUG = entity_slug self.USER_MODEL = user_model + + # todo: only the accounts that do not hava an associated bank account should be available to pick from... account_qs = AccountModel.objects.for_entity( user_model=self.USER_MODEL, entity_model=self.ENTITY_SLUG diff --git a/django_ledger/forms/chart_of_accounts.py b/django_ledger/forms/chart_of_accounts.py new file mode 100644 index 00000000..497cf544 --- /dev/null +++ b/django_ledger/forms/chart_of_accounts.py @@ -0,0 +1,82 @@ +from random import randint + +from django.forms import ModelForm, TextInput, Textarea, HiddenInput +from django.utils.translation import gettext_lazy as _ + +from django_ledger.models.chart_of_accounts import ChartOfAccountModel +from django_ledger.models.entity import EntityModel +from django_ledger.settings import DJANGO_LEDGER_FORM_INPUT_CLASSES + + +class ChartOfAccountsModelCreateForm(ModelForm): + FORM_ID_SEP = '___' + + def __init__(self, entity_model: EntityModel, *args, **kwargs): + self.ENTITY_MODEL = entity_model + super().__init__(*args, **kwargs) + self.fields['entity'].disabled = True + self.fields['entity'].required = True + self.form_id: str = self.get_form_id() + + def clean_entity(self): + return self.ENTITY_MODEL + + def get_form_id(self) -> str: + return f'coa-model-create-form-{self.ENTITY_MODEL.slug}{self.FORM_ID_SEP}{randint(100000, 999999)}' + + class Meta: + model = ChartOfAccountModel + fields = [ + 'entity', + 'name', + 'description' + ] + labels = { + 'name': _('Name'), + 'description': _('Description'), + } + widgets = { + 'entity': HiddenInput(), + 'name': TextInput( + attrs={ + 'class': DJANGO_LEDGER_FORM_INPUT_CLASSES + }), + 'description': Textarea( + attrs={ + 'class': DJANGO_LEDGER_FORM_INPUT_CLASSES + } + ), + } + + +class ChartOfAccountsModelUpdateForm(ModelForm): + + FORM_ID_SEP = '___' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_id: str = self.get_form_id() + + + def get_form_id(self) -> str: + instance: ChartOfAccountModel = self.instance + return f'coa-model-update-form-{instance.slug}{self.FORM_ID_SEP}{randint(100000, 999999)}' + + class Meta: + model = ChartOfAccountModel + fields = [ + 'name', + 'active' + ] + labels = { + 'name': _('Name'), + 'description': _('Description'), + } + widgets = { + 'name': TextInput(attrs={ + 'class': DJANGO_LEDGER_FORM_INPUT_CLASSES + }), + 'description': Textarea(attrs={ + 'class': DJANGO_LEDGER_FORM_INPUT_CLASSES + }), + } diff --git a/django_ledger/forms/coa.py b/django_ledger/forms/coa.py deleted file mode 100644 index bed8a266..00000000 --- a/django_ledger/forms/coa.py +++ /dev/null @@ -1,47 +0,0 @@ -from django.forms import ModelForm, TextInput, Textarea -from django.utils.translation import gettext_lazy as _ - -from django_ledger.models.coa import ChartOfAccountModel -from django_ledger.settings import DJANGO_LEDGER_FORM_INPUT_CLASSES - - -class ChartOfAccountsModelForm(ModelForm): - class Meta: - model = ChartOfAccountModel - fields = [ - 'name', - 'description' - ] - labels = { - 'name': _('Name'), - 'description': _('Description'), - } - widgets = { - 'name': TextInput(attrs={ - 'class': DJANGO_LEDGER_FORM_INPUT_CLASSES - }), - 'description': Textarea(attrs={ - 'class': DJANGO_LEDGER_FORM_INPUT_CLASSES - }), - } - - -class ChartOfAccountsModelUpdateForm(ModelForm): - class Meta: - model = ChartOfAccountModel - fields = [ - 'name', - 'active' - ] - labels = { - 'name': _('Name'), - 'description': _('Description'), - } - widgets = { - 'name': TextInput(attrs={ - 'class': DJANGO_LEDGER_FORM_INPUT_CLASSES - }), - 'description': Textarea(attrs={ - 'class': DJANGO_LEDGER_FORM_INPUT_CLASSES - }), - } diff --git a/django_ledger/io/io_library.py b/django_ledger/io/io_library.py index 177f0e77..63e627f6 100644 --- a/django_ledger/io/io_library.py +++ b/django_ledger/io/io_library.py @@ -22,7 +22,7 @@ from django_ledger.io.io_core import get_localtime from django_ledger.models.accounts import AccountModel, AccountModelQuerySet, CREDIT, DEBIT -from django_ledger.models.coa import ChartOfAccountModel +from django_ledger.models.chart_of_accounts import ChartOfAccountModel from django_ledger.models.entity import EntityModel from django_ledger.models.ledger import LedgerModel, LedgerModelQuerySet diff --git a/django_ledger/migrations/0018_transactionmodel_cleared_transactionmodel_reconciled_and_more.py b/django_ledger/migrations/0018_transactionmodel_cleared_transactionmodel_reconciled_and_more.py new file mode 100644 index 00000000..c1627d2a --- /dev/null +++ b/django_ledger/migrations/0018_transactionmodel_cleared_transactionmodel_reconciled_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 5.1.2 on 2024-11-20 22:26 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_ledger', '0017_alter_accountmodel_unique_together_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='transactionmodel', + name='cleared', + field=models.BooleanField(default=False, verbose_name='Cleared'), + ), + migrations.AddField( + model_name='transactionmodel', + name='reconciled', + field=models.BooleanField(default=False, verbose_name='Reconciled'), + ), + migrations.AlterField( + model_name='chartofaccountmodel', + name='entity', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_ledger.entitymodel', verbose_name='Entity'), + ), + migrations.AddIndex( + model_name='transactionmodel', + index=models.Index(fields=['cleared'], name='django_ledg_cleared_335c0b_idx'), + ), + migrations.AddIndex( + model_name='transactionmodel', + index=models.Index(fields=['reconciled'], name='django_ledg_reconci_75dbc7_idx'), + ), + ] diff --git a/django_ledger/models/__init__.py b/django_ledger/models/__init__.py index e1e8bf79..055bfd16 100644 --- a/django_ledger/models/__init__.py +++ b/django_ledger/models/__init__.py @@ -1,6 +1,6 @@ from django_ledger.models.mixins import * from django_ledger.models.bank_account import * -from django_ledger.models.coa import * +from django_ledger.models.chart_of_accounts import * from django_ledger.models.bill import * from django_ledger.models.invoice import * from django_ledger.models.items import * diff --git a/django_ledger/models/accounts.py b/django_ledger/models/accounts.py index 44a11693..0eec4f2c 100644 --- a/django_ledger/models/accounts.py +++ b/django_ledger/models/accounts.py @@ -1058,6 +1058,10 @@ class AccountModel(AccountModelAbstract): Base Account Model from Account Model Abstract Class """ + class Meta(AccountModelAbstract.Meta): + swappable = 'DJANGO_LEDGER_ACCOUNT_MODEL' + abstract = False + def accountmodel_presave(instance: AccountModel, **kwargs): if instance.role_default is False: diff --git a/django_ledger/models/bank_account.py b/django_ledger/models/bank_account.py index fa978354..5ed4ce18 100644 --- a/django_ledger/models/bank_account.py +++ b/django_ledger/models/bank_account.py @@ -91,7 +91,7 @@ def for_entity(self, entity_slug, user_model) -> BankAccountModelQuerySet: ) -class BackAccountModelAbstract(BankAccountInfoMixIn, CreateUpdateMixIn): +class BankAccountModelAbstract(BankAccountInfoMixIn, CreateUpdateMixIn): """ This is the main abstract class which the BankAccountModel database will inherit from. The BankAccountModel inherits functionality from the following MixIns: @@ -204,7 +204,11 @@ def mark_as_inactive(self, commit: bool = False, raise_exception: bool = True, * ]) -class BankAccountModel(BackAccountModelAbstract): +class BankAccountModel(BankAccountModelAbstract): """ Base Bank Account Model Implementation """ + + class Meta(BankAccountModelAbstract.Meta): + swappable = 'DJANGO_LEDGER_BANK_ACCOUNT_MODEL' + abstract = False diff --git a/django_ledger/models/bill.py b/django_ledger/models/bill.py index e2b256a9..eb140d5a 100644 --- a/django_ledger/models/bill.py +++ b/django_ledger/models/bill.py @@ -23,7 +23,7 @@ from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError, ObjectDoesNotExist from django.db import models, transaction, IntegrityError -from django.db.models import Q, Sum, F, Count +from django.db.models import Q, Sum, F, Count, QuerySet, Manager from django.db.models.signals import pre_save from django.shortcuts import get_object_or_404 from django.urls import reverse @@ -53,7 +53,7 @@ class BillModelValidationError(ValidationError): pass -class BillModelQuerySet(models.QuerySet): +class BillModelQuerySet(QuerySet): """ A custom defined QuerySet for the BillModel. This implements multiple methods or queries needed to get a filtered QuerySet based on the BillModel status. For example, we might want to have list of bills which are paid, unpaid, @@ -170,7 +170,7 @@ def unpaid(self): return self.filter(bill_status__exact=BillModel.BILL_STATUS_APPROVED) -class BillModelManager(models.Manager): +class BillModelManager(Manager): """ A custom defined BillModelManager that will act as an interface to handling the initial DB queries to the BillModel. The default "get_queryset" has been overridden to refer the custom defined @@ -1904,6 +1904,10 @@ class BillModel(BillModelAbstract): Base BillModel from Abstract. """ + class Meta(BillModelAbstract.Meta): + swappable = 'DJANGO_LEDGER_BILL_MODEL' + abstract = False + def billmodel_presave(instance: BillModel, **kwargs): if instance.can_generate_bill_number(): diff --git a/django_ledger/models/coa.py b/django_ledger/models/chart_of_accounts.py similarity index 84% rename from django_ledger/models/coa.py rename to django_ledger/models/chart_of_accounts.py index db45d132..19a623cd 100644 --- a/django_ledger/models/coa.py +++ b/django_ledger/models/chart_of_accounts.py @@ -5,17 +5,41 @@ Chart Of Accounts ----------------- -A Chart of Accounts (CoA) is a crucial collection of logically grouped accounts within a ChartOfAccountModel, -forming the backbone of financial statements. The CoA includes various account roles such as cash, accounts receivable, -expenses, liabilities, and income. For example, the Balance Sheet may have a Fixed Assets heading consisting of -Tangible and Intangible Assets with multiple accounts like Building, Plant & Equipments, and Machinery under -tangible assets. Aggregation of individual account balances based on the Chart of Accounts and AccountModel roles is -essential for preparing Financial Statements. - -All EntityModel must have a default CoA to create any type of transaction. When no explicit CoA is specified, the -default behavior is to use the EntityModel default CoA. Only ONE Chart of Accounts can be used when creating -Journal Entries. No commingling between CoAs is allowed to preserve the integrity of the Journal Entry. +A Chart of Accounts (CoA) is a fundamental component of financial management in Django Ledger. It serves as the +backbone of financial statements and is organized within a ChartOfAccountModel. + +### Key Features + +- **Account Roles**: The CoA includes various account types such as: + - Cash + - Accounts Receivable + - Expenses + - Liabilities + - Income + +- **Hierarchical Structure**: Accounts are logically grouped to form financial statements. For example, the Balance +Sheet may have a structure like this: + - Fixed Assets + - Tangible Assets + - Building + - Plant & Equipment + - Machinery + - Intangible Assets + +- **Financial Statement Preparation**: Individual account balances are aggregated based on the CoA and AccountModel +roles to create comprehensive financial statements. + +### Usage in EntityModel + +- Every EntityModel must have a default CoA to create any type of transaction. +- If no explicit CoA is specified, the EntityModel's default CoA is used. +- Only ONE Chart of Accounts can be used when creating Journal Entries. +- Commingling between different CoAs is not allowed to maintain the integrity of Journal Entries. + +This structure ensures a clear and organized approach to financial management within Django Ledger, facilitating +accurate record-keeping and reporting. """ + from random import choices from string import ascii_lowercase, digits from typing import Optional, Union, Dict @@ -25,7 +49,9 @@ from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.db import models -from django.db.models import Q, F +from django.db.models import Q, F, Count, Manager, QuerySet +from django.db.models.signals import pre_save, post_save +from django.dispatch import receiver from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -47,7 +73,7 @@ class ChartOfAccountsModelValidationError(ValidationError): pass -class ChartOfAccountModelQuerySet(models.QuerySet): +class ChartOfAccountModelQuerySet(QuerySet): def active(self): """ @@ -56,7 +82,7 @@ def active(self): return self.filter(active=True) -class ChartOfAccountModelManager(models.Manager): +class ChartOfAccountModelManager(Manager): """ A custom defined ChartOfAccountModelManager that will act as an interface to handling the initial DB queries to the ChartOfAccountModel. @@ -65,8 +91,23 @@ class ChartOfAccountModelManager(models.Manager): def get_queryset(self): qs = super().get_queryset() return qs.annotate( - _entity_slug=F('entity__slug') - ) + _entity_slug=F('entity__slug'), + accountmodel_total__count=Count( + 'accountmodel', + # excludes coa root accounts... + filter=Q(accountmodel__depth__gt=2) + ), + accountmodel_locked__count=Count( + 'accountmodel', + # excludes coa root accounts... + filter=Q(accountmodel__depth__gt=2) & Q(accountmodel__locked=True) + ), + accountmodel_active__count=Count( + 'accountmodel', + # excludes coa root accounts... + filter=Q(accountmodel__depth__gt=2) & Q(accountmodel__active=True) + ), + ).select_related('entity') def for_user(self, user_model) -> ChartOfAccountModelQuerySet: """ @@ -91,9 +132,9 @@ def for_user(self, user_model) -> ChartOfAccountModelQuerySet: Q(entity__admin=user_model) | Q(entity__managers__in=[user_model]) ) - ).select_related('entity') + ) - def for_entity(self, entity_slug, user_model) -> ChartOfAccountModelQuerySet: + def for_entity(self, entity_model, user_model) -> ChartOfAccountModelQuerySet: """ Fetches a QuerySet of ChartOfAccountsModel associated with a specific EntityModel & UserModel. May pass an instance of EntityModel or a String representing the EntityModel slug. @@ -113,9 +154,9 @@ def for_entity(self, entity_slug, user_model) -> ChartOfAccountModelQuerySet: Returns a ChartOfAccountQuerySet with applied filters. """ qs = self.for_user(user_model) - if isinstance(entity_slug, lazy_loader.get_entity_model()): - return qs.filter(entity=entity_slug).select_related('entity') - return qs.filter(entity__slug__iexact=entity_slug).select_related('entity') + if isinstance(entity_model, lazy_loader.get_entity_model()): + return qs.filter(entity=entity_model) + return qs.filter(entity__slug__iexact=entity_model) class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn): @@ -138,7 +179,6 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn): uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True) entity = models.ForeignKey('django_ledger.EntityModel', - editable=False, verbose_name=_('Entity'), on_delete=models.CASCADE) active = models.BooleanField(default=True, verbose_name=_('Is Active')) @@ -292,7 +332,7 @@ def get_coa_account_tree(self) -> Dict: root_account = self.get_coa_root_node() return AccountModel.dump_bulk(parent=root_account) - def generate_slug(self, raise_exception: bool = False) -> str: + def generate_slug(self, commit: bool = False, raise_exception: bool = False) -> str: """ Generates and assigns a slug based on the ChartOfAccounts model instance EntityModel information. @@ -321,6 +361,14 @@ def generate_slug(self, raise_exception: bool = False) -> str: return self.slug = f'coa-{self.entity.slug[-5:]}-' + ''.join(choices(SLUG_SUFFIX, k=15)) + if commit: + self.save( + update_fields=[ + 'slug', + 'updated' + ] + ) + def configure(self, raise_exception: bool = True): """ A method that properly configures the ChartOfAccounts model and creates the appropriate hierarchy boilerplate @@ -333,7 +381,7 @@ def configure(self, raise_exception: bool = True): Whether to raise an exception if root nodes already exist in the Chart of Accounts (default is True). This indicates that the ChartOfAccountModel instance is already configured. """ - self.generate_slug() + self.generate_slug(commit=False) root_accounts_qs = self.get_coa_root_accounts_qs() existing_root_roles = list(set(acc.role for acc in root_accounts_qs)) @@ -546,6 +594,7 @@ def unlock_all_accounts(self) -> AccountModelQuerySet: account_qs.update(locked=False) return account_qs + def mark_as_default(self, commit: bool = False, raise_exception: bool = False, **kwargs): """ Marks the current Chart of Accounts instances as default for the EntityModel. @@ -563,6 +612,12 @@ def mark_as_default(self, commit: bool = False, raise_exception: bool = False, * message=_(f'The Chart of Accounts {self.slug} is already default') ) return + if not self.can_mark_as_default(): + if raise_exception: + raise ChartOfAccountsModelValidationError( + message=_(f'The Chart of Accounts {self.slug} cannot be marked as default') + ) + return self.entity.default_coa_id = self.uuid self.clean() if commit: @@ -573,6 +628,12 @@ def mark_as_default(self, commit: bool = False, raise_exception: bool = False, * ] ) + def can_mark_as_default(self): + return all([ + self.is_active(), + not self.is_default() + ]) + def can_activate(self) -> bool: """ Check if the ChartOffAccountModel instance can be activated. @@ -710,6 +771,22 @@ def get_coa_list_url(self): } ) + def get_coa_list_inactive_url(self): + return reverse( + viewname='django_ledger:coa-list-inactive', + kwargs={ + 'entity_slug': self.entity_slug + } + ) + + def get_coa_create_url(self): + return reverse( + viewname='django_ledger:coa-create', + kwargs={ + 'entity_slug': self.entity_slug + } + ) + def get_absolute_url(self) -> str: return reverse( viewname='django_ledger:coa-detail', @@ -719,7 +796,20 @@ def get_absolute_url(self) -> str: } ) + def get_update_url(self) -> str: + return reverse( + viewname='django_ledger:coa-update', + kwargs={ + 'entity_slug': self.entity_slug, + 'coa_slug': self.slug + } + ) + def get_account_list_url(self): + + if not self.slug: + self.generate_slug(commit=True) + return reverse( viewname='django_ledger:account-list', kwargs={ @@ -739,13 +829,27 @@ def get_create_coa_account_url(self): def clean(self): self.generate_slug() - if self.is_default() and not self.active: - raise ChartOfAccountsModelValidationError( - _('Default Chart of Accounts cannot be deactivated.') - ) class ChartOfAccountModel(ChartOfAccountModelAbstract): """ Base ChartOfAccounts Model """ + class Meta(ChartOfAccountModelAbstract.Meta): + swappable = 'DJANGO_LEDGER_CHART_OF_ACCOUNTS_MODEL' + abstract = False + + +@receiver(pre_save, sender=ChartOfAccountModel) +def chartofaccountsmodel_presave(instance: ChartOfAccountModelAbstract, **kwargs): + instance.generate_slug() + if instance.is_default() and not instance.active: + raise ChartOfAccountsModelValidationError( + _('Default Chart of Accounts cannot be deactivated.') + ) + + +@receiver(post_save, sender=ChartOfAccountModel) +def chartofaccountsmodel_postsave(instance: ChartOfAccountModelAbstract, **kwargs): + if instance._state.adding: + instance.configure() diff --git a/django_ledger/models/closing_entry.py b/django_ledger/models/closing_entry.py index 5d2a3e6c..4aed17af 100644 --- a/django_ledger/models/closing_entry.py +++ b/django_ledger/models/closing_entry.py @@ -9,6 +9,7 @@ from typing import Optional from uuid import uuid4, UUID +from django.conf import settings from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator from django.db import models @@ -23,6 +24,7 @@ from django_ledger.models.mixins import CreateUpdateMixIn, MarkdownNotesMixIn from django_ledger.models.transactions import TransactionModel from django_ledger.models.utils import lazy_loader +from django_ledger.settings import DJANGO_LEDGER_LEDGER_MODEL class ClosingEntryValidationError(ValidationError): @@ -338,7 +340,9 @@ def get_list_url(self): class ClosingEntryModel(ClosingEntryModelAbstract): - pass + class Meta(ClosingEntryModelAbstract.Meta): + swappable = 'DJANGO_LEDGER_CLOSING_ENTRY_MODEL' + abstract = False # todo: Remove this model! @@ -349,12 +353,6 @@ class ClosingEntryTransactionModelQuerySet(models.QuerySet): class ClosingEntryTransactionModelManager(models.Manager): - # def get_queryset(self): - # return super().get_queryset().select_related( - # 'closing_entry_model', - # 'closing_entry_model__entity_model' - # ) - def for_entity(self, entity_slug): qs = self.get_queryset() if isinstance(entity_slug, lazy_loader.get_entity_model()): diff --git a/django_ledger/models/coa_default.py b/django_ledger/models/coa_default.py index 95d810e5..ac7365a3 100644 --- a/django_ledger/models/coa_default.py +++ b/django_ledger/models/coa_default.py @@ -33,8 +33,8 @@ 1910 asset_adjustment debit Securities Unrealized Gains/Losses root_assets 1920 asset_adjustment debit PPE Unrealized Gains/Losses root_assets 1010 asset_ca_cash debit Cash root_assets - 1200 asset_ca_inv debit Inventory root_assets 1050 asset_ca_mkt_sec debit Short Term Investments root_assets + 1200 asset_ca_inv debit Inventory root_assets 1300 asset_ca_prepaid debit Prepaid Expenses root_assets 1100 asset_ca_recv debit Accounts Receivable root_assets 1110 asset_ca_uncoll credit Uncollectibles root_assets diff --git a/django_ledger/models/customer.py b/django_ledger/models/customer.py index f3737642..9c38667f 100644 --- a/django_ledger/models/customer.py +++ b/django_ledger/models/customer.py @@ -10,7 +10,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.db import models, transaction, IntegrityError -from django.db.models import Q, F, QuerySet +from django.db.models import Q, F, QuerySet, Manager from django.utils.translation import gettext_lazy as _ from django_ledger.models.mixins import ContactInfoMixIn, CreateUpdateMixIn, TaxCollectionMixIn @@ -77,7 +77,7 @@ def visible(self) -> QuerySet: ) -class CustomerModelManager(models.Manager): +class CustomerModelManager(Manager): """ A custom defined CustomerModelManager that will act as an interface to handling the DB queries to the CustomerModel. @@ -329,3 +329,7 @@ class CustomerModel(CustomerModelAbstract): """ Base Customer Model Implementation """ + + class Meta: + swappable = 'DJANGO_LEDGER_CUSTOMER_MODEL' + abstract = False diff --git a/django_ledger/models/data_import.py b/django_ledger/models/data_import.py index 74eed99f..d4be74c7 100644 --- a/django_ledger/models/data_import.py +++ b/django_ledger/models/data_import.py @@ -9,27 +9,26 @@ from django.core.exceptions import ValidationError from django.db import models -from django.db.models import Q, Count, Sum, Case, When, F, Value, DecimalField, BooleanField +from django.db.models import Q, Count, Sum, Case, When, F, Value, DecimalField, BooleanField, Manager, QuerySet from django.db.models.functions import Coalesce from django.db.models.signals import pre_save from django.utils.translation import gettext_lazy as _ from django_ledger.io import ASSET_CA_CASH, CREDIT, DEBIT +from django_ledger.models import JournalEntryModel from django_ledger.models.mixins import CreateUpdateMixIn from django_ledger.models.utils import lazy_loader -from django_ledger.models import JournalEntryModel - class ImportJobModelValidationError(ValidationError): pass -class ImportJobModelQuerySet(models.QuerySet): +class ImportJobModelQuerySet(QuerySet): pass -class ImportJobModelManager(models.Manager): +class ImportJobModelManager(Manager): def get_queryset(self): qs = super().get_queryset() @@ -502,6 +501,10 @@ class ImportJobModel(ImportJobModelAbstract): Transaction Import Job Model Base Class. """ + class Meta(ImportJobModelAbstract.Meta): + swappable = 'DJANGO_LEDGER_IMPORT_JOB_MODEL' + abstract = False + def importjobmodel_presave(instance: ImportJobModel, **kwargs): if instance.is_configured(): @@ -518,3 +521,7 @@ class StagedTransactionModel(StagedTransactionModelAbstract): """ Staged Transaction Model Base Class. """ + + class Meta(StagedTransactionModelAbstract.Meta): + swappable = 'DJANGO_LEDGER_STAGED_TRANSACTION_MODEL' + abstract = False diff --git a/django_ledger/models/entity.py b/django_ledger/models/entity.py index 26833d2c..fd614c0d 100644 --- a/django_ledger/models/entity.py +++ b/django_ledger/models/entity.py @@ -33,7 +33,7 @@ from django.core.exceptions import ValidationError, ObjectDoesNotExist from django.core.validators import MinValueValidator from django.db import models -from django.db.models import Q, F +from django.db.models import Q, F, Model from django.db.models.signals import pre_save from django.urls import reverse from django.utils.text import slugify @@ -44,7 +44,7 @@ from django_ledger.io.io_core import IOMixIn, get_localtime, get_localdate from django_ledger.models.accounts import AccountModel, AccountModelQuerySet, DEBIT, CREDIT from django_ledger.models.bank_account import BankAccountModelQuerySet, BankAccountModel -from django_ledger.models.coa import ChartOfAccountModel, ChartOfAccountModelQuerySet +from django_ledger.models.chart_of_accounts import ChartOfAccountModel, ChartOfAccountModelQuerySet from django_ledger.models.coa_default import CHART_OF_ACCOUNTS_ROOT_MAP from django_ledger.models.customer import CustomerModelQueryset, CustomerModel from django_ledger.models.items import (ItemModelQuerySet, ItemTransactionModelQuerySet, @@ -3038,6 +3038,22 @@ def get_coa_list_url(self) -> str: } ) + def get_coa_list_inactive_url(self) -> str: + return reverse( + viewname='django_ledger:coa-list-inactive', + kwargs={ + 'entity_slug': self.slug + } + ) + + def get_coa_create_url(self) -> str: + return reverse( + viewname='django_ledger:coa-create', + kwargs={ + 'entity_slug': self.slug + } + ) + def get_accounts_url(self) -> str: """ The EntityModel Code of Accounts llist import URL. @@ -3104,10 +3120,14 @@ class EntityModel(EntityModelAbstract): """ Entity Model Base Class From Abstract """ + class Meta(EntityModelAbstract.Meta): + swappable = 'DJANGO_LEDGER_ENTITY_MODEL' + abstract = False + # ## ENTITY STATE.... -class EntityStateModelAbstract(models.Model): +class EntityStateModelAbstract(Model): KEY_JOURNAL_ENTRY = 'je' KEY_PURCHASE_ORDER = 'po' KEY_BILL = 'bill' @@ -3168,6 +3188,10 @@ class EntityStateModel(EntityStateModelAbstract): Entity State Model Base Class from Abstract. """ + class Meta(EntityStateModelAbstract.Meta): + swappable = 'DJANGO_LEDGER_ENTITY_STATE_MODEL' + abstract = False + # ## ENTITY MANAGEMENT..... class EntityManagementModelAbstract(CreateUpdateMixIn): diff --git a/django_ledger/models/estimate.py b/django_ledger/models/estimate.py index 8dcf7680..c989e3f3 100644 --- a/django_ledger/models/estimate.py +++ b/django_ledger/models/estimate.py @@ -1231,7 +1231,8 @@ def update_cost_estimate(self, itemtxs_qs: Optional[ItemTransactionModelQuerySet 'updated' ]) - def update_state(self, itemtxs_qs: Optional[Union[ItemTransactionModelQuerySet, List[ItemTransactionModel]]] = None): + def update_state(self, + itemtxs_qs: Optional[Union[ItemTransactionModelQuerySet, List[ItemTransactionModel]]] = None): itemtxs_qs, _ = self.get_itemtxs_data(queryset=itemtxs_qs) self.update_cost_estimate(itemtxs_qs) self.update_revenue_estimate(itemtxs_qs) @@ -1611,3 +1612,7 @@ class EstimateModel(EstimateModelAbstract): """ Base EstimateModel Class. """ + + class Meta(EstimateModelAbstract.Meta): + swappable = 'DJANGO_LEDGER_ESTIMATE_MODEL' + abstract = False diff --git a/django_ledger/models/invoice.py b/django_ledger/models/invoice.py index 79c211b9..41fd97b0 100644 --- a/django_ledger/models/invoice.py +++ b/django_ledger/models/invoice.py @@ -33,14 +33,16 @@ from django_ledger.io import ASSET_CA_CASH, ASSET_CA_RECEIVABLES, LIABILITY_CL_DEFERRED_REVENUE from django_ledger.io.io_core import get_localtime, get_localdate -from django_ledger.models import lazy_loader, ItemTransactionModelQuerySet, ItemModelQuerySet, ItemModel +from django_ledger.models import ( + lazy_loader, ItemTransactionModelQuerySet, + ItemModelQuerySet, ItemModel, QuerySet, Manager +) from django_ledger.models.entity import EntityModel from django_ledger.models.mixins import ( CreateUpdateMixIn, AccrualMixIn, MarkdownNotesMixIn, PaymentTermsMixIn, ItemizeMixIn ) - from django_ledger.models.signals import ( invoice_status_draft, invoice_status_in_review, @@ -49,7 +51,6 @@ invoice_status_canceled, invoice_status_void ) - from django_ledger.settings import DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING, DJANGO_LEDGER_INVOICE_NUMBER_PREFIX UserModel = get_user_model() @@ -59,7 +60,7 @@ class InvoiceModelValidationError(ValidationError): pass -class InvoiceModelQuerySet(models.QuerySet): +class InvoiceModelQuerySet(QuerySet): """ A custom defined QuerySet for the InvoiceModel. This implements multiple methods or queries that we need to run to get a status of Invoices raised by the entity. @@ -176,7 +177,7 @@ def unpaid(self): return self.filter(invoice_status__exact=InvoiceModel.INVOICE_STATUS_APPROVED) -class InvoiceModelManager(models.Manager): +class InvoiceModelManager(Manager): """ A custom defined InvoiceModel Manager that will act as an interface to handling the DB queries to the InvoiceModel. The default "get_queryset" has been overridden to refer the custom defined "InvoiceModelQuerySet" @@ -572,9 +573,10 @@ def get_migration_data(self, else: self.validate_itemtxs_qs(queryset) - return queryset.select_related('item_model').order_by('item_model__earnings_account__uuid', - 'entity_unit__uuid', - 'item_model__earnings_account__balance_type').values( + return queryset.select_related('item_model').order_by( + 'item_model__earnings_account__uuid', + 'entity_unit__uuid', + 'item_model__earnings_account__balance_type').values( 'item_model__earnings_account__uuid', 'item_model__earnings_account__balance_type', 'item_model__cogs_account__uuid', @@ -1817,6 +1819,10 @@ class InvoiceModel(InvoiceModelAbstract): Base Invoice Model from Abstract. """ + class Meta: + swappable = 'DJANGO_LEDGER_INVOICE_MODEL' + abstract = False + def invoicemodel_presave(instance: InvoiceModel, **kwargs): if instance.can_generate_invoice_number(): diff --git a/django_ledger/models/items.py b/django_ledger/models/items.py index cc476a27..609b390c 100644 --- a/django_ledger/models/items.py +++ b/django_ledger/models/items.py @@ -25,7 +25,7 @@ from django.core.exceptions import ValidationError, ObjectDoesNotExist from django.core.validators import MinValueValidator from django.db import models, transaction, IntegrityError -from django.db.models import Q, Sum, F, ExpressionWrapper, DecimalField, Value, Case, When, QuerySet +from django.db.models import Q, Sum, F, ExpressionWrapper, DecimalField, Value, Case, When, QuerySet, Manager from django.db.models.functions import Coalesce from django.utils.translation import gettext_lazy as _ @@ -42,12 +42,12 @@ class ItemModelValidationError(ValidationError): pass -class UnitOfMeasureModelQuerySet(models.QuerySet): +class UnitOfMeasureModelQuerySet(QuerySet): pass # UNIT OF MEASURES MODEL.... -class UnitOfMeasureModelManager(models.Manager): +class UnitOfMeasureModelManager(Manager): """ A custom defined QuerySet Manager for the UnitOfMeasureModel. """ @@ -149,7 +149,7 @@ def __str__(self): # ITEM MODEL.... -class ItemModelQuerySet(models.QuerySet): +class ItemModelQuerySet(QuerySet): """ A custom-defined ItemModelQuerySet that implements custom QuerySet methods related to the ItemModel. """ @@ -287,7 +287,7 @@ def purchase_orders(self): return self.inventory_all() -class ItemModelManager(models.Manager): +class ItemModelManager(Manager): """ A custom defined ItemModelManager that implement custom QuerySet methods related to the ItemModel """ @@ -849,7 +849,7 @@ def clean(self): # ITEM TRANSACTION MODELS... -class ItemTransactionModelQuerySet(models.QuerySet): +class ItemTransactionModelQuerySet(QuerySet): def is_received(self): return self.filter(po_item_status=ItemTransactionModel.STATUS_RECEIVED) @@ -875,7 +875,7 @@ def get_estimate_aggregate(self): } -class ItemTransactionModelManager(models.Manager): +class ItemTransactionModelManager(Manager): def for_user(self, user_model): qs = self.get_queryset() @@ -1404,20 +1404,31 @@ def clean(self): # FINAL MODEL CLASSES.... - class UnitOfMeasureModel(UnitOfMeasureModelAbstract): """ Base UnitOfMeasureModel from Abstract. """ + class Meta(UnitOfMeasureModelAbstract.Meta): + abstract = False + swappable = 'DJANGO_LEDGER_UNIT_OF_MEASURE_MODEL' + class ItemTransactionModel(ItemTransactionModelAbstract): """ Base ItemTransactionModel from Abstract. """ + class Meta(ItemTransactionModelAbstract.Meta): + abstract = False + swappable = 'DJANGO_LEDGER_ITEM_TRANSACTION_MODEL' + class ItemModel(ItemModelAbstract): """ Base ItemModel from Abstract. """ + + class Meta(ItemModelAbstract.Meta): + abstract = False + swappable = 'DJANGO_LEDGER_ITEM_MODEL' diff --git a/django_ledger/models/journal_entry.py b/django_ledger/models/journal_entry.py index b7d6f771..ad9e5ad1 100644 --- a/django_ledger/models/journal_entry.py +++ b/django_ledger/models/journal_entry.py @@ -487,15 +487,18 @@ def is_verified(self) -> bool: """ return self._verified - def is_balance_valid(self, txs_qs: Optional[TransactionModelQuerySet] = None) -> bool: + # Transaction QuerySet + def is_balance_valid(self, txs_qs: TransactionModelQuerySet, raise_exception: bool = True) -> bool: """ Checks if CREDITs and DEBITs are equal. Parameters ---------- txs_qs: TransactionModelQuerySet - Optional pre-fetched JE instance TransactionModelQuerySet. Will be validated if provided. + raise_exception: bool + Raises JournalEntryValidationError if TransactionModelQuerySet is not valid. + Returns ------- bool @@ -503,32 +506,38 @@ def is_balance_valid(self, txs_qs: Optional[TransactionModelQuerySet] = None) -> """ if len(txs_qs) > 0: balances = self.get_txs_balances(txs_qs=txs_qs, as_dict=True) - return balances[CREDIT] == balances[DEBIT] + is_valid = balances[CREDIT] == balances[DEBIT] + 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] + ) + ) + return is_valid return True - def is_cash_involved(self, txs_qs=None): - return ASSET_CA_CASH in self.get_txs_roles(txs_qs=None) - - def is_operating(self): - return self.activity in [ - self.OPERATING_ACTIVITY - ] + def is_txs_qs_coa_valid(self, txs_qs: TransactionModelQuerySet) -> bool: + """ + Validates that the Chart of Accounts (COA) is valid for the transactions. + Journal Entry transactions can only be associated with one Chart of Accounts (COA). + + + Parameters + ---------- + txs_qs: TransactionModelQuerySet - def is_financing(self): - return self.activity in [ - self.FINANCING_EQUITY, - self.FINANCING_LTD, - self.FINANCING_DIVIDENDS, - self.FINANCING_STD, - self.FINANCING_OTHER - ] + Returns + ------- + True if Transaction CoAs are valid, otherwise False. + """ - def is_investing(self): - return self.activity in [ - self.INVESTING_SECURITIES, - self.INVESTING_PPE, - self.INVESTING_OTHER - ] + if len(txs_qs) > 0: + coa_count = len(set(tx.coa_id for tx in txs_qs)) + return coa_count == 1 + return True def is_txs_qs_valid(self, txs_qs: TransactionModelQuerySet, raise_exception: bool = True) -> bool: """ @@ -560,6 +569,30 @@ def is_txs_qs_valid(self, txs_qs: TransactionModelQuerySet, raise_exception: boo f'associated with LedgerModel {self.uuid}') return is_valid + def is_cash_involved(self, txs_qs=None): + return ASSET_CA_CASH in self.get_txs_roles(txs_qs=None) + + def is_operating(self): + return self.activity in [ + self.OPERATING_ACTIVITY + ] + + def is_financing(self): + return self.activity in [ + self.FINANCING_EQUITY, + self.FINANCING_LTD, + self.FINANCING_DIVIDENDS, + self.FINANCING_STD, + self.FINANCING_OTHER + ] + + def is_investing(self): + return self.activity in [ + self.INVESTING_SECURITIES, + self.INVESTING_PPE, + self.INVESTING_OTHER + ] + def get_entity_unit_name(self, no_unit_name: str = ''): if self.entity_unit_id: return self.entity_unit.name @@ -1155,6 +1188,15 @@ def verify(self, except JournalEntryValidationError as e: raise e + # Transaction CoA if valid + + try: + is_coa_valid = self.is_txs_qs_coa_valid(txs_qs=txs_qs) + if not is_coa_valid: + raise JournalEntryValidationError('Transaction COA is not valid!') + except JournalEntryValidationError as e: + raise e + # if not len(txs_qs): # if raise_exception: # raise JournalEntryValidationError('Journal entry has no transactions.') @@ -1163,7 +1205,7 @@ def verify(self, # if raise_exception: # raise JournalEntryValidationError('At least two transactions required.') - if all([is_balance_valid, is_txs_qs_valid]): + if all([is_balance_valid, is_txs_qs_valid, is_coa_valid]): # activity flag... self.generate_activity(txs_qs=txs_qs, raise_exception=raise_exception) self._verified = True @@ -1350,6 +1392,10 @@ class JournalEntryModel(JournalEntryModelAbstract): Journal Entry Model Base Class From Abstract """ + class Meta(JournalEntryModelAbstract.Meta): + swappable = 'DJANGO_LEDGER_JOURNAL_ENTRY_MODEL' + abstract = False + def journalentrymodel_presave(instance: JournalEntryModel, **kwargs): if instance._state.adding and not instance.ledger.can_edit_journal_entries(): diff --git a/django_ledger/models/ledger.py b/django_ledger/models/ledger.py index 8cc62c65..412cb352 100644 --- a/django_ledger/models/ledger.py +++ b/django_ledger/models/ledger.py @@ -20,7 +20,7 @@ The digest() method executes all necessary aggregations and optimizations in order to push as much work to the Database layer as possible in order to minimize the amount of data being pulled for analysis into the Python memory. -The Django Ledger core model follows the following structure: \n +The Django Ledger core model follows the following structure: EntityModel -< LedgerModel -< JournalEntryModel -< TransactionModel """ from datetime import date @@ -34,6 +34,10 @@ from django.db.models import Q, Min, F, Count from django.urls import reverse from django.utils.translation import gettext_lazy as _ + +from django_ledger.io.io_core import IOMixIn +from django_ledger.models import lazy_loader +from django_ledger.models.mixins import CreateUpdateMixIn from django_ledger.models.signals import ( ledger_posted, ledger_unposted, @@ -43,10 +47,6 @@ ledger_unhidden ) -from django_ledger.io.io_core import IOMixIn -from django_ledger.models import lazy_loader -from django_ledger.models.mixins import CreateUpdateMixIn - LEDGER_ID_CHARS = ascii_lowercase + digits @@ -725,6 +725,9 @@ class LedgerModel(LedgerModelAbstract): """ Base LedgerModel from Abstract. """ + class Meta(LedgerModelAbstract.Meta): + swappable = 'DJANGO_LEDGER_LEDGER_MODEL' + abstract = False def ledgermodel_presave(instance: LedgerModel, **kwargs): diff --git a/django_ledger/models/purchase_order.py b/django_ledger/models/purchase_order.py index 1ba7c5a7..7994c72c 100644 --- a/django_ledger/models/purchase_order.py +++ b/django_ledger/models/purchase_order.py @@ -21,7 +21,7 @@ from django.core.exceptions import ValidationError, ObjectDoesNotExist from django.core.validators import MinLengthValidator from django.db import models, transaction, IntegrityError -from django.db.models import Q, Sum, Count, F +from django.db.models import Q, Sum, Count, F, Manager, QuerySet from django.db.models.functions import Coalesce from django.db.models.signals import pre_save from django.shortcuts import get_object_or_404 @@ -33,8 +33,6 @@ from django_ledger.models.entity import EntityModel from django_ledger.models.items import ItemTransactionModel, ItemTransactionModelQuerySet, ItemModelQuerySet, ItemModel from django_ledger.models.mixins import CreateUpdateMixIn, MarkdownNotesMixIn, ItemizeMixIn -from django_ledger.models.utils import lazy_loader -from django_ledger.settings import DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING, DJANGO_LEDGER_PO_NUMBER_PREFIX from django_ledger.models.signals import ( po_status_draft, po_status_void, @@ -43,6 +41,8 @@ po_status_canceled, po_status_in_review ) +from django_ledger.models.utils import lazy_loader +from django_ledger.settings import DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING, DJANGO_LEDGER_PO_NUMBER_PREFIX PO_NUMBER_CHARS = ascii_uppercase + digits @@ -53,7 +53,7 @@ class PurchaseOrderModelValidationError(ValidationError): pass -class PurchaseOrderModelQuerySet(models.QuerySet): +class PurchaseOrderModelQuerySet(QuerySet): """ A custom defined PurchaseOrderModel QuerySet. """ @@ -100,7 +100,7 @@ def draft(self): return self.filter(po_status__exact=PurchaseOrderModel.PO_STATUS_DRAFT) -class PurchaseOrderModelManager(models.Manager): +class PurchaseOrderModelManager(Manager): """ A custom defined PurchaseOrderModel Manager. """ @@ -1233,6 +1233,10 @@ class PurchaseOrderModel(PurchaseOrderModelAbstract): Purchase Order Base Model """ + class Meta(PurchaseOrderModelAbstract.Meta): + swappable = 'DJANGO_LEDGER_PURCHASE_ORDER_MODEL' + abstract = False + def purchaseordermodel_presave(instance: PurchaseOrderModel, **kwargs): if instance.can_generate_po_number(): diff --git a/django_ledger/models/transactions.py b/django_ledger/models/transactions.py index 55ec8e66..5c955386 100644 --- a/django_ledger/models/transactions.py +++ b/django_ledger/models/transactions.py @@ -21,7 +21,7 @@ from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator from django.db import models -from django.db.models import Q, QuerySet, Manager +from django.db.models import Q, QuerySet, Manager, F from django.db.models.signals import pre_save from django.utils.translation import gettext_lazy as _ @@ -213,7 +213,9 @@ class TransactionModelManager(Manager): def get_queryset(self) -> TransactionModelQuerySet: qs = TransactionModelQuerySet(self.model, using=self._db) - return qs.select_related( + return qs.annotate( + _coa_id=F('account__coa_model_id'), + ).select_related( 'journal_entry', 'account', 'account__coa_model', @@ -497,6 +499,9 @@ class TransactionModelAbstract(CreateUpdateMixIn): verbose_name=_('Tx Description'), help_text=_('A description to be included with this individual transaction')) + cleared = models.BooleanField(default=False, verbose_name=_('Cleared')) + reconciled = models.BooleanField(default=False, verbose_name=_('Reconciled')) + objects = TransactionModelManager() class Meta: @@ -509,7 +514,9 @@ class Meta: models.Index(fields=['account']), models.Index(fields=['journal_entry']), models.Index(fields=['created']), - models.Index(fields=['updated']) + models.Index(fields=['updated']), + models.Index(fields=['cleared']), + models.Index(fields=['reconciled']), ] def __str__(self): @@ -519,6 +526,15 @@ def __str__(self): x4=self.tx_type, x5=self.account.balance_type) + @property + def coa_id(self): + try: + return getattr(self, '_coa_id') + except AttributeError: + if self.account is None: + return None + return self.account.coa_model_id + def clean(self): if self.account_id and self.account.is_root_account(): raise TransactionModelValidationError( @@ -531,6 +547,10 @@ class TransactionModel(TransactionModelAbstract): Base Transaction Model From Abstract. """ + class Meta(TransactionModelAbstract.Meta): + abstract = False + swappable = 'DJANGO_LEDGER_TRANSACTION_MODEL' + def transactionmodel_presave(instance: TransactionModel, **kwargs): """ diff --git a/django_ledger/models/unit.py b/django_ledger/models/unit.py index 91fe5880..21c92e9b 100644 --- a/django_ledger/models/unit.py +++ b/django_ledger/models/unit.py @@ -223,3 +223,7 @@ class EntityUnitModel(EntityUnitModelAbstract): """ Base Model Class for EntityUnitModel """ + + class Meta(EntityUnitModelAbstract.Meta): + swappable = 'DJANGO_LEDGER_ENTITY_UNIT_MODEL' + abstract = False diff --git a/django_ledger/models/vendor.py b/django_ledger/models/vendor.py index 4b9fe2f3..9c8188c5 100644 --- a/django_ledger/models/vendor.py +++ b/django_ledger/models/vendor.py @@ -323,3 +323,7 @@ class VendorModel(VendorModelAbstract): """ Base Vendor Model Implementation """ + + class Meta(VendorModelAbstract.Meta): + swappable = 'DJANGO_LEDGER_VENDOR_MODEL' + abstract = False diff --git a/django_ledger/settings.py b/django_ledger/settings.py index 434178ab..8838bb65 100644 --- a/django_ledger/settings.py +++ b/django_ledger/settings.py @@ -1,9 +1,6 @@ """ Django Ledger created by Miguel Sanda . Copyright© EDMA Group Inc licensed under the GPLv3 Agreement. - -Contributions to this module: - * Miguel Sanda """ import logging from decimal import Decimal @@ -31,6 +28,34 @@ logger.info(f'Django Ledger GraphQL Enabled: {DJANGO_LEDGER_GRAPHQL_SUPPORT_ENABLED}') + +## MODEL ABSTRACTS ## +DJANGO_LEDGER_ACCOUNT_MODEL = getattr(settings, 'DJANGO_LEDGER_ACCOUNT_MODEL', 'django_ledger.AccountModel') +DJANGO_LEDGER_CHART_OF_ACCOUNTS_MODEL = getattr(settings, 'DJANGO_LEDGER_ACCOUNT_MODEL', 'django_ledger.ChartOfAccountModelAbstract') +DJANGO_LEDGER_TRANSACTION_MODEL = getattr(settings, 'DJANGO_LEDGER_TRANSACTION_MODEL', 'django_ledger.TransactionModelAbstract') +DJANGO_LEDGER_JOURNAL_ENTRY_MODEL = getattr(settings, 'DJANGO_LEDGER_JOURNAL_ENTRY_MODEL', 'django_ledger.JournalEntryModelAbstract') +DJANGO_LEDGER_LEDGER_MODEL = getattr(settings, 'DJANGO_LEDGER_LEDGER_MODEL', 'django_ledger.LedgerModel') +DJANGO_LEDGER_ENTITY_MODEL = getattr(settings, 'DJANGO_LEDGER_ENTITY_MODEL', 'django_ledger.EntityModelAbstract') +DJANGO_LEDGER_ENTITY_STATE_MODEL = getattr(settings, 'DJANGO_LEDGER_ENTITY_STATE_MODEL', 'django_ledger.EntityStateModel') + +DJANGO_LEDGER_ENTITY_UNIT_MODEL = getattr(settings, 'DJANGO_LEDGER_ENTITY_UNIT_MODEL', 'django_ledger.EntityUnitModelAbstract') + +DJANGO_LEDGER_ESTIMATE_MODEL = getattr(settings, 'DJANGO_LEDGER_ESTIMATE_MODEL', 'django_ledger.EstimateModelAbstract') +DJANGO_LEDGER_BILL_MODEL = getattr(settings, 'DJANGO_LEDGER_BILL_MODEL', 'django_ledger.BillModelAbstract') +DJANGO_LEDGER_INVOICE_MODEL = getattr(settings, 'DJANGO_LEDGER_INVOICE_MODEL', 'django_ledger.InvoiceModelAbstract') +DJANGO_LEDGER_PURCHASE_ORDER_MODEL = getattr(settings, 'DJANGO_LEDGER_PURCHASE_ORDER_MODEL', 'django_ledger.PurchaseOrderModelAbstract') + +DJANGO_LEDGER_CUSTOMER_MODEL = getattr(settings, 'DJANGO_LEDGER_CUSTOMER_MODEL', 'django_ledger.CustomerModelAbstract') +DJANGO_LEDGER_VENDOR_MODEL = getattr(settings, 'DJANGO_LEDGER_VENDOR_MODEL', 'django_ledger.VendorModelAbstract') + +DJANGO_LEDGER_BANK_ACCOUNT_MODEL = getattr(settings, 'DJANGO_LEDGER_BANK_ACCOUNT_MODEL', 'django_ledger.BackAccountModelAbstract') +DJANGO_LEDGER_CLOSING_ENTRY_MODEL = getattr(settings, 'DJANGO_LEDGER_CLOSING_ENTRY_MODEL', 'django_ledger.ClosingEntryTransactionModelAbstract') +DJANGO_LEDGER_UNIT_OF_MEASURE_MODEL = getattr(settings, 'DJANGO_LEDGER_UNIT_OF_MEASURE_MODEL', 'django_ledger.UnitOfMeasureModelAbstract') +DJANGO_LEDGER_ITEM_TRANSACTION_MODEL = getattr(settings, 'DJANGO_LEDGER_ITEM_TRANSACTION_MODEL', 'django_ledger.ItemTransactionModelAbstract') +DJANGO_LEDGER_ITEM_MODEL = getattr(settings, 'DJANGO_LEDGER_ITEM_MODEL', 'django_ledger.ItemModelAbstract') +DJANGO_LEDGER_STAGED_TRANSACTION_MODEL = getattr(settings, 'DJANGO_LEDGER_STAGED_TRANSACTION_MODEL', 'django_ledger.StagedTransactionModelAbstract') +DJANGO_LEDGER_IMPORT_JOB_MODEL = getattr(settings, 'DJANGO_LEDGER_IMPORT_JOB_MODEL', 'django_ledger.ImportJobModelAbstract') + DJANGO_LEDGER_USE_CLOSING_ENTRIES = getattr(settings, 'DJANGO_LEDGER_USE_CLOSING_ENTRIES', True) DJANGO_LEDGER_DEFAULT_CLOSING_ENTRY_CACHE_TIMEOUT = getattr(settings, 'DJANGO_LEDGER_DEFAULT_CLOSING_ENTRY_CACHE_TIMEOUT', 3600) 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 d3b44041..2ed050d3 100644 --- a/django_ledger/templates/django_ledger/account/tags/accounts_table.html +++ b/django_ledger/templates/django_ledger/account/tags/accounts_table.html @@ -16,6 +16,7 @@ + {% endif %} @@ -37,7 +38,7 @@ - {# #} + @@ -59,7 +60,7 @@ {% endif %} - + {% if account.is_locked %} diff --git a/django_ledger/templates/django_ledger/chart_of_accounts/coa_create.html b/django_ledger/templates/django_ledger/chart_of_accounts/coa_create.html new file mode 100644 index 00000000..bdf22242 --- /dev/null +++ b/django_ledger/templates/django_ledger/chart_of_accounts/coa_create.html @@ -0,0 +1,25 @@ +{% extends 'django_ledger/layouts/content_layout_1.html' %} +{% load i18n %} +{% load static %} +{% load django_ledger %} + +{% block view_content %} +
+
+
+

{% trans 'Create Chart of Accounts' %}

+
+
+
+
+ {% csrf_token %} + {{ form.as_p }} + + Back +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/django_ledger/templates/django_ledger/chart_of_accounts/coa_list.html b/django_ledger/templates/django_ledger/chart_of_accounts/coa_list.html index e1842d4d..3fcf83ad 100644 --- a/django_ledger/templates/django_ledger/chart_of_accounts/coa_list.html +++ b/django_ledger/templates/django_ledger/chart_of_accounts/coa_list.html @@ -1,15 +1,34 @@ {% extends 'django_ledger/layouts/content_layout_1.html' %} {% load i18n %} {% load static %} +{% load icon from django_ledger %} {% block view_content %} -
-
- {% for coa_model in coa_list %} -
- {% include 'django_ledger/chart_of_accounts/includes/coa_card.html' with coa_model=coa_model %} +
+
+
+
+

Chart of Accounts + + {% icon 'carbon:add-alt' 60 %} +

+ {% if not inactive %} + + {% trans 'Show Inactive' %} + {% else %} + + {% trans 'Show Active' %} + {% endif %} +
- {% endfor %} +
+
+ {% for coa_model in coa_list %} +
+ {% include 'django_ledger/chart_of_accounts/includes/coa_card.html' with coa_model=coa_model %} +
+ {% endfor %} +
{% endblock %} diff --git a/django_ledger/templates/django_ledger/chart_of_accounts/coa_update.html b/django_ledger/templates/django_ledger/chart_of_accounts/coa_update.html index ae013765..5a9f5adc 100644 --- a/django_ledger/templates/django_ledger/chart_of_accounts/coa_update.html +++ b/django_ledger/templates/django_ledger/chart_of_accounts/coa_update.html @@ -6,14 +6,14 @@
-
+ {% csrf_token %} {{ form.as_p }} Back
diff --git a/django_ledger/templates/django_ledger/chart_of_accounts/includes/coa_card.html b/django_ledger/templates/django_ledger/chart_of_accounts/includes/coa_card.html index 55c997c7..c1d6b356 100644 --- a/django_ledger/templates/django_ledger/chart_of_accounts/includes/coa_card.html +++ b/django_ledger/templates/django_ledger/chart_of_accounts/includes/coa_card.html @@ -4,7 +4,7 @@
- diff --git a/django_ledger/templates/django_ledger/includes/footer.html b/django_ledger/templates/django_ledger/includes/footer.html index 4f4f5735..abaf08f2 100644 --- a/django_ledger/templates/django_ledger/includes/footer.html +++ b/django_ledger/templates/django_ledger/includes/footer.html @@ -1,8 +1,8 @@