netbox-contract 2.2.0__tar.gz → 2.2.2__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 (75) hide show
  1. {netbox_contract-2.2.0/src/netbox_contract.egg-info → netbox_contract-2.2.2}/PKG-INFO +1 -1
  2. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/pyproject.toml +1 -1
  3. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/__init__.py +1 -1
  4. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/api/serializers.py +25 -6
  5. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/api/views.py +9 -3
  6. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/filtersets.py +2 -4
  7. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/forms.py +29 -6
  8. netbox_contract-2.2.2/src/netbox_contract/migrations/0034_contract_yrc_alter_contract_mrc.py +26 -0
  9. netbox_contract-2.2.2/src/netbox_contract/migrations/0035_accountingdimension_status_and_more.py +24 -0
  10. netbox_contract-2.2.2/src/netbox_contract/migrations/0036_alter_invoiceline_accounting_dimensions.py +19 -0
  11. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/models.py +37 -4
  12. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/tables.py +3 -2
  13. netbox_contract-2.2.2/src/netbox_contract/templates/contract_assignments_bottom.html +23 -0
  14. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/templates/netbox_contract/accountingdimension.html +4 -0
  15. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/templates/netbox_contract/contract.html +13 -7
  16. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/templates/netbox_contract/invoice.html +6 -0
  17. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/urls.py +20 -0
  18. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/views.py +27 -5
  19. {netbox_contract-2.2.0 → netbox_contract-2.2.2/src/netbox_contract.egg-info}/PKG-INFO +1 -1
  20. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract.egg-info/SOURCES.txt +3 -1
  21. netbox_contract-2.2.0/src/netbox_contract/templates/contract_assignments_bottom.html +0 -21
  22. netbox_contract-2.2.0/src/netbox_contract/templates/contract_assignments_bottom_old.html +0 -23
  23. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/LICENSE +0 -0
  24. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/MANIFEST.in +0 -0
  25. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/README.md +0 -0
  26. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/setup.cfg +0 -0
  27. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/setup.py +0 -0
  28. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/api/__init__.py +0 -0
  29. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/api/urls.py +0 -0
  30. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/constants.py +0 -0
  31. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0001_initial.py +0 -0
  32. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0002_alter_contract_circuit_and_more.py +0 -0
  33. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0003_alter_contract_custom_field_data_and_more.py +0 -0
  34. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0004_contract_currency_invoice_currency.py +0 -0
  35. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0005_contract_accounting_dimensions_and_more.py +0 -0
  36. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0006_alter_contract_circuit.py +0 -0
  37. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0007_invoice_date.py +0 -0
  38. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0008_invoice_comments.py +0 -0
  39. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0009_contract_external_reference.py +0 -0
  40. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0010_invoice_contracts.py +0 -0
  41. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0011_auto_20230122_2112.py +0 -0
  42. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0012_remove_invoice_contract.py +0 -0
  43. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0013_contract_documents_invoice_documents.py +0 -0
  44. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0014_contract_end_date.py +0 -0
  45. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0015_contractassignement.py +0 -0
  46. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0016_contract_parent.py +0 -0
  47. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0017_alter_contract_accounting_dimensions.py +0 -0
  48. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0018_contract_external_partie_object_id_and_more.py +0 -0
  49. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0019_auto_20230924_1813.py +0 -0
  50. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0020_alter_contract_external_partie.py +0 -0
  51. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0021_alter_contract_external_partie.py +0 -0
  52. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0022_alter_contract_internal_partie_alter_contract_parent.py +0 -0
  53. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0023_rename_contractassignement_contractassignment_and_more.py +0 -0
  54. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0024_remove_contract_external_partie.py +0 -0
  55. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0025_remove_contract_circuit_and_more.py +0 -0
  56. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0026_auto_20240421_1550.py +0 -0
  57. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0027_invoiceline.py +0 -0
  58. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0028_invoiceline_accounting_dimensions.py +0 -0
  59. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0029_remove_invoiceline_accounting_dimensions_and_more.py +0 -0
  60. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0030_alter_invoiceline_accounting_dimensions.py +0 -0
  61. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0031_contract_invoice_template_invoice_template.py +0 -0
  62. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0032_alter_invoice_period_end_alter_invoice_period_start_and_more.py +0 -0
  63. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/0033_remove_contract_invoice_template.py +0 -0
  64. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/migrations/__init__.py +0 -0
  65. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/navigation.py +0 -0
  66. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/search.py +0 -0
  67. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/template_content.py +0 -0
  68. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/templates/contract_list_bottom.html +0 -0
  69. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/templates/netbox_contract/contractassignment.html +0 -0
  70. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/templates/netbox_contract/invoiceline.html +0 -0
  71. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract/templates/netbox_contract/serviceprovider.html +0 -0
  72. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract.egg-info/dependency_links.txt +0 -0
  73. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract.egg-info/not-zip-safe +0 -0
  74. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/src/netbox_contract.egg-info/requires.txt +0 -0
  75. {netbox_contract-2.2.0 → netbox_contract-2.2.2}/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.0
