netbox-contract 2.2.3__tar.gz → 2.2.5__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 (73) hide show
  1. {netbox_contract-2.2.3/src/netbox_contract.egg-info → netbox_contract-2.2.5}/PKG-INFO +4 -3
  2. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/README.md +3 -2
  3. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/pyproject.toml +1 -1
  4. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/__init__.py +2 -1
  5. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/api/serializers.py +161 -6
  6. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/api/views.py +2 -0
  7. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/filtersets.py +47 -4
  8. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/forms.py +43 -6
  9. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/tables.py +3 -1
  10. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/templates/netbox_contract/contract.html +29 -0
  11. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/templates/netbox_contract/invoice.html +5 -5
  12. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/views.py +21 -3
  13. {netbox_contract-2.2.3 → netbox_contract-2.2.5/src/netbox_contract.egg-info}/PKG-INFO +4 -3
  14. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/LICENSE +0 -0
  15. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/MANIFEST.in +0 -0
  16. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/setup.cfg +0 -0
  17. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/setup.py +0 -0
  18. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/api/__init__.py +0 -0
  19. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/api/urls.py +0 -0
  20. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/constants.py +0 -0
  21. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0001_initial.py +0 -0
  22. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0002_alter_contract_circuit_and_more.py +0 -0
  23. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0003_alter_contract_custom_field_data_and_more.py +0 -0
  24. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0004_contract_currency_invoice_currency.py +0 -0
  25. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0005_contract_accounting_dimensions_and_more.py +0 -0
  26. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0006_alter_contract_circuit.py +0 -0
  27. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0007_invoice_date.py +0 -0
  28. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0008_invoice_comments.py +0 -0
  29. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0009_contract_external_reference.py +0 -0
  30. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0010_invoice_contracts.py +0 -0
  31. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0011_auto_20230122_2112.py +0 -0
  32. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0012_remove_invoice_contract.py +0 -0
  33. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0013_contract_documents_invoice_documents.py +0 -0
  34. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0014_contract_end_date.py +0 -0
  35. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0015_contractassignement.py +0 -0
  36. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0016_contract_parent.py +0 -0
  37. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0017_alter_contract_accounting_dimensions.py +0 -0
  38. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0018_contract_external_partie_object_id_and_more.py +0 -0
  39. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0019_auto_20230924_1813.py +0 -0
  40. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0020_alter_contract_external_partie.py +0 -0
  41. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0021_alter_contract_external_partie.py +0 -0
  42. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0022_alter_contract_internal_partie_alter_contract_parent.py +0 -0
  43. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0023_rename_contractassignement_contractassignment_and_more.py +0 -0
  44. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0024_remove_contract_external_partie.py +0 -0
  45. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0025_remove_contract_circuit_and_more.py +0 -0
  46. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0026_auto_20240421_1550.py +0 -0
  47. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0027_invoiceline.py +0 -0
  48. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0028_invoiceline_accounting_dimensions.py +0 -0
  49. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0029_remove_invoiceline_accounting_dimensions_and_more.py +0 -0
  50. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0030_alter_invoiceline_accounting_dimensions.py +0 -0
  51. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0031_contract_invoice_template_invoice_template.py +0 -0
  52. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0032_alter_invoice_period_end_alter_invoice_period_start_and_more.py +0 -0
  53. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0033_remove_contract_invoice_template.py +0 -0
  54. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0034_contract_yrc_alter_contract_mrc.py +0 -0
  55. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0035_accountingdimension_status_and_more.py +0 -0
  56. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/0036_alter_invoiceline_accounting_dimensions.py +0 -0
  57. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/migrations/__init__.py +0 -0
  58. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/models.py +0 -0
  59. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/navigation.py +0 -0
  60. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/search.py +0 -0
  61. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/template_content.py +0 -0
  62. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/templates/contract_assignments_bottom.html +0 -0
  63. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/templates/contract_list_bottom.html +0 -0
  64. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/templates/netbox_contract/accountingdimension.html +0 -0
  65. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/templates/netbox_contract/contractassignment.html +0 -0
  66. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/templates/netbox_contract/invoiceline.html +0 -0
  67. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/templates/netbox_contract/serviceprovider.html +0 -0
  68. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract/urls.py +0 -0
  69. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract.egg-info/SOURCES.txt +0 -0
  70. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract.egg-info/dependency_links.txt +0 -0
  71. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract.egg-info/not-zip-safe +0 -0
  72. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/src/netbox_contract.egg-info/requires.txt +0 -0
  73. {netbox_contract-2.2.3 → netbox_contract-2.2.5}/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.2.3
