poste
[auf_rh_dae.git] / project / dae / forms.py
CommitLineData
5d680e84 1# -*- encoding: utf-8 -*-
ce110fb9 2
5a1f75cb 3from django import forms
80be36aa 4from django.core.urlresolvers import reverse
2e672700 5from django.forms.models import BaseInlineFormSet
5a1f75cb 6from django.forms.models import inlineformset_factory, modelformset_factory
80be36aa
OL
7from django.db.models import Q, Max, Count
8from django.shortcuts import redirect
9from django.contrib.admin import widgets as admin_widgets
5a1f75cb 10
75f0e87b
DB
11from ajax_select.fields import AutoCompleteSelectField
12
13from auf.django.references import models as ref
14from auf.django.workflow.forms import WorkflowFormMixin
66fefd2f 15from auf.django.workflow.models import WorkflowCommentaire
75f0e87b 16
3383b2d1 17from project import groups
17c90428 18from project.rh import models as rh
17c90428 19from project.dae import models as dae
34950f36 20from project.dae.workflow import POSTE_ETATS_BOUTONS, POSTE_ETAT_FINALISE
1b31de9f 21
f258e4e7 22
2e672700
OL
23class BaseInlineFormSetWithInitial(BaseInlineFormSet):
24 """
25 Cette classe permet de fournir l'option initial aux inlineformsets.
26 Elle devient désuette en django 1.4.
27 """
28 def __init__(self, data=None, files=None, instance=None,
29 save_as_new=False, prefix=None, queryset=None, **kwargs):
30
31 self.initial_extra = kwargs.pop('initial', None)
32
33 from django.db.models.fields.related import RelatedObject
34 if instance is None:
35 self.instance = self.fk.rel.to()
36 else:
37 self.instance = instance
38 self.save_as_new = save_as_new
39 # is there a better way to get the object descriptor?
40 self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
41 if queryset is None:
42 queryset = self.model._default_manager
43 qs = queryset.filter(**{self.fk.name: self.instance})
44 super(BaseInlineFormSetWithInitial, self).__init__(data, files, prefix=prefix,
45 queryset=qs, **kwargs)
46
47 def _construct_form(self, i, **kwargs):
48 if self.is_bound and i < self.initial_form_count():
49 # Import goes here instead of module-level because importing
50 # django.db has side effects.
51 from django.db import connections
52 pk_key = "%s-%s" % (self.add_prefix(i), self.model._meta.pk.name)
53 pk = self.data[pk_key]
54 pk_field = self.model._meta.pk
55 pk = pk_field.get_db_prep_lookup('exact', pk,
56 connection=connections[self.get_queryset().db])
57 if isinstance(pk, list):
58 pk = pk[0]
59 kwargs['instance'] = self._existing_object(pk)
60 if i < self.initial_form_count() and not kwargs.get('instance'):
61 kwargs['instance'] = self.get_queryset()[i]
62 if i >= self.initial_form_count() and self.initial_extra:
63 # Set initial values for extra forms
64 try:
65 kwargs['initial'] = self.initial_extra[i-self.initial_form_count()]
66 except IndexError:
67 pass
68
69 defaults = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
70 if self.is_bound:
71 defaults['data'] = self.data
72 defaults['files'] = self.files
73 if self.initial:
74 try:
75 defaults['initial'] = self.initial[i]
76 except IndexError:
77 pass
78 # Allow extra forms to be empty.
79 if i >= self.initial_form_count():
80 defaults['empty_permitted'] = True
81 defaults.update(kwargs)
82 form = self.form(**defaults)
83 self.add_fields(form, i)
84 return form
85
86
f258e4e7
OL
87def _implantation_choices(obj, request):
88 # TRAITEMENT NORMAL
3383b2d1 89 employe = groups.get_employe_from_user(request.user)
f258e4e7 90 # SERVICE
3383b2d1 91 if groups.is_user_dans_services_centraux(request.user):
5a1f75cb 92 q = Q(**{'id': employe.implantation_id})
f258e4e7
OL
93 # REGION
94 else:
5a1f75cb 95 q = Q(**{'region': employe.implantation.region})
f258e4e7
OL
96
97 # TRAITEMENT DRH
3383b2d1
OL
98 user_groupes = [g.name for g in request.user.groups.all()]
99 if groups.DRH_NIVEAU_1 in user_groupes:
f258e4e7 100 q = Q()
5a1f75cb
EMS
101 return [('', '----------')] + \
102 [(i.id, unicode(i), )for i in ref.Implantation.objects.filter(q)]
103
f258e4e7
OL
104
105def _employe_choices(obj, request):
f258e4e7 106 # TRAITEMENT NORMAL
3383b2d1 107 employe = groups.get_employe_from_user(request.user)
f258e4e7 108 # SERVICE
3383b2d1 109 if groups.is_user_dans_services_centraux(request.user):
072820fc 110 q_dae_region_service = Q(poste__implantation=employe.implantation)
09aa8374 111 q_rh_region_service = Q(poste__implantation=employe.implantation)
f258e4e7
OL
112 # REGION
113 else:
5a1f75cb
EMS
114 q_dae_region_service = Q(
115 poste__implantation__region=employe.implantation.region
116 )
117 q_rh_region_service = Q(
118 poste__implantation__region=employe.implantation.region
119 )
f258e4e7 120 # TRAITEMENT DRH
3383b2d1
OL
121 user_groupes = [g.name for g in request.user.groups.all()]
122 if groups.DRH_NIVEAU_1 in user_groupes:
072820fc
OL
123 q_dae_region_service = Q()
124 q_rh_region_service = Q()
f258e4e7 125
5a1f75cb
EMS
126 # On filtre les employes avec les droits régionaux et on s'assure que
127 # c'est bien le dernier dossier en date pour sortir l'employe. On retient
128 # un employé qui travaille présentement dans la même région que le user
129 # connecté.
130 dossiers_regionaux_ids = [
131 d.id for d in dae.Dossier.objects.filter(q_dae_region_service)
132 ]
133 employes_ids = [
134 d['employe']
135 for d in dae.Dossier.objects
136 .values('employe')
137 .annotate(dernier_dossier=Max('id'))
138 if d['dernier_dossier'] in dossiers_regionaux_ids
139 ]
072820fc
OL
140 dae_employe = dae.Employe.objects.filter(id__in=employes_ids)
141 dae_ = dae_employe.filter(id_rh__isnull=True)
142 copies = dae_employe.filter(Q(id_rh__isnull=False))
f258e4e7 143 id_copies = [p.id_rh_id for p in copies.all()]
072820fc 144
5a1f75cb
EMS
145 dossiers_regionaux_ids = [
146 d.id for d in rh.Dossier.objects.filter(q_rh_region_service)
147 ]
148 employes_ids = [
149 d['employe']
150 for d in rh.Dossier.objects
151 .values('employe')
152 .annotate(dernier_dossier=Max('id'))
153 if d['dernier_dossier'] in dossiers_regionaux_ids
154 ]
155 rhv1 = rh.Employe.objects \
156 .filter(id__in=employes_ids) \
157 .exclude(id__in=id_copies)
158
159 # On ajoute les nouveaux Employés DAE qui ont été crées, mais qui n'ont
160 # pas de Dossier associés
67c15007
OL
161 employes_avec_dae = [d.employe_id for d in dae.Dossier.objects.all()]
162 employes_orphelins = dae.Employe.objects.exclude(id__in=employes_avec_dae)
163
f258e4e7
OL
164 def option_label(employe):
165 return "%s %s" % (employe.nom.upper(), employe.prenom.title())
166
167 return [('', 'Nouvel employé')] + \
5a1f75cb
EMS
168 sorted(
169 [('dae-%s' % p.id, option_label(p))
170 for p in dae_ | copies | employes_orphelins] +
171 [('rh-%s' % p.id, option_label(p)) for p in rhv1],
172 key=lambda t: t[1]
173 )
174
f258e4e7 175
4bce4d24
OL
176def label_poste_display(poste):
177 """Formate un visuel pour un poste dans une liste déroulante"""
23294f7d
OL
178 annee = ""
179 if poste.date_debut:
180 annee = poste.date_debut.year
9c1ff333
OL
181
182 nom = poste.nom
34950f36
OL
183 label = u"%s (%s) %s - %s [%s]" % (
184 annee,
185 poste.implantation.nom_court,
186 nom,
187 poste.type_poste.categorie_emploi.nom,
188 poste.id,
93817ef3 189 )
4bce4d24 190 return label
9cb4de55 191
2e672700 192
874949f3 193PostePieceFormSet = inlineformset_factory(dae.Poste, dae.PostePiece,)
25086dcf 194DossierPieceForm = inlineformset_factory(dae.Dossier, dae.DossierPiece)
2e672700 195
874949f3
OL
196# Ce formset est utilisé dans le cas de la création de poste prépopulé avec les
197# données de RH
198FinancementFormSetInitial = inlineformset_factory(
2e672700
OL
199 dae.Poste,
200 dae.PosteFinancement,
201 formset=BaseInlineFormSetWithInitial,
202 extra=2
5a1f75cb 203)
874949f3
OL
204FinancementFormSet = inlineformset_factory(
205 dae.Poste,
206 dae.PosteFinancement,
207 extra=2
208)
5a1f75cb 209
03b395db
OL
210
211class DossierComparaisonForm(forms.ModelForm):
11f22317 212
03b395db 213 recherche = AutoCompleteSelectField('dossiers', required=False)
5a1f75cb
EMS
214 poste = forms.CharField(
215 max_length=255, widget=forms.TextInput(attrs={'size': '60'})
216 )
03b395db 217
320d7584 218 class Meta:
03b395db 219 model = dae.DossierComparaison
320d7584 220 exclude = ('dossier',)
03b395db 221
320d7584
EMS
222DossierComparaisonFormSet = modelformset_factory(
223 dae.DossierComparaison, extra=3, max_num=3, form=DossierComparaisonForm
25086dcf 224)
03b395db 225
5a1f75cb 226
068d1462 227class PosteComparaisonForm(forms.ModelForm):
11f22317 228
e503e64d 229 recherche = AutoCompleteSelectField('dae_postes', required=False)
068d1462 230
320d7584 231 class Meta:
068d1462 232 model = dae.PosteComparaison
320d7584 233 exclude = ('poste',)
068d1462 234
874949f3
OL
235# Ce formset est utilisé dans le cas de la création de poste prépopulé avec les
236# données de RH
237PosteComparaisonFormSetInitial = inlineformset_factory(
2e672700
OL
238 dae.Poste,
239 dae.PosteComparaison,
240 extra=3,
241 max_num=3,
242 form=PosteComparaisonForm,
2e672700 243 formset=BaseInlineFormSetWithInitial,
25086dcf 244)
874949f3
OL
245PosteComparaisonFormSet = inlineformset_factory(
246 dae.Poste,
247 dae.PosteComparaison,
248 extra=3,
249 max_num=3,
250 form=PosteComparaisonForm,
251)
068d1462 252
5a1f75cb 253
0a085c42
OL
254class FlexibleRemunForm(forms.ModelForm):
255
256 montant_mensuel = forms.DecimalField(required=False)
257 montant = forms.DecimalField(required=True, label='Montant annuel')
258
259 class Meta:
260 model = dae.Remuneration
261
dc4b78a7
OL
262 def clean_devise(self):
263 devise = self.cleaned_data['devise']
67173010
OL
264 if devise.code == 'EUR':
265 return devise
5a1f75cb
EMS
266 implantation = ref.Implantation.objects.get(
267 id=self.data['implantation']
268 )
2455f48d 269 liste_taux = devise.tauxchange_set.order_by('-annee')
dc4b78a7 270 if len(liste_taux) == 0:
5a1f75cb
EMS
271 raise forms.ValidationError(
272 u"La devise %s n'a pas de taux pour l'implantation %s" %
273 (devise, implantation)
274 )
dc4b78a7
OL
275 else:
276 return devise
277
25086dcf
EMS
278RemunForm = inlineformset_factory(
279 dae.Dossier, dae.Remuneration, extra=5, form=FlexibleRemunForm
280)
0a085c42 281
5a1f75cb 282
1b217058 283class PosteForm(forms.ModelForm):
5d680e84 284 """ Formulaire des postes. """
12c7f8a7 285
ea7adc69 286 # On ne propose que les services actifs
5a1f75cb
EMS
287 service = forms.ModelChoiceField(
288 queryset=rh.Service.objects.all(), required=True
289 )
ea7adc69 290
5a1f75cb 291 responsable = AutoCompleteSelectField('responsables', required=True)
12c7f8a7
OL
292 #responsable = forms.ModelChoiceField(
293 # queryset=rh.Poste.objects.select_related(depth=1))
294
295 # La liste des choix est laissée vide. Voir __init__ pour la raison.
296 poste = forms.ChoiceField(label="Nouveau poste ou évolution du poste",
297 choices=(), required=False)
11f22317 298
5a1f75cb
EMS
299 valeur_point_min = forms.ModelChoiceField(
300 queryset=rh.ValeurPoint.actuelles.all(), required=False
301 )
302 valeur_point_max = forms.ModelChoiceField(
303 queryset=rh.ValeurPoint.actuelles.all(), required=False
304 )
11f22317 305
5d680e84
NC
306 class Meta:
307 model = dae.Poste
c3be904d
OL
308 fields = ('type_intervention',
309 'poste', 'implantation', 'type_poste', 'service', 'nom',
154677c3 310 'responsable', 'local', 'expatrie', 'mise_a_disposition',
b15bf543 311 'appel', 'date_debut', 'date_fin',
5d680e84
NC
312 'regime_travail', 'regime_travail_nb_heure_semaine',
313 'classement_min', 'classement_max',
314 'valeur_point_min', 'valeur_point_max',
3d627bfd 315 'devise_min', 'devise_max',
5f61bccb
OL
316 'salaire_min', 'salaire_max',
317 'indemn_expat_min', 'indemn_expat_max',
318 'indemn_fct_min', 'indemn_fct_max',
319 'charges_patronales_min', 'charges_patronales_max',
5d680e84
NC
320 'autre_min', 'autre_max', 'devise_comparaison',
321 'comp_locale_min', 'comp_locale_max',
322 'comp_universite_min', 'comp_universite_max',
323 'comp_fonctionpub_min', 'comp_fonctionpub_max',
324 'comp_ong_min', 'comp_ong_max',
8fa94e8b 325 'comp_autre_min', 'comp_autre_max',
2e092e0c 326 'justification',
8fa94e8b 327 )
c3be904d
OL
328 widgets = dict(type_intervention=forms.RadioSelect(),
329 appel=forms.RadioSelect(),
3d627bfd 330 nom=forms.TextInput(attrs={'size': 60},),
e88caaf0
OL
331 date_debut=admin_widgets.AdminDateWidget(),
332 date_fin=admin_widgets.AdminDateWidget(),
2e092e0c 333 justification=forms.Textarea(attrs={'cols': 80},),
3d627bfd 334 #devise_min=forms.Select(attrs={'disabled':'disabled'}),
335 #devise_max=forms.Select(attrs={'disabled':'disabled'}),
336 )
5d680e84 337
c2458db6 338 def __init__(self, *args, **kwargs):
5d680e84
NC
339 """ Mise à jour dynamique du contenu du menu des postes.
340
341 Si on ne met le menu à jour de cette façon, à chaque instantiation du
342 formulaire, son contenu est mis en cache par le système et il ne
343 reflète pas les changements apportés par les ajouts, modifications,
344 etc...
345
139686f2
NC
346 Aussi, dans ce cas-ci, on ne peut pas utiliser un ModelChoiceField
347 car le "id" de chaque choix est spécial (voir _poste_choices).
348
5d680e84 349 """
c2458db6 350 request = kwargs.pop('request')
5d680e84 351 super(PosteForm, self).__init__(*args, **kwargs)
f258e4e7 352 self.fields['poste'].choices = self._poste_choices(request)
9c1ff333 353
5a1f75cb
EMS
354 self.fields['implantation'].choices = \
355 _implantation_choices(self, request)
5d680e84 356
cc3098d0
OL
357 # Quand le dae.Poste n'existe pas, on recherche dans les dossiers rhv1
358 if self.instance and self.instance.id is None:
359 dossiers = self.instance.get_dossiers()
360 if len(dossiers) > 0:
09aa8374 361 self.initial['service'] = dossiers[0].poste.service
9508a5b8 362
f258e4e7 363 def _poste_choices(self, request):
5d680e84 364 """ Menu déroulant pour les postes.
9c1ff333 365 Constitué des postes de RH
5d680e84 366 """
9c1ff333
OL
367 postes_rh = rh.Poste.objects.ma_region_ou_service(request.user).all()
368 postes_rh = postes_rh.select_related(depth=1)
5d680e84 369
98d51b59 370 return [('', 'Nouveau poste')] + \
9c1ff333
OL
371 sorted([('rh-%s' % p.id, label_poste_display(p)) for p in
372 postes_rh],
5d680e84 373 key=lambda t: t[1])
3ed49093 374
4dd75e7b
OL
375 def clean(self):
376 """
377 Validation conditionnelles de certains champs.
378 """
5a1f75cb 379 cleaned_data = self.cleaned_data
4dd75e7b 380
5a1f75cb
EMS
381 if cleaned_data.get("local") is False \
382 and cleaned_data.get("expatrie") is False:
383 msg = "Le poste doit au moins être ouvert localement " \
384 "ou aux expatriés"
f42c6e20
OL
385 self._errors["local"] = self.error_class([msg])
386 self._errors["expatrie"] = ''
387 raise forms.ValidationError(msg)
f42c6e20 388
4dd75e7b
OL
389 return cleaned_data
390
3ed49093 391
34950f36 392class ChoosePosteForm(forms.Form):
139686f2 393 class Meta:
139686f2
NC
394 fields = ('poste',)
395
396 # La liste des choix est laissée vide. Voir PosteForm.__init__.
34950f36
OL
397 postes_dae = forms.ChoiceField(choices=(), required=False)
398 postes_rh = forms.ChoiceField(choices=(), required=False)
139686f2 399
4ee6d70a 400 def __init__(self, request=None, *args, **kwargs):
139686f2 401 super(ChoosePosteForm, self).__init__(*args, **kwargs)
34950f36
OL
402 self.fields['postes_dae'].choices = self._poste_dae_choices(request)
403 self.fields['postes_rh'].choices = self._poste_rh_choices(request)
139686f2 404
34950f36
OL
405 def _poste_dae_choices(self, request):
406 """ Menu déroulant pour les postes."""
407 postes_dae = dae.Poste.objects.ma_region_ou_service(request.user) \
408 .exclude(etat__in=(POSTE_ETAT_FINALISE, )) \
409 .annotate(num_dae=Count('dae_dossiers')) \
410 .filter(num_dae=0) \
411 .order_by('-date_debut')
139686f2 412
98d51b59 413 return [('', '----------')] + \
34950f36
OL
414 [('dae-%s' % p.id, label_poste_display(p)) for p in postes_dae]
415
416 def _poste_rh_choices(self, request):
417 """ Menu déroulant pour les postes."""
80be36aa
OL
418 postes_dae = dae.Poste.objects.exclude(etat__in=(POSTE_ETAT_FINALISE, ))
419 id_poste_dae_commences = [p.id_rh_id for p in postes_dae if p.id_rh is not None]
34950f36 420 postes_rh = rh.Poste.objects.ma_region_ou_service(request.user) \
80be36aa 421 .exclude(id__in=id_poste_dae_commences) \
34950f36
OL
422 .order_by('-date_debut')
423
424 return [('', '----------')] + \
425 [('rh-%s' % p.id, label_poste_display(p)) for p in postes_rh]
139686f2 426
80be36aa
OL
427 def clean(self):
428 cleaned_data = super(ChoosePosteForm, self).clean()
429 postes_dae = cleaned_data.get("postes_dae")
430 postes_rh = cleaned_data.get("postes_rh")
431 if (postes_dae is u"" and postes_rh is u"") or \
432 (postes_dae is not u"" and postes_rh is not u""):
433 raise forms.ValidationError("Choisissez un poste DAE ou un poste RH")
434 return cleaned_data
435
436 def redirect(self):
437 poste_dae_key = self.cleaned_data.get("postes_dae")
438 if poste_dae_key is not u"":
439 return redirect(reverse('embauche', args=(poste_dae_key,)))
440 poste_rh_key = self.cleaned_data.get("postes_rh")
441 if poste_rh_key is not u"":
442 return redirect("%s?creer_dossier_dae" % reverse('poste', args=(poste_rh_key,)))
139686f2 443
139686f2
NC
444class EmployeForm(forms.ModelForm):
445 """ Formulaire des employés. """
446 class Meta:
447 model = dae.Employe
448 fields = ('employe', 'nom', 'prenom', 'genre')
449
450 # La liste des choix est laissée vide. Voir Poste.__init__ pour la raison.
451 employe = forms.ChoiceField(choices=(), required=False)
452
ac6235f6 453 def __init__(self, *args, **kwargs):
139686f2 454 """ Mise à jour dynamique du contenu du menu des employés. """
ac6235f6 455 request = kwargs.pop('request', None)
139686f2 456 super(EmployeForm, self).__init__(*args, **kwargs)
f258e4e7 457 self.fields['employe'].choices = _employe_choices(self, request)
139686f2 458
139686f2 459
139686f2
NC
460class DossierForm(forms.ModelForm):
461 """ Formulaire des dossiers. """
462 class Meta:
5a1f75cb 463 exclude = ('etat', 'employe', 'poste', 'date_debut',)
139686f2 464 model = dae.Dossier
4d25e2ba 465 widgets = dict(statut_residence=forms.RadioSelect(),
0e0aeb7e
OL
466 contrat_date_debut=admin_widgets.AdminDateWidget(),
467 contrat_date_fin=admin_widgets.AdminDateWidget(),
4d25e2ba 468 )
e6f52402 469
3799cafc 470WF_HELP_TEXT = ""
e0b93e3a 471
5a1f75cb 472
e6f52402 473class PosteWorkflowForm(WorkflowFormMixin):
56589624 474 bouton_libelles = POSTE_ETATS_BOUTONS
5a1f75cb 475
e6f52402
OL
476 class Meta:
477 fields = ('etat', )
478 model = dae.Poste
9536ea21 479
e0b93e3a 480 def __init__(self, *args, **kwargs):
e54b7d5d 481 super(PosteWorkflowForm, self).__init__(*args, **kwargs)
e0b93e3a
OL
482 self.fields['etat'].help_text = WF_HELP_TEXT
483
484
e6f52402 485class DossierWorkflowForm(WorkflowFormMixin):
5a1f75cb
EMS
486 bouton_libelles = POSTE_ETATS_BOUTONS # meme workflow que poste...
487
e6f52402 488 class Meta:
9e40cfbe 489 fields = ('etat', )
e6f52402 490 model = dae.Dossier
e0b93e3a
OL
491
492 def __init__(self, *args, **kwargs):
e54b7d5d 493 super(DossierWorkflowForm, self).__init__(*args, **kwargs)
e0b93e3a 494 self.fields['etat'].help_text = WF_HELP_TEXT
e54b7d5d 495 self._etat_initial = self.instance.etat
e0b93e3a 496
e54b7d5d
EMS
497 def save(self):
498 super(DossierWorkflowForm, self).save()
499 poste = self.instance.poste
66fefd2f
OL
500
501 # créer le commentaire automatique pour le poste associé
502 commentaire = WorkflowCommentaire()
503 commentaire.content_object = poste
504 texte = u"Validation automatique à travers le dossier [%s] de %s\n%s" %(
505 self.instance.id,
506 self.instance,
507 self.data.get('commentaire', ''),
508 )
509 commentaire.texte = texte
510 commentaire.etat_initial = self.instance._etat_courant
511 commentaire.etat_final = self.instance.etat
512 commentaire.owner = self.request.user
513 commentaire.save()
514
515 # force l'état du poste
516 poste.etat = self.instance.etat
517 poste.save()
9536ea21 518
5a1f75cb 519
9536ea21
EMS
520class ContratForm(forms.ModelForm):
521
522 class Meta:
9dfa4296 523 fields = ('type_contrat', 'fichier', )
9536ea21
EMS
524 model = dae.Contrat
525
5a1f75cb 526
c3f0b49f
EMS
527class DAENumeriseeForm(forms.ModelForm):
528
529 class Meta:
530 model = dae.Dossier
531 fields = ('dae_numerisee',)
cbfd7bd4
EMS
532
533
534class DAEFinaliseesSearchForm(forms.Form):
535 q = forms.CharField(
536 label='Recherche', required=False,
537 widget=forms.TextInput(attrs={'size': 40})
538 )
539 importees = forms.ChoiceField(
540 label='Importation', required=False, choices=(
541 ('', ''),
542 ('oui', 'DAE importées seulement'),
543 ('non', 'DAE non-importées seulement'),
544 )
545 )