3
+ Version: 2.2.2
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "netbox-contract"
3
- version = "2.2.0"
3
+ version = "2.2.2"
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.0'
8
+ version = '2.2.2'
9
9
  author = 'Marc Lebreuil'
10
10
  author_email = 'marc@famillelebreuil.net'
11
11
  base_url = 'contracts'
@@ -50,7 +50,7 @@ class NestedInvoiceSerializer(WritableNestedSerializer):
50
50
 
51
51
  class NestedContractAssignmentSerializer(WritableNestedSerializer):
52
52
  url = serializers.HyperlinkedIdentityField(
53
- view_name='plugins-api:netbox_contract-api:ContractAssignment-detail'
53
+ view_name='plugins-api:netbox_contract-api:contractassignment-detail'
54
54
  )
55
55
 
56
56
  class Meta:
@@ -61,18 +61,25 @@ class NestedContractAssignmentSerializer(WritableNestedSerializer):
61
61
 
62
62
  class NestedInvoicelineSerializer(WritableNestedSerializer):
63
63
  url = serializers.HyperlinkedIdentityField(
64
- view_name='plugins-api:netbox_contract-api:InvoiceLine-detail'
64
+ view_name='plugins-api:netbox_contract-api:invoiceline-detail'
65
65
  )
66
66
 
67
67
  class Meta:
68
68
  model = InvoiceLine
69
- fields = ('id', 'url', 'display', 'invoice', 'amount')
70
- brief_fields = ('id', 'url', 'display', 'invoice', 'amount')
69
+ fields = ('id', 'url', 'display', 'invoice', 'accounting_dimensions', 'amount')
70
+ brief_fields = (
71
+ 'id',
72
+ 'url',
73
+ 'display',
74
+ 'invoice',
75
+ 'accounting_dimensions',
76
+ 'amount',
77
+ )
71
78
 
72
79
 
73
80
  class NestedAccountingDimensionSerializer(WritableNestedSerializer):
74
81
  url = serializers.HyperlinkedIdentityField(
75
- view_name='plugins-api:netbox_contract-api:AccountingDimension-detail'
82
+ view_name='plugins-api:netbox_contract-api:accountingdimension-detail'
76
83
  )
77
84
 
78
85
  class Meta:
@@ -227,6 +234,10 @@ class InvoiceLineSerializer(NetBoxModelSerializer):
227
234
  url = serializers.HyperlinkedIdentityField(
228
235
  view_name='plugins-api:netbox_contract-api:invoiceline-detail'
229
236
  )
237
+ invoice = NestedInvoiceSerializer(many=False, required=False)
238
+ accounting_dimensions = NestedAccountingDimensionSerializer(
239
+ many=True, required=False
240
+ )
230
241
 
231
242
  class Meta:
232
243
  model = InvoiceLine
@@ -237,13 +248,21 @@ class InvoiceLineSerializer(NetBoxModelSerializer):
237
248
  'invoice',
238
249
  'amount',
239
250
  'currency',
251
+ 'accounting_dimensions',
240
252
  'comments',
241
253
  'tags',
242
254
  'custom_fields',
243
255
  'created',
244
256
  'last_updated',
245
257
  )