3
+ Version: 2.2.5
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
@@ -62,10 +62,11 @@ PLUGINS_CONFIG = {
62
62
  "project": "",
63
63
  "cost center": ""
64
64
  },
65
- 'mandatory_contract_fields': ['accounting_dimensions'],
65
+ 'mandatory_contract_fields': [],
66
66
  'hidden_contract_fields': [],
67
- 'mandatory_invoice_fields': ['accounting_dimensions'],
67
+ 'mandatory_invoice_fields': [],
68
68
  'hidden_invoice_fields': [],
69
+ 'mandatory_dimensions': [],
69
70
  }
70
71
  }
71
72
 
@@ -46,10 +46,11 @@ PLUGINS_CONFIG = {
46
46
  "project": "",
47
47
  "cost center": ""
48
48
  },
49
- 'mandatory_contract_fields': ['accounting_dimensions'],
49
+ 'mandatory_contract_fields': [],
50
50
  'hidden_contract_fields': [],
51
- 'mandatory_invoice_fields': ['accounting_dimensions'],
51
+ 'mandatory_invoice_fields': [],
52
52
  'hidden_invoice_fields': [],
53
+ 'mandatory_dimensions': [],
53
54
  }
54
55
  }
55
56
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "netbox-contract"
3
- version = "2.2.3"
3
+ version = "2.2.5"
4
4
  authors = [
5
5
  { name="Marc Lebreuil", email="marc@famillelebreuil.net" },
6
6
  ]
@@ -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.2.3'
8
+ version = '2.2.5'
9
9
  author = 'Marc Lebreuil'
10
10
  author_email = 'marc@famillelebreuil.net'
11
11
  base_url = 'contracts'
@@ -22,6 +22,7 @@ class ContractsConfig(PluginConfig):
22
22
  'hidden_contract_fields': [],
23
23
  'mandatory_invoice_fields': [],
24
24
  'hidden_invoice_fields': [],
25
+ 'mandatory_dimensions': [],
25
26
  }
26
27
 
27
28
 
@@ -1,6 +1,7 @@
1
1
  from django.contrib.auth.models import ContentType
2
+ from django.core.exceptions import ObjectDoesNotExist
2
3
  from drf_yasg.utils import swagger_serializer_method
3
- from netbox.api.fields import ContentTypeField
4
+ from netbox.api.fields import ContentTypeField, SerializedPKRelatedField
4
5
  from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
5
6
  from netbox.constants import NESTED_SERIALIZER_PREFIX
6
7
  from rest_framework import serializers
@@ -31,10 +32,46 @@ class NestedContractSerializer(WritableNestedSerializer):
31
32
  url = serializers.HyperlinkedIdentityField(
32
33
  view_name='plugins-api:netbox_contract-api:contract-detail'
33
34
  )
35
+ yrc = serializers.DecimalField(max_digits=10, decimal_places=2, read_only=True)
36
+ tenant = NestedTenantSerializer(many=False, required=False)
37
+ external_partie_object_type = ContentTypeField(queryset=ContentType.objects.all())
38
+ external_partie_object = serializers.SerializerMethodField(read_only=True)
34
39
 
35
40
  class Meta:
36
41
  model = Contract
