netbox-contract 2.1.2__tar.gz → 2.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {netbox_contract-2.1.2/src/netbox_contract.egg-info → netbox_contract-2.2.1}/PKG-INFO +2 -1
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/pyproject.toml +2 -1
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/__init__.py +1 -1
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/api/serializers.py +75 -1
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/api/urls.py +2 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/api/views.py +18 -2
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/filtersets.py +32 -4
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/forms.py +238 -50
- netbox_contract-2.2.1/src/netbox_contract/migrations/0027_invoiceline.py +57 -0
- netbox_contract-2.2.1/src/netbox_contract/migrations/0028_invoiceline_accounting_dimensions.py +17 -0
- netbox_contract-2.2.1/src/netbox_contract/migrations/0029_remove_invoiceline_accounting_dimensions_and_more.py +57 -0
- netbox_contract-2.2.1/src/netbox_contract/migrations/0030_alter_invoiceline_accounting_dimensions.py +21 -0
- netbox_contract-2.2.1/src/netbox_contract/migrations/0031_contract_invoice_template_invoice_template.py +29 -0
- netbox_contract-2.2.1/src/netbox_contract/migrations/0032_alter_invoice_period_end_alter_invoice_period_start_and_more.py +27 -0
- netbox_contract-2.2.1/src/netbox_contract/migrations/0033_remove_contract_invoice_template.py +19 -0
- netbox_contract-2.2.1/src/netbox_contract/migrations/0034_contract_yrc_alter_contract_mrc.py +26 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/models.py +76 -3
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/navigation.py +34 -0
- netbox_contract-2.2.1/src/netbox_contract/search.py +52 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/tables.py +51 -3
- netbox_contract-2.2.1/src/netbox_contract/templates/contract_assignments_bottom.html +23 -0
- netbox_contract-2.2.1/src/netbox_contract/templates/netbox_contract/accountingdimension.html +27 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/templates/netbox_contract/contract.html +13 -7
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/templates/netbox_contract/invoice.html +33 -8
- netbox_contract-2.2.1/src/netbox_contract/templates/netbox_contract/invoiceline.html +42 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/urls.py +74 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/views.py +157 -6
- {netbox_contract-2.1.2 → netbox_contract-2.2.1/src/netbox_contract.egg-info}/PKG-INFO +2 -1
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract.egg-info/SOURCES.txt +10 -1
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract.egg-info/requires.txt +1 -0
- netbox_contract-2.1.2/src/netbox_contract/search.py +0 -29
- netbox_contract-2.1.2/src/netbox_contract/templates/contract_assignments_bottom.html +0 -21
- netbox_contract-2.1.2/src/netbox_contract/templates/contract_assignments_bottom_old.html +0 -23
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/LICENSE +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/MANIFEST.in +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/README.md +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/setup.cfg +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/setup.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/api/__init__.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/constants.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0001_initial.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0002_alter_contract_circuit_and_more.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0003_alter_contract_custom_field_data_and_more.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0004_contract_currency_invoice_currency.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0005_contract_accounting_dimensions_and_more.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0006_alter_contract_circuit.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0007_invoice_date.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0008_invoice_comments.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0009_contract_external_reference.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0010_invoice_contracts.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0011_auto_20230122_2112.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0012_remove_invoice_contract.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0013_contract_documents_invoice_documents.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0014_contract_end_date.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0015_contractassignement.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0016_contract_parent.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0017_alter_contract_accounting_dimensions.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0018_contract_external_partie_object_id_and_more.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0019_auto_20230924_1813.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0020_alter_contract_external_partie.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0021_alter_contract_external_partie.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0022_alter_contract_internal_partie_alter_contract_parent.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0023_rename_contractassignement_contractassignment_and_more.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0024_remove_contract_external_partie.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0025_remove_contract_circuit_and_more.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0026_auto_20240421_1550.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/__init__.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/template_content.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/templates/contract_list_bottom.html +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/templates/netbox_contract/contractassignment.html +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/templates/netbox_contract/serviceprovider.html +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract.egg-info/dependency_links.txt +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract.egg-info/not-zip-safe +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: netbox-contract
|
|
3
|
-
Version: 2.1
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Contract management plugin for Netbox
|
|
5
5
|
Author-email: Marc Lebreuil <marc@famillelebreuil.net>
|
|
6
6
|
Project-URL: Homepage, https://github.com/mlebreuil/netbox-contract
|
|
@@ -12,6 +12,7 @@ Requires-Python: >=3.10
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: python-dateutil
|
|
15
|
+
Requires-Dist: drf_yasg
|
|
15
16
|
|
|
16
17
|
# Contract pluggin
|
|
17
18
|
## Overview
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "netbox-contract"
|
|
3
|
-
version = "2.1
|
|
3
|
+
version = "2.2.1"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name="Marc Lebreuil", email="marc@famillelebreuil.net" },
|
|
6
6
|
]
|
|
@@ -9,6 +9,7 @@ readme = "README.md"
|
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
dependencies = [
|
|
11
11
|
'python-dateutil',
|
|
12
|
+
'drf_yasg',
|
|
12
13
|
]
|
|
13
14
|
classifiers = [
|
|
14
15
|
"Programming Language :: Python :: 3",
|
|
@@ -5,7 +5,7 @@ class ContractsConfig(PluginConfig):
|
|
|
5
5
|
name = 'netbox_contract'
|
|
6
6
|
verbose_name = 'Netbox contract'
|
|
7
7
|
description = 'Contract management plugin for Netbox'
|
|
8
|
-
version = '2.1
|
|
8
|
+
version = '2.2.1'
|
|
9
9
|
author = 'Marc Lebreuil'
|
|
10
10
|
author_email = 'marc@famillelebreuil.net'
|
|
11
11
|
base_url = 'contracts'
|
|
@@ -7,7 +7,14 @@ from rest_framework import serializers
|
|
|
7
7
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
|
8
8
|
from utilities.api import get_serializer_for_model
|
|
9
9
|
|
|
10
|
-
from ..models import
|
|
10
|
+
from ..models import (
|
|
11
|
+
AccountingDimension,
|
|
12
|
+
Contract,
|
|
13
|
+
ContractAssignment,
|
|
14
|
+
Invoice,
|
|
15
|
+
InvoiceLine,
|
|
16
|
+
ServiceProvider,
|
|
17
|
+
)
|
|
11
18
|
|
|
12
19
|
|
|
13
20
|
class NestedServiceProviderSerializer(WritableNestedSerializer):
|
|
@@ -52,6 +59,28 @@ class NestedContractAssignmentSerializer(WritableNestedSerializer):
|
|
|
52
59
|
brief_fields = ('id', 'url', 'display', 'contract', 'content_object')
|
|
53
60
|
|
|
54
61
|
|
|
62
|
+
class NestedInvoicelineSerializer(WritableNestedSerializer):
|
|
63
|
+
url = serializers.HyperlinkedIdentityField(
|
|
64
|
+
view_name='plugins-api:netbox_contract-api:InvoiceLine-detail'
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
class Meta:
|
|
68
|
+
model = InvoiceLine
|
|
69
|
+
fields = ('id', 'url', 'display', 'invoice', 'amount')
|
|
70
|
+
brief_fields = ('id', 'url', 'display', 'invoice', 'amount')
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class NestedAccountingDimensionSerializer(WritableNestedSerializer):
|
|
74
|
+
url = serializers.HyperlinkedIdentityField(
|
|
75
|
+
view_name='plugins-api:netbox_contract-api:AccountingDimension-detail'
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
class Meta:
|
|
79
|
+
model = AccountingDimension
|
|
80
|
+
fields = ('id', 'url', 'display', 'name', 'value')
|
|
81
|
+
brief_fields = ('id', 'url', 'display', 'name', 'value')
|
|
82
|
+
|
|
83
|
+
|
|
55
84
|
class ContractSerializer(NetBoxModelSerializer):
|
|
56
85
|
url = serializers.HyperlinkedIdentityField(
|
|
57
86
|
view_name='plugins-api:netbox_contract-api:contract-detail'
|
|
@@ -192,3 +221,48 @@ class ContractAssignmentSerializer(NetBoxModelSerializer):
|
|
|
192
221
|
)
|
|
193
222
|
context = {'request': self.context['request']}
|
|
194
223
|
return serializer(instance.content_object, context=context).data
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class InvoiceLineSerializer(NetBoxModelSerializer):
|
|
227
|
+
url = serializers.HyperlinkedIdentityField(
|
|
228
|
+
view_name='plugins-api:netbox_contract-api:invoiceline-detail'
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
class Meta:
|
|
232
|
+
model = InvoiceLine
|
|
233
|
+
fields = (
|
|
234
|
+
'id',
|
|
235
|
+
'url',
|
|
236
|
+
'display',
|
|
237
|
+
'invoice',
|
|
238
|
+
'amount',
|
|
239
|
+
'currency',
|
|
240
|
+
'comments',
|
|
241
|
+
'tags',
|
|
242
|
+
'custom_fields',
|
|
243
|
+
'created',
|
|
244
|
+
'last_updated',
|
|
245
|
+
)
|
|
246
|
+
brief_fields = ('invoice', 'amount', 'url', 'display', 'name')
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class AccountingDimensionSerializer(NetBoxModelSerializer):
|
|
250
|
+
url = serializers.HyperlinkedIdentityField(
|
|
251
|
+
view_name='plugins-api:netbox_contract-api:accountingdimension-detail'
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
class Meta:
|
|
255
|
+
model = AccountingDimension
|
|
256
|
+
fields = (
|
|
257
|
+
'id',
|
|
258
|
+
'url',
|
|
259
|
+
'display',
|
|
260
|
+
'name',
|
|
261
|
+
'value',
|
|
262
|
+
'comments',
|
|
263
|
+
'tags',
|
|
264
|
+
'custom_fields',
|
|
265
|
+
'created',
|
|
266
|
+
'last_updated',
|
|
267
|
+
)
|
|
268
|
+
brief_fields = ('name', 'value', 'url', 'display')
|
|
@@ -9,5 +9,7 @@ router.register('contracts', views.ContractViewSet)
|
|
|
9
9
|
router.register('invoices', views.InvoiceViewSet)
|
|
10
10
|
router.register('serviceproviders', views.ServiceProviderViewSet)
|
|
11
11
|
router.register('contractassignment', views.ContractAssignmentViewSet)
|
|
12
|
+
router.register('invoiceline', views.InvoiceLineViewSet)
|
|
13
|
+
router.register('accountingdimension', views.AccountingDimensionViewSet)
|
|
12
14
|
|
|
13
15
|
urlpatterns = router.urls
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
from django.db.models import F
|
|
1
|
+
from django.db.models import Case, F, When
|
|
2
|
+
from django.db.models.functions import Round
|
|
2
3
|
from netbox.api.viewsets import NetBoxModelViewSet
|
|
3
4
|
|
|
4
5
|
from .. import filtersets, models
|
|
5
6
|
from .serializers import (
|
|
7
|
+
AccountingDimensionSerializer,
|
|
6
8
|
ContractAssignmentSerializer,
|
|
7
9
|
ContractSerializer,
|
|
10
|
+
InvoiceLineSerializer,
|
|
8
11
|
InvoiceSerializer,
|
|
9
12
|
ServiceProviderSerializer,
|
|
10
13
|
)
|
|
@@ -12,7 +15,10 @@ from .serializers import (
|
|
|
12
15
|
|
|
13
16
|
class ContractViewSet(NetBoxModelViewSet):
|
|
14
17
|
queryset = models.Contract.objects.prefetch_related('parent', 'tags').annotate(
|
|
15
|
-
|
|
18
|
+
calculated_rc=Round(
|
|
19
|
+
Case(When(yrc__gt=0, then=F('yrc') / 12), default=F('mrc') * 12),
|
|
20
|
+
precision=2,
|
|
21
|
+
)
|
|
16
22
|
)
|
|
17
23
|
serializer_class = ContractSerializer
|
|
18
24
|
|
|
@@ -31,3 +37,13 @@ class ServiceProviderViewSet(NetBoxModelViewSet):
|
|
|
31
37
|
class ContractAssignmentViewSet(NetBoxModelViewSet):
|
|
32
38
|
queryset = models.ContractAssignment.objects.prefetch_related('contract', 'tags')
|
|
33
39
|
serializer_class = ContractAssignmentSerializer
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class InvoiceLineViewSet(NetBoxModelViewSet):
|
|
43
|
+
queryset = models.InvoiceLine.objects.prefetch_related('invoice', 'tags')
|
|
44
|
+
serializer_class = InvoiceLineSerializer
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AccountingDimensionViewSet(NetBoxModelViewSet):
|
|
48
|
+
queryset = models.AccountingDimension.objects.prefetch_related('tags')
|
|
49
|
+
serializer_class = AccountingDimensionSerializer
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
from django.db.models import Q
|
|
2
2
|
from netbox.filtersets import NetBoxModelFilterSet
|
|
3
3
|
|
|
4
|
-
from .models import
|
|
4
|
+
from .models import (
|
|
5
|
+
AccountingDimension,
|
|
6
|
+
Contract,
|
|
7
|
+
ContractAssignment,
|
|
8
|
+
Invoice,
|
|
9
|
+
InvoiceLine,
|
|
10
|
+
ServiceProvider,
|
|
11
|
+
)
|
|
5
12
|
|
|
6
13
|
|
|
7
14
|
class ContractFilterSet(NetBoxModelFilterSet):
|
|
@@ -14,7 +21,7 @@ class ContractFilterSet(NetBoxModelFilterSet):
|
|
|
14
21
|
Q(name__icontains=value)
|
|
15
22
|
| Q(external_reference__icontains=value)
|
|
16
23
|
| Q(comments__icontains=value),
|
|
17
|
-
Q(status__iexact='Active')
|
|
24
|
+
Q(status__iexact='Active'),
|
|
18
25
|
)
|
|
19
26
|
|
|
20
27
|
|
|
@@ -25,8 +32,7 @@ class InvoiceFilterSet(NetBoxModelFilterSet):
|
|
|
25
32
|
|
|
26
33
|
def search(self, queryset, name, value):
|
|
27
34
|
return queryset.filter(
|
|
28
|
-
Q(number__icontains=value)
|
|
29
|
-
| Q(contracts__name__icontains=value)
|
|
35
|
+
Q(number__icontains=value) | Q(contracts__name__icontains=value)
|
|
30
36
|
)
|
|
31
37
|
|
|
32
38
|
|
|
@@ -46,3 +52,25 @@ class ContractAssignmentFilterSet(NetBoxModelFilterSet):
|
|
|
46
52
|
|
|
47
53
|
def search(self, queryset, name, value):
|
|
48
54
|
return queryset.filter(Q(contract__name__icontains=value))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class InvoiceLineFilterSet(NetBoxModelFilterSet):
|
|
58
|
+
class Meta:
|
|
59
|
+
model = InvoiceLine
|
|
60
|
+
fields = ('id', 'invoice')
|
|
61
|
+
|
|
62
|
+
def search(self, queryset, name, value):
|
|
63
|
+
return queryset.filter(
|
|
64
|
+
Q(comments__icontains=value) | Q(invoice__name__icontains=value)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AccountingDimensionFilterSet(NetBoxModelFilterSet):
|
|
69
|
+
class Meta:
|
|
70
|
+
model = AccountingDimension
|
|
71
|
+
fields = ('name', 'value')
|
|
72
|
+
|
|
73
|
+
def search(self, queryset, name, value):
|
|
74
|
+
return queryset.filter(
|
|
75
|
+
Q(comments__icontains=value) | Q(invoice__name__icontains=value)
|
|
76
|
+
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from django import forms
|
|
2
2
|
from django.conf import settings
|
|
3
3
|
from django.contrib.contenttypes.models import ContentType
|
|
4
|
+
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
4
5
|
from extras.filters import TagFilter
|
|
5
6
|
from netbox.forms import (
|
|
6
7
|
NetBoxModelBulkEditForm,
|
|
@@ -24,10 +25,12 @@ from utilities.forms.widgets import DatePicker, HTMXSelect
|
|
|
24
25
|
|
|
25
26
|
from .constants import SERVICE_PROVIDER_MODELS
|
|
26
27
|
from .models import (
|
|
28
|
+
AccountingDimension,
|
|
27
29
|
Contract,
|
|
28
30
|
ContractAssignment,
|
|
29
31
|
InternalEntityChoices,
|
|
30
32
|
Invoice,
|
|
33
|
+
InvoiceLine,
|
|
31
34
|
ServiceProvider,
|
|
32
35
|
StatusChoices,
|
|
33
36
|
)
|
|
@@ -46,6 +49,9 @@ class Dimensions(JSONField):
|
|
|
46
49
|
self.widget.attrs['placeholder'] = str(default_dimensions)
|
|
47
50
|
|
|
48
51
|
|
|
52
|
+
# Contract
|
|
53
|
+
|
|
54
|
+
|
|
49
55
|
class ContractForm(NetBoxModelForm):
|
|
50
56
|
comments = CommentField()
|
|
51
57
|
|
|
@@ -98,11 +104,6 @@ class ContractForm(NetBoxModelForm):
|
|
|
98
104
|
].queryset = ServiceProvider.objects.all()
|
|
99
105
|
self.fields['external_partie_object'].initial = None
|
|
100
106
|
|
|
101
|
-
# initialize accounting dimentsions widget
|
|
102
|
-
# self.fields[
|
|
103
|
-
# 'accounting_dimensions'
|
|
104
|
-
# ].widget.attrs['placeholder'] = '{"key": "value"}'
|
|
105
|
-
|
|
106
107
|
# Initialise fields settings
|
|
107
108
|
mandatory_fields = plugin_settings.get('mandatory_contract_fields')
|
|
108
109
|
for field in mandatory_fields:
|
|
@@ -128,6 +129,7 @@ class ContractForm(NetBoxModelForm):
|
|
|
128
129
|
'renewal_term',
|
|
129
130
|
'currency',
|
|
130
131
|
'accounting_dimensions',
|
|
132
|
+
'yrc',
|
|
131
133
|
'mrc',
|
|
132
134
|
'nrc',
|
|
133
135
|
'invoice_frequency',
|
|
@@ -142,45 +144,13 @@ class ContractForm(NetBoxModelForm):
|
|
|
142
144
|
'end_date': DatePicker(),
|
|
143
145
|
}
|
|
144
146
|
|
|
147
|
+
def clean(self):
|
|
148
|
+
super().clean()
|
|
145
149
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
accounting_dimensions = Dimensions(required=False)
|
|
151
|
-
|
|
152
|
-
def __init__(self, *args, **kwargs):
|
|
153
|
-
super().__init__(*args, **kwargs)
|
|
154
|
-
|
|
155
|
-
# Initialise fields settings
|
|
156
|
-
mandatory_fields = plugin_settings.get('mandatory_invoice_fields')
|
|
157
|
-
for field in mandatory_fields:
|
|
158
|
-
self.fields[field].required = True
|
|
159
|
-
hidden_fields = plugin_settings.get('hidden_invoice_fields')
|
|
160
|
-
for field in hidden_fields:
|
|
161
|
-
if not self.fields[field].required:
|
|
162
|
-
self.fields[field].widget = forms.HiddenInput()
|
|
163
|
-
|
|
164
|
-
class Meta:
|
|
165
|
-
model = Invoice
|
|
166
|
-
fields = (
|
|
167
|
-
'number',
|
|
168
|
-
'date',
|
|
169
|
-
'contracts',
|
|
170
|
-
'period_start',
|
|
171
|
-
'period_end',
|
|
172
|
-
'currency',
|
|
173
|
-
'accounting_dimensions',
|
|
174
|
-
'amount',
|
|
175
|
-
'documents',
|
|
176
|
-
'comments',
|
|
177
|
-
'tags',
|
|
178
|
-
)
|
|
179
|
-
widgets = {
|
|
180
|
-
'date': DatePicker(),
|
|
181
|
-
'period_start': DatePicker(),
|
|
182
|
-
'period_end': DatePicker(),
|
|
183
|
-
}
|
|
150
|
+
if self.cleaned_data['mrc'] and self.cleaned_data['mrc']:
|
|
151
|
+
raise ValidationError(
|
|
152
|
+
'you should set monthly OR yearly recuring costs not both'
|
|
153
|
+
)
|
|
184
154
|
|
|
185
155
|
|
|
186
156
|
class ContractFilterSetForm(NetBoxModelFilterSetForm):
|
|
@@ -193,13 +163,6 @@ class ContractFilterSetForm(NetBoxModelFilterSetForm):
|
|
|
193
163
|
parent = DynamicModelChoiceField(queryset=Contract.objects.all(), required=False)
|
|
194
164
|
|
|
195
165
|
|
|
196
|
-
class InvoiceFilterSetForm(NetBoxModelFilterSetForm):
|
|
197
|
-
model = Invoice
|
|
198
|
-
contracts = DynamicModelMultipleChoiceField(
|
|
199
|
-
queryset=Contract.objects.all(), required=False
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
|
|
203
166
|
class ContractCSVForm(NetBoxModelImportForm):
|
|
204
167
|
external_partie_object_type = CSVContentTypeField(
|
|
205
168
|
queryset=ContentType.objects.all(),
|
|
@@ -239,6 +202,7 @@ class ContractCSVForm(NetBoxModelImportForm):
|
|
|
239
202
|
'renewal_term',
|
|
240
203
|
'currency',
|
|
241
204
|
'accounting_dimensions',
|
|
205
|
+
'yrc',
|
|
242
206
|
'mrc',
|
|
243
207
|
'nrc',
|
|
244
208
|
'invoice_frequency',
|
|
@@ -271,6 +235,124 @@ class ContractBulkEditForm(NetBoxModelBulkEditForm):
|
|
|
271
235
|
model = Contract
|
|
272
236
|
|
|
273
237
|
|
|
238
|
+
# Invoice
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class InvoiceForm(NetBoxModelForm):
|
|
242
|
+
contracts = DynamicModelMultipleChoiceField(
|
|
243
|
+
queryset=Contract.objects.all(), required=False
|
|
244
|
+
)
|
|
245
|
+
accounting_dimensions = Dimensions(required=False)
|
|
246
|
+
|
|
247
|
+
def __init__(self, *args, **kwargs):
|
|
248
|
+
super().__init__(*args, **kwargs)
|
|
249
|
+
|
|
250
|
+
# Initialise fields settings
|
|
251
|
+
mandatory_fields = plugin_settings.get('mandatory_invoice_fields')
|
|
252
|
+
for field in mandatory_fields:
|
|
253
|
+
self.fields[field].required = True
|
|
254
|
+
hidden_fields = plugin_settings.get('hidden_invoice_fields')
|
|
255
|
+
for field in hidden_fields:
|
|
256
|
+
if not self.fields[field].required:
|
|
257
|
+
self.fields[field].widget = forms.HiddenInput()
|
|
258
|
+
|
|
259
|
+
def clean(self):
|
|
260
|
+
super().clean()
|
|
261
|
+
|
|
262
|
+
# template checks
|
|
263
|
+
if self.cleaned_data['template']:
|
|
264
|
+
# Check that there is only one invoice template per contract
|
|
265
|
+
contracts = self.cleaned_data['contracts']
|
|
266
|
+
for contract in contracts:
|
|
267
|
+
for invoice in contract.invoices.all():
|
|
268
|
+
if invoice.template and invoice.pk != self.instance.pk:
|
|
269
|
+
raise ValidationError(
|
|
270
|
+
'Only one invoice template allowed per contract'
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Prefix the invoice name with _template
|
|
274
|
+
self.cleaned_data['number'] = '_invoice_template_' + contract.name
|
|
275
|
+
|
|
276
|
+
# set the periode start and end date to null
|
|
277
|
+
self.cleaned_data['period_start'] = None
|
|
278
|
+
self.cleaned_data['period_end'] = None
|
|
279
|
+
|
|
280
|
+
def save(self, *args, **kwargs):
|
|
281
|
+
is_new = not bool(self.instance.pk)
|
|
282
|
+
|
|
283
|
+
instance = super().save(*args, **kwargs)
|
|
284
|
+
|
|
285
|
+
if is_new and not self.cleaned_data['template']:
|
|
286
|
+
contracts = self.cleaned_data['contracts']
|
|
287
|
+
|
|
288
|
+
for contract in contracts:
|
|
289
|
+
try:
|
|
290
|
+
template_exists = True
|
|
291
|
+
invoice_template = Invoice.objects.get(
|
|
292
|
+
template=True, contracts=contract
|
|
293
|
+
)
|
|
294
|
+
except ObjectDoesNotExist:
|
|
295
|
+
template_exists = False
|
|
296
|
+
|
|
297
|
+
if template_exists:
|
|
298
|
+
first = True
|
|
299
|
+
for line in invoice_template.invoicelines.all():
|
|
300
|
+
dimensions = line.accounting_dimensions.all()
|
|
301
|
+
line.pk = None
|
|
302
|
+
line.id = None
|
|
303
|
+
line._state.adding = True
|
|
304
|
+
line.invoice = self.instance
|
|
305
|
+
|
|
306
|
+
# adjust the first invoice line amount
|
|
307
|
+
amount = self.cleaned_data['amount']
|
|
308
|
+
if (
|
|
309
|
+
first
|
|
310
|
+
and amount != invoice_template.total_invoicelines_amount
|
|
311
|
+
):
|
|
312
|
+
line.amount = (
|
|
313
|
+
line.amount
|
|
314
|
+
+ amount
|
|
315
|
+
- invoice_template.total_invoicelines_amount
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
line.save()
|
|
319
|
+
|
|
320
|
+
for dimension in dimensions:
|
|
321
|
+
line.accounting_dimensions.add(dimension)
|
|
322
|
+
first = False
|
|
323
|
+
|
|
324
|
+
return instance
|
|
325
|
+
|
|
326
|
+
class Meta:
|
|
327
|
+
model = Invoice
|
|
328
|
+
fields = (
|
|
329
|
+
'number',
|
|
330
|
+
'date',
|
|
331
|
+
'contracts',
|
|
332
|
+
'template',
|
|
333
|
+
'period_start',
|
|
334
|
+
'period_end',
|
|
335
|
+
'currency',
|
|
336
|
+
'accounting_dimensions',
|
|
337
|
+
'amount',
|
|
338
|
+
'documents',
|
|
339
|
+
'comments',
|
|
340
|
+
'tags',
|
|
341
|
+
)
|
|
342
|
+
widgets = {
|
|
343
|
+
'date': DatePicker(),
|
|
344
|
+
'period_start': DatePicker(),
|
|
345
|
+
'period_end': DatePicker(),
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class InvoiceFilterSetForm(NetBoxModelFilterSetForm):
|
|
350
|
+
model = Invoice
|
|
351
|
+
contracts = DynamicModelMultipleChoiceField(
|
|
352
|
+
queryset=Contract.objects.all(), required=False
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
274
356
|
class InvoiceCSVForm(NetBoxModelImportForm):
|
|
275
357
|
contracts = CSVModelChoiceField(
|
|
276
358
|
queryset=Contract.objects.all(),
|
|
@@ -284,6 +366,7 @@ class InvoiceCSVForm(NetBoxModelImportForm):
|
|
|
284
366
|
'number',
|
|
285
367
|
'date',
|
|
286
368
|
'contracts',
|
|
369
|
+
'template',
|
|
287
370
|
'period_start',
|
|
288
371
|
'period_end',
|
|
289
372
|
'currency',
|
|
@@ -368,3 +451,108 @@ class ContractAssignmentImportForm(NetBoxModelImportForm):
|
|
|
368
451
|
class Meta:
|
|
369
452
|
model = ContractAssignment
|
|
370
453
|
fields = ['content_type', 'object_id', 'contract', 'tags']
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# InvoiceLine
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class InvoiceLineForm(NetBoxModelForm):
|
|
460
|
+
invoice = DynamicModelChoiceField(queryset=Invoice.objects.all())
|
|
461
|
+
accounting_dimensions = forms.ModelMultipleChoiceField(
|
|
462
|
+
queryset=AccountingDimension.objects.all(), required=False
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
def clean(self):
|
|
466
|
+
super().clean()
|
|
467
|
+
|
|
468
|
+
# check for duplicate dimensions
|
|
469
|
+
accounting_dimensions = self.cleaned_data['accounting_dimensions']
|
|
470
|
+
dimensions_names = []
|
|
471
|
+
for dimension in accounting_dimensions:
|
|
472
|
+
if dimension.name in dimensions_names:
|
|
473
|
+
raise ValidationError('duplicate accounting dimension')
|
|
474
|
+
else:
|
|
475
|
+
dimensions_names.append(dimension.name)
|
|
476
|
+
|
|
477
|
+
class Meta:
|
|
478
|
+
model = InvoiceLine
|
|
479
|
+
fields = [
|
|
480
|
+
'invoice',
|
|
481
|
+
'currency',
|
|
482
|
+
'amount',
|
|
483
|
+
'accounting_dimensions',
|
|
484
|
+
'comments',
|
|
485
|
+
'tags',
|
|
486
|
+
]
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
class InvoiceLineFilterSetForm(NetBoxModelFilterSetForm):
|
|
490
|
+
model = InvoiceLine
|
|
491
|
+
invoice = DynamicModelChoiceField(queryset=Invoice.objects.all())
|
|
492
|
+
accounting_dimensions = DynamicModelMultipleChoiceField(
|
|
493
|
+
queryset=AccountingDimension.objects.all()
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
class InvoiceLineImportForm(NetBoxModelImportForm):
|
|
498
|
+
invoice = CSVModelChoiceField(
|
|
499
|
+
queryset=Invoice.objects.all(),
|
|
500
|
+
to_field_name='number',
|
|
501
|
+
help_text='Invoice number',
|
|
502
|
+
)
|
|
503
|
+
accounting_dimensions = CSVModelChoiceField(
|
|
504
|
+
queryset=AccountingDimension.objects.all(),
|
|
505
|
+
help_text='accounting dimention in the form name, value',
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
class Meta:
|
|
509
|
+
model = InvoiceLine
|
|
510
|
+
fields = [
|
|
511
|
+
'invoice',
|
|
512
|
+
'currency',
|
|
513
|
+
'amount',
|
|
514
|
+
'accounting_dimensions',
|
|
515
|
+
'comments',
|
|
516
|
+
'tags',
|
|
517
|
+
]
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
class InvoiceLineBulkEditForm(NetBoxModelBulkEditForm):
|
|
521
|
+
invoice = DynamicModelChoiceField(queryset=Invoice.objects.all(), required=False)
|
|
522
|
+
accounting_dimensions = DynamicModelMultipleChoiceField(
|
|
523
|
+
queryset=AccountingDimension.objects.all(), required=False
|
|
524
|
+
)
|
|
525
|
+
model = InvoiceLine
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
# AccountingDimension
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
class AccountingDimensionForm(NetBoxModelForm):
|
|
532
|
+
class Meta:
|
|
533
|
+
model = AccountingDimension
|
|
534
|
+
fields = [
|
|
535
|
+
'name',
|
|
536
|
+
'value',
|
|
537
|
+
'comments',
|
|
538
|
+
'tags',
|
|
539
|
+
]
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
class AccountingDimensionFilterSetForm(NetBoxModelFilterSetForm):
|
|
543
|
+
model = AccountingDimension
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
class AccountingDimensionImportForm(NetBoxModelImportForm):
|
|
547
|
+
class Meta:
|
|
548
|
+
model = AccountingDimension
|
|
549
|
+
fields = [
|
|
550
|
+
'name',
|
|
551
|
+
'value',
|
|
552
|
+
'comments',
|
|
553
|
+
'tags',
|
|
554
|
+
]
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
class AccountingDimensionBulkEditForm(NetBoxModelBulkEditForm):
|
|
558
|
+
model = AccountingDimension
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Generated by Django 5.0.6 on 2024-06-23 13:12
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
import taggit.managers
|
|
5
|
+
import utilities.json
|
|
6
|
+
from django.db import migrations, models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
dependencies = [
|
|
11
|
+
('extras', '0115_convert_dashboard_widgets'),
|
|
12
|
+
('netbox_contract', '0026_auto_20240421_1550'),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.CreateModel(
|
|
17
|
+
name='InvoiceLine',
|
|
18
|
+
fields=[
|
|
19
|
+
(
|
|
20
|
+
'id',
|
|
21
|
+
models.BigAutoField(
|
|
22
|
+
auto_created=True, primary_key=True, serialize=False
|
|
23
|
+
),
|
|
24
|
+
),
|
|
25
|
+
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
|
26
|
+
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
|
27
|
+
(
|
|
28
|
+
'custom_field_data',
|
|
29
|
+
models.JSONField(
|
|
30
|
+
blank=True,
|
|
31
|
+
default=dict,
|
|
32
|
+
encoder=utilities.json.CustomFieldJSONEncoder,
|
|
33
|
+
),
|
|
34
|
+
),
|
|
35
|
+
('currency', models.CharField(default='usd', max_length=3)),
|
|
36
|
+
('amount', models.DecimalField(decimal_places=2, max_digits=10)),
|
|
37
|
+
('comments', models.TextField(blank=True)),
|
|
38
|
+
(
|
|
39
|
+
'invoice',
|
|
40
|
+
models.ForeignKey(
|
|
41
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
42
|
+
related_name='invoicelines',
|
|
43
|
+
to='netbox_contract.invoice',
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
(
|
|
47
|
+
'tags',
|
|
48
|
+
taggit.managers.TaggableManager(
|
|
49
|
+
through='extras.TaggedItem', to='extras.Tag'
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
],
|
|
53
|
+
options={
|
|
54
|
+
'abstract': False,
|
|
55
|
+
},
|
|
56
|
+
),
|
|
57
|
+
]
|
netbox_contract-2.2.1/src/netbox_contract/migrations/0028_invoiceline_accounting_dimensions.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Generated by Django 5.0.6 on 2024-06-30 20:19
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
('netbox_contract', '0027_invoiceline'),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.AddField(
|
|
13
|
+
model_name='invoiceline',
|
|
14
|
+
name='accounting_dimensions',
|
|
15
|
+
field=models.JSONField(null=True),
|
|
16
|
+
),
|
|
17
|
+
]
|