246
- brief_fields = ('invoice', 'amount', 'url', 'display', 'name')
258
+ brief_fields = (
259
+ 'invoice',
260
+ 'accounting_dimensions',
261
+ 'amount',
262
+ 'url',
263
+ 'display',
264
+ 'name',
265
+ )
247
266
 
248
267
 
249
268
  class AccountingDimensionSerializer(NetBoxModelSerializer):
@@ -1,4 +1,5 @@
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
@@ -14,7 +15,10 @@ from .serializers import (
14
15
 
15
16
  class ContractViewSet(NetBoxModelViewSet):
16
17
  queryset = models.Contract.objects.prefetch_related('parent', 'tags').annotate(
17
- 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
+ )
18
22
  )
19
23
  serializer_class = ContractSerializer
20
24
 
@@ -36,7 +40,9 @@ class ContractAssignmentViewSet(NetBoxModelViewSet):
36
40
 
37
41
 
38
42
  class InvoiceLineViewSet(NetBoxModelViewSet):
39
- queryset = models.InvoiceLine.objects.prefetch_related('invoice', 'tags')
43
+ queryset = models.InvoiceLine.objects.prefetch_related(
44
+ 'invoice', 'accounting_dimensions', 'tags'
45
+ )
40
46
  serializer_class = InvoiceLineSerializer
41
47
 
42
48
 
@@ -61,7 +61,7 @@ class InvoiceLineFilterSet(NetBoxModelFilterSet):
61
61
 
62
62
  def search(self, queryset, name, value):
63
63
  return queryset.filter(
64
- Q(comments__icontains=value) | Q(invoice__name__icontains=value)
64
+ Q(comments__icontains=value) | Q(invoice__number__icontains=value)
65
65
  )
66
66
 
67
67
 
@@ -71,6 +71,4 @@ class AccountingDimensionFilterSet(NetBoxModelFilterSet):
71
71
  fields = ('name', 'value')
72
72
 
73
73
  def search(self, queryset, name, value):
74
- return queryset.filter(
75
- Q(comments__icontains=value) | Q(invoice__name__icontains=value)
76
- )
74
+ return queryset.filter(Q(comments__icontains=value) | Q(name__icontains=value))
@@ -26,6 +26,7 @@ from utilities.forms.widgets import DatePicker, HTMXSelect
26
26
  from .constants import SERVICE_PROVIDER_MODELS
27
27
  from .models import (
28
28
  AccountingDimension,
29
+ AccountingDimensionStatusChoices,
29
30
  Contract,
30
31
  ContractAssignment,
31
32
  InternalEntityChoices,
@@ -129,6 +130,7 @@ class ContractForm(NetBoxModelForm):
129
130
  'renewal_term',
130
131
  'currency',
131
132
  'accounting_dimensions',
133
+ 'yrc',
132
134
  'mrc',
133
135
  'nrc',
134
136
  'invoice_frequency',
@@ -143,6 +145,14 @@ class ContractForm(NetBoxModelForm):
143
145
  'end_date': DatePicker(),
144
146
  }
145
147
 
148
+ def clean(self):
149
+ super().clean()
150
+
151
+ if self.cleaned_data['mrc'] and self.cleaned_data['yrc']:
152
+ raise ValidationError(
153
+ 'you should set monthly OR yearly recuring costs not both'
154
+ )
155
+
146
156
 
147
157
  class ContractFilterSetForm(NetBoxModelFilterSetForm):
148
158
  model = Contract
@@ -193,6 +203,7 @@ class ContractCSVForm(NetBoxModelImportForm):
193
203
  'renewal_term',
194
204
  'currency',
195
205
  'accounting_dimensions',
206
+ 'yrc',
196
207
  'mrc',
197
208
  'nrc',
198
209
  'invoice_frequency',
@@ -261,7 +272,11 @@ class InvoiceForm(NetBoxModelForm):
261
272
  )
262
273
 
263
274
  # Prefix the invoice name with _template
264
- self.cleaned_data['number'] = '_template_' + self.cleaned_data['number']
275
+ self.cleaned_data['number'] = '_invoice_template_' + contract.name
276
+
277
+ # set the periode start and end date to null
278
+ self.cleaned_data['period_start'] = None
279
+ self.cleaned_data['period_end'] = None
265
280
 
