Skip to content

Commit

Permalink
Merge pull request #71 from mlebreuil/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
mlebreuil authored Jun 18, 2023
2 parents 8431ef1 + 96e4e17 commit 1831746
Show file tree
Hide file tree
Showing 17 changed files with 160 additions and 28 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,12 @@ Add support for Netbox 3.5 which become the minimum version supported to accomod

* [#60](https://github.com/mlebreuil/netbox-contract/issues/60) Update contract quick search to also filter on fields "External reference" and "Comments".
* [#49](https://github.com/mlebreuil/netbox-contract/issues/49) Manage permissions.

#### version 2.0.4

* Add bulk update capability for contract assignement
* [#63](https://github.com/mlebreuil/netbox-contract/issues/63) Correct an API issue on the invoice object.
* [#64](https://github.com/mlebreuil/netbox-contract/issues/64) Add hierarchy to contract; New parent field created.
* [#65](https://github.com/mlebreuil/netbox-contract/issues/65) Add end date to contact import form.
* Removed the possibility of add or modify circuits to contracts. The field becomes read only and will be removed in next major release.
* Make accounting dimensions optional.
2 changes: 2 additions & 0 deletions assignement_import.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
content_type,object_id,contract
devices.device,1,3
2 changes: 2 additions & 0 deletions contract.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
name,external_partie,internal_partie,tenant,status,start_date,end_date,mrc,nrc,invoice_frequency
Contract10,ServiceProvider1,Nagra USA,Tenant1,Active,2023-06-01,2024-05-30,100,1000,1
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "netbox-contract"
version = "2.0.3"
version = "2.0.4"
authors = [
{ name="Marc Lebreuil", email="[email protected]" },
]
Expand Down
2 changes: 1 addition & 1 deletion src/netbox_contract/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class ContractsConfig(PluginConfig):
name = 'netbox_contract'
verbose_name = 'Netbox contract'
description = 'Contract management plugin for Netbox'
version = '2.0.3'
version = '2.0.4'
author = 'Marc Lebreuil'
author_email = '[email protected]'
base_url = 'contracts'
Expand Down
3 changes: 2 additions & 1 deletion src/netbox_contract/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,12 @@ class ContractSerializer(NetBoxModelSerializer):
)
circuit= NestedCircuitSerializer(many=True, required=False)
external_partie = NestedServiceProviderSerializer(many=False)
parent = NestedContracSerializer(many=False, required=False)

class Meta:
model = Contract
fields = (
'id', 'url','display', 'name', 'status', 'external_partie','internal_partie','circuit','comments',
'id', 'url','display', 'name', 'status', 'external_partie','internal_partie','parent','circuit','comments',
'tags', 'custom_fields', 'created', 'last_updated',
)

Expand Down
6 changes: 4 additions & 2 deletions src/netbox_contract/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
from .serializers import ContractSerializer, InvoiceSerializer, ServiceProviderSerializer, ContractAssignementSerializer

class ContractViewSet(NetBoxModelViewSet):
queryset = models.Contract.objects.prefetch_related('circuit','tags')
queryset = models.Contract.objects.prefetch_related(
'parent','circuit','tags'
)
serializer_class = ContractSerializer

class InvoiceViewSet(NetBoxModelViewSet):
queryset = models.Invoice.objects.prefetch_related(
'contract', 'tags'
'contracts', 'tags'
)
serializer_class = InvoiceSerializer
filterset_class = filtersets.InvoiceFilterSet
Expand Down
2 changes: 1 addition & 1 deletion src/netbox_contract/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class ContractFilterSet(NetBoxModelFilterSet):

class Meta:
model = Contract
fields = ('id', 'external_partie', 'internal_partie', 'status','circuit')
fields = ('id', 'external_partie', 'internal_partie', 'status','parent','circuit')

def search(self, queryset, name, value):
return queryset.filter( Q(name__icontains=value)
Expand Down
77 changes: 61 additions & 16 deletions src/netbox_contract/forms.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
import django_filters
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelBulkEditForm, NetBoxModelImportForm
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, MultipleChoiceField, CSVModelChoiceField, SlugField
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, MultipleChoiceField, CSVModelChoiceField, SlugField, CSVContentTypeField
from utilities.forms.widgets import DatePicker
from extras.filters import TagFilter
from circuits.models import Circuit
from tenancy.models import Tenant
from .models import Contract, Invoice, ServiceProvider, StatusChoices, ContractAssignement

class ContractForm(NetBoxModelForm):
comments = CommentField()
circuit=DynamicModelMultipleChoiceField(
queryset=Circuit.objects.all(),
required=False
)

external_partie=DynamicModelChoiceField(
queryset=ServiceProvider.objects.all()
)
tenant=DynamicModelChoiceField(
queryset=Tenant.objects.all()
)
parent=DynamicModelChoiceField(
queryset=Contract.objects.all(),
required=False
)

class Meta:
model = Contract
fields = ('name', 'external_partie', 'external_reference', 'internal_partie','tenant', 'status',
'start_date', 'end_date','initial_term', 'renewal_term', 'currency','accounting_dimensions',
'mrc', 'nrc','invoice_frequency','circuit', 'documents', 'comments', 'tags')
'mrc', 'nrc','invoice_frequency','parent','documents', 'comments', 'tags')

widgets = {
'start_date': DatePicker(),
Expand All @@ -46,10 +52,14 @@ class Meta:

class ContractFilterSetForm(NetBoxModelFilterSetForm):
model = Contract
external_partie=DynamicModelMultipleChoiceField(
external_partie=DynamicModelChoiceField(
queryset=ServiceProvider.objects.all(),
required=False
)
tenant=DynamicModelChoiceField(
queryset=Tenant.objects.all(),
required=False
)
external_reference=forms.CharField(
required=False
)
Expand All @@ -64,6 +74,10 @@ class ContractFilterSetForm(NetBoxModelFilterSetForm):
queryset=Circuit.objects.all(),
required=False
)
parent=DynamicModelChoiceField(
queryset=Contract.objects.all(),
required=False
)

class InvoiceFilterSetForm(NetBoxModelFilterSetForm):
model = Invoice
Expand All @@ -73,28 +87,40 @@ class InvoiceFilterSetForm(NetBoxModelFilterSetForm):
)

class ContractCSVForm(NetBoxModelImportForm):
circuit = CSVModelChoiceField(
queryset=Circuit.objects.all(),
external_partie = CSVModelChoiceField(
queryset=ServiceProvider.objects.all(),
to_field_name='name',
help_text='Service provider name'
)
tenant = CSVModelChoiceField(
queryset=Tenant.objects.all(),
to_field_name='name',
help_text='Tenant name',
required=False
)
parent = CSVModelChoiceField(
queryset=Contract.objects.all(),
to_field_name='name',
help_text='Related Circuit'
help_text='Contract name',
required=False
)

class Meta:
model = Contract
fields = [
'name', 'external_partie', 'internal_partie','tenant', 'status',
'start_date', 'initial_term', 'renewal_term', 'mrc', 'nrc',
'invoice_frequency', 'circuit'
'start_date', 'end_date','initial_term', 'renewal_term', 'mrc', 'nrc',
'invoice_frequency', 'parent'
]

class ContractBulkEditForm(NetBoxModelBulkEditForm):
name = forms.CharField(
max_length=100,
required=True
)
external_partie = forms.CharField(
max_length=30,
required=True
external_partie = DynamicModelChoiceField(
queryset=ServiceProvider.objects.all(),
required=False
)
external_reference=forms.CharField(
max_length=100,
Expand All @@ -106,7 +132,12 @@ class ContractBulkEditForm(NetBoxModelBulkEditForm):
)
comments = CommentField()
circuit=DynamicModelChoiceField(
queryset=Circuit.objects.all()
queryset=Circuit.objects.all(),
required=False
)
parent = DynamicModelChoiceField(
queryset=Contract.objects.all(),
required=False
)

nullable_fields = (
Expand Down Expand Up @@ -181,6 +212,7 @@ class ContractAssignementForm(NetBoxModelForm):
contract=DynamicModelChoiceField(
queryset=Contract.objects.all()
)

class Meta:
model = ContractAssignement
fields = ['content_type', 'object_id', 'contract','tags']
Expand All @@ -194,3 +226,16 @@ class ContractAssignementFilterSetForm(NetBoxModelFilterSetForm):
contract=DynamicModelChoiceField(
queryset=Contract.objects.all()
)

class ContractAssignementImportForm(NetBoxModelImportForm):
content_type = CSVContentTypeField(
queryset=ContentType.objects.all(),
help_text="Content Type in the form <app>.<model>"
)
contract = CSVModelChoiceField(
queryset=Contract.objects.all(),
help_text="Contract id"
)
class Meta:
model = ContractAssignement
fields = ['content_type', 'object_id', 'contract','tags']
25 changes: 25 additions & 0 deletions src/netbox_contract/migrations/0016_contract_parent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 4.1.9 on 2023-06-18 14:45

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
("netbox_contract", "0015_contractassignement"),
]

operations = [
migrations.AddField(
model_name="contract",
name="parent",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="child",
to="netbox_contract.contract",
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.1.9 on 2023-06-18 16:05

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("netbox_contract", "0016_contract_parent"),
]

operations = [
migrations.AlterField(
model_name="contract",
name="accounting_dimensions",
field=models.JSONField(blank=True, null=True),
),
]
12 changes: 10 additions & 2 deletions src/netbox_contract/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ class Contract(NetBoxModel):
default=CurrencyChoices.CURRENCY_USD
)
accounting_dimensions = models.JSONField(
null=True
null=True,
blank=True
)
mrc = models.DecimalField(
verbose_name = "Monthly recuring cost",
Expand All @@ -164,14 +165,21 @@ class Contract(NetBoxModel):
)
circuit = models.ManyToManyField(Circuit,
related_name='contracts',
blank=True,
blank=True
)
documents = models.URLField(
blank=True
)
comments = models.TextField(
blank=True
)
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
related_name='child',
null=True,
blank=True
)

def get_absolute_url(self):
return reverse('plugins:netbox_contract:contract', args=[self.pk])
Expand Down
7 changes: 6 additions & 1 deletion src/netbox_contract/navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,13 @@
buttons=serviceprovider_buttons,
permissions=['netbox_contract.view_serviceprovider']
)
contract_assignemnt_menu_item = PluginMenuItem(
link='plugins:netbox_contract:contractassignement_list',
link_text='Contract assignements',
permissions=['netbox_contract.view_contractassignement']
)

items = (contract_menu_item,invoices_menu_item,service_provider_menu_item)
items = (contract_menu_item,invoices_menu_item,service_provider_menu_item, contract_assignemnt_menu_item)

if plugin_settings.get("top_level_menu"):
menu = PluginMenu(
Expand Down
7 changes: 5 additions & 2 deletions src/netbox_contract/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,16 @@ class ContractListTable(NetBoxTable):
linkify=True
)
circuit = tables.ManyToManyColumn()
parent = tables.Column(
linkify=True
)

class Meta(NetBoxTable.Meta):
model = Contract
fields = ('pk', 'id', 'name', 'circuit', 'external_partie',
'external_reference','internal_partie', 'status', 'mrc',
'comments', 'actions')
default_columns = ('name', 'status', 'circuit')
'parent','comments', 'actions')
default_columns = ('name', 'status', 'parent','circuit')

class ContractListBottomTable(NetBoxTable):
name = tables.Column(
Expand Down
6 changes: 6 additions & 0 deletions src/netbox_contract/templates/netbox_contract/contract.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ <h5 class="card-header">Contract</h5>
<th scope="row">Invoice frequency</th>
<td>{{ object.invoice_frequency }}</td>
</tr>
<tr>
<th scope="row">Parent</th>
<td>
<a href="{{ object.parent.get_absolute_url }}">{{ object.parent.name }}</a>
</td>
</tr>
{% if object.documents %}
<tr>
<th scope="row">Documents</th>
Expand Down
1 change: 1 addition & 0 deletions src/netbox_contract/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
# Contract assignements
path('assignements/', views.ContractAssignementListView.as_view(), name='contractassignement_list'),
path('assignements/add/', views.ContractAssignementEditView.as_view(), name='contractassignement_add'),
path('assignements/import/', views.ContractAssignementBulkImportView.as_view(), name='contractassignement_import'),
path('assignements/<int:pk>/', views.ContractAssignementView.as_view(), name='contractassignement'),
path('assignements/<int:pk>/edit/', views.ContractAssignementEditView.as_view(), name='contractassignement_edit'),
path('assignements/<int:pk>/delete/', views.ContractAssignementDeleteView.as_view(), name='contractassignement_delete'),
Expand Down
7 changes: 6 additions & 1 deletion src/netbox_contract/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class ContractAssignementEditView(generic.ObjectEditView):
form = forms.ContractAssignementForm

def alter_object(self, instance, request, args, kwargs):
if not instance.pk:
if not instance.pk and kwargs:
# Assign the object based on URL kwargs
content_type = get_object_or_404(ContentType, pk=request.GET.get('content_type'))
instance.object = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id'))
Expand All @@ -75,6 +75,11 @@ def get_extra_addanother_params(self, request):
class ContractAssignementDeleteView(generic.ObjectDeleteView):
queryset = models.ContractAssignement.objects.all()

class ContractAssignementBulkImportView(generic.BulkImportView):
queryset = models.ContractAssignement.objects.all()
model_form = forms.ContractAssignementImportForm
table = tables.ContractAssignementListTable

# Contract views

class ContractView(generic.ObjectView):
Expand Down

0 comments on commit 1831746

Please sign in to comment.