From 731ffb797cc3b9cf98ef5bb3f8c6f17f19c14ebf Mon Sep 17 00:00:00 2001 From: Marc Lebreuil Date: Thu, 1 Aug 2024 19:33:13 +0000 Subject: [PATCH 1/4] Fix - issue 142 --- docs/contract.md | 12 ++++++- pyproject.toml | 2 +- src/netbox_contract/__init__.py | 2 +- src/netbox_contract/api/views.py | 8 +++-- src/netbox_contract/forms.py | 16 +++++++++- .../0034_contract_yrc_alter_contract_mrc.py | 26 +++++++++++++++ src/netbox_contract/models.py | 13 +++++++- .../templates/netbox_contract/contract.html | 4 +++ .../templates/netbox_contract/invoice.html | 6 ++++ src/netbox_contract/views.py | 32 ++++++++++++++++--- 10 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 src/netbox_contract/migrations/0034_contract_yrc_alter_contract_mrc.py diff --git a/docs/contract.md b/docs/contract.md index c36f693..803a0ce 100644 --- a/docs/contract.md +++ b/docs/contract.md @@ -1,7 +1,17 @@ # Contract +## Contract details + ![Contract](img/contract.png "contract") -Linked objects: +- 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. +- 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. + +## Linked objects: ![Contract linked objects](img/contract_linked_objects.png "contract linked objects") + +- Assignments: the assignement of contract to objects is managed from each object's detail view. diff --git a/pyproject.toml b/pyproject.toml index 746b688..581ffd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "netbox-contract" -version = "2.2.0" +version = "2.2.1" authors = [ { name="Marc Lebreuil", email="marc@famillelebreuil.net" }, ] diff --git a/src/netbox_contract/__init__.py b/src/netbox_contract/__init__.py index 581a217..36e1d13 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.0' + version = '2.2.1' author = 'Marc Lebreuil' author_email = 'marc@famillelebreuil.net' base_url = 'contracts' diff --git a/src/netbox_contract/api/views.py b/src/netbox_contract/api/views.py index d4d2edb..2f4c770 100644 --- a/src/netbox_contract/api/views.py +++ b/src/netbox_contract/api/views.py @@ -1,4 +1,5 @@ -from django.db.models import F +from django.db.models import Case, F, When +from django.db.models.functions import Round from netbox.api.viewsets import NetBoxModelViewSet from .. import filtersets, models @@ -14,7 +15,10 @@ class ContractViewSet(NetBoxModelViewSet): queryset = models.Contract.objects.prefetch_related('parent', 'tags').annotate( - yrc=F('mrc') * 12 + calculated_rc=Round( + Case(When(yrc__gt=0, then=F('yrc') / 12), default=F('mrc') * 12), + precision=2, + ) ) serializer_class = ContractSerializer diff --git a/src/netbox_contract/forms.py b/src/netbox_contract/forms.py index 64fe1df..e737756 100644 --- a/src/netbox_contract/forms.py +++ b/src/netbox_contract/forms.py @@ -129,6 +129,7 @@ class Meta: 'renewal_term', 'currency', 'accounting_dimensions', + 'yrc', 'mrc', 'nrc', 'invoice_frequency', @@ -143,6 +144,14 @@ class Meta: 'end_date': DatePicker(), } + def clean(self): + super().clean() + + if self.cleaned_data['mrc'] and self.cleaned_data['mrc']: + raise ValidationError( + 'you should set monthly OR yearly recuring costs not both' + ) + class ContractFilterSetForm(NetBoxModelFilterSetForm): model = Contract @@ -193,6 +202,7 @@ class Meta: 'renewal_term', 'currency', 'accounting_dimensions', + 'yrc', 'mrc', 'nrc', 'invoice_frequency', @@ -261,7 +271,11 @@ def clean(self): ) # Prefix the invoice name with _template - self.cleaned_data['number'] = '_template_' + self.cleaned_data['number'] + self.cleaned_data['number'] = '_invoice_template_' + contract.name + + # set the periode start and end date to null + self.cleaned_data['period_start'] = None + self.cleaned_data['period_end'] = None def save(self, *args, **kwargs): is_new = not bool(self.instance.pk) diff --git a/src/netbox_contract/migrations/0034_contract_yrc_alter_contract_mrc.py b/src/netbox_contract/migrations/0034_contract_yrc_alter_contract_mrc.py new file mode 100644 index 0000000..ead6efd --- /dev/null +++ b/src/netbox_contract/migrations/0034_contract_yrc_alter_contract_mrc.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.6 on 2024-08-01 15:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('netbox_contract', '0033_remove_contract_invoice_template'), + ] + + operations = [ + migrations.AddField( + model_name='contract', + name='yrc', + field=models.DecimalField( + blank=True, decimal_places=2, max_digits=10, null=True + ), + ), + migrations.AlterField( + model_name='contract', + name='mrc', + field=models.DecimalField( + blank=True, decimal_places=2, max_digits=10, null=True + ), + ), + ] diff --git a/src/netbox_contract/models.py b/src/netbox_contract/models.py index 9e58f5a..a79e913 100644 --- a/src/netbox_contract/models.py +++ b/src/netbox_contract/models.py @@ -130,8 +130,19 @@ class Contract(NetBoxModel): max_length=3, choices=CurrencyChoices, default=CurrencyChoices.CURRENCY_USD ) accounting_dimensions = models.JSONField(null=True, blank=True) + yrc = models.DecimalField( + verbose_name='yearly recuring cost', + max_digits=10, + decimal_places=2, + blank=True, + null=True, + ) mrc = models.DecimalField( - verbose_name='Monthly recuring cost', max_digits=10, decimal_places=2 + verbose_name='Monthly recuring cost', + max_digits=10, + decimal_places=2, + blank=True, + null=True, ) nrc = models.DecimalField( verbose_name='None recuring cost', default=0, max_digits=10, decimal_places=2 diff --git a/src/netbox_contract/templates/netbox_contract/contract.html b/src/netbox_contract/templates/netbox_contract/contract.html index 1bc4909..05a7adc 100644 --- a/src/netbox_contract/templates/netbox_contract/contract.html +++ b/src/netbox_contract/templates/netbox_contract/contract.html @@ -85,6 +85,10 @@
Contract
Yearly recuring costs {{ object.yrc }} + + calculated corresponding Yearly or Monthly value + {{ object.calculated_rc }} + Non recuring costs {{ object.nrc }} diff --git a/src/netbox_contract/templates/netbox_contract/invoice.html b/src/netbox_contract/templates/netbox_contract/invoice.html index 72eed86..c7dcdf0 100644 --- a/src/netbox_contract/templates/netbox_contract/invoice.html +++ b/src/netbox_contract/templates/netbox_contract/invoice.html @@ -19,10 +19,15 @@
Invoices
Number {{ object.number }} + + Template + {{ object.template }} + Date {{ object.date }} + {% if not object.template %} Period start {{ object.period_start }} @@ -31,6 +36,7 @@
Invoices
Period end {{ object.period_end }} + {% endif %} Currency {{ object.currency }} diff --git a/src/netbox_contract/views.py b/src/netbox_contract/views.py index 72b6acf..caef4f8 100644 --- a/src/netbox_contract/views.py +++ b/src/netbox_contract/views.py @@ -4,7 +4,8 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist -from django.db.models import F +from django.db.models import Case, F, When +from django.db.models.functions import Round from django.shortcuts import get_object_or_404, render from netbox.views import generic from netbox.views.generic.utils import get_prerequisite_model @@ -129,7 +130,12 @@ class ContractAssignmentBulkImportView(generic.BulkImportView): @register_model_view(Contract) class ContractView(generic.ObjectView): - queryset = Contract.objects.annotate(yrc=F('mrc') * 12) + queryset = Contract.objects.annotate( + calculated_rc=Round( + Case(When(yrc__gt=0, then=F('yrc') / 12), default=F('mrc') * 12), + precision=2, + ) + ) def get_extra_context(self, request, instance): invoices_table = tables.InvoiceListTable(instance.invoices.all()) @@ -152,7 +158,12 @@ def get_extra_context(self, request, instance): class ContractListView(generic.ObjectListView): - queryset = Contract.objects.annotate(yrc=F('mrc') * 12) + queryset = Contract.objects.annotate( + calculated_rc=Round( + Case(When(yrc__gt=0, then=F('yrc') / 12), default=F('mrc') * 12), + precision=2, + ) + ) table = tables.ContractListTable filterset = filtersets.ContractFilterSet filterset_form = forms.ContractFilterSetForm @@ -267,7 +278,9 @@ def get(self, request, *args, **kwargs): contract = Contract.objects.get(pk=initial_data['contracts']) try: - last_invoice = contract.invoices.latest('period_end') + last_invoice = contract.invoices.filter(template=False).latest( + 'period_end' + ) new_period_start = last_invoice.period_end + timedelta(days=1) except ObjectDoesNotExist: if contract.start_date: @@ -281,7 +294,16 @@ def get(self, request, *args, **kwargs): new_period_end = new_period_start + delta - timedelta(days=1) initial_data['period_end'] = new_period_end - initial_data['amount'] = contract.mrc * contract.invoice_frequency + if contract.yrc: + if contract.invoice_frequency == 12: + initial_data['amount'] = contract.yrc + else: + initial_data['amount'] = round( + contract.yrc / 12 * contract.invoice_frequency, 2 + ) + else: + initial_data['amount'] = contract.mrc * contract.invoice_frequency + initial_data['currency'] = contract.currency if contract.accounting_dimensions: initial_data['accounting_dimensions'] = contract.accounting_dimensions From 31c13d500686263f7209f87719f16a10e7f6d8fa Mon Sep 17 00:00:00 2001 From: Marc Lebreuil Date: Thu, 1 Aug 2024 19:37:56 +0000 Subject: [PATCH 2/4] update CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c73efcb..ab02a89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ ## Version 2 +### Version 2.2.1 + +* [142](https://github.com/mlebreuil/netbox-contract/issues/142) Gives the option to enter contract yearly recuring costs instead of only monthly recuring costs. +Corresponding value is used to calculate the invoices amount without rounding approximations. + ### Version 2.2.0 * [140](https://github.com/mlebreuil/netbox-contract/issues/140) Add the "Invoice line" and "Accounting dimension" models. In order to simplify invoices creation, it is possible to selsct one invoice as the template for each contract; Its accounting lines will automatically be copied to the new invoices for the contract. The amount of the first line will be updated so that the sum of the amount for each invoice line match the invoice amount. From 5806aeda9fc7e2a48d27d7064815483adcdb60ea Mon Sep 17 00:00:00 2001 From: Marc Lebreuil Date: Mon, 12 Aug 2024 19:55:50 +0000 Subject: [PATCH 3/4] Close issue 148 Changes to be committed: modified: src/netbox_contract/tables.py modified: src/netbox_contract/templates/contract_assignments_bottom.html deleted: src/netbox_contract/templates/contract_assignments_bottom_old.html modified: src/netbox_contract/templates/netbox_contract/contract.html --- src/netbox_contract/tables.py | 5 ++-- .../contract_assignments_bottom.html | 20 ++++++++-------- .../contract_assignments_bottom_old.html | 23 ------------------- .../templates/netbox_contract/contract.html | 16 +++++++------ 4 files changed, 23 insertions(+), 41 deletions(-) delete mode 100644 src/netbox_contract/templates/contract_assignments_bottom_old.html diff --git a/src/netbox_contract/tables.py b/src/netbox_contract/tables.py index 95ea92b..acbfe06 100644 --- a/src/netbox_contract/tables.py +++ b/src/netbox_contract/tables.py @@ -42,14 +42,15 @@ class Meta(NetBoxTable.Meta): class ContractAssignmentObjectTable(NetBoxTable): contract = tables.Column(linkify=True) actions = columns.ActionsColumn(actions=('edit', 'delete')) - contract__external_partie_object = tables.Column(linkify=True) + contract__external_partie_object = tables.Column( + verbose_name='Partner', linkify=True + ) class Meta(NetBoxTable.Meta): model = ContractAssignment fields = ( 'pk', 'contract', - 'contract__external_partie_object_type', 'contract__external_partie_object', 'contract__status', 'contract__start_date', diff --git a/src/netbox_contract/templates/contract_assignments_bottom.html b/src/netbox_contract/templates/contract_assignments_bottom.html index a03fcc6..b919c19 100644 --- a/src/netbox_contract/templates/contract_assignments_bottom.html +++ b/src/netbox_contract/templates/contract_assignments_bottom.html @@ -3,18 +3,20 @@
-
Contracts Assignments
+
+ Contracts Assignments + {% if perms.netbox_contract.add_contractassignment %} + + {% endif %} +
{% if assignments_table %} {% render_table assignments_table %} {% endif %} - {% if perms.netbox_contract.add_contractassignment %} - - {% endif %}
diff --git a/src/netbox_contract/templates/contract_assignments_bottom_old.html b/src/netbox_contract/templates/contract_assignments_bottom_old.html deleted file mode 100644 index e103047..0000000 --- a/src/netbox_contract/templates/contract_assignments_bottom_old.html +++ /dev/null @@ -1,23 +0,0 @@ -{% load render_table from django_tables2 %} -{% if perms.netbox_contract.view_contract %} -
-
-
-
Contracts Assignments
- {% if assignments_table %} -
- {% render_table assignments_table %} -
- {% endif %} - {% if perms.netbox_contract.add_contractassignment %} - - {% endif %} -
-
-
-{% endif %} \ No newline at end of file diff --git a/src/netbox_contract/templates/netbox_contract/contract.html b/src/netbox_contract/templates/netbox_contract/contract.html index 05a7adc..a53c2b3 100644 --- a/src/netbox_contract/templates/netbox_contract/contract.html +++ b/src/netbox_contract/templates/netbox_contract/contract.html @@ -142,16 +142,18 @@
childs
-
Invoices
- {% render_table invoices_table %} - {% if perms.netbox_contract.add_invoice %} - + {% endif %} + + {% render_table invoices_table %}
From e230c5f6d9164649043960cbb68c19907150ab00 Mon Sep 17 00:00:00 2001 From: Marc Lebreuil Date: Mon, 12 Aug 2024 19:59:02 +0000 Subject: [PATCH 4/4] update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab02a89..50deb18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * [142](https://github.com/mlebreuil/netbox-contract/issues/142) Gives the option to enter contract yearly recuring costs instead of only monthly recuring costs. Corresponding value is used to calculate the invoices amount without rounding approximations. +* [148](https://github.com/mlebreuil/netbox-contract/issues/148) Update tables format to match the new Netbox UI design. ### Version 2.2.0