netbox-contract 2.1.2__tar.gz → 2.2.0__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.0}/PKG-INFO +2 -1
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/pyproject.toml +2 -1
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/__init__.py +1 -1
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/api/serializers.py +75 -1
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/api/urls.py +2 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/api/views.py +12 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/filtersets.py +32 -4
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/forms.py +226 -52
- netbox_contract-2.2.0/src/netbox_contract/migrations/0027_invoiceline.py +57 -0
- netbox_contract-2.2.0/src/netbox_contract/migrations/0028_invoiceline_accounting_dimensions.py +17 -0
- netbox_contract-2.2.0/src/netbox_contract/migrations/0029_remove_invoiceline_accounting_dimensions_and_more.py +57 -0
- netbox_contract-2.2.0/src/netbox_contract/migrations/0030_alter_invoiceline_accounting_dimensions.py +21 -0
- netbox_contract-2.2.0/src/netbox_contract/migrations/0031_contract_invoice_template_invoice_template.py +29 -0
- netbox_contract-2.2.0/src/netbox_contract/migrations/0032_alter_invoice_period_end_alter_invoice_period_start_and_more.py +27 -0
- netbox_contract-2.2.0/src/netbox_contract/migrations/0033_remove_contract_invoice_template.py +19 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/models.py +64 -2
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/navigation.py +34 -0
- netbox_contract-2.2.0/src/netbox_contract/search.py +52 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/tables.py +48 -1
- netbox_contract-2.2.0/src/netbox_contract/templates/netbox_contract/accountingdimension.html +27 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/templates/netbox_contract/invoice.html +27 -8
- netbox_contract-2.2.0/src/netbox_contract/templates/netbox_contract/invoiceline.html +42 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/urls.py +74 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/views.py +130 -1
- {netbox_contract-2.1.2 → netbox_contract-2.2.0/src/netbox_contract.egg-info}/PKG-INFO +2 -1
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract.egg-info/SOURCES.txt +9 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/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 → netbox_contract-2.2.0}/LICENSE +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/MANIFEST.in +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/README.md +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/setup.cfg +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/setup.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/api/__init__.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/constants.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0001_initial.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0002_alter_contract_circuit_and_more.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0003_alter_contract_custom_field_data_and_more.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0004_contract_currency_invoice_currency.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0005_contract_accounting_dimensions_and_more.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0006_alter_contract_circuit.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0007_invoice_date.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0008_invoice_comments.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0009_contract_external_reference.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0010_invoice_contracts.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0011_auto_20230122_2112.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0012_remove_invoice_contract.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0013_contract_documents_invoice_documents.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0014_contract_end_date.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0015_contractassignement.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0016_contract_parent.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0017_alter_contract_accounting_dimensions.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0018_contract_external_partie_object_id_and_more.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0019_auto_20230924_1813.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0020_alter_contract_external_partie.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0021_alter_contract_external_partie.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0022_alter_contract_internal_partie_alter_contract_parent.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0023_rename_contractassignement_contractassignment_and_more.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0024_remove_contract_external_partie.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0025_remove_contract_circuit_and_more.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/0026_auto_20240421_1550.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/migrations/__init__.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/template_content.py +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/templates/contract_assignments_bottom.html +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/templates/contract_assignments_bottom_old.html +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/templates/contract_list_bottom.html +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/templates/netbox_contract/contract.html +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/templates/netbox_contract/contractassignment.html +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract/templates/netbox_contract/serviceprovider.html +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract.egg-info/dependency_links.txt +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/src/netbox_contract.egg-info/not-zip-safe +0 -0
- {netbox_contract-2.1.2 → netbox_contract-2.2.0}/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.
|
|
3
|
+
Version: 2.2.0
|
|
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.
|
|
3
|
+
version = "2.2.0"
|
|
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.
|
|
8
|
+
version = '2.2.0'
|
|
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
|
|
@@ -3,8 +3,10 @@ from netbox.api.viewsets import NetBoxModelViewSet
|
|
|
3
3
|
|
|
4
4
|
from .. import filtersets, models
|
|
5
5
|
from .serializers import (
|
|
6
|
+
AccountingDimensionSerializer,
|
|
6
7
|
ContractAssignmentSerializer,
|
|
7
8
|
ContractSerializer,
|
|
9
|
+
InvoiceLineSerializer,
|
|
8
10
|
InvoiceSerializer,
|
|
9
11
|
ServiceProviderSerializer,
|
|
10
12
|
)
|
|
@@ -31,3 +33,13 @@ class ServiceProviderViewSet(NetBoxModelViewSet):
|
|
|
31
33
|
class ContractAssignmentViewSet(NetBoxModelViewSet):
|
|
32
34
|
queryset = models.ContractAssignment.objects.prefetch_related('contract', 'tags')
|
|
33
35
|
serializer_class = ContractAssignmentSerializer
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class InvoiceLineViewSet(NetBoxModelViewSet):
|
|
39
|
+
queryset = models.InvoiceLine.objects.prefetch_related('invoice', 'tags')
|
|
40
|
+
serializer_class = InvoiceLineSerializer
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AccountingDimensionViewSet(NetBoxModelViewSet):
|
|
44
|
+
queryset = models.AccountingDimension.objects.prefetch_related('tags')
|
|
45
|
+
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:
|
|
@@ -143,46 +144,6 @@ class ContractForm(NetBoxModelForm):
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
|
|
146
|
-
class InvoiceForm(NetBoxModelForm):
|
|
147
|
-
contracts = DynamicModelMultipleChoiceField(
|
|
148
|
-
queryset=Contract.objects.all(), required=False
|
|
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
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
147
|
class ContractFilterSetForm(NetBoxModelFilterSetForm):
|
|
187
148
|
model = Contract
|
|
188
149
|
|
|
@@ -193,13 +154,6 @@ class ContractFilterSetForm(NetBoxModelFilterSetForm):
|
|
|
193
154
|
parent = DynamicModelChoiceField(queryset=Contract.objects.all(), required=False)
|
|
194
155
|
|
|
195
156
|
|
|
196
|
-
class InvoiceFilterSetForm(NetBoxModelFilterSetForm):
|
|
197
|
-
model = Invoice
|
|
198
|
-
contracts = DynamicModelMultipleChoiceField(
|
|
199
|
-
queryset=Contract.objects.all(), required=False
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
|
|
203
157
|
class ContractCSVForm(NetBoxModelImportForm):
|
|
204
158
|
external_partie_object_type = CSVContentTypeField(
|
|
205
159
|
queryset=ContentType.objects.all(),
|
|
@@ -271,6 +225,120 @@ class ContractBulkEditForm(NetBoxModelBulkEditForm):
|
|
|
271
225
|
model = Contract
|
|
272
226
|
|
|
273
227
|
|
|
228
|
+
# Invoice
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class InvoiceForm(NetBoxModelForm):
|
|
232
|
+
contracts = DynamicModelMultipleChoiceField(
|
|
233
|
+
queryset=Contract.objects.all(), required=False
|
|
234
|
+
)
|
|
235
|
+
accounting_dimensions = Dimensions(required=False)
|
|
236
|
+
|
|
237
|
+
def __init__(self, *args, **kwargs):
|
|
238
|
+
super().__init__(*args, **kwargs)
|
|
239
|
+
|
|
240
|
+
# Initialise fields settings
|
|
241
|
+
mandatory_fields = plugin_settings.get('mandatory_invoice_fields')
|
|
242
|
+
for field in mandatory_fields:
|
|
243
|
+
self.fields[field].required = True
|
|
244
|
+
hidden_fields = plugin_settings.get('hidden_invoice_fields')
|
|
245
|
+
for field in hidden_fields:
|
|
246
|
+
if not self.fields[field].required:
|
|
247
|
+
self.fields[field].widget = forms.HiddenInput()
|
|
248
|
+
|
|
249
|
+
def clean(self):
|
|
250
|
+
super().clean()
|
|
251
|
+
|
|
252
|
+
# template checks
|
|
253
|
+
if self.cleaned_data['template']:
|
|
254
|
+
# Check that there is only one invoice template per contract
|
|
255
|
+
contracts = self.cleaned_data['contracts']
|
|
256
|
+
for contract in contracts:
|
|
257
|
+
for invoice in contract.invoices.all():
|
|
258
|
+
if invoice.template and invoice.pk != self.instance.pk:
|
|
259
|
+
raise ValidationError(
|
|
260
|
+
'Only one invoice template allowed per contract'
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Prefix the invoice name with _template
|
|
264
|
+
self.cleaned_data['number'] = '_template_' + self.cleaned_data['number']
|
|
265
|
+
|
|
266
|
+
def save(self, *args, **kwargs):
|
|
267
|
+
is_new = not bool(self.instance.pk)
|
|
268
|
+
|
|
269
|
+
instance = super().save(*args, **kwargs)
|
|
270
|
+
|
|
271
|
+
if is_new and not self.cleaned_data['template']:
|
|
272
|
+
contracts = self.cleaned_data['contracts']
|
|
273
|
+
|
|
274
|
+
for contract in contracts:
|
|
275
|
+
try:
|
|
276
|
+
template_exists = True
|
|
277
|
+
invoice_template = Invoice.objects.get(
|
|
278
|
+
template=True, contracts=contract
|
|
279
|
+
)
|
|
280
|
+
except ObjectDoesNotExist:
|
|
281
|
+
template_exists = False
|
|
282
|
+
|
|
283
|
+
if template_exists:
|
|
284
|
+
first = True
|
|
285
|
+
for line in invoice_template.invoicelines.all():
|
|
286
|
+
dimensions = line.accounting_dimensions.all()
|
|
287
|
+
line.pk = None
|
|
288
|
+
line.id = None
|
|
289
|
+
line._state.adding = True
|
|
290
|
+
line.invoice = self.instance
|
|
291
|
+
|
|
292
|
+
# adjust the first invoice line amount
|
|
293
|
+
amount = self.cleaned_data['amount']
|
|
294
|
+
if (
|
|
295
|
+
first
|
|
296
|
+
and amount != invoice_template.total_invoicelines_amount
|
|
297
|
+
):
|
|
298
|
+
line.amount = (
|
|
299
|
+
line.amount
|
|
300
|
+
+ amount
|
|
301
|
+
- invoice_template.total_invoicelines_amount
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
line.save()
|
|
305
|
+
|
|
306
|
+
for dimension in dimensions:
|
|
307
|
+
line.accounting_dimensions.add(dimension)
|
|
308
|
+
first = False
|
|
309
|
+
|
|
310
|
+
return instance
|
|
311
|
+
|
|
312
|
+
class Meta:
|
|
313
|
+
model = Invoice
|
|
314
|
+
fields = (
|
|
315
|
+
'number',
|
|
316
|
+
'date',
|
|
317
|
+
'contracts',
|
|
318
|
+
'template',
|
|
319
|
+
'period_start',
|
|
320
|
+
'period_end',
|
|
321
|
+
'currency',
|
|
322
|
+
'accounting_dimensions',
|
|
323
|
+
'amount',
|
|
324
|
+
'documents',
|
|
325
|
+
'comments',
|
|
326
|
+
'tags',
|
|
327
|
+
)
|
|
328
|
+
widgets = {
|
|
329
|
+
'date': DatePicker(),
|
|
330
|
+
'period_start': DatePicker(),
|
|
331
|
+
'period_end': DatePicker(),
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class InvoiceFilterSetForm(NetBoxModelFilterSetForm):
|
|
336
|
+
model = Invoice
|
|
337
|
+
contracts = DynamicModelMultipleChoiceField(
|
|
338
|
+
queryset=Contract.objects.all(), required=False
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
274
342
|
class InvoiceCSVForm(NetBoxModelImportForm):
|
|
275
343
|
contracts = CSVModelChoiceField(
|
|
276
344
|
queryset=Contract.objects.all(),
|
|
@@ -284,6 +352,7 @@ class InvoiceCSVForm(NetBoxModelImportForm):
|
|
|
284
352
|
'number',
|
|
285
353
|
'date',
|
|
286
354
|
'contracts',
|
|
355
|
+
'template',
|
|
287
356
|
'period_start',
|
|
288
357
|
'period_end',
|
|
289
358
|
'currency',
|
|
@@ -368,3 +437,108 @@ class ContractAssignmentImportForm(NetBoxModelImportForm):
|
|
|
368
437
|
class Meta:
|
|
369
438
|
model = ContractAssignment
|
|
370
439
|
fields = ['content_type', 'object_id', 'contract', 'tags']
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
# InvoiceLine
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class InvoiceLineForm(NetBoxModelForm):
|
|
446
|
+
invoice = DynamicModelChoiceField(queryset=Invoice.objects.all())
|
|
447
|
+
accounting_dimensions = forms.ModelMultipleChoiceField(
|
|
448
|
+
queryset=AccountingDimension.objects.all(), required=False
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
def clean(self):
|
|
452
|
+
super().clean()
|
|
453
|
+
|
|
454
|
+
# check for duplicate dimensions
|
|
455
|
+
accounting_dimensions = self.cleaned_data['accounting_dimensions']
|
|
456
|
+
dimensions_names = []
|
|
457
|
+
for dimension in accounting_dimensions:
|
|
458
|
+
if dimension.name in dimensions_names:
|
|
459
|
+
raise ValidationError('duplicate accounting dimension')
|
|
460
|
+
else:
|
|
461
|
+
dimensions_names.append(dimension.name)
|
|
462
|
+
|
|
463
|
+
class Meta:
|
|
464
|
+
model = InvoiceLine
|
|
465
|
+
fields = [
|
|
466
|
+
'invoice',
|
|
467
|
+
'currency',
|
|
468
|
+
'amount',
|
|
469
|
+
'accounting_dimensions',
|
|
470
|
+
'comments',
|
|
471
|
+
'tags',
|
|
472
|
+
]
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class InvoiceLineFilterSetForm(NetBoxModelFilterSetForm):
|
|
476
|
+
model = InvoiceLine
|
|
477
|
+
invoice = DynamicModelChoiceField(queryset=Invoice.objects.all())
|
|
478
|
+
accounting_dimensions = DynamicModelMultipleChoiceField(
|
|
479
|
+
queryset=AccountingDimension.objects.all()
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
class InvoiceLineImportForm(NetBoxModelImportForm):
|
|
484
|
+
invoice = CSVModelChoiceField(
|
|
485
|
+
queryset=Invoice.objects.all(),
|
|
486
|
+
to_field_name='number',
|
|
487
|
+
help_text='Invoice number',
|
|
488
|
+
)
|
|
489
|
+
accounting_dimensions = CSVModelChoiceField(
|
|
490
|
+
queryset=AccountingDimension.objects.all(),
|
|
491
|
+
help_text='accounting dimention in the form name, value',
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
class Meta:
|
|
495
|
+
model = InvoiceLine
|
|
496
|
+
fields = [
|
|
497
|
+
'invoice',
|
|
498
|
+
'currency',
|
|
499
|
+
'amount',
|
|
500
|
+
'accounting_dimensions',
|
|
501
|
+
'comments',
|
|
502
|
+
'tags',
|
|
503
|
+
]
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
class InvoiceLineBulkEditForm(NetBoxModelBulkEditForm):
|
|
507
|
+
invoice = DynamicModelChoiceField(queryset=Invoice.objects.all(), required=False)
|
|
508
|
+
accounting_dimensions = DynamicModelMultipleChoiceField(
|
|
509
|
+
queryset=AccountingDimension.objects.all(), required=False
|
|
510
|
+
)
|
|
511
|
+
model = InvoiceLine
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# AccountingDimension
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
class AccountingDimensionForm(NetBoxModelForm):
|
|
518
|
+
class Meta:
|
|
519
|
+
model = AccountingDimension
|
|
520
|
+
fields = [
|
|
521
|
+
'name',
|
|
522
|
+
'value',
|
|
523
|
+
'comments',
|
|
524
|
+
'tags',
|
|
525
|
+
]
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
class AccountingDimensionFilterSetForm(NetBoxModelFilterSetForm):
|
|
529
|
+
model = AccountingDimension
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
class AccountingDimensionImportForm(NetBoxModelImportForm):
|
|
533
|
+
class Meta:
|
|
534
|
+
model = AccountingDimension
|
|
535
|
+
fields = [
|
|
536
|
+
'name',
|
|
537
|
+
'value',
|
|
538
|
+
'comments',
|
|
539
|
+
'tags',
|
|
540
|
+
]
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
class AccountingDimensionBulkEditForm(NetBoxModelBulkEditForm):
|
|
544
|
+
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.0/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
|
+
]
|