Skip to content

Commit

Permalink
Merge pull request #150 from mlebreuil/develop
Browse files Browse the repository at this point in the history
v2.2.1
  • Loading branch information
mlebreuil authored Aug 14, 2024
2 parents 35e4368 + 922c9ed commit 2296dcf
Show file tree
Hide file tree
Showing 14 changed files with 138 additions and 53 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@

## Version 2

### Version 2.2.1

* [142](https://github.com/mlebreuil/netbox-contract/issues/142) Gives the option to enter contract yearly recuring costs instead of only monthly recuring costs.
Corresponding value is used to calculate the invoices amount without rounding approximations.
* [148](https://github.com/mlebreuil/netbox-contract/issues/148) Update tables format to match the new Netbox UI design.

### 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.
Expand Down
12 changes: 11 additions & 1 deletion docs/contract.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
# Contract

## Contract details

![Contract](img/contract.png "contract")

Linked objects:
- External partie type: either an Circuit provider or Contract Service provider.
- Accounting dimensions: Will be copied to each invoice. Also this is still working the use invoice templates with accounting dimensions should be prefered.
- Monthly / Yearly recuring costs: Only one of these two options can be used for each contract. The value will be used, along with the invoice frequency, to calculate each invoice amount.
- Invoice frequency : The number of month that each invoice covers.
- Parent: Contrats can be arranged in a parent / child hierarchie.

## Linked objects:

![Contract linked objects](img/contract_linked_objects.png "contract linked objects")

- Assignments: the assignement of contract to objects is managed from each object's detail view.
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.2.0"
version = "2.2.1"
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 @@ -5,7 +5,7 @@ class ContractsConfig(PluginConfig):
name = 'netbox_contract'
verbose_name = 'Netbox contract'
description = 'Contract management plugin for Netbox'
version = '2.2.0'
version = '2.2.1'
author = 'Marc Lebreuil'
author_email = '[email protected]'
base_url = 'contracts'
Expand Down
8 changes: 6 additions & 2 deletions src/netbox_contract/api/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.db.models import F
from django.db.models import Case, F, When
from django.db.models.functions import Round
from netbox.api.viewsets import NetBoxModelViewSet

from .. import filtersets, models
Expand All @@ -14,7 +15,10 @@

class ContractViewSet(NetBoxModelViewSet):
queryset = models.Contract.objects.prefetch_related('parent', 'tags').annotate(
yrc=F('mrc') * 12
calculated_rc=Round(
Case(When(yrc__gt=0, then=F('yrc') / 12), default=F('mrc') * 12),
precision=2,
)
)
serializer_class = ContractSerializer

Expand Down
16 changes: 15 additions & 1 deletion src/netbox_contract/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ class Meta:
'renewal_term',
'currency',
'accounting_dimensions',
'yrc',
'mrc',
'nrc',
'invoice_frequency',
Expand All @@ -143,6 +144,14 @@ class Meta:
'end_date': DatePicker(),
}

def clean(self):
super().clean()

if self.cleaned_data['mrc'] and self.cleaned_data['mrc']:
raise ValidationError(
'you should set monthly OR yearly recuring costs not both'
)


class ContractFilterSetForm(NetBoxModelFilterSetForm):
model = Contract
Expand Down Expand Up @@ -193,6 +202,7 @@ class Meta:
'renewal_term',
'currency',
'accounting_dimensions',
'yrc',
'mrc',
'nrc',
'invoice_frequency',
Expand Down Expand Up @@ -261,7 +271,11 @@ def clean(self):
)

# Prefix the invoice name with _template
self.cleaned_data['number'] = '_template_' + self.cleaned_data['number']
self.cleaned_data['number'] = '_invoice_template_' + contract.name

# set the periode start and end date to null
self.cleaned_data['period_start'] = None
self.cleaned_data['period_end'] = None

def save(self, *args, **kwargs):
is_new = not bool(self.instance.pk)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 5.0.6 on 2024-08-01 15:43

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
('netbox_contract', '0033_remove_contract_invoice_template'),
]

