Skip to content

Commit 0fcd6a2

Browse files
committed
resolves #196
1 parent 13a85c4 commit 0fcd6a2

File tree

6 files changed

+200
-98
lines changed

6 files changed

+200
-98
lines changed

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[flake8]
22
max-line-length = 120
3-
extend-ignore = W504,E501
3+
extend-ignore = W504,E501,C901
44
exclude =
55
.git,
66
__pycache__,

scripts/netbox-contract.py

Lines changed: 147 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,125 @@
1-
from datetime import date
2-
from decimal import *
3-
from django.core.exceptions import ObjectDoesNotExist
1+
from datetime import date, timedelta
2+
from decimal import Decimal, InvalidOperation
3+
44
from django.db.models import Count
5-
from extras.scripts import *
6-
from netbox_contract.models import Contract, Invoice, InvoiceLine, AccountingDimension, StatusChoices
5+
from extras.scripts import ChoiceVar, IntegerVar, ObjectVar, Script, StringVar
6+
7+
from netbox_contract.models import (
8+
AccountingDimension,
9+
Contract,
10+
Invoice,
11+
InvoiceLine,
12+
StatusChoices,
13+
)
714

8-
name = "Contracts related scripts"
15+
name = 'Contracts related scripts'
916

10-
AMOUNT_PRECEDENCE = (
11-
('invoice', 'Invoice'),
12-
('dimensions', 'dimensions')
13-
)
17+
AMOUNT_PRECEDENCE = (('invoice', 'Invoice'), ('dimensions', 'dimensions'))
1418

15-
class update_expired_contract_status(Script):
1619

20+
class update_expired_contract_status(Script):
1721
class Meta:
18-
name = "Update expired contracts status"
19-
description = "Update the status of contract with end date prior to today's date"
22+
name = 'Update expired contracts status'
23+
description = (
24+
"Update the status of contract with end date prior to today's date"
25+
)
2026
commit_default = False
2127

2228
def run(self, data, commit):
23-
2429
username = self.request.user.username
25-
self.log_info(f"Running as user {username}")
30+
self.log_info(f'Running as user {username}')
2631

2732
output = []
2833

29-
expired_contracts = Contract.objects.filter(end_date__lte = date.today()).filter(status = StatusChoices.STATUS_ACTIVE )
34+
expired_contracts = Contract.objects.filter(end_date__lte=date.today()).filter(
35+
status=StatusChoices.STATUS_ACTIVE
36+
)
3037
expired_contracts.update(status=StatusChoices.STATUS_CANCELED)
3138

3239
return '\n'.join(output)
3340

34-
class create_invoice_template(Script):
3541

42+
class create_invoice_template(Script):
3643
class Meta:
37-
name = "Create invoice templates"
38-
description = "Convert the Accounting dimensions json field in Contracts to invoice template"
44+
name = 'Create invoice templates'
45+
description = 'Convert the Accounting dimensions json field in Contracts to invoice template'
3946
commit_default = False
4047

4148
def run(self, data, commit):
42-
4349
username = self.request.user.username
44-
self.log_info(f"Running as user {username}")
50+
self.log_info(f'Running as user {username}')
4551

4652
output = []
4753

48-
self.log_info(f"Creating invoice templates from contract dimensions")
54+
self.log_info('Creating invoice templates from contract dimensions')
4955

5056
# Create invoice templates for each active template
51-
for contract in Contract.objects.filter(status = StatusChoices.STATUS_ACTIVE ):
52-
self.log_info(f"Processing contract {contract.name}")
57+
for contract in Contract.objects.filter(status=StatusChoices.STATUS_ACTIVE):
58+
self.log_info(f'Processing contract {contract.name}')
5359

5460
# check if invoice template exist
55-
invoice_template = Invoice.objects.filter(template=True, contracts=contract).first()
61+
invoice_template = Invoice.objects.filter(
62+
template=True, contracts=contract
63+
).first()
5664

57-
if invoice_template :
58-
self.log_info(f"Template already exists for {contract.name}")
65+
if invoice_template:
66+
self.log_info(f'Template already exists for {contract.name}')
5967
continue
60-
68+
6169
# if the invoice template does not exists create it
6270
if contract.accounting_dimensions:
6371
if contract.mrc is not None:
6472
amount = contract.mrc * contract.invoice_frequency
6573
else:
6674
amount = contract.yrc / 12 * contract.invoice_frequency
6775
invoice_template = Invoice(
68-
template = True,
69-
number = f"_invoice_template_{contract.name}",
70-
period_start = None,
71-
period_end = None,
72-
amount = amount,
73-
accounting_dimensions = contract.accounting_dimensions
76+
template=True,
77+
number=f'_invoice_template_{contract.name}',
78+
period_start=None,
79+
period_end=None,
80+
amount=amount,
81+
accounting_dimensions=contract.accounting_dimensions,
7482
)
7583
invoice_template.save()
7684
invoice_template.contracts.add(contract)
77-
self.log_info(f"Template {invoice_template.number} created for {contract.name}")
85+
self.log_info(
86+
f'Template {invoice_template.number} created for {contract.name}'
87+
)
7888