266
281
  def save(self, *args, **kwargs):
267
282
  is_new = not bool(self.instance.pk)
@@ -474,10 +489,7 @@ class InvoiceLineForm(NetBoxModelForm):
474
489
 
475
490
  class InvoiceLineFilterSetForm(NetBoxModelFilterSetForm):
476
491
  model = InvoiceLine
477
- invoice = DynamicModelChoiceField(queryset=Invoice.objects.all())
478
- accounting_dimensions = DynamicModelMultipleChoiceField(
479
- queryset=AccountingDimension.objects.all()
480
- )
492
+ invoice = DynamicModelChoiceField(queryset=Invoice.objects.all(), required=False)
481
493
 
482
494
 
483
495
  class InvoiceLineImportForm(NetBoxModelImportForm):
@@ -488,7 +500,8 @@ class InvoiceLineImportForm(NetBoxModelImportForm):
488
500
  )
489
501
  accounting_dimensions = CSVModelChoiceField(
490
502
  queryset=AccountingDimension.objects.all(),
491
- help_text='accounting dimention in the form name, value',
503
+ to_field_name='id',
504
+ help_text='accounting dimension id',
492
505
  )
493
506
 
494
507
  class Meta:
@@ -520,6 +533,7 @@ class AccountingDimensionForm(NetBoxModelForm):
520
533
  fields = [
521
534
  'name',
522
535
  'value',
536
+ 'status',
523
537
  'comments',
524
538
  'tags',
525
539
  ]
@@ -528,17 +542,26 @@ class AccountingDimensionForm(NetBoxModelForm):
528
542
  class AccountingDimensionFilterSetForm(NetBoxModelFilterSetForm):
529
543
  model = AccountingDimension
530
544
 
545
+ name = forms.CharField(required=False)
546
+ value = forms.CharField(required=False)
547
+ status = forms.ChoiceField(choices=AccountingDimensionStatusChoices, required=False)
548
+
531
549
 
532
550
  class AccountingDimensionImportForm(NetBoxModelImportForm):
551
+ status = CSVChoiceField(choices=StatusChoices, help_text='Contract status')
552
+
533
553
  class Meta:
534
554
  model = AccountingDimension
535
555
  fields = [
536
556
  'name',
537
557
  'value',
558
+ 'status',
538
559
  'comments',
539
560
  'tags',
540
561
  ]
541
562
 
542
563
 
543
564
  class AccountingDimensionBulkEditForm(NetBoxModelBulkEditForm):
565
+ name = forms.CharField(max_length=20, required=False)
566
+ value = forms.CharField(max_length=20, required=False)
544
567
  model = AccountingDimension
@@ -0,0 +1,26 @@
1
+ # Generated by Django 5.0.6 on 2024-08-01 15:43
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ('netbox_contract', '0033_remove_contract_invoice_template'),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.AddField(
13
+ model_name='contract',
14
+ name='yrc',
15
+ field=models.DecimalField(
16
+ blank=True, decimal_places=2, max_digits=10, null=True
17
+ ),
18
+ ),
19
+ migrations.AlterField(
20
+ model_name='contract',
21
+ name='mrc',
22
+ field=models.DecimalField(
23
+ blank=True, decimal_places=2, max_digits=10, null=True
24
+ ),
25
+ ),
26
+ ]
@@ -0,0 +1,24 @@
1
+ # Generated by Django 5.0.6 on 2024-08-18 10:03
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ('extras', '0115_convert_dashboard_widgets'),
9
+ ('netbox_contract', '0034_contract_yrc_alter_contract_mrc'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='accountingdimension',
15
+ name='status',
16
+ field=models.CharField(default='Active', max_length=50),
17
+ ),
18
+ migrations.AddConstraint(
19
+ model_name='accountingdimension',
20
+ constraint=models.UniqueConstraint(
21
+ fields=('name', 'value'), name='unique_accounting_dimension'
22
+ ),
23
+ ),
24
+ ]
@@ -0,0 +1,19 @@
1
+ # Generated by Django 5.0.6 on 2024-08-18 19:37
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ('netbox_contract', '0035_accountingdimension_status_and_more'),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.AlterField(
13
+ model_name='invoiceline',
14
+ name='accounting_dimensions',
15
+ field=models.ManyToManyField(
16
+ blank=True, to='netbox_contract.accountingdimension'
17
+ ),
18
+ ),
19
+ ]
@@ -20,6 +20,18 @@ class StatusChoices(ChoiceSet):
20
20
  ]
