diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b4fb13..c73efcb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,10 @@
## Version 2
+### Version 2.2.0
+
+* [140](https://github.com/mlebreuil/netbox-contract/issues/140) Add the "Invoice line" and "Accounting dimension" models. In order to simplify invoices creation, it is possible to selsct one invoice as the template for each contract; Its accounting lines will automatically be copied to the new invoices for the contract. The amount of the first line will be updated so that the sum of the amount for each invoice line match the invoice amount.
+
### Version 2.1.2
* [127](https://github.com/mlebreuil/netbox-contract/issues/135) Fix service provider creation issue
diff --git a/docs/accounting_dimensions.md b/docs/accounting_dimensions.md
new file mode 100644
index 0000000..09b5d7f
--- /dev/null
+++ b/docs/accounting_dimensions.md
@@ -0,0 +1,5 @@
+# Accounting dimensions
+
+![Accounting dimensions](img/accrounting_dimensions.png "accounting dimensions")
+
+
diff --git a/docs/contract.md b/docs/contract.md
new file mode 100644
index 0000000..c36f693
--- /dev/null
+++ b/docs/contract.md
@@ -0,0 +1,7 @@
+# Contract
+
+![Contract](img/contract.png "contract")
+
+Linked objects:
+
+![Contract linked objects](img/contract_linked_objects.png "contract linked objects")
diff --git a/docs/img/accounting_dimensions.png b/docs/img/accounting_dimensions.png
new file mode 100644
index 0000000..d7504e6
Binary files /dev/null and b/docs/img/accounting_dimensions.png differ
diff --git a/docs/img/contract.png b/docs/img/contract.png
new file mode 100644
index 0000000..16f8125
Binary files /dev/null and b/docs/img/contract.png differ
diff --git a/docs/img/contract_linked_objects.png b/docs/img/contract_linked_objects.png
new file mode 100644
index 0000000..9a55fd2
Binary files /dev/null and b/docs/img/contract_linked_objects.png differ
diff --git a/docs/img/invoice.png b/docs/img/invoice.png
new file mode 100644
index 0000000..ab20101
Binary files /dev/null and b/docs/img/invoice.png differ
diff --git a/docs/img/invoice_line.png b/docs/img/invoice_line.png
new file mode 100644
index 0000000..8b7724c
Binary files /dev/null and b/docs/img/invoice_line.png differ
diff --git a/docs/img/invoice_linked_objects.png b/docs/img/invoice_linked_objects.png
new file mode 100644
index 0000000..c150349
Binary files /dev/null and b/docs/img/invoice_linked_objects.png differ
diff --git a/docs/invoice.md b/docs/invoice.md
new file mode 100644
index 0000000..21ca721
--- /dev/null
+++ b/docs/invoice.md
@@ -0,0 +1,7 @@
+# Invoice
+
+![Invoice](img/invoice.png "invoice")
+
+Linked objects:
+
+![Invoice linked objects](img/invoice_linked_objects.png "invoice linked objects")
diff --git a/docs/invoice_line.md b/docs/invoice_line.md
new file mode 100644
index 0000000..89a3960
--- /dev/null
+++ b/docs/invoice_line.md
@@ -0,0 +1,5 @@
+# Invoice line
+
+![Invoice line](img/invoice_line.png "invoice line")
+
+
diff --git a/mkdocs.yml b/mkdocs.yml
index d5c8af6..d916d2c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -4,6 +4,10 @@ repo_url: https://github.com/mlebreuil/netbox-contract
repo_name: netbox-contract
nav:
- Home: index.md
+ - Contract: contract.md
+ - Invoice: invoice.md
+ - Invoice line: invoice_line.md
+ - Accounting dimensions: accounting_dimensions.md
- Contributing: contributing.md
- Changelog: changelog.md
theme:
diff --git a/pyproject.toml b/pyproject.toml
index a3c256b..746b688 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "netbox-contract"
-version = "2.1.3"
+version = "2.2.0"
authors = [
{ name="Marc Lebreuil", email="marc@famillelebreuil.net" },
]
diff --git a/src/netbox_contract/__init__.py b/src/netbox_contract/__init__.py
index ce920fa..581a217 100644
--- a/src/netbox_contract/__init__.py
+++ b/src/netbox_contract/__init__.py
@@ -5,7 +5,7 @@ class ContractsConfig(PluginConfig):
name = 'netbox_contract'
verbose_name = 'Netbox contract'
description = 'Contract management plugin for Netbox'
- version = '2.1.3'
+ version = '2.2.0'
author = 'Marc Lebreuil'
author_email = 'marc@famillelebreuil.net'
base_url = 'contracts'
diff --git a/src/netbox_contract/api/serializers.py b/src/netbox_contract/api/serializers.py
index 0aea2b3..ea2d905 100644
--- a/src/netbox_contract/api/serializers.py
+++ b/src/netbox_contract/api/serializers.py
@@ -7,7 +7,14 @@
from tenancy.api.nested_serializers import NestedTenantSerializer
from utilities.api import get_serializer_for_model
-from ..models import Contract, ContractAssignment, Invoice, ServiceProvider
+from ..models import (
+ AccountingDimension,
+ Contract,
+ ContractAssignment,
+ Invoice,
+ InvoiceLine,
+ ServiceProvider,
+)
class NestedServiceProviderSerializer(WritableNestedSerializer):
@@ -52,6 +59,28 @@ class Meta:
brief_fields = ('id', 'url', 'display', 'contract', 'content_object')
+class NestedInvoicelineSerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='plugins-api:netbox_contract-api:InvoiceLine-detail'
+ )
+
+ class Meta:
+ model = InvoiceLine
+ fields = ('id', 'url', 'display', 'invoice', 'amount')
+ brief_fields = ('id', 'url', 'display', 'invoice', 'amount')
+
+
+class NestedAccountingDimensionSerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='plugins-api:netbox_contract-api:AccountingDimension-detail'
+ )
+
+ class Meta:
+ model = AccountingDimension
+ fields = ('id', 'url', 'display', 'name', 'value')
+ brief_fields = ('id', 'url', 'display', 'name', 'value')
+
+
class ContractSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_contract-api:contract-detail'
@@ -192,3 +221,48 @@ def get_content_object(self, instance):
)
context = {'request': self.context['request']}
return serializer(instance.content_object, context=context).data
+
+
+class InvoiceLineSerializer(NetBoxModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='plugins-api:netbox_contract-api:invoiceline-detail'
+ )
+
+ class Meta:
+ model = InvoiceLine
+ fields = (
+ 'id',
+ 'url',
+ 'display',
+ 'invoice',
+ 'amount',
+ 'currency',
+ 'comments',
+ 'tags',
+ 'custom_fields',
+ 'created',
+ 'last_updated',
+ )
+ brief_fields = ('invoice', 'amount', 'url', 'display', 'name')
+
+
+class AccountingDimensionSerializer(NetBoxModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='plugins-api:netbox_contract-api:accountingdimension-detail'
+ )
+
+ class Meta:
+ model = AccountingDimension
+ fields = (
+ 'id',
+ 'url',
+ 'display',
+ 'name',
+ 'value',
+ 'comments',
+ 'tags',
+ 'custom_fields',
+ 'created',
+ 'last_updated',
+ )
+ brief_fields = ('name', 'value', 'url', 'display')
diff --git a/src/netbox_contract/api/urls.py b/src/netbox_contract/api/urls.py
index e3506c7..8c3502a 100644
--- a/src/netbox_contract/api/urls.py
+++ b/src/netbox_contract/api/urls.py
@@ -9,5 +9,7 @@
router.register('invoices', views.InvoiceViewSet)
router.register('serviceproviders', views.ServiceProviderViewSet)
router.register('contractassignment', views.ContractAssignmentViewSet)
+router.register('invoiceline', views.InvoiceLineViewSet)
+router.register('accountingdimension', views.AccountingDimensionViewSet)
urlpatterns = router.urls
diff --git a/src/netbox_contract/api/views.py b/src/netbox_contract/api/views.py
index 2114dc2..d4d2edb 100644
--- a/src/netbox_contract/api/views.py
+++ b/src/netbox_contract/api/views.py
@@ -3,8 +3,10 @@
from .. import filtersets, models
from .serializers import (
+ AccountingDimensionSerializer,
ContractAssignmentSerializer,
ContractSerializer,
+ InvoiceLineSerializer,
InvoiceSerializer,
ServiceProviderSerializer,
)
@@ -31,3 +33,13 @@ class ServiceProviderViewSet(NetBoxModelViewSet):
class ContractAssignmentViewSet(NetBoxModelViewSet):
queryset = models.ContractAssignment.objects.prefetch_related('contract', 'tags')
serializer_class = ContractAssignmentSerializer
+
+
+class InvoiceLineViewSet(NetBoxModelViewSet):
+ queryset = models.InvoiceLine.objects.prefetch_related('invoice', 'tags')
+ serializer_class = InvoiceLineSerializer
+
+
+class AccountingDimensionViewSet(NetBoxModelViewSet):
+ queryset = models.AccountingDimension.objects.prefetch_related('tags')
+ serializer_class = AccountingDimensionSerializer
diff --git a/src/netbox_contract/filtersets.py b/src/netbox_contract/filtersets.py
index 767b12b..d156483 100644
--- a/src/netbox_contract/filtersets.py
+++ b/src/netbox_contract/filtersets.py
@@ -1,7 +1,14 @@
from django.db.models import Q
from netbox.filtersets import NetBoxModelFilterSet
-from .models import Contract, ContractAssignment, Invoice, ServiceProvider
+from .models import (
+ AccountingDimension,
+ Contract,
+ ContractAssignment,
+ Invoice,
+ InvoiceLine,
+ ServiceProvider,
+)
class ContractFilterSet(NetBoxModelFilterSet):
@@ -14,7 +21,7 @@ def search(self, queryset, name, value):
Q(name__icontains=value)
| Q(external_reference__icontains=value)
| Q(comments__icontains=value),
- Q(status__iexact='Active')
+ Q(status__iexact='Active'),
)
@@ -25,8 +32,7 @@ class Meta:
def search(self, queryset, name, value):
return queryset.filter(
- Q(number__icontains=value)
- | Q(contracts__name__icontains=value)
+ Q(number__icontains=value) | Q(contracts__name__icontains=value)
)
@@ -46,3 +52,25 @@ class Meta:
def search(self, queryset, name, value):
return queryset.filter(Q(contract__name__icontains=value))
+
+
+class InvoiceLineFilterSet(NetBoxModelFilterSet):
+ class Meta:
+ model = InvoiceLine
+ fields = ('id', 'invoice')
+
+ def search(self, queryset, name, value):
+ return queryset.filter(
+ Q(comments__icontains=value) | Q(invoice__name__icontains=value)
+ )
+
+
+class AccountingDimensionFilterSet(NetBoxModelFilterSet):
+ class Meta:
+ model = AccountingDimension
+ fields = ('name', 'value')
+
+ def search(self, queryset, name, value):
+ return queryset.filter(
+ Q(comments__icontains=value) | Q(invoice__name__icontains=value)
+ )
diff --git a/src/netbox_contract/forms.py b/src/netbox_contract/forms.py
index 6799b6a..64fe1df 100644
--- a/src/netbox_contract/forms.py
+++ b/src/netbox_contract/forms.py
@@ -1,6 +1,7 @@
from django import forms
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ObjectDoesNotExist, ValidationError
from extras.filters import TagFilter
from netbox.forms import (
NetBoxModelBulkEditForm,
@@ -24,10 +25,12 @@
from .constants import SERVICE_PROVIDER_MODELS
from .models import (
+ AccountingDimension,
Contract,
ContractAssignment,
InternalEntityChoices,
Invoice,
+ InvoiceLine,
ServiceProvider,
StatusChoices,
)
@@ -46,6 +49,9 @@ def __init__(self, *args, **kwargs):
self.widget.attrs['placeholder'] = str(default_dimensions)
+# Contract
+
+
class ContractForm(NetBoxModelForm):
comments = CommentField()
@@ -98,11 +104,6 @@ def __init__(self, *args, **kwargs):
].queryset = ServiceProvider.objects.all()
self.fields['external_partie_object'].initial = None
- # initialize accounting dimentsions widget
- # self.fields[
- # 'accounting_dimensions'
- # ].widget.attrs['placeholder'] = '{"key": "value"}'
-
# Initialise fields settings
mandatory_fields = plugin_settings.get('mandatory_contract_fields')
for field in mandatory_fields:
@@ -143,46 +144,6 @@ class Meta:
}
-class InvoiceForm(NetBoxModelForm):
- contracts = DynamicModelMultipleChoiceField(
- queryset=Contract.objects.all(), required=False
- )
- accounting_dimensions = Dimensions(required=False)
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- # Initialise fields settings
- mandatory_fields = plugin_settings.get('mandatory_invoice_fields')
- for field in mandatory_fields:
- self.fields[field].required = True
- hidden_fields = plugin_settings.get('hidden_invoice_fields')
- for field in hidden_fields:
- if not self.fields[field].required:
- self.fields[field].widget = forms.HiddenInput()
-
- class Meta:
- model = Invoice
- fields = (
- 'number',
- 'date',
- 'contracts',
- 'period_start',
- 'period_end',
- 'currency',
- 'accounting_dimensions',
- 'amount',
- 'documents',
- 'comments',
- 'tags',
- )
- widgets = {
- 'date': DatePicker(),
- 'period_start': DatePicker(),
- 'period_end': DatePicker(),
- }
-
-
class ContractFilterSetForm(NetBoxModelFilterSetForm):
model = Contract
@@ -193,13 +154,6 @@ class ContractFilterSetForm(NetBoxModelFilterSetForm):
parent = DynamicModelChoiceField(queryset=Contract.objects.all(), required=False)
-class InvoiceFilterSetForm(NetBoxModelFilterSetForm):
- model = Invoice
- contracts = DynamicModelMultipleChoiceField(
- queryset=Contract.objects.all(), required=False
- )
-
-
class ContractCSVForm(NetBoxModelImportForm):
external_partie_object_type = CSVContentTypeField(
queryset=ContentType.objects.all(),
@@ -271,6 +225,120 @@ class ContractBulkEditForm(NetBoxModelBulkEditForm):
model = Contract
+# Invoice
+
+
+class InvoiceForm(NetBoxModelForm):
+ contracts = DynamicModelMultipleChoiceField(
+ queryset=Contract.objects.all(), required=False
+ )
+ accounting_dimensions = Dimensions(required=False)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Initialise fields settings
+ mandatory_fields = plugin_settings.get('mandatory_invoice_fields')
+ for field in mandatory_fields:
+ self.fields[field].required = True
+ hidden_fields = plugin_settings.get('hidden_invoice_fields')
+ for field in hidden_fields:
+ if not self.fields[field].required:
+ self.fields[field].widget = forms.HiddenInput()
+
+ def clean(self):
+ super().clean()
+
+ # template checks
+ if self.cleaned_data['template']:
+ # Check that there is only one invoice template per contract
+ contracts = self.cleaned_data['contracts']
+ for contract in contracts:
+ for invoice in contract.invoices.all():
+ if invoice.template and invoice.pk != self.instance.pk:
+ raise ValidationError(
+ 'Only one invoice template allowed per contract'
+ )
+
+ # Prefix the invoice name with _template
+ self.cleaned_data['number'] = '_template_' + self.cleaned_data['number']
+
+ def save(self, *args, **kwargs):
+ is_new = not bool(self.instance.pk)
+
+ instance = super().save(*args, **kwargs)
+
+ if is_new and not self.cleaned_data['template']:
+ contracts = self.cleaned_data['contracts']
+
+ for contract in contracts:
+ try:
+ template_exists = True
+ invoice_template = Invoice.objects.get(
+ template=True, contracts=contract
+ )
+ except ObjectDoesNotExist:
+ template_exists = False
+
+ if template_exists:
+ first = True
+ for line in invoice_template.invoicelines.all():
+ dimensions = line.accounting_dimensions.all()
+ line.pk = None
+ line.id = None
+ line._state.adding = True
+ line.invoice = self.instance
+
+ # adjust the first invoice line amount
+ amount = self.cleaned_data['amount']
+ if (
+ first
+ and amount != invoice_template.total_invoicelines_amount
+ ):
+ line.amount = (
+ line.amount
+ + amount
+ - invoice_template.total_invoicelines_amount
+ )
+
+ line.save()
+
+ for dimension in dimensions:
+ line.accounting_dimensions.add(dimension)
+ first = False
+
+ return instance
+
+ class Meta:
+ model = Invoice
+ fields = (
+ 'number',
+ 'date',
+ 'contracts',
+ 'template',
+ 'period_start',
+ 'period_end',
+ 'currency',
+ 'accounting_dimensions',
+ 'amount',
+ 'documents',
+ 'comments',
+ 'tags',
+ )
+ widgets = {
+ 'date': DatePicker(),
+ 'period_start': DatePicker(),
+ 'period_end': DatePicker(),
+ }
+
+
+class InvoiceFilterSetForm(NetBoxModelFilterSetForm):
+ model = Invoice
+ contracts = DynamicModelMultipleChoiceField(
+ queryset=Contract.objects.all(), required=False
+ )
+
+
class InvoiceCSVForm(NetBoxModelImportForm):
contracts = CSVModelChoiceField(
queryset=Contract.objects.all(),
@@ -284,6 +352,7 @@ class Meta:
'number',
'date',
'contracts',
+ 'template',
'period_start',
'period_end',
'currency',
@@ -368,3 +437,108 @@ class ContractAssignmentImportForm(NetBoxModelImportForm):
class Meta:
model = ContractAssignment
fields = ['content_type', 'object_id', 'contract', 'tags']
+
+
+# InvoiceLine
+
+
+class InvoiceLineForm(NetBoxModelForm):
+ invoice = DynamicModelChoiceField(queryset=Invoice.objects.all())
+ accounting_dimensions = forms.ModelMultipleChoiceField(
+ queryset=AccountingDimension.objects.all(), required=False
+ )
+
+ def clean(self):
+ super().clean()
+
+ # check for duplicate dimensions
+ accounting_dimensions = self.cleaned_data['accounting_dimensions']
+ dimensions_names = []
+ for dimension in accounting_dimensions:
+ if dimension.name in dimensions_names:
+ raise ValidationError('duplicate accounting dimension')
+ else:
+ dimensions_names.append(dimension.name)
+
+ class Meta:
+ model = InvoiceLine
+ fields = [
+ 'invoice',
+ 'currency',
+ 'amount',
+ 'accounting_dimensions',
+ 'comments',
+ 'tags',
+ ]
+
+
+class InvoiceLineFilterSetForm(NetBoxModelFilterSetForm):
+ model = InvoiceLine
+ invoice = DynamicModelChoiceField(queryset=Invoice.objects.all())
+ accounting_dimensions = DynamicModelMultipleChoiceField(
+ queryset=AccountingDimension.objects.all()
+ )
+
+
+class InvoiceLineImportForm(NetBoxModelImportForm):
+ invoice = CSVModelChoiceField(
+ queryset=Invoice.objects.all(),
+ to_field_name='number',
+ help_text='Invoice number',
+ )
+ accounting_dimensions = CSVModelChoiceField(
+ queryset=AccountingDimension.objects.all(),
+ help_text='accounting dimention in the form name, value',
+ )
+
+ class Meta:
+ model = InvoiceLine
+ fields = [
+ 'invoice',
+ 'currency',
+ 'amount',
+ 'accounting_dimensions',
+ 'comments',
+ 'tags',
+ ]
+
+
+class InvoiceLineBulkEditForm(NetBoxModelBulkEditForm):
+ invoice = DynamicModelChoiceField(queryset=Invoice.objects.all(), required=False)
+ accounting_dimensions = DynamicModelMultipleChoiceField(
+ queryset=AccountingDimension.objects.all(), required=False
+ )
+ model = InvoiceLine
+
+
+# AccountingDimension
+
+
+class AccountingDimensionForm(NetBoxModelForm):
+ class Meta:
+ model = AccountingDimension
+ fields = [
+ 'name',
+ 'value',
+ 'comments',
+ 'tags',
+ ]
+
+
+class AccountingDimensionFilterSetForm(NetBoxModelFilterSetForm):
+ model = AccountingDimension
+
+
+class AccountingDimensionImportForm(NetBoxModelImportForm):
+ class Meta:
+ model = AccountingDimension
+ fields = [
+ 'name',
+ 'value',
+ 'comments',
+ 'tags',
+ ]
+
+
+class AccountingDimensionBulkEditForm(NetBoxModelBulkEditForm):
+ model = AccountingDimension
diff --git a/src/netbox_contract/migrations/0027_invoiceline.py b/src/netbox_contract/migrations/0027_invoiceline.py
new file mode 100644
index 0000000..306ab8e
--- /dev/null
+++ b/src/netbox_contract/migrations/0027_invoiceline.py
@@ -0,0 +1,57 @@
+# Generated by Django 5.0.6 on 2024-06-23 13:12
+
+import django.db.models.deletion
+import taggit.managers
+import utilities.json
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('extras', '0115_convert_dashboard_widgets'),
+ ('netbox_contract', '0026_auto_20240421_1550'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='InvoiceLine',
+ fields=[
+ (
+ 'id',
+ models.BigAutoField(
+ auto_created=True, primary_key=True, serialize=False
+ ),
+ ),
+ ('created', models.DateTimeField(auto_now_add=True, null=True)),
+ ('last_updated', models.DateTimeField(auto_now=True, null=True)),
+ (
+ 'custom_field_data',
+ models.JSONField(
+ blank=True,
+ default=dict,
+ encoder=utilities.json.CustomFieldJSONEncoder,
+ ),
+ ),
+ ('currency', models.CharField(default='usd', max_length=3)),
+ ('amount', models.DecimalField(decimal_places=2, max_digits=10)),
+ ('comments', models.TextField(blank=True)),
+ (
+ 'invoice',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='invoicelines',
+ to='netbox_contract.invoice',
+ ),
+ ),
+ (
+ 'tags',
+ taggit.managers.TaggableManager(
+ through='extras.TaggedItem', to='extras.Tag'
+ ),
+ ),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ ]
diff --git a/src/netbox_contract/migrations/0028_invoiceline_accounting_dimensions.py b/src/netbox_contract/migrations/0028_invoiceline_accounting_dimensions.py
new file mode 100644
index 0000000..28f058f
--- /dev/null
+++ b/src/netbox_contract/migrations/0028_invoiceline_accounting_dimensions.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.0.6 on 2024-06-30 20:19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('netbox_contract', '0027_invoiceline'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='invoiceline',
+ name='accounting_dimensions',
+ field=models.JSONField(null=True),
+ ),
+ ]
diff --git a/src/netbox_contract/migrations/0029_remove_invoiceline_accounting_dimensions_and_more.py b/src/netbox_contract/migrations/0029_remove_invoiceline_accounting_dimensions_and_more.py
new file mode 100644
index 0000000..fa2eb1b
--- /dev/null
+++ b/src/netbox_contract/migrations/0029_remove_invoiceline_accounting_dimensions_and_more.py
@@ -0,0 +1,57 @@
+# Generated by Django 5.0.6 on 2024-07-13 09:43
+
+import taggit.managers
+import utilities.json
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('extras', '0115_convert_dashboard_widgets'),
+ ('netbox_contract', '0028_invoiceline_accounting_dimensions'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='invoiceline',
+ name='accounting_dimensions',
+ ),
+ migrations.CreateModel(
+ name='AccountingDimension',
+ fields=[
+ (
+ 'id',
+ models.BigAutoField(
+ auto_created=True, primary_key=True, serialize=False
+ ),
+ ),
+ ('created', models.DateTimeField(auto_now_add=True, null=True)),
+ ('last_updated', models.DateTimeField(auto_now=True, null=True)),
+ (
+ 'custom_field_data',
+ models.JSONField(
+ blank=True,
+ default=dict,
+ encoder=utilities.json.CustomFieldJSONEncoder,
+ ),
+ ),
+ ('name', models.CharField(max_length=20)),
+ ('value', models.CharField(max_length=20)),
+ ('comments', models.TextField(blank=True)),
+ (
+ 'tags',
+ taggit.managers.TaggableManager(
+ through='extras.TaggedItem', to='extras.Tag'
+ ),
+ ),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.AddField(
+ model_name='invoiceline',
+ name='accounting_dimensions',
+ field=models.ManyToManyField(to='netbox_contract.accountingdimension'),
+ ),
+ ]
diff --git a/src/netbox_contract/migrations/0030_alter_invoiceline_accounting_dimensions.py b/src/netbox_contract/migrations/0030_alter_invoiceline_accounting_dimensions.py
new file mode 100644
index 0000000..c13763a
--- /dev/null
+++ b/src/netbox_contract/migrations/0030_alter_invoiceline_accounting_dimensions.py
@@ -0,0 +1,21 @@
+# Generated by Django 5.0.6 on 2024-07-13 12:39
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('netbox_contract', '0029_remove_invoiceline_accounting_dimensions_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='invoiceline',
+ name='accounting_dimensions',
+ field=models.ManyToManyField(
+ blank=True,
+ related_name='invoicelines',
+ to='netbox_contract.accountingdimension',
+ ),
+ ),
+ ]
diff --git a/src/netbox_contract/migrations/0031_contract_invoice_template_invoice_template.py b/src/netbox_contract/migrations/0031_contract_invoice_template_invoice_template.py
new file mode 100644
index 0000000..2e4ab2d
--- /dev/null
+++ b/src/netbox_contract/migrations/0031_contract_invoice_template_invoice_template.py
@@ -0,0 +1,29 @@
+# Generated by Django 5.0.6 on 2024-07-16 13:06
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('netbox_contract', '0030_alter_invoiceline_accounting_dimensions'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='contract',
+ name='invoice_template',
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='contract',
+ to='netbox_contract.invoice',
+ ),
+ ),
+ migrations.AddField(
+ model_name='invoice',
+ name='template',
+ field=models.BooleanField(blank=True, null=True),
+ ),
+ ]
diff --git a/src/netbox_contract/migrations/0032_alter_invoice_period_end_alter_invoice_period_start_and_more.py b/src/netbox_contract/migrations/0032_alter_invoice_period_end_alter_invoice_period_start_and_more.py
new file mode 100644
index 0000000..68e2148
--- /dev/null
+++ b/src/netbox_contract/migrations/0032_alter_invoice_period_end_alter_invoice_period_start_and_more.py
@@ -0,0 +1,27 @@
+# Generated by Django 5.0.6 on 2024-07-23 19:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('netbox_contract', '0031_contract_invoice_template_invoice_template'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='invoice',
+ name='period_end',
+ field=models.DateField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name='invoice',
+ name='period_start',
+ field=models.DateField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name='invoice',
+ name='template',
+ field=models.BooleanField(blank=True, default=False, null=True),
+ ),
+ ]
diff --git a/src/netbox_contract/migrations/0033_remove_contract_invoice_template.py b/src/netbox_contract/migrations/0033_remove_contract_invoice_template.py
new file mode 100644
index 0000000..e3a179f
--- /dev/null
+++ b/src/netbox_contract/migrations/0033_remove_contract_invoice_template.py
@@ -0,0 +1,19 @@
+# Generated by Django 5.0.6 on 2024-07-24 20:40
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ (
+ 'netbox_contract',
+ '0032_alter_invoice_period_end_alter_invoice_period_start_and_more',
+ ),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='contract',
+ name='invoice_template',
+ ),
+ ]
diff --git a/src/netbox_contract/models.py b/src/netbox_contract/models.py
index fc10d4c..9e58f5a 100644
--- a/src/netbox_contract/models.py
+++ b/src/netbox_contract/models.py
@@ -1,5 +1,6 @@
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from netbox.models import NetBoxModel
@@ -40,6 +41,22 @@ class CurrencyChoices(ChoiceSet):
]
+class AccountingDimension(NetBoxModel):
+ name = models.CharField(max_length=20)
+ value = models.CharField(max_length=20)
+ comments = models.TextField(blank=True)
+
+ def get_absolute_url(self):
+ return reverse('plugins:netbox_contract:accountingdimension', args=[self.pk])
+
+ @property
+ def dimension(self):
+ return ''.join([self.name, ':', self.value])
+
+ def __str__(self):
+ return self.dimension
+
+
class ServiceProvider(ContactsMixin, NetBoxModel):
name = models.CharField(max_length=100)
slug = models.SlugField(max_length=100, unique=True)
@@ -145,14 +162,15 @@ def __str__(self):
class Invoice(NetBoxModel):
number = models.CharField(max_length=100)
+ template = models.BooleanField(blank=True, null=True, default=False)
date = models.DateField(blank=True, null=True)
contracts = models.ManyToManyField(
Contract,
related_name='invoices',
blank=True,
)
- period_start = models.DateField()
- period_end = models.DateField()
+ period_start = models.DateField(blank=True, null=True)
+ period_end = models.DateField(blank=True, null=True)
currency = models.CharField(
max_length=3, choices=CurrencyChoices, default=CurrencyChoices.CURRENCY_USD
)
@@ -169,3 +187,47 @@ def __str__(self):
def get_absolute_url(self):
return reverse('plugins:netbox_contract:invoice', args=[self.pk])
+
+ @property
+ def total_invoicelines_amount(self):
+ """
+ Calculates the total amount for all related InvoiceLines.
+ """
+ return sum(invoiceline.amount for invoiceline in self.invoicelines.all())
+
+
+class InvoiceLine(NetBoxModel):
+ invoice = models.ForeignKey(
+ to='Invoice', on_delete=models.CASCADE, related_name='invoicelines'
+ )
+ currency = models.CharField(
+ max_length=3, choices=CurrencyChoices, default=CurrencyChoices.CURRENCY_USD
+ )
+ amount = models.DecimalField(max_digits=10, decimal_places=2)
+ accounting_dimensions = models.ManyToManyField(
+ AccountingDimension, related_name='invoicelines', blank=True
+ )
+ comments = models.TextField(blank=True)
+
+ def get_absolute_url(self):
+ return reverse('plugins:netbox_contract:invoiceline', args=[self.pk])
+
+ def clean(self):
+ super().clean()
+ # Check that the sum of the invoice line amount is not greater the invoice amount
+ amount = self.amount
+ invoice = self.invoice
+ is_new = not bool(self.pk)
+ if is_new:
+ if amount > (invoice.amount - invoice.total_invoicelines_amount):
+ raise ValidationError(
+ 'Sum of invoice line amount greater than invoice amount'
+ )
+ else:
+ previous_amount = self.__class__.objects.get(pk=self.pk).amount
+ if amount > (
+ invoice.amount - invoice.total_invoicelines_amount + previous_amount
+ ):
+ raise ValidationError(
+ 'Sum of invoice line amount greater than invoice amount'
+ )
diff --git a/src/netbox_contract/navigation.py b/src/netbox_contract/navigation.py
index 0434a04..7f21d98 100644
--- a/src/netbox_contract/navigation.py
+++ b/src/netbox_contract/navigation.py
@@ -21,6 +21,24 @@
)
]
+invoiceline_buttons = [
+ PluginMenuButton(
+ link='plugins:netbox_contract:invoiceline_add',
+ title='Add',
+ icon_class='mdi mdi-plus-thick',
+ permissions=['netbox_contract.add_invoice'],
+ )
+]
+
+accountingdimension_buttons = [
+ PluginMenuButton(
+ link='plugins:netbox_contract:accountingdimension_add',
+ title='Add',
+ icon_class='mdi mdi-plus-thick',
+ permissions=['netbox_contract.add_invoice'],
+ )
+]
+
serviceprovider_buttons = [
PluginMenuButton(
link='plugins:netbox_contract:serviceprovider_add',
@@ -44,6 +62,20 @@
permissions=['netbox_contract.view_invoice'],
)
+invoicelines_menu_item = PluginMenuItem(
+ link='plugins:netbox_contract:invoiceline_list',
+ link_text='Invoice lines',
+ buttons=invoiceline_buttons,
+ permissions=['netbox_contract.view_invoice'],
+)
+
+accounting_dimensions_menu_item = PluginMenuItem(
+ link='plugins:netbox_contract:accountingdimension_list',
+ link_text='Accounting dimensions',
+ buttons=accountingdimension_buttons,
+ permissions=['netbox_contract.view_invoice'],
+)
+
service_provider_menu_item = PluginMenuItem(
link='plugins:netbox_contract:serviceprovider_list',
link_text='Service Providers',
@@ -59,6 +91,8 @@
items = (
contract_menu_item,
invoices_menu_item,
+ invoicelines_menu_item,
+ accounting_dimensions_menu_item,
service_provider_menu_item,
contract_assignemnt_menu_item,
)
diff --git a/src/netbox_contract/search.py b/src/netbox_contract/search.py
index ae7e431..87e383b 100644
--- a/src/netbox_contract/search.py
+++ b/src/netbox_contract/search.py
@@ -1,6 +1,6 @@
from netbox.search import SearchIndex
-from .models import Contract, Invoice, ServiceProvider
+from .models import AccountingDimension, Contract, Invoice, InvoiceLine, ServiceProvider
class ServiceProviderIndex(SearchIndex):
@@ -26,4 +26,27 @@ class InvoiceIndex(SearchIndex):
('comments', 5000),
)
-indexes = [ServiceProviderIndex,ContractIndex,InvoiceIndex]
+
+class InvoiceLineIndex(SearchIndex):
+ model = InvoiceLine
+ fields = (
+ ('invoice', 100),
+ ('comments', 5000),
+ )
+
+
+class AccountingDimensionIndex(SearchIndex):
+ model = AccountingDimension
+ fields = (
+ ('name', 20),
+ ('value', 20),
+ )
+
+
+indexes = [
+ ServiceProviderIndex,
+ ContractIndex,
+ InvoiceIndex,
+ InvoiceLineIndex,
+ AccountingDimensionIndex,
+]
diff --git a/src/netbox_contract/tables.py b/src/netbox_contract/tables.py
index e73adf3..95ea92b 100644
--- a/src/netbox_contract/tables.py
+++ b/src/netbox_contract/tables.py
@@ -1,7 +1,14 @@
import django_tables2 as tables
from netbox.tables import NetBoxTable, columns
-from .models import Contract, ContractAssignment, Invoice, ServiceProvider
+from .models import (
+ AccountingDimension,
+ Contract,
+ ContractAssignment,
+ Invoice,
+ InvoiceLine,
+ ServiceProvider,
+)
class ContractAssignmentListTable(NetBoxTable):
@@ -188,3 +195,43 @@ class Meta(NetBoxTable.Meta):
model = ServiceProvider
fields = ('pk', 'name', 'slug', 'portal_url')
default_columns = ('name', 'portal_url')
+
+
+class InvoiceLineListTable(NetBoxTable):
+ invoice = tables.Column(linkify=True)
+ accounting_dimensions = tables.ManyToManyColumn(linkify=True)
+
+ class Meta(NetBoxTable.Meta):
+ model = InvoiceLine
+ fields = (
+ 'pk',
+ 'invoice',
+ 'amount',
+ 'currency',
+ 'accounting_dimensions',
+ 'comments',
+ )
+ default_columns = (
+ 'pk',
+ 'invoice',
+ 'amount',
+ 'currency',
+ 'accounting_dimensions',
+ 'comments',
+ )
+
+
+class AccountingDimensionListTable(NetBoxTable):
+ class Meta(NetBoxTable.Meta):
+ model = AccountingDimension
+ fields = (
+ 'pk',
+ 'name',
+ 'value',
+ 'comments',
+ )
+ default_columns = (
+ 'name',
+ 'value',
+ 'comments',
+ )
diff --git a/src/netbox_contract/templates/netbox_contract/accountingdimension.html b/src/netbox_contract/templates/netbox_contract/accountingdimension.html
new file mode 100644
index 0000000..d04d638
--- /dev/null
+++ b/src/netbox_contract/templates/netbox_contract/accountingdimension.html
@@ -0,0 +1,27 @@
+{% extends 'generic/object.html' %}
+{% load plugins %}
+{% load render_table from django_tables2 %}
+
+{% block content %}
+
+
+
+
+
+
+ name |
+ {{ object.name }} |
+
+
+ value |
+ {{ object.value }} |
+
+
+
+ {% include 'inc/panels/custom_fields.html' %}
+ {% include 'inc/panels/tags.html' %}
+ {% include 'inc/panels/comments.html' %}
+
+
+{% plugin_right_page object %}
+{% endblock content %}
\ No newline at end of file
diff --git a/src/netbox_contract/templates/netbox_contract/invoice.html b/src/netbox_contract/templates/netbox_contract/invoice.html
index c4d720f..72eed86 100644
--- a/src/netbox_contract/templates/netbox_contract/invoice.html
+++ b/src/netbox_contract/templates/netbox_contract/invoice.html
@@ -53,20 +53,39 @@
{% endif %}
+
+ Invoice lines total |
+ {{ object.total_invoicelines_amount }} |
+
-
-
-
-
- {% render_table contracts_table %}
-
-
-
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
+
+
+
+
+ {% render_table invoicelines_table %}
+
+
+
+
+
+
+
+ {% render_table contracts_table %}
+
+
+
{% plugin_right_page object %}
{% endblock content %}
\ No newline at end of file
diff --git a/src/netbox_contract/templates/netbox_contract/invoiceline.html b/src/netbox_contract/templates/netbox_contract/invoiceline.html
new file mode 100644
index 0000000..ce74359
--- /dev/null
+++ b/src/netbox_contract/templates/netbox_contract/invoiceline.html
@@ -0,0 +1,42 @@
+{% extends 'generic/object.html' %}
+{% load plugins %}
+{% load render_table from django_tables2 %}
+{% block breadcrumbs %}
+ {{ block.super }}
+
+ {{ invoice.number }}
+
+{% endblock %}
+{% block content %}
+
+
+
+
+
+
+ Invoice |
+
+ {{ object.invoice.number }}
+ |
+
+
+ Amount |
+ {{ object.amount }} |
+
+
+ Currency |
+ {{ object.currency }} |
+
+
+ Accounting dimentions |
+ {{ object.accounting_dimensions }} |
+
+
+
+ {% include 'inc/panels/custom_fields.html' %}
+ {% include 'inc/panels/tags.html' %}
+ {% include 'inc/panels/comments.html' %}
+
+
+{% plugin_right_page object %}
+{% endblock content %}
\ No newline at end of file
diff --git a/src/netbox_contract/urls.py b/src/netbox_contract/urls.py
index bf4867d..8a028c9 100644
--- a/src/netbox_contract/urls.py
+++ b/src/netbox_contract/urls.py
@@ -166,4 +166,78 @@
name='contractassignment_changelog',
kwargs={'model': models.ContractAssignment},
),
+ # InvoiceLine
+ path(
+ 'invoiceline/',
+ views.InvoiceLineListView.as_view(),
+ name='invoiceline_list',
+ ),
+ path(
+ 'invoiceline/add/',
+ views.InvoiceLineEditView.as_view(),
+ name='invoiceline_add',
+ ),
+ path(
+ 'invoiceline/import/',
+ views.InvoiceLineBulkImportView.as_view(),
+ name='invoiceline_import',
+ ),
+ path(
+ 'invoiceline//',
+ views.InvoiceLineView.as_view(),
+ name='invoiceline',
+ ),
+ path(
+ 'invoiceline//edit/',
+ views.InvoiceLineEditView.as_view(),
+ name='invoiceline_edit',
+ ),
+ path(
+ 'invoiceline//delete/',
+ views.InvoiceLineDeleteView.as_view(),
+ name='invoiceline_delete',
+ ),
+ path(
+ 'invoiceline//changelog/',
+ ObjectChangeLogView.as_view(),
+ name='invoiceline_changelog',
+ kwargs={'model': models.InvoiceLine},
+ ),
+ # AccountingDimension
+ path(
+ 'accountingdimension/',
+ views.AccountingDimensionListView.as_view(),
+ name='accountingdimension_list',
+ ),
+ path(
+ 'accountingdimension/add/',
+ views.AccountingDimensionEditView.as_view(),
+ name='accountingdimension_add',
+ ),
+ path(
+ 'accountingdimension/import/',
+ views.AccountingDimensionBulkImportView.as_view(),
+ name='accountingdimension_import',
+ ),
+ path(
+ 'accountingdimension//',
+ views.AccountingDimensionView.as_view(),
+ name='accountingdimension',
+ ),
+ path(
+ 'accountingdimension//edit/',
+ views.AccountingDimensionEditView.as_view(),
+ name='accountingdimension_edit',
+ ),
+ path(
+ 'accountingdimension//delete/',
+ views.AccountingDimensionDeleteView.as_view(),
+ name='accountingdimension_delete',
+ ),
+ path(
+ 'accountingdimension//changelog/',
+ ObjectChangeLogView.as_view(),
+ name='accountingdimension_changelog',
+ kwargs={'model': models.AccountingDimension},
+ ),
)
diff --git a/src/netbox_contract/views.py b/src/netbox_contract/views.py
index 12f230a..72b6acf 100644
--- a/src/netbox_contract/views.py
+++ b/src/netbox_contract/views.py
@@ -14,7 +14,14 @@
from utilities.views import register_model_view
from . import filtersets, forms, tables
-from .models import Contract, ContractAssignment, Invoice, ServiceProvider
+from .models import (
+ AccountingDimension,
+ Contract,
+ ContractAssignment,
+ Invoice,
+ InvoiceLine,
+ ServiceProvider,
+)
plugin_settings = settings.PLUGINS_CONFIG['netbox_contract']
@@ -218,11 +225,15 @@ class InvoiceView(generic.ObjectView):
def get_extra_context(self, request, instance):
contracts_table = tables.ContractListTable(instance.contracts.all())
contracts_table.configure(request)
+ invoicelines_table = tables.InvoiceLineListTable(instance.invoicelines.all())
+ invoicelines_table.columns.hide('invoice')
+ invoicelines_table.configure(request)
hidden_fields = plugin_settings.get('hidden_invoice_fields')
return {
'hidden_fields': hidden_fields,
'contracts_table': contracts_table,
+ 'invoicelines_table': invoicelines_table,
}
@@ -313,3 +324,121 @@ class InvoiceBulkDeleteView(generic.BulkDeleteView):
queryset = Invoice.objects.all()
filterset = filtersets.InvoiceFilterSet
table = tables.InvoiceListTable
+
+
+# InvoiceLine
+
+
+class InvoiceLineView(generic.ObjectView):
+ queryset = InvoiceLine.objects.all()
+
+
+class InvoiceLineListView(generic.ObjectListView):
+ queryset = InvoiceLine.objects.all()
+ table = tables.InvoiceLineListTable
+ filterset = filtersets.InvoiceLineFilterSet
+ filterset_form = forms.InvoiceLineFilterSetForm
+
+
+class InvoiceLineEditView(generic.ObjectEditView):
+ queryset = InvoiceLine.objects.all()
+ form = forms.InvoiceLineForm
+
+ def get(self, request, *args, **kwargs):
+ """
+ GET request handler
+ Overrides the ObjectEditView function to include form initialization
+ with data from the parent invoice object
+
+ Args:
+ request: The current request
+ """
+ obj = self.get_object(**kwargs)
+ obj = self.alter_object(obj, request, args, kwargs)
+ model = self.queryset.model
+
+ initial_data = normalize_querydict(request.GET)
+ if 'invoice' in initial_data.keys():
+ invoice = Invoice.objects.get(pk=initial_data['invoice'])
+ initial_data['amount'] = invoice.amount - invoice.total_invoicelines_amount
+
+ form = self.form(instance=obj, initial=initial_data)
+ restrict_form_fields(form, request.user)
+
+ return render(
+ request,
+ self.template_name,
+ {
+ 'model': model,
+ 'object': obj,
+ 'form': form,
+ 'return_url': self.get_return_url(request, obj),
+ 'prerequisite_model': get_prerequisite_model(self.queryset),
+ **self.get_extra_context(request, obj),
+ },
+ )
+
+
+class InvoiceLineDeleteView(generic.ObjectDeleteView):
+ queryset = InvoiceLine.objects.all()
+
+
+class InvoiceLineBulkImportView(generic.BulkImportView):
+ queryset = InvoiceLine.objects.all()
+ model_form = forms.InvoiceLineImportForm
+ table = tables.InvoiceLineListTable
+
+
+class InvoiceLineBulkEditView(generic.BulkEditView):
+ queryset = InvoiceLine.objects.annotate()
+ filterset = filtersets.InvoiceLineFilterSet
+ table = tables.InvoiceLineListTable
+ form = forms.InvoiceLineBulkEditForm
+
+
+class InvoiceLineBulkDeleteView(generic.BulkDeleteView):
+ queryset = InvoiceLine.objects.annotate()
+ filterset = filtersets.InvoiceLineFilterSet
+ table = tables.InvoiceLineListTable
+
+
+# Accounting dimension
+
+
+class AccountingDimensionView(generic.ObjectView):
+ queryset = AccountingDimension.objects.all()
+
+
+class AccountingDimensionListView(generic.ObjectListView):
+ queryset = AccountingDimension.objects.all()
+ table = tables.AccountingDimensionListTable
+ filterset = filtersets.AccountingDimensionFilterSet
+ filterset_form = forms.AccountingDimensionFilterSetForm
+
+
+class AccountingDimensionEditView(generic.ObjectEditView):
+ queryset = AccountingDimension.objects.all()
+ form = forms.AccountingDimensionForm
+
+
+class AccountingDimensionDeleteView(generic.ObjectDeleteView):
+ queryset = AccountingDimension.objects.all()
+
+
+class AccountingDimensionBulkImportView(generic.BulkImportView):
+ queryset = AccountingDimension.objects.all()
+ model_form = forms.AccountingDimensionImportForm
+ table = tables.AccountingDimensionListTable
+
+
+class AccountingDimensionBulkEditView(generic.BulkEditView):
+ queryset = AccountingDimension.objects.annotate()
+ filterset = filtersets.AccountingDimensionFilterSet
+ table = tables.AccountingDimensionListTable
+ form = forms.AccountingDimensionBulkEditForm
+
+
+class AccountingDimensionBulkDeleteView(generic.BulkDeleteView):
+ queryset = AccountingDimension.objects.annotate()
+ filterset = filtersets.AccountingDimensionFilterSet
+ table = tables.AccountingDimensionListTable
diff --git a/utils/README.md b/utils/README.md
index 55b3452..9770166 100644
--- a/utils/README.md
+++ b/utils/README.md
@@ -39,7 +39,7 @@ cd netbox
git clone -b master --depth 1 https://github.com/netbox-community/netbox.git .
```
-You do not need to create the Betbox system user
+You do not need to create the Netbox system user
### generate secret key
@@ -52,20 +52,13 @@ update the netbox-contract/utils/netbox-configuration.py with this secret key
### update netbox configuration
```
-sudo cp netbox-contract/utils/netbox-configuration.py netbox/netbox/netbox/configuration.py
+cp netbox-contract/utils/netbox-configuration.py netbox/netbox/netbox/configuration.py
```
### Run the Upgrade Script
```bash
-sudo netbox/upgrade.sh
-```
-
-Chnage the ownership of the creaed virtual environment
-
-```bash
-cd netbox
-sudo chown -R vscode:vscode venv
+netbox/upgrade.sh
```
### Create a Super User
@@ -86,14 +79,10 @@ python3 netbox/netbox/manage.py runserver
For development, install the plugin from the local file system:
```bash
+python3 -m pip uninstall netbox-contract
python3 -m pip install -e netbox-contract
```
-Update netbox configuration to configure the plugin:
-
-```bash
-sudo cp netbox-contract/utils/netbox-configuration-final.py netbox/netbox/netbox/configuration.py
-```
run database migrations:
```bash
@@ -104,7 +93,7 @@ install pre-commit:
```bash
cd netbox-contract
-python -m pip install pre-commit
+python3 -m pip install pre-commit
pre-commit install
```
@@ -119,15 +108,9 @@ python3 netbox/netbox/manage.py runserver
```bash
cd netbox
-sudo git checkout master
-sudo git pull origin master
-sudo ./upgrade.sh
-```
-
-Change the owner of virtual environment:
-
-```bash
-sudo chown -R vscode:vscode venv
+git checkout master
+git pull origin master
+./upgrade.sh
```
Reinstall the pluggin from the local filesystem (see below).