Skip to content

Commit

Permalink
Merge pull request #175 from mlebreuil/develop
Browse files Browse the repository at this point in the history
v2.2.4
  • Loading branch information
mlebreuil authored Aug 27, 2024
2 parents 2be4eef + b1973a8 commit 3a7738e
Show file tree
Hide file tree
Showing 10 changed files with 253 additions and 18 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@

## Version 2

### Version 2.2.4

* [166](https://github.com/mlebreuil/netbox-contract/issues/166) Review the Contract view to include invoice template details and lines.
* [161](https://github.com/mlebreuil/netbox-contract/issues/161) Change the invoice block title if the invoice is a template.
* [160](https://github.com/mlebreuil/netbox-contract/issues/160) Add more fields to the invoice and contract bulk edit forms.
* [165](https://github.com/mlebreuil/netbox-contract/issues/165) Fix Invoice and invoiceline creation through api.

### Version 2.2.3

* Fix accounting dimensions access through Dynamic Object Fields
Expand Down
6 changes: 6 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# API

note: When creating invoices and invoice lines through the API, the corresponding contracts respectively accounting dimensions, must be referenced as a list of id.



1 change: 1 addition & 0 deletions docs/invoice_line.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
![Invoice line](img/invoice_line.png "invoice line")



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.3"
version = "2.2.4"
authors = [
{ name="Marc Lebreuil", email="marc@famillelebreuil.net" },
]
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.3'
version = '2.2.4'
author = 'Marc Lebreuil'
author_email = 'marc@famillelebreuil.net'
base_url = 'contracts'
Expand Down
167 changes: 161 additions & 6 deletions src/netbox_contract/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.contrib.auth.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from drf_yasg.utils import swagger_serializer_method
from netbox.api.fields import ContentTypeField
from netbox.api.fields import ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
from netbox.constants import NESTED_SERIALIZER_PREFIX
from rest_framework import serializers
Expand Down Expand Up @@ -31,10 +32,46 @@ class NestedContractSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_contract-api:contract-detail'
)
yrc = serializers.DecimalField(max_digits=10, decimal_places=2, read_only=True)
tenant = NestedTenantSerializer(many=False, required=False)
external_partie_object_type = ContentTypeField(queryset=ContentType.objects.all())
external_partie_object = serializers.SerializerMethodField(read_only=True)

class Meta:
model = Contract
fields = ('id', 'url', 'display', 'name')
fields = fields = (
'id',
'url',
'display',
'name',
'external_partie_object_type',
'external_partie_object_id',
'external_partie_object',
'external_reference',
'internal_partie',
'tenant',
'status',
'start_date',
'end_date',
'initial_term',
'renewal_term',
'currency',
'accounting_dimensions',
'mrc',
'yrc',
'nrc',
'invoice_frequency',
'comments',
)

@swagger_serializer_method(serializer_or_field=serializers.JSONField)
def get_external_partie_object(self, instance):
serializer = get_serializer_for_model(
instance.external_partie_object_type.model_class(),
prefix=NESTED_SERIALIZER_PREFIX,
)
context = {'request': self.context['request']}
return serializer(instance.external_partie_object, context=context).data


class NestedInvoiceSerializer(WritableNestedSerializer):
Expand Down Expand Up @@ -134,8 +171,25 @@ class Meta:
'url',
'display',
'name',
'external_partie_object_type',
'external_partie_object_id',
'external_partie_object',
'external_reference',
'internal_partie',
'tenant',
'status',
'start_date',
'end_date',
'initial_term',
'renewal_term',
'currency',
'accounting_dimensions',
'mrc',
'yrc',
'nrc',
'invoice_frequency',
'comments',
'parent',
)

@swagger_serializer_method(serializer_or_field=serializers.JSONField)
Expand All @@ -152,7 +206,12 @@ class InvoiceSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_contract-api:invoice-detail'
)
contracts = NestedContractSerializer(many=True, required=False)
contracts = SerializedPKRelatedField(
queryset=Contract.objects.all(),
serializer=ContractSerializer,
required=False,
many=True,
)

class Meta:
model = Invoice
Expand All @@ -162,6 +221,7 @@ class Meta:
'display',
'number',
'date',
'template',
'contracts',
'period_start',
'period_end',
Expand All @@ -174,7 +234,87 @@ class Meta:
'created',
'last_updated',
)
brief_fields = ('id', 'url', 'display', 'number', 'contracts')
brief_fields = (
'id',
'url',
'display',
'number',
'date',
'template',
'contracts',
'period_start',
'period_end',
'currency',
'accounting_dimensions',
'amount',
'comments',
)

def validate(self, data):
data = super().validate(data)

# template checks
if data['template']:
# Check that there is only one invoice template per contract
contracts = data['contracts']
for contract in contracts:
for invoice in contract.invoices.all():
if invoice.template and invoice != self.instance:
raise serializers.ValidationError(
'Only one invoice template allowed per contract'
)

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

