From e70f46d1175a41d0c9db0edb9dff728a16619b2b Mon Sep 17 00:00:00 2001 From: Marc Lebreuil Date: Tue, 3 Sep 2024 20:12:22 +0000 Subject: [PATCH 1/5] Add filtering by invoice name and contract number --- CHANGELOG.md | 4 ++++ pyproject.toml | 2 +- src/netbox_contract/__init__.py | 2 +- src/netbox_contract/filtersets.py | 8 ++++++-- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e66d62f..ac8d2e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ ## Version 2 +### Version 2.2.5 + +* [178](https://github.com/mlebreuil/netbox-contract/issues/178) Add the possibility to filter on invoice number, and contract name through the API. + ### Version 2.2.4 * [166](https://github.com/mlebreuil/netbox-contract/issues/166) Review the Contract view to include invoice template details and lines. diff --git a/pyproject.toml b/pyproject.toml index 70b1fba..107a6b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "netbox-contract" -version = "2.2.4" +version = "2.2.5" authors = [ { name="Marc Lebreuil", email="marc@famillelebreuil.net" }, ] diff --git a/src/netbox_contract/__init__.py b/src/netbox_contract/__init__.py index c26a161..d98e980 100644 --- a/src/netbox_contract/__init__.py +++ b/src/netbox_contract/__init__.py @@ -5,7 +5,7 @@ class ContractsConfig(PluginConfig): name = 'netbox_contract' verbose_name = 'Netbox contract' description = 'Contract management plugin for Netbox' - version = '2.2.4' + version = '2.2.5' author = 'Marc Lebreuil' author_email = 'marc@famillelebreuil.net' base_url = 'contracts' diff --git a/src/netbox_contract/filtersets.py b/src/netbox_contract/filtersets.py index df0481d..daf3426 100644 --- a/src/netbox_contract/filtersets.py +++ b/src/netbox_contract/filtersets.py @@ -1,3 +1,4 @@ +import django_filters from django.db.models import Q from netbox.filtersets import NetBoxModelFilterSet @@ -8,13 +9,16 @@ Invoice, InvoiceLine, ServiceProvider, + StatusChoices, ) class ContractFilterSet(NetBoxModelFilterSet): + status = django_filters.MultipleChoiceFilter(choices=StatusChoices, null_value=None) + class Meta: model = Contract - fields = ('id', 'internal_partie', 'status', 'parent') + fields = ('id', 'name', 'internal_partie', 'external_reference', 'parent') def search(self, queryset, name, value): return queryset.filter( @@ -28,7 +32,7 @@ def search(self, queryset, name, value): class InvoiceFilterSet(NetBoxModelFilterSet): class Meta: model = Invoice - fields = ('id', 'contracts') + fields = ('id', 'number', 'template', 'contracts') def search(self, queryset, name, value): return queryset.filter( From 8864dfd35244bb3d2564852bda045b29f3deca10 Mon Sep 17 00:00:00 2001 From: Marc Lebreuil Date: Sun, 8 Sep 2024 17:30:56 +0000 Subject: [PATCH 2/5] Improve filtering --- CHANGELOG.md | 1 + src/netbox_contract/api/views.py | 2 ++ src/netbox_contract/filtersets.py | 47 ++++++++++++++++++++++++++++--- src/netbox_contract/forms.py | 20 ++++++++++--- 4 files changed, 62 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac8d2e0..8184b0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Version 2.2.5 * [178](https://github.com/mlebreuil/netbox-contract/issues/178) Add the possibility to filter on invoice number, and contract name through the API. +* Generally improve filtering options ### Version 2.2.4 diff --git a/src/netbox_contract/api/views.py b/src/netbox_contract/api/views.py index 904270c..eb65d33 100644 --- a/src/netbox_contract/api/views.py +++ b/src/netbox_contract/api/views.py @@ -21,6 +21,7 @@ class ContractViewSet(NetBoxModelViewSet): ) ) serializer_class = ContractSerializer + filterset_class = filtersets.ContractFilterSet class InvoiceViewSet(NetBoxModelViewSet): @@ -44,6 +45,7 @@ class InvoiceLineViewSet(NetBoxModelViewSet): 'invoice', 'accounting_dimensions', 'tags' ) serializer_class = InvoiceLineSerializer + filterset_class = filtersets.InvoiceLineFilterSet class AccountingDimensionViewSet(NetBoxModelViewSet): diff --git a/src/netbox_contract/filtersets.py b/src/netbox_contract/filtersets.py index daf3426..493c602 100644 --- a/src/netbox_contract/filtersets.py +++ b/src/netbox_contract/filtersets.py @@ -1,11 +1,15 @@ import django_filters from django.db.models import Q from netbox.filtersets import NetBoxModelFilterSet +from tenancy.filtersets import TenancyFilterSet from .models import ( AccountingDimension, + AccountingDimensionStatusChoices, Contract, ContractAssignment, + CurrencyChoices, + InternalEntityChoices, Invoice, InvoiceLine, ServiceProvider, @@ -13,12 +17,26 @@ ) -class ContractFilterSet(NetBoxModelFilterSet): +class ContractFilterSet(NetBoxModelFilterSet, TenancyFilterSet): status = django_filters.MultipleChoiceFilter(choices=StatusChoices, null_value=None) + internal_partie = django_filters.MultipleChoiceFilter( + choices=InternalEntityChoices, null_value=None + ) + currency = django_filters.MultipleChoiceFilter( + choices=CurrencyChoices, null_value=None + ) class Meta: model = Contract - fields = ('id', 'name', 'internal_partie', 'external_reference', 'parent') + fields = ( + 'id', + 'name', + 'external_reference', + 'start_date', + 'end_date', + 'initial_term', + 'parent', + ) def search(self, queryset, name, value): return queryset.filter( @@ -30,9 +48,22 @@ def search(self, queryset, name, value): class InvoiceFilterSet(NetBoxModelFilterSet): + currency = django_filters.MultipleChoiceFilter( + choices=CurrencyChoices, null_value=None + ) + class Meta: model = Invoice - fields = ('id', 'number', 'template', 'contracts') + fields = ( + 'id', + 'number', + 'template', + 'date', + 'contracts', + 'period_start', + 'period_end', + 'amount', + ) def search(self, queryset, name, value): return queryset.filter( @@ -59,9 +90,13 @@ def search(self, queryset, name, value): class InvoiceLineFilterSet(NetBoxModelFilterSet): + currency = django_filters.MultipleChoiceFilter( + choices=CurrencyChoices, null_value=None + ) + class Meta: model = InvoiceLine - fields = ('id', 'invoice') + fields = ('id', 'invoice', 'accounting_dimensions') def search(self, queryset, name, value): return queryset.filter( @@ -70,6 +105,10 @@ def search(self, queryset, name, value): class AccountingDimensionFilterSet(NetBoxModelFilterSet): + status = django_filters.MultipleChoiceFilter( + choices=AccountingDimensionStatusChoices, null_value=None + ) + class Meta: model = AccountingDimension fields = ('name', 'value') diff --git a/src/netbox_contract/forms.py b/src/netbox_contract/forms.py index 27b71e7..c4185be 100644 --- a/src/netbox_contract/forms.py +++ b/src/netbox_contract/forms.py @@ -2,7 +2,6 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist, ValidationError -from extras.filters import TagFilter from netbox.forms import ( NetBoxModelBulkEditForm, NetBoxModelFilterSetForm, @@ -10,6 +9,7 @@ NetBoxModelImportForm, ) from tenancy.models import Tenant +from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, @@ -21,6 +21,7 @@ DynamicModelMultipleChoiceField, JSONField, SlugField, + TagFilterField, ) from utilities.forms.widgets import DatePicker, HTMXSelect @@ -161,9 +162,11 @@ class ContractFilterSetForm(NetBoxModelFilterSetForm): tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False) external_reference = forms.CharField(required=False) - internal_partie = forms.CharField(required=False) + internal_partie = forms.ChoiceField(choices=InternalEntityChoices, required=False) status = forms.ChoiceField(choices=StatusChoices, required=False) + currency = forms.ChoiceField(choices=CurrencyChoices, required=False) parent = DynamicModelChoiceField(queryset=Contract.objects.all(), required=False) + tag = TagFilterField(model) class ContractCSVForm(NetBoxModelImportForm): @@ -359,9 +362,15 @@ class Meta: class InvoiceFilterSetForm(NetBoxModelFilterSetForm): model = Invoice + number = forms.CharField(required=False) + template = forms.NullBooleanField( + required=False, widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES) + ) + currency = forms.ChoiceField(choices=CurrencyChoices, required=False) contracts = DynamicModelMultipleChoiceField( queryset=Contract.objects.all(), required=False ) + tag = TagFilterField(model) class InvoiceCSVForm(NetBoxModelImportForm): @@ -425,7 +434,8 @@ class Meta: class ServiceProviderFilterSetForm(NetBoxModelFilterSetForm): model = ServiceProvider - tag = TagFilter() + name = forms.CharField(required=False) + tag = TagFilterField(model) class ServiceProviderCSVForm(NetBoxModelImportForm): @@ -514,9 +524,11 @@ class Meta: class InvoiceLineFilterSetForm(NetBoxModelFilterSetForm): model = InvoiceLine invoice = DynamicModelChoiceField(queryset=Invoice.objects.all(), required=False) - accounting_dimensions = DynamicModelChoiceField( + accounting_dimensions = DynamicModelMultipleChoiceField( queryset=AccountingDimension.objects.all(), required=False ) + currency = forms.ChoiceField(choices=CurrencyChoices, required=False) + tag = TagFilterField(model) class InvoiceLineImportForm(NetBoxModelImportForm): From 4d76d1561c293bfc656cc230be4cb2e777728e43 Mon Sep 17 00:00:00 2001 From: Marc Lebreuil Date: Sun, 8 Sep 2024 19:41:57 +0000 Subject: [PATCH 3/5] Order accounting dimensions in tables --- CHANGELOG.md | 3 ++- src/netbox_contract/tables.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8184b0b..f2b3009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,9 @@ ### Version 2.2.5 -* [178](https://github.com/mlebreuil/netbox-contract/issues/178) Add the possibility to filter on invoice number, and contract name through the API. * Generally improve filtering options +* [178](https://github.com/mlebreuil/netbox-contract/issues/178) Add the possibility to filter on invoice number, and contract name through the API. +* [176](https://github.com/mlebreuil/netbox-contract/issues/176) Order accounting dimensions in tables alphabetically. ### Version 2.2.4 diff --git a/src/netbox_contract/tables.py b/src/netbox_contract/tables.py index acbfe06..5d0bede 100644 --- a/src/netbox_contract/tables.py +++ b/src/netbox_contract/tables.py @@ -200,7 +200,9 @@ class Meta(NetBoxTable.Meta): class InvoiceLineListTable(NetBoxTable): invoice = tables.Column(linkify=True) - accounting_dimensions = tables.ManyToManyColumn(linkify=True) + accounting_dimensions = tables.ManyToManyColumn( + linkify=True, filter=lambda qs: qs.order_by('name') + ) class Meta(NetBoxTable.Meta): model = InvoiceLine From 616fec6edfddd446f823a994cc70863ffb3ab41e Mon Sep 17 00:00:00 2001 From: Marc Lebreuil Date: Sun, 8 Sep 2024 20:15:07 +0000 Subject: [PATCH 4/5] Allow mandatory accounting dimensions --- CHANGELOG.md | 1 + README.md | 5 +++-- src/netbox_contract/__init__.py | 1 + src/netbox_contract/forms.py | 6 ++++++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2b3009..5ae7410 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * Generally improve filtering options * [178](https://github.com/mlebreuil/netbox-contract/issues/178) Add the possibility to filter on invoice number, and contract name through the API. * [176](https://github.com/mlebreuil/netbox-contract/issues/176) Order accounting dimensions in tables alphabetically. +* [171](https://github.com/mlebreuil/netbox-contract/issues/171) It is now poaaible to define madatory accounting dimension by specifying their names in the 'mandatory_dimensions' list in the plugin settings. (see the "Customize the plugin" paragraph in the README.md file) ### Version 2.2.4 diff --git a/README.md b/README.md index 7018f6e..011d8ab 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,11 @@ PLUGINS_CONFIG = { "project": "", "cost center": "" }, - 'mandatory_contract_fields': ['accounting_dimensions'], + 'mandatory_contract_fields': [], 'hidden_contract_fields': [], - 'mandatory_invoice_fields': ['accounting_dimensions'], + 'mandatory_invoice_fields': [], 'hidden_invoice_fields': [], + 'mandatory_dimensions': [], } } diff --git a/src/netbox_contract/__init__.py b/src/netbox_contract/__init__.py index d98e980..11568ca 100644 --- a/src/netbox_contract/__init__.py +++ b/src/netbox_contract/__init__.py @@ -22,6 +22,7 @@ class ContractsConfig(PluginConfig): 'hidden_contract_fields': [], 'mandatory_invoice_fields': [], 'hidden_invoice_fields': [], + 'mandatory_dimensions': [], } diff --git a/src/netbox_contract/forms.py b/src/netbox_contract/forms.py index c4185be..e7682b3 100644 --- a/src/netbox_contract/forms.py +++ b/src/netbox_contract/forms.py @@ -509,6 +509,12 @@ def clean(self): else: dimensions_names.append(dimension.name) + # Make sure mandatory dimensions are present + mandatory_dimensions = plugin_settings.get('mandatory_dimensions') + for dimension in mandatory_dimensions: + if dimension not in dimensions_names: + raise ValidationError(f'dimension {dimension} missing') + class Meta: model = InvoiceLine fields = [ From e1ce2cec7f0fb9a567421975046900f5bd78dbef Mon Sep 17 00:00:00 2001 From: Marc Lebreuil Date: Sun, 8 Sep 2024 20:56:19 +0000 Subject: [PATCH 5/5] More documentation --- docs/accounting_dimensions.md | 30 ++++++++++++++++++++++++++++++ docs/contract.md | 2 +- docs/invoice.md | 15 +++++++++++++++ docs/invoice_line.md | 10 ++++++++-- 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/docs/accounting_dimensions.md b/docs/accounting_dimensions.md index 09b5d7f..cd087da 100644 --- a/docs/accounting_dimensions.md +++ b/docs/accounting_dimensions.md @@ -1,5 +1,35 @@ # Accounting dimensions +> [!NOTE] +> account is considered a accounting dimensions as any other. + +It is possible through the plugin config attribure 'mandatory_dimensions' to set some mandatory dimensions. the attribute will take a list of dimension names. For instance: + +```python +# configuration.py +PLUGINS_CONFIG = { + 'netbox_contract': { + 'top_level_menu': True, + 'mandatory_contract_fields': [], + 'hidden_contract_fields': [], + 'mandatory_invoice_fields': [], + 'hidden_invoice_fields': [], + 'mandatory_dimensions': ['account','project'], + } +} + +``` + +Refer to the readme file for more information. + +> [!WARNING] +> Accounting dimensions used to be set with a simple json field. Although the field is still available, it is recommended to add dimensions through invoice lines. You will find in the script folder a file which can be imported as netbox custom scripts module which contains a script to perform the migration. You wil need to adjust the script to your needs. + ![Accounting dimensions](img/accrounting_dimensions.png "accounting dimensions") +- name: The name of the accounting dimensions (Account, Project, Entity ...) +- value: The value for this dimension. +- status: If the accounting dimension can still be used for new invoice lines. +- Comments: Self explanatory + diff --git a/docs/contract.md b/docs/contract.md index 803a0ce..ea0f228 100644 --- a/docs/contract.md +++ b/docs/contract.md @@ -5,7 +5,7 @@ ![Contract](img/contract.png "contract") - External partie type: either an Circuit provider or Contract Service provider. -- Accounting dimensions: Will be copied to each invoice. Also this is still working the use invoice templates with accounting dimensions should be prefered. +- Accounting dimensions: Will be copied to each invoice. Although this this field is still available the use invoice templates with accounting dimensions should be prefered. - Monthly / Yearly recuring costs: Only one of these two options can be used for each contract. The value will be used, along with the invoice frequency, to calculate each invoice amount. - Invoice frequency : The number of month that each invoice covers. - Parent: Contrats can be arranged in a parent / child hierarchie. diff --git a/docs/invoice.md b/docs/invoice.md index 21ca721..a63cbbe 100644 --- a/docs/invoice.md +++ b/docs/invoice.md @@ -1,7 +1,22 @@ # Invoice +New invoices shold be created from the corresponding contract. Most of their fields will be derived from the contract invoice template. An invoice template is an invoice object which "Template" field is set to true. There can be only one invoice per contract. +Each invoice will be linked to one or more invoice line. + ![Invoice](img/invoice.png "invoice") +- Number: The invoice number. Should correspond to your accounting sysstem invoice number. +- Template: Whether this isvoice is an invoice template for the corresponding contract(s). There can be only one invoice template per contract. The template will be used to define automatically the fields of a nre invoice, including the correpsonding accounting lines with their dimensions. +- Date: the date of the invoice +- Contracts: The contracts linked to the invoice. +- Period_start: The start of the contract periode covered by this invoice. +- Period_end: The end of the contract period covered by this invoice. +- currency: The currency of the invoice +- accounting_dimensions: The use of this field is deprecated. Invoice lines and the corresponding accounting dimensions should be used instead. +- Amount: The amount of the invoice +- Documents: A link to the corresponding document. This field is deprecated and the document plugin should be used instead. +- Comments: self explanatory. + Linked objects: ![Invoice linked objects](img/invoice_linked_objects.png "invoice linked objects") diff --git a/docs/invoice_line.md b/docs/invoice_line.md index fd4394c..c68715c 100644 --- a/docs/invoice_line.md +++ b/docs/invoice_line.md @@ -1,6 +1,12 @@ # Invoice line -![Invoice line](img/invoice_line.png "invoice line") - +An invoice line correspond to the accouning lines for the invoice. +You can define several accounting lines for an invoice but the sum of each line amount should match the invoice amount. this is enforced if you create the invoice line through the web ui. +![Invoice line](img/invoice_line.png "invoice line") +- Invoice: The corresponding invoice. +- Currency: The currency of the invoice +- Amount: the amount of the invoice. Whether you take into account VAT in this amount depends on the way your budget is contructed. +- Accounting dimensions: The accounting dimensions for the invoice. +- Comments: Self explanatory \ No newline at end of file