37
- fields = ('id', 'url', 'display', 'name')
42
+ fields = fields = (
43
+ 'id',
44
+ 'url',
45
+ 'display',
46
+ 'name',
47
+ 'external_partie_object_type',
48
+ 'external_partie_object_id',
49
+ 'external_partie_object',
50
+ 'external_reference',
51
+ 'internal_partie',
52
+ 'tenant',
53
+ 'status',
54
+ 'start_date',
55
+ 'end_date',
56
+ 'initial_term',
57
+ 'renewal_term',
58
+ 'currency',
59
+ 'accounting_dimensions',
60
+ 'mrc',
61
+ 'yrc',
62
+ 'nrc',
63
+ 'invoice_frequency',
64
+ 'comments',
65
+ )
66
+
67
+ @swagger_serializer_method(serializer_or_field=serializers.JSONField)
68
+ def get_external_partie_object(self, instance):
69
+ serializer = get_serializer_for_model(
70
+ instance.external_partie_object_type.model_class(),
71
+ prefix=NESTED_SERIALIZER_PREFIX,
72
+ )
73
+ context = {'request': self.context['request']}
74
+ return serializer(instance.external_partie_object, context=context).data
38
75
 
39
76
 
40
77
  class NestedInvoiceSerializer(WritableNestedSerializer):
@@ -134,8 +171,25 @@ class ContractSerializer(NetBoxModelSerializer):
134
171
  'url',
135
172
  'display',
136
173
  'name',
174
+ 'external_partie_object_type',
175
+ 'external_partie_object_id',
137
176
  'external_partie_object',
177
+ 'external_reference',
178
+ 'internal_partie',
179
+ 'tenant',
138
180
  'status',
181
+ 'start_date',
182
+ 'end_date',
183
+ 'initial_term',
184
+ 'renewal_term',
185
+ 'currency',
186
+ 'accounting_dimensions',
187
+ 'mrc',
188
+ 'yrc',
189
+ 'nrc',
190
+ 'invoice_frequency',
191
+ 'comments',
192
+ 'parent',
139
193
  )
140
194
 
141
195
  @swagger_serializer_method(serializer_or_field=serializers.JSONField)
@@ -152,7 +206,12 @@ class InvoiceSerializer(NetBoxModelSerializer):
152
206
  url = serializers.HyperlinkedIdentityField(
153
207
  view_name='plugins-api:netbox_contract-api:invoice-detail'
154
208
  )
155
- contracts = NestedContractSerializer(many=True, required=False)
209
+ contracts = SerializedPKRelatedField(
210
+ queryset=Contract.objects.all(),
211
+ serializer=ContractSerializer,
212
+ required=False,
213
+ many=True,
214
+ )
156
215
 
157
216
  class Meta:
158
217
  model = Invoice
@@ -162,6 +221,7 @@ class InvoiceSerializer(NetBoxModelSerializer):
162
221
  'display',
163
222
  'number',
164
223
  'date',
224
+ 'template',
165
225
  'contracts',
166
226
  'period_start',
167
227
  'period_end',
@@ -174,7 +234,87 @@ class InvoiceSerializer(NetBoxModelSerializer):
174
234
  'created',
175
235
  'last_updated',
176
236
  )