7989
return '\n'.join(output)
8090

81-
class create_invoice_lines(Script):
8291

92+
class create_invoice_lines(Script):
8393
class Meta:
84-
name = "Create invoice lines"
85-
description = "Convert the Accounting dimensions json field in invoices to invoice lines"
94+
name = 'Create invoice lines'
95+
description = (
96+
'Convert the Accounting dimensions json field in invoices to invoice lines'
97+
)
8698
commit_default = False
8799

88100
ignore = StringVar(
89-
label="Ignore",
90-
description="Accounting dimensions to be ignored. List of string separated by comma.",
101+
label='Ignore',
102+
description='Accounting dimensions to be ignored. List of string separated by comma.',
91103
required=False,
92-
regex=r"^\w+(,\w+)*$"
104+
regex=r'^\w+(,\w+)*$',
93105
)
94106

95107
amount_precedence = ChoiceVar(
96-
label="Amount precedence",
97-
description="Select if the dimension amount or the invoice amount take precedence,",
98-
choices = AMOUNT_PRECEDENCE,
99-
required=False
108+
label='Amount precedence',
109+
description='Select if the dimension amount or the invoice amount take precedence,',
110+
choices=AMOUNT_PRECEDENCE,
111+
required=False,
100112
)
101113

102114
line_amount_key = StringVar(
103-
label="Line amount key",
104-
description="Key name for line amount in the accounting dimension json with multiple lines",
115+
label='Line amount key',
116+
description='Key name for line amount in the accounting dimension json with multiple lines',
105117
required=True,
106118
)
107119

108120
def run(self, data, commit):
109-
110121
username = self.request.user.username
111-
self.log_info(f"Running as user {username}")
122+
self.log_info(f'Running as user {username}')
112123

113124
output = []
114125

@@ -118,123 +129,164 @@ def run(self, data, commit):
118129
if data['ignore']:
119130
exclude.extend(data['ignore'].split(','))
120131

121-
self.log_info(f"Creating invoice lines from invoices dimensions")
122-
self.log_info(f"Ignoring dimensions {exclude}")
123-
self.log_info(f"Line amount key {line_amount_key}")
132+
self.log_info('Creating invoice lines from invoices dimensions')
133+
self.log_info(f'Ignoring dimensions {exclude}')
134+
self.log_info(f'Line amount key {line_amount_key}')
124135

125136
# import existing dimensions
126-
dimensions={}
137+
dimensions = {}
127138
dims = AccountingDimension.objects.all()
128139
if dims.exists():
129140
for dim in dims:
130-
dimensions[f"{dim.name}_{dim.value}"] = dim
141+
dimensions[f'{dim.name}_{dim.value}'] = dim
131142

132143
# Get all invoices without invoice lines
133-
invoices = Invoice.objects.annotate(numberoflines=Count("invoicelines"))
144+
invoices = Invoice.objects.annotate(numberoflines=Count('invoicelines'))
134145

135146
# Create invoice lines for each invoice
136147
for invoice in invoices:
137148
if invoice.numberoflines > 0:
138-
self.log_info(f"Invoice skipped {invoice.number}. Exiting lines")
149+
self.log_info(f'Invoice skipped {invoice.number}. Exiting lines')
139150
continue
140151

141-
self.log_info(f"Processing Invoice {invoice.number}")
142-
152+
self.log_info(f'Processing Invoice {invoice.number}')
153+
143154
total_invoice_lines_amount = 0
144155

145156
# Create invoice template lines
146157
# Check if several lines have to be created
147158
if isinstance(invoice.accounting_dimensions, list):
148-
# if the accounting dimensions is a list we assume that we have an "amount"
159+
# if the accounting dimensions is a list we assume that we have an "amount"
149160
lines = invoice.accounting_dimensions
150161
else:
151162
lines = [invoice.accounting_dimensions]
152163

153164
single_line_invoice = len(lines) == 1
154165

155166
for line in lines:
156-
if single_line_invoice and data['amount_precedence']=='invoice':
167+
if single_line_invoice and data['amount_precedence'] == 'invoice':
157168
amount = invoice.amount
158-
else:
169+
else:
159170
# Retrieving with get reduce the repetition of code
160171
amount = line.get(line_amount_key)
161172
# Checking first the case "not exist" allow us to remove one indent level
162173
# NOTE: This works fine because None is not a valid value in this case.
163174
if not amount:
164-
self.log_warning(f"Multiple lines or dimensions precedence and no amount for line")
175+
self.log_warning(
176+
'Multiple lines or dimensions precedence and no amount for line'
177+
)
165178
continue
166179
# The try-except part is the same and can be extracted
167-
if isinstance(amount , str):
168-
amount = amount.replace(",",".").replace(" ","")
180+
if isinstance(amount, str):
181+
amount = amount.replace(',', '.').replace(' ', '')
169182
try:
170183
amount = Decimal(line[line_amount_key])
171-
except:
172-
self.log_warning(f"Wrong number format {line[line_amount_key]}")
173-
output.append(f"{invoice.number}: dimensions amount format to be updated")
184+
except InvalidOperation:
185+
self.log_warning(f'Wrong number format {line[line_amount_key]}')
186+
output.append(
187+
f'{invoice.number}: dimensions amount format to be updated'
188+
)
174189