# set the periode start and end date to null
data['period_start'] = None
data['period_end'] = None
return data

def create(self, validated_data):
instance = super().create(validated_data)

if not instance.template:
contracts = instance.contracts.all()

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 = instance

# adjust the first invoice line amount
amount = validated_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 ServiceProviderSerializer(NetBoxModelSerializer):
Expand Down Expand Up @@ -235,8 +375,11 @@ class InvoiceLineSerializer(NetBoxModelSerializer):
view_name='plugins-api:netbox_contract-api:invoiceline-detail'
)
invoice = NestedInvoiceSerializer(many=False, required=False)
accounting_dimensions = NestedAccountingDimensionSerializer(
many=True, required=False
accounting_dimensions = SerializedPKRelatedField(
queryset=AccountingDimension.objects.all(),
serializer=NestedAccountingDimensionSerializer,
required=False,
many=True,
)

class Meta:
Expand Down Expand Up @@ -264,6 +407,18 @@ class Meta:
'name',
)

def validate(self, data):
super().validate(data)
# check for duplicate dimensions
accounting_dimensions = data['accounting_dimensions']
dimensions_names = []
for dimension in accounting_dimensions:
if dimension.name in dimensions_names:
raise serializers.ValidationError('duplicate accounting dimension')
else:
dimensions_names.append(dimension.name)
return data


class AccountingDimensionSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
Expand Down
23 changes: 21 additions & 2 deletions src/netbox_contract/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
AccountingDimensionStatusChoices,
Contract,
ContractAssignment,
CurrencyChoices,
InternalEntityChoices,
Invoice,
InvoiceLine,
Expand Down Expand Up @@ -227,13 +228,17 @@ def clean_external_partie_object_id(self):

class ContractBulkEditForm(NetBoxModelBulkEditForm):
name = forms.CharField(max_length=100, required=False)

external_reference = forms.CharField(max_length=100, required=False)
internal_partie = forms.ChoiceField(choices=InternalEntityChoices, required=False)
tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False)
accounting_dimensions = Dimensions(required=False)
comments = CommentField(required=False)
parent = DynamicModelChoiceField(queryset=Contract.objects.all(), required=False)

nullable_fields = ('comments',)
nullable_fields = (
'accounting_dimensions',
'comments',
)
model = Contract


Expand Down Expand Up @@ -386,9 +391,23 @@ class Meta:

class InvoiceBulkEditForm(NetBoxModelBulkEditForm):
number = forms.CharField(max_length=100, required=True)
template = forms.BooleanField(required=False)
date = forms.DateField(required=False)
contracts = DynamicModelMultipleChoiceField(
queryset=Contract.objects.all(), required=False
)
period_start = forms.DateField(required=False)
period_end = forms.DateField(required=False)
currency = forms.ChoiceField(choices=CurrencyChoices, required=False)
accounting_dimensions = Dimensions(required=False)
amount = forms.DecimalField(max_digits=10, decimal_places=2, required=False)
documents = forms.URLField(required=False)
comments = CommentField()
nullable_fields = (
'accounting_dimensions',
'comments',
)

model = Invoice


Expand Down
29 changes: 29 additions & 0 deletions src/netbox_contract/templates/netbox_contract/contract.html
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,33 @@ <h5 class="card-header">Contract</h5>
<div class="col col-md-6">
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% if invoice_template %}
<div class="card">
<h5 class="card-header">
Invoice template
</h5>
<table class="table table-hover attr-table">
<tr>
<th scope="row">Number</th>
<td>
<a href="{{ invoice_template.get_absolute_url }}">{{ invoice_template.number }}</a>
</td>
</tr>
<tr>
<th scope="row">Currency</th>
<td>{{ invoice_template.currency }}</td>
</tr>
<tr>
<th scope="row">Total amount</th>
<td>{{ invoice_template.total_invoicelines_amount }}</td>
</tr>
</table>
<h5 class="card-header">
Invoice template lines
</h5>
{% render_table invoicelines_table %}
</div>
{% endif %}
</div>
</div>
<div class="row">
Expand All @@ -130,6 +157,7 @@ <h5 class="card-header">Assignments</h5>
</div>
</div>
</div>
{% if childs_table %}
<div class="row">
<div class="col col-md-12">
<div class="card">
Expand All @@ -138,6 +166,7 @@ <h5 class="card-header">childs</h5>
</div>
</div>
</div>
{% endif %}
{% if perms.netbox_contract.view_invoice %}
<div class="row">
<div class="col col-md-12">
Expand Down
10 changes: 5 additions & 5 deletions src/netbox_contract/templates/netbox_contract/invoice.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">Invoices</h5>
{% if object.template %}
<h5 class="card-header">Invoice template</h5>
{% else %}
<h5 class="card-header">Invoice</h5>
{% endif %}
<table class="table table-hover attr-table">
<tr>
<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>
Expand Down
Loading

0 comments on commit 3a7738e

Please sign in to comment.