177
- brief_fields = ('id', 'url', 'display', 'number', 'contracts')
237
+ brief_fields = (
238
+ 'id',
239
+ 'url',
240
+ 'display',
241
+ 'number',
242
+ 'date',
243
+ 'template',
244
+ 'contracts',
245
+ 'period_start',
246
+ 'period_end',
247
+ 'currency',
248
+ 'accounting_dimensions',
249
+ 'amount',
250
+ 'comments',
251
+ )
252
+
253
+ def validate(self, data):
254
+ data = super().validate(data)
255
+
256
+ # template checks
257
+ if data['template']:
258
+ # Check that there is only one invoice template per contract
259
+ contracts = data['contracts']
260
+ for contract in contracts:
261
+ for invoice in contract.invoices.all():
262
+ if invoice.template and invoice != self.instance:
263
+ raise serializers.ValidationError(
264
+ 'Only one invoice template allowed per contract'
265
+ )
266
+
267
+ # Prefix the invoice name with _template
268
+ data['number'] = '_invoice_template_' + contract.name
269
+
270
+ # set the periode start and end date to null
271
+ data['period_start'] = None
272
+ data['period_end'] = None
273
+ return data
274
+
275
+ def create(self, validated_data):
276
+ instance = super().create(validated_data)
277
+
278
+ if not instance.template:
279
+ contracts = instance.contracts.all()
280
+
281
+ for contract in contracts:
282
+ try:
283
+ template_exists = True
284
+ invoice_template = Invoice.objects.get(
285
+ template=True, contracts=contract
286
+ )
287
+ except ObjectDoesNotExist:
288
+ template_exists = False
289
+
290
+ if template_exists:
291
+ first = True
292
+ for line in invoice_template.invoicelines.all():
293
+ dimensions = line.accounting_dimensions.all()
294
+ line.pk = None
295
+ line.id = None
296
+ line._state.adding = True
297
+ line.invoice = instance
298
+
299
+ # adjust the first invoice line amount
300
+ amount = validated_data['amount']
301
+ if (
302
+ first
303
+ and amount != invoice_template.total_invoicelines_amount
304
+ ):
305
+ line.amount = (
306
+ line.amount
307
+ + amount
308
+ - invoice_template.total_invoicelines_amount
309
+ )
310
+
311
+ line.save()
312
+
313
+ for dimension in dimensions:
314
+ line.accounting_dimensions.add(dimension)
315
+ first = False
316
+
317
+ return instance
178
318
 
179
319
 
180
320
  class ServiceProviderSerializer(NetBoxModelSerializer):
@@ -235,8 +375,11 @@ class InvoiceLineSerializer(NetBoxModelSerializer):
235
375
  view_name='plugins-api:netbox_contract-api:invoiceline-detail'
236
376
  )
237
377
  invoice = NestedInvoiceSerializer(many=False, required=False)
238
- accounting_dimensions = NestedAccountingDimensionSerializer(
239
- many=True, required=False
378
+ accounting_dimensions = SerializedPKRelatedField(
379
+ queryset=AccountingDimension.objects.all(),
380
+ serializer=NestedAccountingDimensionSerializer,
381
+ required=False,
382
+ many=True,
240
383
  )
241
384
 
242
385
  class Meta:
@@ -264,6 +407,18 @@ class InvoiceLineSerializer(NetBoxModelSerializer):
264
407
  'name',
265
408
  )
266
409
 
410
+ def validate(self, data):
411
+ super().validate(data)
412
+ # check for duplicate dimensions
413
+ accounting_dimensions = data['accounting_dimensions']
414
+ dimensions_names = []
415
+ for dimension in accounting_dimensions:
416
+ if dimension.name in dimensions_names:
417
+ raise serializers.ValidationError('duplicate accounting dimension')
418
+ else:
419
+ dimensions_names.append(dimension.name)
420
+ return data
421
+
267
422
 
268
423
  class AccountingDimensionSerializer(NetBoxModelSerializer):
269
424
  url = serializers.HyperlinkedIdentityField(
@@ -21,6 +21,7 @@ class ContractViewSet(NetBoxModelViewSet):
21
21
  )
22
22
  )
23
23
  serializer_class = ContractSerializer
24
+ filterset_class = filtersets.ContractFilterSet
24
25
 
25
26
 
26
27
  class InvoiceViewSet(NetBoxModelViewSet):
@@ -44,6 +45,7 @@ class InvoiceLineViewSet(NetBoxModelViewSet):
44
45
  'invoice', 'accounting_dimensions', 'tags'
45
46
  )
46
47
  serializer_class = InvoiceLineSerializer
48
+ filterset_class = filtersets.InvoiceLineFilterSet
47
49
 
48
50
 
49
51
  class AccountingDimensionViewSet(NetBoxModelViewSet):
@@ -1,20 +1,42 @@
1
+ import django_filters
1
2
  from django.db.models import Q