operations = [
migrations.AddField(
model_name='contract',
name='yrc',
field=models.DecimalField(
blank=True, decimal_places=2, max_digits=10, null=True
),
),
migrations.AlterField(
model_name='contract',
name='mrc',
field=models.DecimalField(
blank=True, decimal_places=2, max_digits=10, null=True
),
),
]
13 changes: 12 additions & 1 deletion src/netbox_contract/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,19 @@ class Contract(NetBoxModel):
max_length=3, choices=CurrencyChoices, default=CurrencyChoices.CURRENCY_USD
)
accounting_dimensions = models.JSONField(null=True, blank=True)
yrc = models.DecimalField(
verbose_name='yearly recuring cost',
max_digits=10,
decimal_places=2,
blank=True,
null=True,
)
mrc = models.DecimalField(
verbose_name='Monthly recuring cost', max_digits=10, decimal_places=2
verbose_name='Monthly recuring cost',
max_digits=10,
decimal_places=2,
blank=True,
null=True,
)
nrc = models.DecimalField(
verbose_name='None recuring cost', default=0, max_digits=10, decimal_places=2
Expand Down
5 changes: 3 additions & 2 deletions src/netbox_contract/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,15 @@ class Meta(NetBoxTable.Meta):
class ContractAssignmentObjectTable(NetBoxTable):
contract = tables.Column(linkify=True)
actions = columns.ActionsColumn(actions=('edit', 'delete'))
contract__external_partie_object = tables.Column(linkify=True)
contract__external_partie_object = tables.Column(
verbose_name='Partner', linkify=True
)

class Meta(NetBoxTable.Meta):
model = ContractAssignment
fields = (
'pk',
'contract',
'contract__external_partie_object_type',
'contract__external_partie_object',
'contract__status',
'contract__start_date',
Expand Down
20 changes: 11 additions & 9 deletions src/netbox_contract/templates/contract_assignments_bottom.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@
<div class="row">
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">Contracts Assignments</h5>
<h5 class="card-header">
Contracts Assignments
{% if perms.netbox_contract.add_contractassignment %}
<div class="card-actions">
<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">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
Add assignment
</a>
</div>
{% endif %}
</h5>
{% if assignments_table %}
{% render_table assignments_table %}
{% endif %}
{% if perms.netbox_contract.add_contractassignment %}
<div class="card-footer text-end noprint">
<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">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
Add assignment
</a>
</div>
{% endif %}
</div>
</div>
</div>
Expand Down
23 changes: 0 additions & 23 deletions src/netbox_contract/templates/contract_assignments_bottom_old.html

This file was deleted.

20 changes: 13 additions & 7 deletions src/netbox_contract/templates/netbox_contract/contract.html
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ <h5 class="card-header">Contract</h5>
<th scope="row">Yearly recuring costs</th>
<td>{{ object.yrc }}</td>
</tr>
<tr>
<th scope="row">calculated corresponding Yearly or Monthly value</th>
<td>{{ object.calculated_rc }}</td>
</tr>
<tr>
<th scope="row">Non recuring costs</th>
<td>{{ object.nrc }}</td>
Expand Down Expand Up @@ -138,16 +142,18 @@ <h5 class="card-header">childs</h5>
<div class="row">
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">Invoices</h5>
{% render_table invoices_table %}
{% if perms.netbox_contract.add_invoice %}
<div class="card-footer text-end noprint">
<a href="{% url 'plugins:netbox_contract:invoice_add' %}?contracts={{ object.pk }}" class="btn btn-primary">
<h5 class="card-header">
Invoices
{% if perms.netbox_contract.add_invoice %}
<div class="card-actions">
<a href="{% url 'plugins:netbox_contract:invoice_add' %}?contracts={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
Add an invoice
</a>
</div>
{% endif %}
</div>
{% endif %}
</h5>
{% render_table invoices_table %}
</div>
</div>
</div>
Expand Down
6 changes: 6 additions & 0 deletions src/netbox_contract/templates/netbox_contract/invoice.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@ <h5 class="card-header">Invoices</h5>
<th scope="row">Number</th>
<td>{{ object.number }}</td>
</tr>
<tr>
<th scope="row">Template</th>
<td>{{ object.template }}</td>
</tr>
<tr>
<th scope="row">Date</th>
<td>{{ object.date }}</td>
</tr>
{% if not object.template %}
<tr>
<th scope="row">Period start</th>
<td>{{ object.period_start }}</td>
Expand All @@ -31,6 +36,7 @@ <h5 class="card-header">Invoices</h5>
<th scope="row">Period end</th>
<td>{{ object.period_end }}</td>
</tr>
{% endif %}
<tr>
<th scope="row">Currency</th>
<td>{{ object.currency }}</td>
Expand Down
32 changes: 27 additions & 5 deletions src/netbox_contract/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import F
from django.db.models import Case, F, When
from django.db.models.functions import Round
from django.shortcuts import get_object_or_404, render
from netbox.views import generic
from netbox.views.generic.utils import get_prerequisite_model
Expand Down Expand Up @@ -129,7 +130,12 @@ class ContractAssignmentBulkImportView(generic.BulkImportView):

@register_model_view(Contract)
class ContractView(generic.ObjectView):
queryset = Contract.objects.annotate(yrc=F('mrc') * 12)
queryset = Contract.objects.annotate(
calculated_rc=Round(
Case(When(yrc__gt=0, then=F('yrc') / 12), default=F('mrc') * 12),
precision=2,
)
)

def get_extra_context(self, request, instance):
invoices_table = tables.InvoiceListTable(instance.invoices.all())
Expand All @@ -152,7 +158,12 @@ def get_extra_context(self, request, instance):


class ContractListView(generic.ObjectListView):
queryset = Contract.objects.annotate(yrc=F('mrc') * 12)
queryset = Contract.objects.annotate(
calculated_rc=Round(
Case(When(yrc__gt=0, then=F('yrc') / 12), default=F('mrc') * 12),
precision=2,
)
)
table = tables.ContractListTable
filterset = filtersets.ContractFilterSet
filterset_form = forms.ContractFilterSetForm
Expand Down Expand Up @@ -267,7 +278,9 @@ def get(self, request, *args, **kwargs):
contract = Contract.objects.get(pk=initial_data['contracts'])

try:
last_invoice = contract.invoices.latest('period_end')
last_invoice = contract.invoices.filter(template=False).latest(
'period_end'
)
new_period_start = last_invoice.period_end + timedelta(days=1)
except ObjectDoesNotExist:
if contract.start_date:
Expand All @@ -281,7 +294,16 @@ def get(self, request, *args, **kwargs):
new_period_end = new_period_start + delta - timedelta(days=1)
initial_data['period_end'] = new_period_end

initial_data['amount'] = contract.mrc * contract.invoice_frequency
if contract.yrc:
if contract.invoice_frequency == 12:
initial_data['amount'] = contract.yrc
else:
initial_data['amount'] = round(
contract.yrc / 12 * contract.invoice_frequency, 2
)
else:
initial_data['amount'] = contract.mrc * contract.invoice_frequency

initial_data['currency'] = contract.currency
if contract.accounting_dimensions:
initial_data['accounting_dimensions'] = contract.accounting_dimensions
Expand Down

0 comments on commit 2296dcf

Please sign in to comment.