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.
Files changed (74) hide show
  1. {netbox_contract-2.1.2/src/netbox_contract.egg-info → netbox_contract-2.2.1}/PKG-INFO +2 -1
  2. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/pyproject.toml +2 -1
  3. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/__init__.py +1 -1
  4. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/api/serializers.py +75 -1
  5. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/api/urls.py +2 -0
  6. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/api/views.py +18 -2
  7. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/filtersets.py +32 -4
  8. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/forms.py +238 -50
  9. netbox_contract-2.2.1/src/netbox_contract/migrations/0027_invoiceline.py +57 -0
  10. netbox_contract-2.2.1/src/netbox_contract/migrations/0028_invoiceline_accounting_dimensions.py +17 -0
  11. netbox_contract-2.2.1/src/netbox_contract/migrations/0029_remove_invoiceline_accounting_dimensions_and_more.py +57 -0
  12. netbox_contract-2.2.1/src/netbox_contract/migrations/0030_alter_invoiceline_accounting_dimensions.py +21 -0
  13. netbox_contract-2.2.1/src/netbox_contract/migrations/0031_contract_invoice_template_invoice_template.py +29 -0
  14. netbox_contract-2.2.1/src/netbox_contract/migrations/0032_alter_invoice_period_end_alter_invoice_period_start_and_more.py +27 -0
  15. netbox_contract-2.2.1/src/netbox_contract/migrations/0033_remove_contract_invoice_template.py +19 -0
  16. netbox_contract-2.2.1/src/netbox_contract/migrations/0034_contract_yrc_alter_contract_mrc.py +26 -0
  17. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/models.py +76 -3
  18. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/navigation.py +34 -0
  19. netbox_contract-2.2.1/src/netbox_contract/search.py +52 -0
  20. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/tables.py +51 -3
  21. netbox_contract-2.2.1/src/netbox_contract/templates/contract_assignments_bottom.html +23 -0
  22. netbox_contract-2.2.1/src/netbox_contract/templates/netbox_contract/accountingdimension.html +27 -0
  23. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/templates/netbox_contract/contract.html +13 -7
  24. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/templates/netbox_contract/invoice.html +33 -8
  25. netbox_contract-2.2.1/src/netbox_contract/templates/netbox_contract/invoiceline.html +42 -0
  26. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/urls.py +74 -0
  27. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/views.py +157 -6
  28. {netbox_contract-2.1.2 → netbox_contract-2.2.1/src/netbox_contract.egg-info}/PKG-INFO +2 -1
  29. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract.egg-info/SOURCES.txt +10 -1
  30. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract.egg-info/requires.txt +1 -0
  31. netbox_contract-2.1.2/src/netbox_contract/search.py +0 -29
  32. netbox_contract-2.1.2/src/netbox_contract/templates/contract_assignments_bottom.html +0 -21
  33. netbox_contract-2.1.2/src/netbox_contract/templates/contract_assignments_bottom_old.html +0 -23
  34. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/LICENSE +0 -0
  35. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/MANIFEST.in +0 -0
  36. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/README.md +0 -0
  37. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/setup.cfg +0 -0
  38. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/setup.py +0 -0
  39. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/api/__init__.py +0 -0
  40. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/constants.py +0 -0
  41. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0001_initial.py +0 -0
  42. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0002_alter_contract_circuit_and_more.py +0 -0
  43. {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
  44. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0004_contract_currency_invoice_currency.py +0 -0
  45. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0005_contract_accounting_dimensions_and_more.py +0 -0
  46. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0006_alter_contract_circuit.py +0 -0
  47. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0007_invoice_date.py +0 -0
  48. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0008_invoice_comments.py +0 -0
  49. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0009_contract_external_reference.py +0 -0
  50. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0010_invoice_contracts.py +0 -0
  51. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0011_auto_20230122_2112.py +0 -0
  52. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0012_remove_invoice_contract.py +0 -0
  53. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0013_contract_documents_invoice_documents.py +0 -0
  54. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0014_contract_end_date.py +0 -0
  55. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0015_contractassignement.py +0 -0
  56. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0016_contract_parent.py +0 -0
  57. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0017_alter_contract_accounting_dimensions.py +0 -0
  58. {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
  59. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0019_auto_20230924_1813.py +0 -0
  60. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0020_alter_contract_external_partie.py +0 -0
  61. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0021_alter_contract_external_partie.py +0 -0
  62. {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
  63. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0023_rename_contractassignement_contractassignment_and_more.py +0 -0
  64. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0024_remove_contract_external_partie.py +0 -0
  65. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0025_remove_contract_circuit_and_more.py +0 -0
  66. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/0026_auto_20240421_1550.py +0 -0
  67. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/migrations/__init__.py +0 -0
  68. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/template_content.py +0 -0
  69. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/templates/contract_list_bottom.html +0 -0
  70. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/templates/netbox_contract/contractassignment.html +0 -0
  71. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract/templates/netbox_contract/serviceprovider.html +0 -0
  72. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract.egg-info/dependency_links.txt +0 -0
  73. {netbox_contract-2.1.2 → netbox_contract-2.2.1}/src/netbox_contract.egg-info/not-zip-safe +0 -0
  74. {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.2
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.2"
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.2'
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 Contract, ContractAssignment, Invoice, ServiceProvider
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
- yrc=F('mrc') * 12
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 Contract, ContractAssignment, Invoice, ServiceProvider
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
- 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
- }
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
+ ]
@@ -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
+ ]