2
3
  from netbox.filtersets import NetBoxModelFilterSet
4
+ from tenancy.filtersets import TenancyFilterSet
3
5
 
4
6
  from .models import (
5
7
  AccountingDimension,
8
+ AccountingDimensionStatusChoices,
6
9
  Contract,
7
10
  ContractAssignment,
11
+ CurrencyChoices,
12
+ InternalEntityChoices,
8
13
  Invoice,
9
14
  InvoiceLine,
10
15
  ServiceProvider,
16
+ StatusChoices,
11
17
  )
12
18
 
13
19
 
14
- class ContractFilterSet(NetBoxModelFilterSet):
20
+ class ContractFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
21
+ status = django_filters.MultipleChoiceFilter(choices=StatusChoices, null_value=None)
22
+ internal_partie = django_filters.MultipleChoiceFilter(
23
+ choices=InternalEntityChoices, null_value=None
24
+ )
25
+ currency = django_filters.MultipleChoiceFilter(
26
+ choices=CurrencyChoices, null_value=None
27
+ )
28
+
15
29
  class Meta:
16
30
  model = Contract
17
- fields = ('id', 'internal_partie', 'status', 'parent')
31
+ fields = (
32
+ 'id',
33
+ 'name',
34
+ 'external_reference',
35
+ 'start_date',
36
+ 'end_date',
37
+ 'initial_term',
38
+ 'parent',
39
+ )
18
40
 
19
41
  def search(self, queryset, name, value):