21
21
 
22
22
 
23
+ class AccountingDimensionStatusChoices(ChoiceSet):
24
+ key = 'AccountingDimension.status'
25
+
26
+ STATUS_ACTIVE = 'Active'
27
+ STATUS_INACTIVE = 'Inactive'
28
+
29
+ CHOICES = [
30
+ (STATUS_ACTIVE, 'Active', 'green'),
31
+ (STATUS_INACTIVE, 'Inactive', 'red'),
32
+ ]
33
+
34
+
23
35
  class InternalEntityChoices(ChoiceSet):
24
36
  key = 'Contract.internal_partie'
25
37
 
@@ -44,6 +56,11 @@ class CurrencyChoices(ChoiceSet):
44
56
  class AccountingDimension(NetBoxModel):
45
57
  name = models.CharField(max_length=20)
46
58
  value = models.CharField(max_length=20)
59
+ status = models.CharField(
60
+ max_length=50,
61
+ choices=AccountingDimensionStatusChoices,
62
+ default=StatusChoices.STATUS_ACTIVE,
63
+ )
47
64
  comments = models.TextField(blank=True)
48
65
 
49
66
  def get_absolute_url(self):
@@ -56,6 +73,13 @@ class AccountingDimension(NetBoxModel):
56
73
  def __str__(self):
57
74
  return self.dimension
58
75
 
76
+ class Meta:
77
+ constraints = [
78
+ models.UniqueConstraint(
79
+ fields=['name', 'value'], name='unique_accounting_dimension'
80
+ )
81
+ ]
82
+
59
83
 
60
84
  class ServiceProvider(ContactsMixin, NetBoxModel):
61
85
  name = models.CharField(max_length=100)
@@ -130,8 +154,19 @@ class Contract(NetBoxModel):
130
154
  max_length=3, choices=CurrencyChoices, default=CurrencyChoices.CURRENCY_USD
131
155
  )
132
156
  accounting_dimensions = models.JSONField(null=True, blank=True)
157
+ yrc = models.DecimalField(
158
+ verbose_name='yearly recuring cost',
159
+ max_digits=10,
160
+ decimal_places=2,
161
+ blank=True,
162
+ null=True,
163
+ )
133
164
  mrc = models.DecimalField(
134
- verbose_name='Monthly recuring cost', max_digits=10, decimal_places=2
165
+ verbose_name='Monthly recuring cost',
166
+ max_digits=10,
167
+ decimal_places=2,
168
+ blank=True,
169
+ null=True,
135
170
  )
136
171
  nrc = models.DecimalField(
137
172
  verbose_name='None recuring cost', default=0, max_digits=10, decimal_places=2
@@ -204,9 +239,7 @@ class InvoiceLine(NetBoxModel):
204
239
  max_length=3, choices=CurrencyChoices, default=CurrencyChoices.CURRENCY_USD
205
240
  )
206
241
  amount = models.DecimalField(max_digits=10, decimal_places=2)
207
- accounting_dimensions = models.ManyToManyField(
208
- AccountingDimension, related_name='invoicelines', blank=True
209
- )
242
+ accounting_dimensions = models.ManyToManyField(AccountingDimension, blank=True)
210
243
  comments = models.TextField(blank=True)
211
244
 
212
245
  def get_absolute_url(self):
@@ -42,14 +42,15 @@ class ContractAssignmentListTable(NetBoxTable):
42
42
  class ContractAssignmentObjectTable(NetBoxTable):
43
43
  contract = tables.Column(linkify=True)
44
44
  actions = columns.ActionsColumn(actions=('edit', 'delete'))
