Skip to content

Commit

Permalink
Merge pull request #164 from mlebreuil/develop
Browse files Browse the repository at this point in the history
v2.2.3
  • Loading branch information
mlebreuil authored Aug 22, 2024
2 parents 0858c5f + 7b1df14 commit 2be4eef
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 6 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.3

* Fix accounting dimensions access through Dynamic Object Fields
* Fix invoice creation from contract.
* Add scripts to convert accounting dimensions in the json fields of contract and invoices to invoice template, invoicelines and dimensions objects.

### Version 2.2.2

* [154](https://github.com/mlebreuil/netbox-contract/issues/154) Fix edit and delete bulk operations on dimensions and invoice lines.
Expand Down
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.2"
version = "2.2.3"
authors = [
{ name="Marc Lebreuil", email="[email protected]" },
]
Expand Down
240 changes: 240 additions & 0 deletions scripts/netbox-contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
from datetime import date
from decimal import *
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Count
from extras.scripts import *
from netbox_contract.models import Contract, Invoice, InvoiceLine, AccountingDimension, StatusChoices

name = "Contracts related scripts"

AMOUNT_PRECEDENCE = (
('invoice', 'Invoice'),
('dimensions', 'dimensions')
)

class update_expired_contract_status(Script):

class Meta:
name = "Update expired contracts status"
description = "Update the status of contract with end date prior to today's date"
commit_default = False

def run(self, data, commit):

username = self.request.user.username
self.log_info(f"Running as user {username}")

output = []

expired_contracts = Contract.objects.filter(end_date__lte = date.today()).filter(status = StatusChoices.STATUS_ACTIVE )
expired_contracts.update(status=StatusChoices.STATUS_CANCELED)

return '\n'.join(output)

class create_invoice_template(Script):

class Meta:
name = "Create invoice templates"
description = "Convert the Accounting dimensions json field in Contracts to invoice template"
commit_default = False

def run(self, data, commit):

username = self.request.user.username
self.log_info(f"Running as user {username}")

output = []

self.log_info(f"Creating invoice templates from contract dimensions")

# Create invoice templates for each active template
for contract in Contract.objects.filter(status = StatusChoices.STATUS_ACTIVE ):
self.log_info(f"Processing contract {contract.name}")

# check if invoice template exist
invoice_template = Invoice.objects.filter(template=True, contracts=contract).first()

if invoice_template :
self.log_info(f"Template already exists for {contract.name}")
continue

# if the invoice template does not exists create it
if contract.accounting_dimensions:
if contract.mrc is not None:
amount = contract.mrc * contract.invoice_frequency
else:
amount = contract.yrc / 12 * contract.invoice_frequency
invoice_template = Invoice(
template = True,
number = f"_invoice_template_{contract.name}",
period_start = None,
period_end = None,
amount = amount,
accounting_dimensions = contract.accounting_dimensions
)
invoice_template.save()
invoice_template.contracts.add(contract)
self.log_info(f"Template {invoice_template.number} created for {contract.name}")

return '\n'.join(output)

class create_invoice_lines(Script):

class Meta:
name = "Create invoice lines"
description = "Convert the Accounting dimensions json field in invoices to invoice lines"
commit_default = False

ignore = StringVar(
label="Ignore",
description="Accounting dimensions to be ignored. List of string separated by comma.",
required=False,
regex=r"^\w+(,\w+)*$"
)

amount_precedence = ChoiceVar(
label="Amount precedence",
description="Select if the dimension amount or the invoice amount take precedence,",
choices = AMOUNT_PRECEDENCE,
required=False
)

line_amount_key = StringVar(
label="Line amount key",
description="Key name for line amount in the accounting dimension json with multiple lines",
required=True,
)

def run(self, data, commit):

username = self.request.user.username
self.log_info(f"Running as user {username}")

output = []

line_amount_key = data['line_amount_key']

exclude = [line_amount_key]
if data['ignore']:
exclude.extend(data['ignore'].split(','))

self.log_info(f"Creating invoice lines from invoices dimensions")
self.log_info(f"Ignoring dimensions {exclude}")
self.log_info(f"Line amount key {line_amount_key}")

# import existing dimensions
dimensions={}
dims = AccountingDimension.objects.all()
if dims.exists():
for dim in dims:
dimensions[f"{dim.name}_{dim.value}"] = dim

# Get all invoices without invoice lines
invoices = Invoice.objects.annotate(numberoflines=Count("invoicelines"))

# Create invoice lines for each invoice
for invoice in invoices:
if invoice.numberoflines > 0:
self.log_info(f"Invoice skipped {invoice.number}. Exiting lines")
continue

self.log_info(f"Processing Invoice {invoice.number}")

total_invoice_lines_amount = 0

# Create invoice template lines
# Check if several lines have to be created
if isinstance(invoice.accounting_dimensions, list):
# if the accounting dimensions is a list we assume that we have an "amount"
lines = invoice.accounting_dimensions
else:
lines = [invoice.accounting_dimensions]

single_line_invoice = len(lines) == 1

for line in lines:
if single_line_invoice and data['amount_precedence']=='invoice':
amount = invoice.amount
else:
# Retrieving with get reduce the repetition of code
amount = line.get(line_amount_key)
# Checking first the case "not exist" allow us to remove one indent level
# NOTE: This works fine because None is not a valid value in this case.
if not amount:
self.log_warning(f"Multiple lines or dimensions precedence and no amount for line")
continue
# The try-except part is the same and can be extracted
if isinstance(amount , str):
amount = amount.replace(",",".").replace(" ","")
try:
amount = Decimal(line[line_amount_key])
except:
self.log_warning(f"Wrong number format {line[line_amount_key]}")
output.append(f"{invoice.number}: dimensions amount format to be updated")

invoice_line = InvoiceLine(
invoice = invoice,
currency = invoice.currency,
amount = amount,
)
invoice_line.save()
self.log_info(f"Invoice line {invoice_line.id} created for {invoice.number}")
total_invoice_lines_amount = total_invoice_lines_amount + amount

# create and add dimensions
for key, value in line.items():
if key not in exclude and value is not None:
dimkey = f"{key}_{value}"
if dimkey not in dimensions.keys():
dimension = AccountingDimension(
name = key,
value = str(value)
)
dimension.save()
dimensions[dimkey] = dimension
invoice_line.accounting_dimensions.add(dimensions[dimkey])
self.log_info(f"Accounting dimensions added to Invoice line {invoice_line.id}")


if total_invoice_lines_amount != invoice.amount:
self.log_warning(f"The total of invoice lines and invoice amount do not match.")
output.append(f"{invoice.number}: Sum of invoice lines amount to be checked")

return '\n'.join(output)

class bulk_replace_accounting_dimension(Script):

class Meta:
name = "Replace accounting dimension"
description = "Replace one accounting dimension by another one for all lines"
commit_default = False

current = ObjectVar(
label="Current dimension",
description="The accounting dimension to be replaced.",
model=AccountingDimension
)

new = ObjectVar(
label="New accounting dimension",
description="The new accounting dimension",
model=AccountingDimension
)

def run(self, data, commit):

username = self.request.user.username
self.log_info(f"Running as user {username}")

output = []

current_dimension = data["current"]
new_dimension = data["new"]

lines = InvoiceLine.objects.filter(accounting_dimensions=current_dimension)
for line in lines:
line.accounting_dimensions.remove(current_dimension)
line.accounting_dimensions.add(new_dimension)
self.log_info(f"invoice {line.invoice.number} updated")

return '\n'.join(output)
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.2'
version = '2.2.3'
author = 'Marc Lebreuil'
author_email = '[email protected]'
base_url = 'contracts'
Expand Down
2 changes: 1 addition & 1 deletion src/netbox_contract/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,4 +284,4 @@ class Meta:
'created',
'last_updated',
)
brief_fields = ('name', 'value', 'url', 'display')
brief_fields = ('id', 'name', 'value', 'url', 'display')
12 changes: 10 additions & 2 deletions src/netbox_contract/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
CSVChoiceField,
CSVContentTypeField,
CSVModelChoiceField,
CSVModelMultipleChoiceField,
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
JSONField,
Expand Down Expand Up @@ -240,6 +241,10 @@ class ContractBulkEditForm(NetBoxModelBulkEditForm):