20
42
  return queryset.filter(
@@ -26,9 +48,22 @@ class ContractFilterSet(NetBoxModelFilterSet):
26
48
 
27
49
 
28
50
  class InvoiceFilterSet(NetBoxModelFilterSet):
51
+ currency = django_filters.MultipleChoiceFilter(
52
+ choices=CurrencyChoices, null_value=None
53
+ )
54
+
29
55
  class Meta:
30
56
  model = Invoice
31
- fields = ('id', 'contracts')
57
+ fields = (
58
+ 'id',
59
+ 'number',
60
+ 'template',
61
+ 'date',
62
+ 'contracts',
63
+ 'period_start',
64
+ 'period_end',
65
+ 'amount',
66
+ )
32
67
 
33
68
  def search(self, queryset, name, value):
34
69
  return queryset.filter(
@@ -55,9 +90,13 @@ class ContractAssignmentFilterSet(NetBoxModelFilterSet):
55
90
 
56
91
 
57
92
  class InvoiceLineFilterSet(NetBoxModelFilterSet):
93
+ currency = django_filters.MultipleChoiceFilter(
94
+ choices=CurrencyChoices, null_value=None
95
+ )
96
+
58
97
  class Meta:
59
98
  model = InvoiceLine
60
- fields = ('id', 'invoice')
99
+ fields = ('id', 'invoice', 'accounting_dimensions')
61
100
 
62
101
  def search(self, queryset, name, value):
63
102
  return queryset.filter(
@@ -66,6 +105,10 @@ class InvoiceLineFilterSet(NetBoxModelFilterSet):
66
105
 
67
106
 
68
107
  class AccountingDimensionFilterSet(NetBoxModelFilterSet):
108
+ status = django_filters.MultipleChoiceFilter(
109
+ choices=AccountingDimensionStatusChoices, null_value=None
110
+ )
111
+
69
112
  class Meta:
70
113
  model = AccountingDimension
71
114
  fields = ('name', 'value')
@@ -2,7 +2,6 @@ from django import forms
2
2
  from django.conf import settings
3
3
  from django.contrib.contenttypes.models import ContentType
4
4
  from django.core.exceptions import ObjectDoesNotExist, ValidationError
5
- from extras.filters import TagFilter
6
5
  from netbox.forms import (
7
6
  NetBoxModelBulkEditForm,
8
7
  NetBoxModelFilterSetForm,
@@ -10,6 +9,7 @@ from netbox.forms import (
10
9
  NetBoxModelImportForm,
11
10
  )
12
11
  from tenancy.models import Tenant
12
+ from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES
13
13
  from utilities.forms.fields import (
14
14
  CommentField,
15
15
  ContentTypeChoiceField,
@@ -21,6 +21,7 @@ from utilities.forms.fields import (
21
21
  DynamicModelMultipleChoiceField,
22
22
  JSONField,
23
23
  SlugField,
24
+ TagFilterField,
24
25
  )
25
26
  from utilities.forms.widgets import DatePicker, HTMXSelect
26
27
 
@@ -30,6 +31,7 @@ from .models import (
30
31
  AccountingDimensionStatusChoices,
31
32
  Contract,
32
33
  ContractAssignment,
34
+ CurrencyChoices,
33
35
  InternalEntityChoices,
34
36
  Invoice,
35
37
  InvoiceLine,
@@ -160,9 +162,11 @@ class ContractFilterSetForm(NetBoxModelFilterSetForm):
160
162
 
161
163
  tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False)
162
164
  external_reference = forms.CharField(required=False)
163
- internal_partie = forms.CharField(required=False)
165
+ internal_partie = forms.ChoiceField(choices=InternalEntityChoices, required=False)
164
166
  status = forms.ChoiceField(choices=StatusChoices, required=False)
167
+ currency = forms.ChoiceField(choices=CurrencyChoices, required=False)
165
168
  parent = DynamicModelChoiceField(queryset=Contract.objects.all(), required=False)
169
+ tag = TagFilterField(model)
166
170
 
167
171
 
168
172
  class ContractCSVForm(NetBoxModelImportForm):
@@ -227,13 +231,17 @@ class ContractCSVForm(NetBoxModelImportForm):
227
231
 
228
232
  class ContractBulkEditForm(NetBoxModelBulkEditForm):
229
233
  name = forms.CharField(max_length=100, required=False)
230
-
231
234
  external_reference = forms.CharField(max_length=100, required=False)
232
235
  internal_partie = forms.ChoiceField(choices=InternalEntityChoices, required=False)
236
+ tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False)
237
+ accounting_dimensions = Dimensions(required=False)
233
238
  comments = CommentField(required=False)
234
239
  parent = DynamicModelChoiceField(queryset=Contract.objects.all(), required=False)
235
240
 
236
- nullable_fields = ('comments',)
241
+ nullable_fields = (
242
+ 'accounting_dimensions',
243
+ 'comments',
244
+ )
237
245
  model = Contract
238
246
 
239
247
 
@@ -354,9 +362,15 @@ class InvoiceForm(NetBoxModelForm):
354
362
 
355
363
  class InvoiceFilterSetForm(NetBoxModelFilterSetForm):
356
364
  model = Invoice
365
+ number = forms.CharField(required=False)
366
+ template = forms.NullBooleanField(
367
+ required=False, widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES)
368
+ )
369
+ currency = forms.ChoiceField(choices=CurrencyChoices, required=False)
357
370
  contracts = DynamicModelMultipleChoiceField(
358
371
  queryset=Contract.objects.all(), required=False
359
372
  )
373
+ tag = TagFilterField(model)
360
374
 
361
375
 
362
376
  class InvoiceCSVForm(NetBoxModelImportForm):
@@ -386,9 +400,23 @@ class InvoiceCSVForm(NetBoxModelImportForm):
386
400
 
387
401
  class InvoiceBulkEditForm(NetBoxModelBulkEditForm):
388
402
  number = forms.CharField(max_length=100, required=True)
403
+ template = forms.BooleanField(required=False)
404
+ date = forms.DateField(required=False)
389
405
  contracts = DynamicModelMultipleChoiceField(
390
406
  queryset=Contract.objects.all(), required=False
391
407
  )
408
+ period_start = forms.DateField(required=False)
409
+ period_end = forms.DateField(required=False)
410
+ currency = forms.ChoiceField(choices=CurrencyChoices, required=False)
411
+ accounting_dimensions = Dimensions(required=False)
412
+ amount = forms.DecimalField(max_digits=10, decimal_places=2, required=False)
413
+ documents = forms.URLField(required=False)
414
+ comments = CommentField()
415
+ nullable_fields = (
416
+ 'accounting_dimensions',
417
+ 'comments',
418
+ )
419
+
392
420
  model = Invoice
393
421
 
394
422
 
@@ -406,7 +434,8 @@ class ServiceProviderForm(NetBoxModelForm):
406
434
 
407
435
  class ServiceProviderFilterSetForm(NetBoxModelFilterSetForm):
408
436
  model = ServiceProvider
409
- tag = TagFilter()
437
+ name = forms.CharField(required=False)
438
+ tag = TagFilterField(model)
410
439
 
411
440
 
412
441
  class ServiceProviderCSVForm(NetBoxModelImportForm):
@@ -480,6 +509,12 @@ class InvoiceLineForm(NetBoxModelForm):
480
509
  else:
481
510
  dimensions_names.append(dimension.name)
482
511
 
512
+ # Make sure mandatory dimensions are present
513
+ mandatory_dimensions = plugin_settings.get('mandatory_dimensions')
514
+ for dimension in mandatory_dimensions:
515
+ if dimension not in dimensions_names:
516
+ raise ValidationError(f'dimension {dimension} missing')
517
+
483
518
  class Meta:
484
519
  model = InvoiceLine
485
520
  fields = [
@@ -495,9 +530,11 @@ class InvoiceLineForm(NetBoxModelForm):
495
530
  class InvoiceLineFilterSetForm(NetBoxModelFilterSetForm):
496
531
  model = InvoiceLine
497
532
  invoice = DynamicModelChoiceField(queryset=Invoice.objects.all(), required=False)
498
- accounting_dimensions = DynamicModelChoiceField(
533
+ accounting_dimensions = DynamicModelMultipleChoiceField(
499
534
  queryset=AccountingDimension.objects.all(), required=False
500
535
  )
536
+ currency = forms.ChoiceField(choices=CurrencyChoices, required=False)
537
+ tag = TagFilterField(model)
501
538
 
502
539
 
503
540
  class InvoiceLineImportForm(NetBoxModelImportForm):
@@ -200,7 +200,9 @@ class ServiceProviderListTable(NetBoxTable):
200
200
 
201
201
  class InvoiceLineListTable(NetBoxTable):
202
202
  invoice = tables.Column(linkify=True)
203
- accounting_dimensions = tables.ManyToManyColumn(linkify=True)
203
+ accounting_dimensions = tables.ManyToManyColumn(
204
+ linkify=True, filter=lambda qs: qs.order_by('name')
205
+ )
204
206
 
205
207
  class Meta(NetBoxTable.Meta):
206
208
  model = InvoiceLine
@@ -120,6 +120,33 @@
120
120
  <div class="col col-md-6">
121
121
  {% include 'inc/panels/tags.html' %}
122
122
  {% include 'inc/panels/comments.html' %}
123
+ {% if invoice_template %}
124
+ <div class="card">
125
+ <h5 class="card-header">
126
+ Invoice template
127
+ </h5>
128
+ <table class="table table-hover attr-table">
129
+ <tr>
130
+ <th scope="row">Number</th>
131
+ <td>
132
+ <a href="{{ invoice_template.get_absolute_url }}">{{ invoice_template.number }}</a>
133
+ </td>
134
+ </tr>
135
+ <tr>
136
+ <th scope="row">Currency</th>
137
+ <td>{{ invoice_template.currency }}</td>
138
+ </tr>
139
+ <tr>
140
+ <th scope="row">Total amount</th>
141
+ <td>{{ invoice_template.total_invoicelines_amount }}</td>
142
+ </tr>
143
+ </table>
144
+ <h5 class="card-header">
145
+ Invoice template lines
146
+ </h5>
147
+ {% render_table invoicelines_table %}
148
+ </div>
149
+ {% endif %}
123
150
  </div>
124
151
  </div>
125
152
  <div class="row">
@@ -130,6 +157,7 @@
130
157
  </div>
131
158
  </div>
132
159
  </div>
160
+ {% if childs_table %}
133
161
  <div class="row">
134
162
  <div class="col col-md-12">
135
163
  <div class="card">
@@ -138,6 +166,7 @@
138
166
  </div>
139
167
  </div>
140
168
  </div>
169
+ {% endif %}
141
170
  {% if perms.netbox_contract.view_invoice %}
142
171
  <div class="row">
143
172
  <div class="col col-md-12">
@@ -13,16 +13,16 @@
13
13
  <div class="row mb-3">
14
14
  <div class="col col-md-6">
15
15
  <div class="card">
16
- <h5 class="card-header">Invoices</h5>
16
+ {% if object.template %}
17
+ <h5 class="card-header">Invoice template</h5>
18
+ {% else %}
19
+ <h5 class="card-header">Invoice</h5>
20
+ {% endif %}
17
21
  <table class="table table-hover attr-table">
18
22
  <tr>
19
23
  <th scope="row">Number</th>
20
24
  <td>{{ object.number }}</td>
21
25
  </tr>
22
- <tr>
23
- <th scope="row">Template</th>
24
- <td>{{ object.template }}</td>
25
- </tr>
26
26
  <tr>
27
27
  <th scope="row">Date</th>
28
28
  <td>{{ object.date }}</td>
@@ -138,20 +138,38 @@ class ContractView(generic.ObjectView):
138
138
  )
139
139
 
140
140
  def get_extra_context(self, request, instance):
141
- invoices_table = tables.InvoiceListTable(instance.invoices.all())
141
+ invoices_table = tables.InvoiceListTable(
142
+ instance.invoices.exclude(template=True)
143
+ )
144
+ invoices_table.columns.hide('contracts')
142
145
  invoices_table.configure(request)
143
146
  assignments_table = tables.ContractAssignmentContractTable(
144
147
  instance.assignments.all()
145
148
  )
149
+ invoice_template = instance.invoices.filter(template=True).first()
150
+ if invoice_template:
151
+ invoicelines_table = tables.InvoiceLineListTable(
152
+ invoice_template.invoicelines.all()
153
+ )
154
+ invoicelines_table.columns.hide('invoice')
155
+ invoicelines_table.columns.hide('currency')
156
+ invoicelines_table.configure(request)
157
+ else:
158
+ invoicelines_table = None
146
159
  assignments_table.configure(request)
147
- childs_table = tables.ContractListBottomTable(instance.childs.all())
148
- childs_table.configure(request)
160
+ if instance.childs.all():
161
+ childs_table = tables.ContractListBottomTable(instance.childs.all())
162
+ childs_table.configure(request)
163
+ else:
164
+ childs_table = None
149
165
 
150
166
  hidden_fields = plugin_settings.get('hidden_contract_fields')
151
167
 
152
168
  return {
153
169
  'hidden_fields': hidden_fields,
154
170
  'invoices_table': invoices_table,
171
+ 'invoice_template': invoice_template,
172
+ 'invoicelines_table': invoicelines_table,
155
173
  'assignments_table': assignments_table,
156
174
  'childs_table': childs_table,
157
175
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: netbox-contract
3
- Version: 2.2.3
3
+ Version: 2.2.5
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
@@ -62,10 +62,11 @@ PLUGINS_CONFIG = {
62
62
  "project": "",
63
63
  "cost center": ""
64
64
  },
65
- 'mandatory_contract_fields': ['accounting_dimensions'],
65
+ 'mandatory_contract_fields': [],
66
66
  'hidden_contract_fields': [],
67
- 'mandatory_invoice_fields': ['accounting_dimensions'],
67
+ 'mandatory_invoice_fields': [],
68
68
  'hidden_invoice_fields': [],
69
+ 'mandatory_dimensions': [],
69
70
  }
70
71
  }
71
72
 
File without changes