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