175190
invoice_line = InvoiceLine(
176-
invoice = invoice,
177-
currency = invoice.currency,
178-
amount = amount,
191+
invoice=invoice,
192+
currency=invoice.currency,
193+
amount=amount,
179194
)
180195
invoice_line.save()
181-
self.log_info(f"Invoice line {invoice_line.id} created for {invoice.number}")
196+
self.log_info(
197+
f'Invoice line {invoice_line.id} created for {invoice.number}'
198+
)
182199
total_invoice_lines_amount = total_invoice_lines_amount + amount
183200

184201
# create and add dimensions
185202
for key, value in line.items():
186203
if key not in exclude and value is not None:
187-
dimkey = f"{key}_{value}"
204+
dimkey = f'{key}_{value}'
188205
if dimkey not in dimensions.keys():
189-
dimension = AccountingDimension(
190-
name = key,
191-
value = str(value)
192-
)
206+
dimension = AccountingDimension(name=key, value=str(value))
193207
dimension.save()
194208
dimensions[dimkey] = dimension
195209
invoice_line.accounting_dimensions.add(dimensions[dimkey])
196-
self.log_info(f"Accounting dimensions added to Invoice line {invoice_line.id}")
197-
210+
self.log_info(
211+
f'Accounting dimensions added to Invoice line {invoice_line.id}'
212+
)
198213

199214
if total_invoice_lines_amount != invoice.amount:
200-
self.log_warning(f"The total of invoice lines and invoice amount do not match.")
201-
output.append(f"{invoice.number}: Sum of invoice lines amount to be checked")
215+
self.log_warning(
216+
'The total of invoice lines and invoice amount do not match.'
217+
)
218+
output.append(
219+
f'{invoice.number}: Sum of invoice lines amount to be checked'
220+
)
202221

203222
return '\n'.join(output)
204-
205-
class bulk_replace_accounting_dimension(Script):
206223

224+
225+
class bulk_replace_accounting_dimension(Script):
207226
class Meta:
208-
name = "Replace accounting dimension"
209-
description = "Replace one accounting dimension by another one for all lines"
227+
name = 'Replace accounting dimension'
228+
description = 'Replace one accounting dimension by another one for all lines'
210229
commit_default = False
211230

212231
current = ObjectVar(
213-
label="Current dimension",
214-
description="The accounting dimension to be replaced.",
215-
model=AccountingDimension
232+
label='Current dimension',
233+
description='The accounting dimension to be replaced.',
234+
model=AccountingDimension,
216235
)
217236

218237
new = ObjectVar(
219-
label="New accounting dimension",
220-
description="The new accounting dimension",
221-
model=AccountingDimension
238+
label='New accounting dimension',
239+
description='The new accounting dimension',
240+
model=AccountingDimension,
222241
)
223242

224243
def run(self, data, commit):
225-
226244
username = self.request.user.username
227-
self.log_info(f"Running as user {username}")
245+
self.log_info(f'Running as user {username}')
228246

229247
output = []
230248

231-
current_dimension = data["current"]
232-
new_dimension = data["new"]
249+
current_dimension = data['current']
250+
new_dimension = data['new']
233251

234252
lines = InvoiceLine.objects.filter(accounting_dimensions=current_dimension)
235253
for line in lines:
236254
line.accounting_dimensions.remove(current_dimension)
237255
line.accounting_dimensions.add(new_dimension)
238-
self.log_info(f"invoice {line.invoice.number} updated")
256+
self.log_info(f'invoice {line.invoice.number} updated')
257+
258+
return '\n'.join(output)
259+
260+
261+
class Check_contract_end(Script):
262+
class Meta:
263+
name = 'Check contract end'
264+
description = 'Check which contract will end '
265+
commit_default = False
266+
267+
days_before_notice = IntegerVar(
268+
label='Days before notice',
269+
description='Report on contract with notice periode approaching',
270+
)
271+
272+
def run(self, data, commit):
273+
username = self.request.user.username
274+
self.log_info(f'Running as user {username}')
275+
276+
output = []
277+
278+
days_before_notice = data['days_before_notice']
279+
280+
contracts = Contract.objects.filter(status=StatusChoices.STATUS_ACTIVE)
281+
for contract in contracts:
282+
if contract.notice_date <= date.today() + timedelta(
283+
days=days_before_notice
284+
):
285+
self.log_info(
286+
f'Contract {contract} end date: {contract.end_date} - notice : {contract.notice_period} days'
287+
)
288+
output.append(
289+
f'{contract.name} - end date: {contract.end_date} - notice : {contract.notice_period} days'
290+
)
239291

240292
return '\n'.join(output)

0 commit comments

Comments
 (0)