class InvoiceForm(NetBoxModelForm):
number = forms.CharField(
max_length=100,
help_text='Invoice template name will be overriden to _invoice_template_contract name',
)
contracts = DynamicModelMultipleChoiceField(
queryset=Contract.objects.all(), required=False
)
Expand Down Expand Up @@ -459,7 +464,7 @@ class Meta:

class InvoiceLineForm(NetBoxModelForm):
invoice = DynamicModelChoiceField(queryset=Invoice.objects.all())
accounting_dimensions = forms.ModelMultipleChoiceField(
accounting_dimensions = DynamicModelMultipleChoiceField(
queryset=AccountingDimension.objects.all(), required=False
)

Expand Down Expand Up @@ -490,6 +495,9 @@ class Meta:
class InvoiceLineFilterSetForm(NetBoxModelFilterSetForm):
model = InvoiceLine
invoice = DynamicModelChoiceField(queryset=Invoice.objects.all(), required=False)
accounting_dimensions = DynamicModelChoiceField(
queryset=AccountingDimension.objects.all(), required=False
)


class InvoiceLineImportForm(NetBoxModelImportForm):
Expand All @@ -498,7 +506,7 @@ class InvoiceLineImportForm(NetBoxModelImportForm):
to_field_name='number',
help_text='Invoice number',
)
accounting_dimensions = CSVModelChoiceField(
accounting_dimensions = CSVModelMultipleChoiceField(
queryset=AccountingDimension.objects.all(),
to_field_name='id',
help_text='accounting dimension id',
Expand Down
2 changes: 1 addition & 1 deletion src/netbox_contract/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ def get(self, request, *args, **kwargs):
contract = Contract.objects.get(pk=initial_data['contracts'])

try:
last_invoice = contract.invoices.filter(template=False).latest(
last_invoice = contract.invoices.exclude(template=True).latest(
'period_end'
)
new_period_start = last_invoice.period_end + timedelta(days=1)
Expand Down
File renamed without changes.

0 comments on commit 2be4eef

Please sign in to comment.