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
+
4
4
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
+ )
7
14
8
- name = " Contracts related scripts"
15
+ name = ' Contracts related scripts'
9
16
10
- AMOUNT_PRECEDENCE = (
11
- ('invoice' , 'Invoice' ),
12
- ('dimensions' , 'dimensions' )
13
- )
17
+ AMOUNT_PRECEDENCE = (('invoice' , 'Invoice' ), ('dimensions' , 'dimensions' ))
14
18
15
- class update_expired_contract_status (Script ):
16
19
20
+ class update_expired_contract_status (Script ):
17
21
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
+ )
20
26
commit_default = False
21
27
22
28
def run (self , data , commit ):
23
-
24
29
username = self .request .user .username
25
- self .log_info (f" Running as user { username } " )
30
+ self .log_info (f' Running as user { username } ' )
26
31
27
32
output = []
28
33
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
+ )
30
37
expired_contracts .update (status = StatusChoices .STATUS_CANCELED )
31
38
32
39
return '\n ' .join (output )
33
40
34
- class create_invoice_template (Script ):
35
41
42
+ class create_invoice_template (Script ):
36
43
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'
39
46
commit_default = False
40
47
41
48
def run (self , data , commit ):
42
-
43
49
username = self .request .user .username
44
- self .log_info (f" Running as user { username } " )
50
+ self .log_info (f' Running as user { username } ' )
45
51
46
52
output = []
47
53
48
- self .log_info (f" Creating invoice templates from contract dimensions" )
54
+ self .log_info (' Creating invoice templates from contract dimensions' )
49
55
50
56
# 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 } ' )
53
59
54
60
# 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 ()
56
64
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 } ' )
59
67
continue
60
-
68
+
61
69
# if the invoice template does not exists create it
62
70
if contract .accounting_dimensions :
63
71
if contract .mrc is not None :
64
72
amount = contract .mrc * contract .invoice_frequency
65
73
else :
66
74
amount = contract .yrc / 12 * contract .invoice_frequency
67
75
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 ,
74
82
)
75
83
invoice_template .save ()
76
84
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
+ )
78
88
79
89
return '\n ' .join (output )
80
90
81
- class create_invoice_lines (Script ):
82
91
92
+ class create_invoice_lines (Script ):
83
93
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
+ )
86
98
commit_default = False
87
99
88
100
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.' ,
91
103
required = False ,
92
- regex = r" ^\w+(,\w+)*$"
104
+ regex = r' ^\w+(,\w+)*$' ,
93
105
)
94
106
95
107
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 ,
100
112
)
101
113
102
114
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' ,
105
117
required = True ,
106
118
)
107
119
108
120
def run (self , data , commit ):
109
-
110
121
username = self .request .user .username
111
- self .log_info (f" Running as user { username } " )
122
+ self .log_info (f' Running as user { username } ' )
112
123
113
124
output = []
114
125
@@ -118,123 +129,164 @@ def run(self, data, commit):
118
129
if data ['ignore' ]:
119
130
exclude .extend (data ['ignore' ].split (',' ))
120
131
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 } ' )
124
135
125
136
# import existing dimensions
126
- dimensions = {}
137
+ dimensions = {}
127
138
dims = AccountingDimension .objects .all ()
128
139
if dims .exists ():
129
140
for dim in dims :
130
- dimensions [f" { dim .name } _{ dim .value } " ] = dim
141
+ dimensions [f' { dim .name } _{ dim .value } ' ] = dim
131
142
132
143
# Get all invoices without invoice lines
133
- invoices = Invoice .objects .annotate (numberoflines = Count (" invoicelines" ))
144
+ invoices = Invoice .objects .annotate (numberoflines = Count (' invoicelines' ))
134
145
135
146
# Create invoice lines for each invoice
136
147
for invoice in invoices :
137
148
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' )
139
150
continue
140
151
141
- self .log_info (f" Processing Invoice { invoice .number } " )
142
-
152
+ self .log_info (f' Processing Invoice { invoice .number } ' )
153
+
143
154
total_invoice_lines_amount = 0
144
155
145
156
# Create invoice template lines
146
157
# Check if several lines have to be created
147
158
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"
149
160
lines = invoice .accounting_dimensions
150
161
else :
151
162
lines = [invoice .accounting_dimensions ]
152
163
153
164
single_line_invoice = len (lines ) == 1
154
165
155
166
for line in lines :
156
- if single_line_invoice and data ['amount_precedence' ]== 'invoice' :
167
+ if single_line_invoice and data ['amount_precedence' ] == 'invoice' :
157
168
amount = invoice .amount
158
- else :
169
+ else :
159
170
# Retrieving with get reduce the repetition of code
160
171
amount = line .get (line_amount_key )
161
172
# Checking first the case "not exist" allow us to remove one indent level
162
173
# NOTE: This works fine because None is not a valid value in this case.
163
174
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
+ )
165
178
continue
166
179
# 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 (' ' , '' )
169
182
try :
170
183
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
+ )
174
189
175
190
invoice_line = InvoiceLine (
176
- invoice = invoice ,
177
- currency = invoice .currency ,
178
- amount = amount ,
191
+ invoice = invoice ,
192
+ currency = invoice .currency ,
193
+ amount = amount ,
179
194
)
180
195
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
+ )
182
199
total_invoice_lines_amount = total_invoice_lines_amount + amount
183
200
184
201
# create and add dimensions
185
202
for key , value in line .items ():
186
203
if key not in exclude and value is not None :
187
- dimkey = f" { key } _{ value } "
204
+ dimkey = f' { key } _{ value } '
188
205
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 ))
193
207
dimension .save ()
194
208
dimensions [dimkey ] = dimension
195
209
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
+ )
198
213
199
214
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
+ )
202
221
203
222
return '\n ' .join (output )
204
-
205
- class bulk_replace_accounting_dimension (Script ):
206
223
224
+
225
+ class bulk_replace_accounting_dimension (Script ):
207
226
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'
210
229
commit_default = False
211
230
212
231
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 ,
216
235
)
217
236
218
237
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 ,
222
241
)
223
242
224
243
def run (self , data , commit ):
225
-
226
244
username = self .request .user .username
227
- self .log_info (f" Running as user { username } " )
245
+ self .log_info (f' Running as user { username } ' )
228
246
229
247
output = []
230
248
231
- current_dimension = data [" current" ]
232
- new_dimension = data [" new" ]
249
+ current_dimension = data [' current' ]
250
+ new_dimension = data [' new' ]
233
251
234
252
lines = InvoiceLine .objects .filter (accounting_dimensions = current_dimension )
235
253
for line in lines :
236
254
line .accounting_dimensions .remove (current_dimension )
237
255
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
+ )
239
291
240
292
return '\n ' .join (output )
0 commit comments