45
- contract__external_partie_object = tables.Column(linkify=True)
45
+ contract__external_partie_object = tables.Column(
46
+ verbose_name='Partner', linkify=True
47
+ )
46
48
 
47
49
  class Meta(NetBoxTable.Meta):
48
50
  model = ContractAssignment
49
51
  fields = (
50
52
  'pk',
51
53
  'contract',
52
- 'contract__external_partie_object_type',
53
54
  'contract__external_partie_object',
54
55
  'contract__status',
55
56
  'contract__start_date',
@@ -0,0 +1,23 @@
1
+ {% load render_table from django_tables2 %}
2
+ {% if perms.netbox_contract.view_contract %}
3
+ <div class="row">
4
+ <div class="col col-md-12">
5
+ <div class="card">
6
+ <h5 class="card-header">
7
+ Contracts Assignments
8
+ {% if perms.netbox_contract.add_contractassignment %}
9
+ <div class="card-actions">
10
+ <a href="{% url 'plugins:netbox_contract:contractassignment_add' %}?content_type={{ object|content_type_id }}&object_id={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
11
+ <i class="mdi mdi-plus-thick" aria-hidden="true"></i>
12
+ Add assignment
13
+ </a>
14
+ </div>
15
+ {% endif %}
16
+ </h5>
17
+ {% if assignments_table %}
18
+ {% render_table assignments_table %}
19
+ {% endif %}
20
+ </div>
21
+ </div>
22
+ </div>
23
+ {% endif %}
@@ -16,6 +16,10 @@
16
16
  <th scope="row">value</th>
17
17
  <td>{{ object.value }}</td>
18
18
  </tr>
19
+ <tr>
20
+ <th scope="row">status</th>
21
+ <td>{{ object.status }}</td>
22
+ </tr>
19
23
  </table>
20
24
  </div>
21
25
  {% include 'inc/panels/custom_fields.html' %}
@@ -85,6 +85,10 @@
85
85
  <th scope="row">Yearly recuring costs</th>
86
86
  <td>{{ object.yrc }}</td>
87
87
  </tr>
88
+ <tr>
89
+ <th scope="row">calculated corresponding Yearly or Monthly value</th>
90
+ <td>{{ object.calculated_rc }}</td>
91
+ </tr>
88
92
  <tr>
89
93
  <th scope="row">Non recuring costs</th>
90
94
  <td>{{ object.nrc }}</td>
@@ -138,16 +142,18 @@
138
142
  <div class="row">
139
143
  <div class="col col-md-12">
140
144
  <div class="card">
141
- <h5 class="card-header">Invoices</h5>
142
- {% render_table invoices_table %}
143
- {% if perms.netbox_contract.add_invoice %}
144
- <div class="card-footer text-end noprint">
145
- <a href="{% url 'plugins:netbox_contract:invoice_add' %}?contracts={{ object.pk }}" class="btn btn-primary">
145
+ <h5 class="card-header">
146
+ Invoices
147
+ {% if perms.netbox_contract.add_invoice %}
148
+ <div class="card-actions">
149
+ <a href="{% url 'plugins:netbox_contract:invoice_add' %}?contracts={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
146
150
  <i class="mdi mdi-plus-thick" aria-hidden="true"></i>
147
151
  Add an invoice
148
152
  </a>
149
- </div>
150
- {% endif %}
153
+ </div>
154
+ {% endif %}
155
+ </h5>
156
+ {% render_table invoices_table %}
151
157
  </div>
152
158
  </div>
153
159
  </div>
@@ -19,10 +19,15 @@
19
19
  <th scope="row">Number</th>
20
20
  <td>{{ object.number }}</td>
21
21
  </tr>
22
+ <tr>
23
+ <th scope="row">Template</th>
24
+ <td>{{ object.template }}</td>
25
+ </tr>
22
26
  <tr>
23
27
  <th scope="row">Date</th>
24
28
  <td>{{ object.date }}</td>
25
29
  </tr>
30
+ {% if not object.template %}
26
31
  <tr>
27
32
  <th scope="row">Period start</th>
28
33
  <td>{{ object.period_start }}</td>
@@ -31,6 +36,7 @@
31
36
  <th scope="row">Period end</th>
32
37
  <td>{{ object.period_end }}</td>
33
38
  </tr>
39
+ {% endif %}
34
40
  <tr>
35
41
  <th scope="row">Currency</th>
36
42
  <td>{{ object.currency }}</td>
@@ -182,6 +182,16 @@ urlpatterns = (
182
182
  views.InvoiceLineBulkImportView.as_view(),
183
183
  name='invoiceline_import',
184
184
  ),
185
+ path(
186
+ 'invoiceline/edit/',
187
+ views.InvoiceLineBulkEditView.as_view(),
188
+ name='invoiceline_bulk_edit',
189
+ ),
190
+ path(
191
+ 'invoiceline/delete/',
192
+ views.InvoiceLineBulkDeleteView.as_view(),
193
+ name='invoiceline_bulk_delete',
194
+ ),
185
195
  path(
186
196
  'invoiceline/<int:pk>/',
187
197
  views.InvoiceLineView.as_view(),
@@ -219,6 +229,16 @@ urlpatterns = (
219
229
  views.AccountingDimensionBulkImportView.as_view(),
220
230
  name='accountingdimension_import',
221
231
  ),
232
+ path(
233
+ 'accountingdimension/edit/',
234
+ views.AccountingDimensionBulkEditView.as_view(),
235
+ name='accountingdimension_bulk_edit',
236
+ ),
237
+ path(
238
+ 'accountingdimension/delete/',
239
+ views.AccountingDimensionBulkDeleteView.as_view(),
240
+ name='accountingdimension_bulk_delete',
241
+ ),
222
242
  path(
223
243
  'accountingdimension/<int:pk>/',
224
244
  views.AccountingDimensionView.as_view(),
@@ -4,7 +4,8 @@ from dateutil.relativedelta import relativedelta
4
4
  from django.conf import settings
5
5
  from django.contrib.contenttypes.models import ContentType
6
6
  from django.core.exceptions import ObjectDoesNotExist
7
- from django.db.models import F
7
+ from django.db.models import Case, F, When
8
+ from django.db.models.functions import Round
8
9
  from django.shortcuts import get_object_or_404, render
9
10
  from netbox.views import generic
10
11
  from netbox.views.generic.utils import get_prerequisite_model
@@ -129,7 +130,12 @@ class ContractAssignmentBulkImportView(generic.BulkImportView):
129
130
 
130
131
  @register_model_view(Contract)
131
132
  class ContractView(generic.ObjectView):
132
- queryset = Contract.objects.annotate(yrc=F('mrc') * 12)
133
+ queryset = Contract.objects.annotate(
134
+ calculated_rc=Round(
135
+ Case(When(yrc__gt=0, then=F('yrc') / 12), default=F('mrc') * 12),
136
+ precision=2,
137
+ )
138
+ )
133
139
 
134
140
  def get_extra_context(self, request, instance):
135
141
  invoices_table = tables.InvoiceListTable(instance.invoices.all())
@@ -152,7 +158,12 @@ class ContractView(generic.ObjectView):
152
158
 
153
159
 
154
160
  class ContractListView(generic.ObjectListView):
155
- queryset = Contract.objects.annotate(yrc=F('mrc') * 12)
161
+ queryset = Contract.objects.annotate(
162
+ calculated_rc=Round(
163
+ Case(When(yrc__gt=0, then=F('yrc') / 12), default=F('mrc') * 12),
164
+ precision=2,
165
+ )
166
+ )
156
167
  table = tables.ContractListTable
157
168
  filterset = filtersets.ContractFilterSet
158
169
  filterset_form = forms.ContractFilterSetForm
@@ -267,7 +278,9 @@ class InvoiceEditView(generic.ObjectEditView):
267
278
  contract = Contract.objects.get(pk=initial_data['contracts'])
268
279
 
269
280
  try:
270
- last_invoice = contract.invoices.latest('period_end')
281
+ last_invoice = contract.invoices.filter(template=False).latest(
282
+ 'period_end'
283
+ )
271
284
  new_period_start = last_invoice.period_end + timedelta(days=1)
272
285
  except ObjectDoesNotExist:
273
286
  if contract.start_date:
@@ -281,7 +294,16 @@ class InvoiceEditView(generic.ObjectEditView):
281
294
  new_period_end = new_period_start + delta - timedelta(days=1)
282
295
  initial_data['period_end'] = new_period_end
283
296
 
284
- initial_data['amount'] = contract.mrc * contract.invoice_frequency
297
+ if contract.yrc:
298
+ if contract.invoice_frequency == 12:
299
+ initial_data['amount'] = contract.yrc
300
+ else:
301
+ initial_data['amount'] = round(
302
+ contract.yrc / 12 * contract.invoice_frequency, 2
303
+ )
304
+ else:
305
+ initial_data['amount'] = contract.mrc * contract.invoice_frequency
306
+
285
307
  initial_data['currency'] = contract.currency
286
308
  if contract.accounting_dimensions:
287
309
  initial_data['accounting_dimensions'] = contract.accounting_dimensions
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: netbox-contract
3
- Version: 2.2.0
3
+ Version: 2.2.2
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
@@ -57,9 +57,11 @@ src/netbox_contract/migrations/0030_alter_invoiceline_accounting_dimensions.py
57
57
  src/netbox_contract/migrations/0031_contract_invoice_template_invoice_template.py
58
58
  src/netbox_contract/migrations/0032_alter_invoice_period_end_alter_invoice_period_start_and_more.py
59
59
  src/netbox_contract/migrations/0033_remove_contract_invoice_template.py
60
+ src/netbox_contract/migrations/0034_contract_yrc_alter_contract_mrc.py
61
+ src/netbox_contract/migrations/0035_accountingdimension_status_and_more.py
62
+ src/netbox_contract/migrations/0036_alter_invoiceline_accounting_dimensions.py
60
63
  src/netbox_contract/migrations/__init__.py
61
64
  src/netbox_contract/templates/contract_assignments_bottom.html
62
- src/netbox_contract/templates/contract_assignments_bottom_old.html
63
65
  src/netbox_contract/templates/contract_list_bottom.html
64
66
  src/netbox_contract/templates/netbox_contract/accountingdimension.html
65
67
  src/netbox_contract/templates/netbox_contract/contract.html
@@ -1,21 +0,0 @@
1
- {% load render_table from django_tables2 %}
2
- {% if perms.netbox_contract.view_contract %}
3
- <div class="row">
4
- <div class="col col-md-12">
5
- <div class="card">
6
- <h5 class="card-header">Contracts Assignments</h5>
7
- {% if assignments_table %}
8
- {% render_table assignments_table %}
9
- {% endif %}
10
- {% if perms.netbox_contract.add_contractassignment %}
11
- <div class="card-footer text-end noprint">
12
- <a href="{% url 'plugins:netbox_contract:contractassignment_add' %}?content_type={{ object|content_type_id }}&object_id={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary">
13
- <i class="mdi mdi-plus-thick" aria-hidden="true"></i>
14
- Add assignment
15
- </a>
16
- </div>
17
- {% endif %}
18
- </div>
19
- </div>
20
- </div>
21
- {% endif %}
@@ -1,23 +0,0 @@
1
- {% load render_table from django_tables2 %}
2
- {% if perms.netbox_contract.view_contract %}
3
- <div class="row">
4
- <div class="col col-md-12">
5
- <div class="card">
6
- <h5 class="card-header">Contracts Assignments</h5>
7
- {% if assignments_table %}
8
- <div class="card-body table-responsive">
9
- {% render_table assignments_table %}
10
- </div>
11
- {% endif %}
12
- {% if perms.netbox_contract.add_contractassignment %}
13
- <div class="card-footer text-end noprint">
14
- <a href="{% url 'plugins:netbox_contract:contractassignment_add' %}?content_type={{ object|content_type_id }}&object_id={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary">
15
- <i class="mdi mdi-plus-thick" aria-hidden="true"></i>
16
- Add assignment
17
- </a>
18
- </div>
19
- {% endif %}
20
- </div>
21
- </div>
22
- </div>
23
- {% endif %}
File without changes