Added remunform to consulter view
[auf_rh_dae.git] / project / dae / views.py
CommitLineData
5d680e84 1# -*- encoding: utf-8 -*-
f87fe1a1 2
cea09938 3from datetime import date, datetime
139686f2 4
cea09938 5from auf.django.permissions.decorators import get_object
5a1f75cb 6from django.contrib import messages
7f432a0b 7from django.contrib.auth.decorators import login_required, user_passes_test
5a1f75cb 8from django.contrib.contenttypes.models import ContentType
44055779 9from django.core.paginator import Paginator, InvalidPage
66fefd2f 10from django.db.models import Q, Count
898d7967 11from django.http import Http404, HttpResponse, HttpResponseNotFound
890b80e7 12from django.shortcuts import redirect, render, get_object_or_404
e3563bcf 13from sendfile import sendfile
cea09938 14from simplejson import dumps
75f0e87b 15
5a1f75cb
EMS
16from project.dae import models as dae
17from project.dae.decorators import \
acbc95a1 18 dae_groupe_requis, \
8684fcaa
EMS
19 poste_dans_ma_region_ou_service, \
20 dossier_dans_ma_region_ou_service, \
21 vieux_dossier_dans_ma_region_ou_service, \
22 employe_dans_ma_region_ou_service, \
23 dossier_est_modifiable, \
24 poste_est_modifiable, get_contrat
874949f3 25from project.dae.forms import FinancementFormSet, FinancementFormSetInitial
c4e96e66
EMS
26from project.dae.forms import \
27 PosteComparaisonFormSet, PosteComparaisonFormSetInitial
5a1f75cb 28from project.dae.forms import \
874949f3
OL
29 PosteWorkflowForm, PosteForm, PostePieceFormSet, \
30 DossierWorkflowForm, ChoosePosteForm, \
5a1f75cb
EMS
31 EmployeForm, DossierForm, DossierPieceForm, \
32 DossierComparaisonFormSet, RemunForm, ContratForm, DAENumeriseeForm, \
661da766 33 label_poste_display, DAEFinaliseesSearchForm, \
36fc5c09 34 remun_formset_factory, ReadOnlyRemunFormSet
5a1f75cb
EMS
35from project.dae.mail import send_drh_finalisation_mail
36from project.dae.workflow import \
37 DOSSIER_ETAT_FINALISE, DOSSIER_ETAT_REGION_FINALISATION, \
38 DOSSIER_ETAT_DRH_FINALISATION, POSTE_ETAT_FINALISE
25f2c148
OL
39from project.decorators import redirect_interdiction,\
40 drh_or_admin_required,\
41 in_drh_or_admin,\
42 in_one_of_group
5a1f75cb 43from project.rh import models as rh
25f2c148 44from project import groups
8684fcaa 45
ed1982f3 46
661da766
BS
47@login_required
48@drh_or_admin_required
49def tableau_remuneration(request, dossier_id):
50 dossier = get_object_or_404(dae.Dossier, pk=dossier_id)
51 form = RemunForm(instance=dossier)
52 return render(
53 request,
54 'dae/table_remuneration.html', {
bc8dcc0e 55 'remunForm': form,
661da766
BS
56 })
57
58
cea09938
EMS
59# Helpers
60
0a085c42
OL
61def devises():
62 liste = []
63 for d in rh.Devise.objects.all():
64 annee = date.today().year
65 taux = rh.TauxChange.objects.filter(annee=annee, devise=d)
66 data = {}
67 if len(taux) == 0:
68 data['taux_euro'] = 0
69 else:
70 data['taux_euro'] = taux[0].taux
378ffe6c 71 data['devise_code'] = d.id
0a085c42
OL
72 liste.append(data)
73 return liste
74
5a1f75cb 75
5633fa41 76@dae_groupe_requis
5d680e84 77def index(request):
890b80e7 78 return render(request, 'dae/index.html', {})
5d680e84 79
e3563bcf
EMS
80
81### POSTE
82
5633fa41
OL
83@dae_groupe_requis
84@poste_dans_ma_region_ou_service
c0413a6f
OL
85def poste_consulter(request, key):
86 source, id = key.split('-')
87 poste = get_object_or_404(dae.Poste, pk=id)
8684fcaa 88
e6f52402 89 if request.POST:
5a1f75cb
EMS
90 validationForm = PosteWorkflowForm(
91 request.POST, instance=poste, request=request
92 )
e6f52402 93 if validationForm.is_valid():
9e91202f 94 validationForm.save()
5a1f75cb
EMS
95 messages.add_message(
96 request, messages.SUCCESS, "La validation a été enregistrée."
97 )
18c6d4c0 98 return redirect('dae_postes_liste')
e6f52402
OL
99 else:
100 validationForm = PosteWorkflowForm(instance=poste, request=request)
8684fcaa 101
5a1f75cb
EMS
102 comparaisons_internes = \
103 poste.dae_comparaisons_internes.ma_region_ou_service(request.user)
580630f0 104 return render(request, 'dae/poste_consulter.html', {
1de3da13
EMS
105 'poste': poste,
106 'validationForm': validationForm,
580630f0 107 'comparaisons_internes': comparaisons_internes,
be46739d 108 'importer': request.user.is_superuser,
580630f0 109 })
868a9322 110
5a1f75cb 111
be46739d 112@user_passes_test(lambda u: u.is_superuser)
580630f0 113@drh_or_admin_required
6fcc7644
EMS
114def poste_importer(request, id):
115 poste_dae = get_object_or_404(dae.Poste, id=id)
116 if request.method == 'POST':
117 if 'confirmer' in request.POST:
118 poste_rh = poste_dae.importer_dans_rh()
119 return redirect('admin:rh_poste_change', poste_rh.id)
120 else:
121 return redirect('poste_consulter', 'dae-' + id)
122 else:
890b80e7
DB
123 c = {
124 'poste': poste_dae,
125 }
126 return render(request, 'dae/poste_importer.html', c)
6fcc7644
EMS
127
128
5633fa41 129@dae_groupe_requis
1b217058 130@poste_dans_ma_region_ou_service
6e80b20c 131@poste_est_modifiable
25f2c148
OL
132@in_one_of_group((groups.CORRESPONDANT_RH,
133 groups.ADMINISTRATEURS,
134 groups.DIRECTEUR_DE_BUREAU,
135 groups.DRH_NIVEAU_1,
136 groups.DRH_NIVEAU_2))
3ed49093 137def poste(request, key=None):
5d680e84
NC
138 """ Formulaire pour un poste.
139
140 Permet de créer ou modifier un poste. Si le poste n'existe que dans rh_v1
141 il est automatiquement copié dans dae.
142
143 """
80be36aa
OL
144 if 'creer_dossier_dae' in request.GET:
145 creer_dossier_dae = True
146 else:
147 creer_dossier_dae = False
874949f3
OL
148
149 def _dupliquer_poste(poste_dae, poste_rh):
150 """
151 Recopie les fields d'un poste RH dans un poste DAE
152 avec ceux-ci précédemment crées
153 """
154 exclus = ('id', 'supprime', 'date_creation',
155 'user_creation', 'date_modification',
156 'user_modification', )
157 fields = [f for f in poste_rh._meta.fields if f.name not in exclus]
158 for field in fields:
159 setattr(poste_dae, field.name, getattr(poste_rh, field.name))
160 return poste_dae
161
5d680e84
NC
162 poste, data, vars = None, dict(), dict()
163
874949f3
OL
164 # Sans key, c'est un nouveau poste
165 if key is None:
166 new = True
167 else:
168 new = False
169
73516366
OL
170 # Type intervention
171 if 'type_intervention' in request.GET:
172 data['type_intervention'] = request.GET['type_intervention']
67ae0181
OL
173 if creer_dossier_dae:
174 data['type_intervention'] = request.GET['creer_dossier_dae']
175
874949f3
OL
176 # Poste existant
177 poste_rh = None
178 if not new:
139686f2 179 source, id = key.split('-')
5d680e84 180
139686f2 181 if source == 'dae':
5d680e84 182 poste = get_object_or_404(dae.Poste, pk=id)
73516366 183 data['poste'] = key
139686f2 184 elif source == 'rh':
2e672700 185 poste_rh = get_object_or_404(rh.Poste, pk=id)
2e672700 186 poste = dae.Poste(id_rh=poste_rh)
874949f3
OL
187 # Initialisation avec les valeurs du poste de rh_v1
188 poste = _dupliquer_poste(poste, poste_rh)
73516366 189 data['poste'] = 'rh-' + str(poste.id_rh_id)
5d680e84 190
2e672700 191 # prépopuler pour la modification de poste
874949f3
OL
192 if poste_rh is not None:
193 FinancementForm = FinancementFormSetInitial
194 PosteComparaisonForm = PosteComparaisonFormSetInitial
2e672700 195
2e672700
OL
196 qs_financements = poste_rh.rh_financements.all()
197 qs_comparaisons = poste_rh.rh_comparaisons_internes.all()
874949f3
OL
198 financements = [{'type': f.type, 'pourcentage': f.pourcentage,
199 'commentaire': f.commentaire} for f in qs_financements]
200 comparaisons = [{'implantation': c.implantation, 'nom': c.nom,
201 'montant': c.montant, 'devise': c.devise} for c in qs_comparaisons]
202 # formulaires normaux, avec modifications des objects FK
203 else:
204 FinancementForm = FinancementFormSet
205 PosteComparaisonForm = PosteComparaisonFormSet
206 financements = []
207 comparaisons = []
2e672700 208
5d680e84 209 if request.POST:
3ed49093 210 data.update(dict(request.POST.items()))
f258e4e7 211 form = PosteForm(data, instance=poste, request=request)
874949f3 212 financementForm = FinancementForm(request.POST, instance=poste, )
c4e96e66
EMS
213 piecesForm = PostePieceFormSet(
214 request.POST, request.FILES, instance=poste
215 )
874949f3 216 comparaisons_formset = PosteComparaisonForm(
c4bfc2a0 217 request.POST,
874949f3 218 instance=poste,
c4bfc2a0 219 )
5a1f75cb
EMS
220 if form.is_valid() and piecesForm.is_valid() and \
221 financementForm.is_valid() and comparaisons_formset.is_valid():
5d680e84 222 poste = form.save()
eb8c3edb
OL
223 piecesForm.instance = poste
224 piecesForm.save()
151e7bd0
OL
225 financementForm.instance = poste
226 financementForm.save()
320d7584
EMS
227
228 # Ne remplacer que les comparaisons de ma région
229 comparaisons = comparaisons_formset.save(commit=False)
230 for comparaison in comparaisons:
231 comparaison.poste = poste
232 comparaison.save()
233
2e672700
OL
234 # dans le cas d'une modification de poste de RH, on recopie les PJ
235 if poste_rh is not None:
236 for piece in poste_rh.rh_pieces.all():
237 dae.PostePiece(poste=poste, nom=piece.nom,
238 fichier=piece.fichier).save()
5a1f75cb
EMS
239 messages.add_message(
240 request, messages.SUCCESS,
241 "Le poste %s a été sauvegardé." % poste
242 )
80be36aa
OL
243 if creer_dossier_dae:
244 return redirect('embauche', key='dae-%s' % poste.id)
245
5a1f75cb 246 if 'save' in request.POST:
5bc760f9
OL
247 return redirect('poste_consulter', key='dae-%s' % poste.id)
248 else:
249 return redirect('poste', key='dae-%s' % poste.id)
8684fcaa 250
9cb4de55 251 else:
5a1f75cb
EMS
252 messages.add_message(
253 request, messages.ERROR,
254 'Il y a des erreurs dans le formulaire.'
255 )
8684fcaa 256
5d680e84
NC
257 else:
258 # 'initial' évite la validation prémature lors d'une copie de poste de
259 # rh_v1 vers dae.
f258e4e7 260 form = PosteForm(initial=data, instance=poste, request=request)
874949f3 261 piecesForm = PostePieceFormSet(instance=poste)
2e672700 262
874949f3 263 if poste_rh is not None:
c4e96e66
EMS
264 financementForm = FinancementForm(
265 initial=financements, instance=poste
266 )
874949f3 267 comparaisons_formset = PosteComparaisonForm(
2e672700 268 initial=comparaisons,
874949f3 269 instance=poste,
829eb351 270 )
874949f3 271 # cas de la création d'un nouveau poste
829eb351 272 else:
874949f3
OL
273 financementForm = FinancementForm(instance=poste)
274 comparaisons_formset = PosteComparaisonForm(instance=poste)
5d680e84 275
320d7584
EMS
276 vars.update(dict(
277 form=form, poste=poste, poste_key=key, piecesForm=piecesForm,
278 financementForm=financementForm,
2e672700
OL
279 comparaisons_formset=comparaisons_formset,
280 poste_rh=poste_rh,
80be36aa 281 creer_dossier_dae=creer_dossier_dae,
320d7584 282 ))
5d680e84 283
890b80e7 284 return render(request, 'dae/poste.html', vars)
3ed49093 285
5a1f75cb 286
5633fa41 287@dae_groupe_requis
498881f4 288def postes_liste(request):
0f23302a 289 """ Liste des postes. """
92741270
EMS
290 content_type = ContentType.objects.get_for_model(dae.Poste)
291 extra_select = {'derniere_validation': (
292 "SELECT MAX(date) FROM workflow_workflowcommentaire "
5a1f75cb
EMS
293 "WHERE content_type_id = '%s' AND object_id = dae_poste.id" %
294 content_type.id
92741270
EMS
295 )}
296 postes_a_traiter = dae.Poste.objects.mes_choses_a_faire(request.user) \
66fefd2f
OL
297 .annotate(num_dae=Count('dae_dossiers')) \
298 .filter(num_dae=0) \
cea09938 299 .extra(select=extra_select).order_by('-id')
92741270 300 postes_en_cours = dae.Poste.objects.ma_region_ou_service(request.user) \
66fefd2f
OL
301 .annotate(num_dae=Count('dae_dossiers')) \
302 .filter(num_dae=0) \
5a1f75cb
EMS
303 .extra(select=extra_select) \
304 .filter(~Q(etat=POSTE_ETAT_FINALISE)) \
cea09938
EMS
305 .order_by('-id')
306 return render(request, 'dae/postes_liste.html', {
92741270
EMS
307 'postes_a_traiter': postes_a_traiter,
308 'postes_en_cours': postes_en_cours,
cea09938 309 })
98d51b59 310
5a1f75cb 311
e3563bcf 312@login_required
67a94eaf 313def poste_piece(request, id, filename):
e3563bcf
EMS
314 """Téléchargement d'une pièce jointe à un poste."""
315 piece = get_object_or_404(dae.PostePiece, pk=id)
5a1f75cb
EMS
316 if dae.Poste.objects.ma_region_ou_service(request.user) \
317 .filter(id=piece.poste_id).exists():
67a94eaf 318 return sendfile(request, piece.fichier.path)
e3563bcf
EMS
319 else:
320 return redirect_interdiction(request)
321
322
323### DOSSIER
324
cb1d62b5 325def filtered_type_remun():
5a1f75cb
EMS
326 defaut = (2, 3, 8, 17) # salaire de base, indemnité de fonction,
327 # charges patronales
0a085c42 328 return rh.TypeRemuneration.objects.filter(pk__in=defaut)
cb1d62b5 329
5a1f75cb 330
5633fa41 331@dae_groupe_requis
62d3903d 332@dossier_dans_ma_region_ou_service
5d5a57a4
OL
333def embauche_consulter(request, dossier_id):
334 dossier = get_object_or_404(dae.Dossier, pk=dossier_id)
8684fcaa 335 etat_precedent = dossier.etat
e6f52402
OL
336
337 if request.POST:
5a1f75cb
EMS
338 validationForm = DossierWorkflowForm(
339 request.POST, instance=dossier, request=request
340 )
e6f52402 341 if validationForm.is_valid():
8684fcaa 342 if etat_precedent == DOSSIER_ETAT_REGION_FINALISATION and \
5a1f75cb
EMS
343 validationForm.cleaned_data['etat'] == \
344 DOSSIER_ETAT_DRH_FINALISATION:
8684fcaa 345 send_drh_finalisation_mail(request, dossier)
e6f52402 346 validationForm.save()
5a1f75cb
EMS
347 messages.add_message(
348 request, messages.SUCCESS, "La validation a été enregistrée."
349 )
18c6d4c0 350 return redirect('dae_embauches_liste')
e6f52402
OL
351 else:
352 validationForm = DossierWorkflowForm(instance=dossier, request=request)
9536ea21 353
5a1f75cb
EMS
354 comparaisons_internes = \
355 dossier.poste.dae_comparaisons_internes.ma_region_ou_service(
356 request.user
357 )
a4125771 358 comparaisons = dossier.dae_comparaisons.ma_region_ou_service(request.user)
36fc5c09 359 remunForm = ReadOnlyRemunFormSet(instance=dossier)
580630f0 360 return render(request, 'dae/embauche_consulter.html', {
291bbfd9 361 'dossier': dossier,
36fc5c09 362 'devises': devises(),
291bbfd9 363 'validationForm': validationForm,
320d7584 364 'comparaisons_internes': comparaisons_internes,
580630f0 365 'comparaisons': comparaisons,
36fc5c09 366 'remunForm': remunForm,
be46739d 367 'importer': request.user.is_superuser,
580630f0 368 })
5a1f75cb 369
c4e96e66 370
7f432a0b 371@user_passes_test(lambda u: u.is_superuser)
5633fa41 372@dae_groupe_requis
fb0ed970
EMS
373@dossier_dans_ma_region_ou_service
374def embauche_importer(request, dossier_id=None):
375 dossier_dae = get_object_or_404(dae.Dossier, id=dossier_id)
47b60f16
EMS
376 if request.method == 'POST':
377 if 'confirmer' in request.POST:
378 dossier_rh = dossier_dae.importer_dans_rh()
379 return redirect('admin:rh_dossier_change', dossier_rh.id)
380 else:
381 return redirect('embauches_finalisees')
382 else:
890b80e7
DB
383 c = {
384 'dossier': dossier_dae,
385 }
386 return render(request, 'dae/embauche_importer.html', c)
fb0ed970
EMS
387
388
389@dae_groupe_requis
577dcb77 390def embauche_choisir_poste(request):
80be36aa
OL
391 if request.POST:
392 form = ChoosePosteForm(data=request.POST, request=request)
393 if form.is_valid():
394 return form.redirect()
395 else:
396 form = ChoosePosteForm(request=request)
890b80e7 397 c = {
80be36aa 398 'form': form,
890b80e7
DB
399 }
400 return render(request, 'dae/embauche-choisir-poste.html', c)
577dcb77 401
5a1f75cb 402
577dcb77 403@dae_groupe_requis
62d3903d 404@dossier_dans_ma_region_ou_service
62cfa562 405@dossier_est_modifiable
62d3903d 406def embauche(request, key=None, dossier_id=None):
139686f2 407 """ Formulaire d'autorisation d'embauche. """
139686f2 408
577dcb77
EMS
409 # Récupérer ou créer un poste et un dossier
410 source, id = key.split('-')
411 if source != 'dae':
412 return Http404
413 poste = get_object_or_404(dae.Poste, pk=id)
414
415 if request.POST:
416 if request.POST['employe'] == '':
417 # Nouvel employé
418 employe = dae.Employe()
419 else:
420 employe_source, id = request.POST['employe'].split('-')
421 if employe_source == 'dae':
422 # Employé DAE
423 employe = get_object_or_404(dae.Employe, pk=id)
424 elif employe_source == 'rh':
425 # Employé RH, on le copie dans DAE
426 e = get_object_or_404(rh.Employe, pk=id)
427 employe = dae.Employe(id_rh=e, prenom=e.prenom, nom=e.nom,
428 genre=e.genre)
139686f2 429 else:
577dcb77
EMS
430 raise Http404
431
5a1f75cb
EMS
432 employe_form = EmployeForm(
433 request.POST, instance=employe, request=request
434 )
577dcb77
EMS
435
436 if employe_form.is_valid():
437 data = dict(request.POST.items())
438 employe = employe_form.save()
439 data['employe'] = 'dae-%s' % employe.id
440 employe_form = EmployeForm(data, instance=employe, request=request)
441
442 if not dossier_id:
443 dossier = dae.Dossier(poste=poste, employe=employe)
444 else:
445 dossier = get_object_or_404(dae.Dossier, pk=dossier_id)
16b1454e 446 dossier.employe = employe_form.instance
577dcb77
EMS
447
448 dossier_form = DossierForm(request.POST, instance=dossier)
5a1f75cb
EMS
449 piecesForm = DossierPieceForm(
450 request.POST, request.FILES, instance=dossier
451 )
320d7584
EMS
452 comparaisons_formset = DossierComparaisonFormSet(
453 request.POST,
5a1f75cb
EMS
454 queryset=dossier.dae_comparaisons.ma_region_ou_service(
455 request.user
456 )
320d7584 457 )
4718c21c 458
577dcb77
EMS
459 remunForm = RemunForm(request.POST, instance=dossier)
460
55360d7d
EMS
461 if employe_form.is_valid() and \
462 dossier_form.is_valid() and \
577dcb77 463 piecesForm.is_valid() and \
320d7584 464 comparaisons_formset.is_valid() and \
577dcb77
EMS
465 remunForm.is_valid():
466 employe.save()
55360d7d 467 dossier_form.save()
577dcb77 468 piecesForm.save()
577dcb77 469 remunForm.save()
320d7584
EMS
470
471 # Ne remplacer que les comparaisons de ma région
472 comparaisons = comparaisons_formset.save(commit=False)
473 for comparaison in comparaisons:
474 comparaison.dossier = dossier
475 comparaison.save()
476
5a1f75cb
EMS
477 messages.success(
478 request, "Le dossier %s a été sauvegardé." % dossier
479 )
480 if 'save' in request.POST:
577dcb77 481 return redirect('embauche_consulter', dossier_id=dossier.id)
768d7e1b 482 else:
5a1f75cb
EMS
483 return redirect(
484 'embauche', key=dossier.poste.key, dossier_id=dossier.id
485 )
9536ea21 486
577dcb77 487 else:
5a1f75cb
EMS
488 messages.add_message(
489 request, messages.ERROR,
490 'Il y a des erreurs dans le formulaire.'
491 )
9536ea21 492
577dcb77
EMS
493 else:
494 # Initialisation d'un formulaire vide
495 if dossier_id:
496 dossier = get_object_or_404(dae.Dossier, pk=dossier_id)
497 employe = dossier.employe
498 data = dict(employe='dae-%s' % employe.id)
5a1f75cb
EMS
499 employe_form = EmployeForm(
500 initial=data, instance=employe, request=request
501 )
ed1982f3
NC
502 else:
503 dossier_rh = rh.Dossier()
504 poste_rh = poste.id_rh
577dcb77
EMS
505 dossier = pre_filled_dossier(dossier_rh, 'new', poste_rh)
506 employe_form = EmployeForm(request=request)
507
508 dossier_form = DossierForm(instance=dossier)
509 piecesForm = DossierPieceForm(instance=dossier)
320d7584 510 comparaisons_formset = DossierComparaisonFormSet(
5a1f75cb
EMS
511 queryset=dossier.dae_comparaisons.ma_region_ou_service(
512 request.user
513 )
320d7584 514 )
577dcb77
EMS
515 remunForm = RemunForm(instance=dossier)
516
829eb351 517 try:
5a1f75cb
EMS
518 comparaisons_internes = \
519 dossier.poste.dae_comparaisons_internes.ma_region_ou_service(
520 request.user
521 )
829eb351
EMS
522 except dae.Poste.DoesNotExist:
523 comparaisons_internes = []
890b80e7 524 c = {
577dcb77
EMS
525 'type_remun': filtered_type_remun(),
526 'devises': devises(),
527 'poste': poste,
528 'dossier': dossier,
529 'piecesForm': piecesForm,
530 'remunForm': remunForm,
320d7584 531 'comparaisons_formset': comparaisons_formset,
291bbfd9 532 'forms': dict(employe=employe_form, dossier=dossier_form, ),
890b80e7
DB
533 'comparaisons_internes': comparaisons_internes,
534 }
535 return render(request, 'dae/embauche.html', c)
139686f2 536
5a1f75cb 537
5633fa41 538@dae_groupe_requis
62d3903d 539@dossier_dans_ma_region_ou_service
0140cbd2 540def embauches_liste(request):
541 """ Liste des embauches. """
92741270
EMS
542 content_type = ContentType.objects.get_for_model(dae.Dossier)
543 extra_select = {'derniere_validation': (
544 "SELECT MAX(date) FROM workflow_workflowcommentaire "
5a1f75cb
EMS
545 "WHERE content_type_id = '%s' AND object_id = dae_dossier.id" %
546 content_type.id
92741270 547 )}
5a1f75cb
EMS
548 embauches_a_traiter = dae.Dossier.objects \
549 .mes_choses_a_faire(request.user) \
cea09938 550 .extra(select=extra_select).order_by('-id')
5a1f75cb
EMS
551 embauches_en_cours = dae.Dossier.objects \
552 .ma_region_ou_service(request.user) \
553 .extra(select=extra_select) \
cea09938 554 .order_by('-id') \
5a1f75cb 555 .exclude(etat=DOSSIER_ETAT_FINALISE)
890b80e7 556 c = {
92741270
EMS
557 'embauches_a_traiter': embauches_a_traiter,
558 'embauches_en_cours': embauches_en_cours,
890b80e7
DB
559 }
560 return render(request, 'dae/embauches_liste.html', c)
355c80c8 561
5a1f75cb 562
44055779
EMS
563@dae_groupe_requis
564def embauches_finalisees(request):
565 """Liste des embauches finalisées."""
bed096fc
EMS
566
567 ### POST
568
569 if request.method == 'POST':
570 if 'supprimer' in request.POST:
571 ids = request.POST.getlist('ids')
572 dossiers = dae.Dossier.objects.filter(id__in=ids)
573 count = dossiers.count()
574 if count > 0:
575 dossiers.delete()
576 messages.success(request, u'%d dossiers supprimés' % count)
577 return redirect(request.get_full_path())
578
579 ### GET
580
44055779
EMS
581 embauches = dae.Dossier.objects.ma_region_ou_service(request.user) \
582 .filter(etat=DOSSIER_ETAT_FINALISE)
583
cbfd7bd4
EMS
584 # Recherche
585 search_form = DAEFinaliseesSearchForm(request.GET)
586 if search_form.is_valid():
587 q = search_form.cleaned_data.get('q').strip()
588 importees = search_form.cleaned_data.get('importees')
589 if q:
590 criteria = [
c4e96e66
EMS
591 Q(**{
592 'poste__implantation__zone_administrative__nom__icontains':
593 word
594 }) |
b0cf30b8 595 Q(poste__implantation__zone_administrative__code=word) |
cbfd7bd4
EMS
596 Q(poste__implantation__nom__icontains=word) |
597 Q(poste__nom__icontains=word) |
598 Q(employe__nom__icontains=word) |
599 Q(employe__prenom__icontains=word)
600 for word in q.split()
601 ]
602 embauches = embauches.filter(*criteria)
603 if importees == 'oui':
604 embauches = embauches.exclude(dossier_rh=None)
605 elif importees == 'non':
606 embauches = embauches.filter(dossier_rh=None)
607
44055779
EMS
608 # Tri
609 tri = request.GET.get('tri', None)
7652a4c3 610 if tri and tri.startswith('-'):
44055779
EMS
611 dir = '-'
612 tri = tri[1:]
613 else:
614 dir = ''
615 if tri == 'region':
5a1f75cb 616 embauches = embauches.order_by(
b0cf30b8 617 dir + 'poste__implantation__zone_administrative__nom'
5a1f75cb 618 )
44055779
EMS
619 elif tri == 'implantation':
620 embauches = embauches.order_by(dir + 'poste__implantation__nom')
621 elif tri == 'poste':
622 embauches = embauches.order_by(dir + 'poste__nom')
623 elif tri == 'personne':
8684fcaa
EMS
624 embauches = embauches.order_by(dir + 'employe__nom',
625 dir + 'employe__prenom')
44055779
EMS
626 elif tri == 'date_debut':
627 embauches = embauches.order_by(dir + 'debut_contrat')
628 elif tri == 'date_fin':
629 embauches = embauches.order_by(dir + 'fin_contrat')
630
631 # Pagination
632 paginator = Paginator(embauches, 20)
633 try:
634 page = paginator.page(request.GET.get('page', 1))
635 except InvalidPage:
636 page = paginator.page(1)
1b31de9f 637
580630f0 638 return render(request, 'dae/embauches_finalisees.html', {
cbfd7bd4 639 'embauches': page,
890b80e7 640 'search_form': search_form,
580630f0
EMS
641 'importer': in_drh_or_admin(request.user)
642 })
44055779 643
5a1f75cb 644
139686f2
NC
645def employe(request, key):
646 """ Récupération AJAX de l'employé pour la page d'embauche. """
647 data = dict(employe=key)
648
649 if key == '':
650 # Nouvel employé
651 employe = dae.Employe()
652 else:
653 # Employé existant
654 source, id = key.split('-')
655
656 if source == 'dae':
657 employe = get_object_or_404(dae.Employe, pk=id)
658 elif source == 'rh':
659 e = get_object_or_404(rh.Employe, id=id)
660 # Initialisation avec les valeurs de l'employé de rh_v1
661 employe = dae.Employe(id_rh=e)
662 for field in ('prenom', 'nom', 'genre'):
663 setattr(employe, field, getattr(e, field))
664
5a1f75cb
EMS
665 return HttpResponse(
666 EmployeForm(initial=data, instance=employe, request=request).as_table()
667 )
668
139686f2 669
9536ea21
EMS
670### CONTRATS
671
672@dae_groupe_requis
673@get_contrat
67a94eaf
EMS
674def contrat(request, contrat, filename):
675 return sendfile(request, contrat.fichier.path)
9536ea21 676
5a1f75cb 677
9536ea21
EMS
678@dae_groupe_requis
679@get_contrat
680def contrat_supprimer(request, contrat):
681 if request.method == 'POST':
682 if 'oui' in request.POST:
683 contrat.delete()
684 return redirect('embauche_consulter', dossier_id=contrat.dossier.id)
890b80e7
DB
685 c = {
686 'contrat': contrat,
687 }
688 return render(request, 'dae/contrat-supprimer.html', c)
9536ea21 689
5a1f75cb 690
9536ea21
EMS
691@dae_groupe_requis
692@dossier_dans_ma_region_ou_service
693def embauche_ajouter_contrat(request, dossier_id=None):
694 dossier = get_object_or_404(dae.Dossier, pk=dossier_id)
695 if request.method == 'POST':
696 form = ContratForm(request.POST, request.FILES)
697 if form.is_valid():
698 contrat = form.save(commit=False)
699 contrat.dossier = dossier
9dfa4296
OL
700 contrat.date_debut = dossier.contrat_date_debut
701 contrat.date_fin = dossier.contrat_date_fin
9536ea21
EMS
702 contrat.save()
703 return redirect('embauche_consulter', dossier_id=dossier.id)
704 else:
705 form = ContratForm()
1b31de9f 706
890b80e7
DB
707 c = {
708 'form': form,
709 }
710 return render(request, 'dae/embauche-ajouter-contrat.html', c)
9536ea21 711
5a1f75cb 712
c3f0b49f
EMS
713### DAE NUMERISEE
714
715@get_object(dae.Dossier, 'consulter')
716def dae_numerisee(request, dossier):
717 return sendfile(request, dossier.dae_numerisee.path)
718
5a1f75cb 719
c3f0b49f
EMS
720@get_object(dae.Dossier, 'modifier_dae_numerisee')
721def dae_numerisee_modifier(request, dossier):
722 if request.method == 'POST':
723 form = DAENumeriseeForm(request.POST, request.FILES, instance=dossier)
724 if form.is_valid():
725 form.save()
726 return redirect('embauche_consulter', dossier_id=dossier.id)
727 else:
728 form = DAENumeriseeForm(instance=dossier)
1b31de9f 729
890b80e7
DB
730 c = {
731 'form': form,
732 }
733 return render(request, 'dae/dae_numerisee_modifier.html', c)
c3f0b49f 734
5a1f75cb 735
c3f0b49f
EMS
736@get_object(dae.Dossier, 'modifier_dae_numerisee')
737def dae_numerisee_supprimer(request, dossier):
738 if request.method == 'POST':
739 if 'oui' in request.POST:
740 dossier.dae_numerisee = None
741 dossier.save()
742 return redirect('embauche_consulter', dossier_id=dossier.id)
890b80e7 743 return render(request, 'dae/dae_numerisee_supprimer.html', {})
5a1f75cb 744
c3f0b49f 745
04380fba 746# AJAX SECURISE
5a1f75cb 747
04380fba
OL
748@dae_groupe_requis
749@employe_dans_ma_region_ou_service
139686f2
NC
750def dossier(request, poste_key, employe_key):
751 """ Récupération AJAX du dossier pour la page d'embauche. """
752 data = dict()
753
754 poste_source, poste_id = poste_key.split('-')
755 poste = get_object_or_404(dae.Poste, pk=poste_id)
179f6b49
OL
756
757 # Récupérer la devise de l'implantation lié au poste
758 implantation_devise = poste.get_default_devise()
5a1f75cb 759 data.update({'devise': implantation_devise})
9536ea21 760
e27db04c
OL
761 if poste.id_rh_id is not None:
762 poste_rh = get_object_or_404(rh.Poste, pk=poste.id_rh_id)
763 else:
764 poste_rh = None
139686f2 765
eabaed81 766 # NOUVEL EMPLOYE
139686f2
NC
767 if employe_key == '':
768 employe_source = 'new'
eabaed81 769 employe = None
139686f2 770 dossier_rh = rh.Dossier()
ed1982f3 771 dossier = pre_filled_dossier(dossier_rh, employe_source, poste_rh)
139686f2 772
eabaed81 773 # EMPLOYE DAE
eabaed81 774 if employe_key.startswith('dae'):
5a1f75cb
EMS
775 employe_source, employe_id = employe_key.split('-')
776 employe_dae = get_object_or_404(dae.Employe, pk=employe_id)
777
778 # récupération de l'ancien dossier rh v1 pour l'employe DAE
779 try:
780 dossier_rh = rh.Dossier.objects.get(
781 employe=employe_dae.id_rh_id, date_fin=None
782 )
783 except (rh.Dossier.DoesNotExist):
784 dossier_rh = rh.Dossier()
785
786 # on tente de récupérer le dossier DAE, au pire on le contruit en le
787 # prépoluant avec son dossier rh v1.
788 try:
789 dossier = dae.Dossier.objects.get(employe=employe_dae, poste=poste)
790 except (dae.Dossier.DoesNotExist):
791 dossier = pre_filled_dossier(dossier_rh, employe_source, poste_rh)
792 employe = employe_dae.id_rh
793
eabaed81 794 # EMPLOYE RH v1
eabaed81 795 if employe_key.startswith('rh'):
5a1f75cb
EMS
796 employe_source, employe_id = employe_key.split('-')
797 employe_rh = get_object_or_404(rh.Employe, pk=employe_id)
798
799 # récupération de l'ancien dossier rh v1 pour l'employe rh v1, s'il
800 # n'en a pas, on en fournit un nouveau qui servira uniquement un
801 # créer un nouveau dossier DAE.
802 try:
803 dossier_rh = rh.Dossier.objects.get(
804 employe=employe_rh, date_fin=None
805 )
806 except (rh.Dossier.DoesNotExist):
807 dossier_rh = rh.Dossier()
808 dossier = pre_filled_dossier(dossier_rh, employe_source, poste_rh)
809 employe = employe_rh
da3ca955 810
eabaed81
OL
811 dossier_form = DossierForm(initial=data, instance=dossier)
812 vars = dict(form=dossier_form, poste=poste, employe=employe)
890b80e7 813 return render(request, 'dae/embauche-dossier.html', vars)
139686f2 814
5a1f75cb
EMS
815
816# Cette fonction est appelée à partir de fonctions déjà sécurisée
ed1982f3
NC
817def pre_filled_dossier(dossier_rh, employe_source, poste_rh):
818 dossier = dae.Dossier()
819
820 if employe_source != 'new' and dossier_rh.id:
821 dossier.statut_anterieur = dossier_rh.statut
822
823 # Certains dossiers ont un classement à zéro
824 if dossier_rh.classement_id > 0:
825 dossier.classement_anterieur = dossier_rh.classement
826
827 # Récupération du salaire de base
5a1f75cb
EMS
828 remun = dossier_rh.remunerations() \
829 .filter(type=1).order_by('-date_debut')
ed1982f3
NC
830 if remun:
831 dossier.salaire_anterieur = remun[0].montant
80bcc96b 832 dossier.devise_anterieur = remun[0].devise
ed1982f3
NC
833
834 # Récupération du titulaire précédent
835 try:
09aa8374 836 dossiers = rh.Dossier.objects.order_by('-date_debut')
32f26ff3 837 if poste_rh:
09aa8374 838 dossiers = dossiers.filter(poste=poste_rh)
32f26ff3
AJ
839 else:
840 dossiers = rh.Dossier.objects.none()
ed1982f3
NC
841 if len(dossiers):
842 # Ce bloc ignore toutes les erreurs, car les données de rh
843 # manquantes peuvent en générer
844 d = dossiers[0]
845 try:
846 titulaire = d.employe
847 dossier.employe_anterieur = titulaire
848 dossier.classement_titulaire_anterieur = d.classement
849 dossier.statut_titulaire_anterieur = d.statut
5a1f75cb
EMS
850 remun = d.remunerations().filter(type=1) \
851 .order_by('-date_debut')[0]
80bcc96b
OL
852 dossier.salaire_titulaire_anterieur = remun.montant
853 dossier.devise_titulaire_anterieur = remun.devise
ed1982f3
NC
854 except:
855 pass
856 # TODO: afficher l'info, les champs ne sont pas dans le
857 # modèle dae.Dossier: nom, prenom, classement, salaire
858 pass
859
860 except (rh.Dossier.DoesNotExist):
861 dossier_rh = rh.Dossier()
862
863 return dossier
864
c4e96e66 865
cf4e6a30
OL
866def _get_salaire_traitement(dossier):
867 """
868 Type de remun traitement derniers commencant a la meme date
869 """
04380fba 870 data = {}
3803341b 871 # Toutes les rémunérations d'un dossier
c4e96e66
EMS
872 remunerations = [
873 r for r in dossier.remunerations().order_by('-date_debut')
874 if r.type.nature_remuneration == "Traitement"
875 ]
876
3803341b
OL
877 # On prend les dernières avec le postulat que les rémun à la même date
878 # constituent le dernier salaire
879 if len(remunerations) > 0:
880 date_debut = remunerations[0].date_debut
881 remunerations = [r for r in remunerations if r.date_debut == date_debut]
882
883 montant = 0.0
884 montant_euros = 0.0
885 devise = None
886
887 # Les remun sont sensées être dans la même devise
888 for r in remunerations:
889 montant += float(r.montant)
890 montant_euros += r.montant_euros()
891 devise = r.devise.id
892
893 data['devise'] = devise
894 data['montant'] = montant
895 data['montant_euros'] = montant_euros
cf4e6a30
OL
896 return data
897
c4e96e66 898
cf4e6a30
OL
899@dae_groupe_requis
900@vieux_dossier_dans_ma_region_ou_service
901def dossier_resume(request, dossier_id=None):
902 try:
903 dossier = rh.Dossier.objects.get(id=dossier_id)
904 except:
898d7967 905 return HttpResponseNotFound("Ce dossier n'est pas accessible")
c4e96e66 906
cf4e6a30
OL
907 data = {}
908 data['personne'] = unicode(dossier.employe)
b2f3b033
OL
909 if dossier.classement is not None:
910 data['classement'] = dossier.classement.id
cf4e6a30
OL
911 if dossier.statut is not None:
912 data['statut'] = dossier.statut.id
913 data['implantation'] = dossier.poste.implantation.id
914 data['poste'] = dossier.poste.nom
915 data.update(_get_salaire_traitement(dossier))
85668061 916 return HttpResponse(dumps(data))
f87fe1a1 917
5a1f75cb 918
068d1462
OL
919@dae_groupe_requis
920@vieux_dossier_dans_ma_region_ou_service
921def poste_resume(request, dossier_id=None):
922 """
923 On travaille, en réalité sur le dossier mais on cache
924 l'identité de la personne.
925 """
926 try:
927 dossier = rh.Dossier.objects.get(id=dossier_id)
928 except:
898d7967 929 return HttpResponseNotFound("Ce dossier n'est pas accessible")
068d1462
OL
930
931 data = {}
09aa8374 932 data['implantation'] = dossier.poste.implantation.id
83c5ebb7 933 data['poste'] = dossier.poste.nom
5d84008f
OL
934 data['statut'] = dossier.statut_id
935 data['classement'] = dossier.classement_id
cf4e6a30 936 data.update(_get_salaire_traitement(dossier))
068d1462
OL
937 return HttpResponse(dumps(data))
938
5a1f75cb 939
6d047148 940def liste_postes(request):
8684fcaa 941 """ Appel AJAX :
6d047148
OL
942 input : implantation_id
943 output : JSON liste de valeur point
944 """
945 method = request.method
946 params = getattr(request, method, [])
947 data = []
948
5a1f75cb
EMS
949 if 'implantation_id' in params \
950 and params.get('implantation_id') is not u"":
6d047148 951 implantation_id = params.get('implantation_id')
9c1ff333
OL
952 q = Q(implantation__id=implantation_id)
953 else:
954 q = Q()
9536ea21 955
9c1ff333
OL
956 postes_rh = rh.Poste.objects.ma_region_ou_service(request.user).filter(q)
957 postes_rh = postes_rh.select_related(depth=1)
4bce4d24 958
5a1f75cb 959 data = [('', 'Nouveau poste')] + \
9c1ff333
OL
960 sorted([('rh-%s' % p.id, label_poste_display(p)) for p in
961 postes_rh],
962 key=lambda t: t[1])
6d047148
OL
963 return HttpResponse(dumps(data))
964
5a1f75cb 965
e3563bcf 966@login_required
67a94eaf 967def dossier_piece(request, id, filename):
e3563bcf
EMS
968 """Téléchargement d'une pièce jointe à un poste."""
969 piece = get_object_or_404(dae.DossierPiece, pk=id)
5a1f75cb
EMS
970 if dae.Dossier.objects.ma_region_ou_service(request.user) \
971 .filter(id=piece.dossier_id).exists():
67a94eaf 972 return sendfile(request, piece.fichier.path)
e3563bcf
EMS
973 else:
974 return redirect_interdiction(request)
975
5a1f75cb 976
04380fba 977# AJAX SECURITE non nécessaire
5a1f75cb 978
04380fba 979def coefficient(request):
9536ea21 980 """ Appel AJAX :
04380fba
OL
981 input : classement
982 output : coefficient
983 """
984 method = request.method
985 params = getattr(request, method, [])
986 data = dict()
987 if 'classement' in params and params.get('classement') is not u"":
988 classement = params.get('classement')
989 classement = rh.Classement.objects.get(pk=classement)
990 data['coefficient'] = classement.coefficient
991 else:
992 data['coefficient'] = 0
993 return HttpResponse(dumps(data))
994
995
3d627bfd 996def devise(request):
8684fcaa 997 """ Appel AJAX :
3d627bfd 998 input : valeur_point
8e30e17f 999 output : devise, devise_code, taux_euro
3d627bfd 1000 """
f87fe1a1
OL
1001 method = request.method
1002 params = getattr(request, method, [])
3d627bfd 1003 data = dict()
8684fcaa 1004 if 'valeur_point' in params and params.get('valeur_point') is not u"":
f87fe1a1 1005 valeur_point = params.get('valeur_point')
3d627bfd 1006 valeur_point = rh.ValeurPoint.objects.get(pk=valeur_point)
1007 annee = valeur_point.annee
3f5cbabe 1008 try:
5a1f75cb
EMS
1009 taux = rh.TauxChange.objects.get(
1010 annee=annee, devise=valeur_point.devise
1011 )
898d7967
EMS
1012 except rh.TauxChange.DoesNotExist:
1013 return HttpResponseNotFound(
1014 u"Taux de change introuvable pour la devise %s "
1015 u"pour l'année %d" % (valeur_point.devise.code, annee)
1016 )
1017 except rh.TauxChange.MultipleObjectsReturned:
1018 return HttpResponseNotFound(
5a1f75cb
EMS
1019 u"Il existe plusieurs taux pour la devise %s "
1020 u"cette année-là : %s" % (valeur_point.devise.code, annee)
1021 )
3f5cbabe 1022
3d627bfd 1023 data['devise'] = taux.devise.id
f87fe1a1 1024 data['valeur'] = valeur_point.valeur
3d627bfd 1025 data['devise_code'] = taux.devise.code
8e30e17f 1026 data['taux_euro'] = taux.taux
be3c51e9 1027 else:
898d7967 1028 return HttpResponseNotFound("Vous devez choisir une valeur de point")
3d627bfd 1029 return HttpResponse(dumps(data))
9536ea21 1030
5a1f75cb 1031
3d627bfd 1032def devise_code(request):
8684fcaa 1033 """ Appel AJAX :
3d627bfd 1034 input : devise
8e30e17f 1035 output : devise_code, taux_euro
3d627bfd 1036 """
f87fe1a1
OL
1037 method = request.method
1038 params = getattr(request, method, [])
3d627bfd 1039 data = dict()
f87fe1a1
OL
1040 if 'devise' in params:
1041 devise = params.get('devise')
3d627bfd 1042 devise = rh.Devise.objects.get(pk=devise)
8e30e17f 1043 annee = date.today().year
1044 taux = rh.TauxChange.objects.filter(annee=annee, devise=devise)
86f1e48d 1045 if len(taux) == 0:
898d7967 1046 return HttpResponseNotFound("Le taux n'est pas disponible")
3d627bfd 1047 data['devise_code'] = devise.code
8e30e17f 1048 data['taux_euro'] = taux[0].taux
3d627bfd 1049 return HttpResponse(dumps(data))
85668061 1050
5a1f75cb 1051
cb1d62b5
NC
1052def add_remun(request, dossier, type_remun):
1053 dossier = get_object_or_404(dae.Dossier, pk=dossier)
1054 type_remun = get_object_or_404(rh.TypeRemuneration, pk=type_remun)
1055 dae.Remuneration(dossier=dossier, devise=dossier.devise,
1056 type=type_remun).save()
1057
890b80e7
DB
1058 c = {
1059 'dossier': dossier,
1060 }
1061 return render(request, 'dae/embauche-remun.html', c)
03b395db 1062
5a1f75cb 1063
04380fba
OL
1064def salaire(request, implantation, devise, classement):
1065 if not devise or not classement:
1066 raise Http404
1067
04380fba 1068 taux = rh.TauxChange.objects.filter(devise=devise).order_by('-annee')
5a1f75cb
EMS
1069 vp = rh.ValeurPoint.objects \
1070 .filter(implantation=implantation, devise=devise) \
1071 .order_by('-annee')
aa512122
OL
1072
1073 if vp.count() == 0:
7167d28f
OL
1074 status = u"pas de valeur de point pour le couple \
1075implantation/devise (%s/%s)" % (implantation, devise)
1076 return HttpResponse(dumps(dict(status=status)))
aa512122
OL
1077
1078 if taux.count() == 0:
7167d28f
OL
1079 status = u"Pas de taux pour la devise %s" % devise
1080 return HttpResponse(dumps(dict(status=status)))
04380fba
OL
1081
1082 classement = get_object_or_404(rh.Classement, pk=classement)
e2968ebb
EMS
1083 if classement.coefficient is None:
1084 raise Http404
90716392 1085 taux, vp = taux[0].taux, vp[0].valeur
04380fba 1086
90716392 1087 salaire_euro = round(vp * classement.coefficient * taux, 2)
7167d28f
OL
1088 data = dict(status='OK',
1089 salaire_euro=salaire_euro, taux=taux,
04380fba
OL
1090 salaire_devise=round(salaire_euro / taux, 2))
1091
1092 return HttpResponse(dumps(data))
1093
5a1f75cb 1094
04380fba 1095def liste_valeurs_point(request):
8684fcaa 1096 """ Appel AJAX :
04380fba
OL
1097 input : implantation_id
1098 output : JSON liste de valeur point
03b395db 1099 """
04380fba
OL
1100 method = request.method
1101 params = getattr(request, method, [])
1102 data = []
cea09938 1103 annee_courante = datetime.now().year
5a1f75cb
EMS
1104 if 'implantation_id' in params \
1105 and params.get('implantation_id') is not u"":
04380fba 1106 implantation_id = params.get('implantation_id')
5a1f75cb
EMS
1107 preselectionne = rh.ValeurPoint.objects \
1108 .filter(implantation=implantation_id, annee=annee_courante) \
1109 .order_by("-annee")
7ad0549c 1110 for o in preselectionne:
5a1f75cb
EMS
1111 data.append({
1112 'id': o.id,
1113 'label': o.__unicode__(),
1114 'devise': o.devise_id,
1115 'suggestion': True
1116 })
b1f7765e 1117 else:
7ad0549c
OL
1118 preselectionne = rh.ValeurPoint.objects.none()
1119
5a1f75cb
EMS
1120 liste_complete = rh.ValeurPoint.objects \
1121 .filter(annee__in=(annee_courante,)) \
1122 .order_by("-annee")
7ad0549c 1123 for o in liste_complete.exclude(id__in=[p.id for p in preselectionne]):
5a1f75cb
EMS
1124 data.append({
1125 'id': o.id,
1126 'label': o.__unicode__(),
1127 'devise': o.devise_id,
1128 'suggestion': False
1129 })
3f5cbabe 1130 return HttpResponse(dumps(data, indent=4))