admin.py 44.5 KB
Newer Older
Administrator committed
1 2 3
from django.contrib import admin
from outils.common_imports import *
from outils.for_view_imports import *
4
from outils.common import OdooAPI
Administrator committed
5 6 7
from members.models import CagetteUser
from members.models import CagetteMembers
from members.models import CagetteMember
8
from shifts.models import CagetteServices
9
from shifts.models import CagetteShift
10
from members_space.models import CagetteMembersSpace
Administrator committed
11
from outils.common import MConfig
12
from datetime import datetime, date
Administrator committed
13 14 15

default_msettings = {'msg_accueil': {'title': 'Message borne accueil',
                                             'type': 'textarea',
16 17
                                             'value': '',
                                             'sort_order': 1
Administrator committed
18 19 20
                                            },
                     'no_picture_member_advice': {'title': 'Message avertissement membre sans photo',
                                             'type': 'textarea',
21 22
                                             'value': '',
                                             'sort_order': 2
Administrator committed
23
                                      },
24
                     'shop_opening_hours': {
25 26
                                                'title': 'Horaires ouverture magasin',
                                                'type': 'textarea',
27 28
                                                'value': '',
                                                'sort_order': 3
29
                                            },
30
                      'abcd_calendar_link': {
31 32
                                                'title': 'Lien vers le calendrier ABCD',
                                                'type': 'text',
33 34 35
                                                'value': '',
                                                'class': 'link',
                                                'sort_order': 4
36
                       },
37
                       'forms_link': {
38 39
                                                'title': 'Lien vers la page des formulaires',
                                                'type': 'text',
40 41 42
                                                'value': '',
                                                'class': 'link',
                                                'sort_order': 5
43
                       },
44
                       'unsuscribe_form_link': {
45 46
                                                'title': 'Lien vers le formulaire de ré-inscription',
                                                'type': 'text',
47 48 49
                                                'value': '',
                                                'class': 'link',
                                                'sort_order': 6
50
                       },
Etienne Freiss committed
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
                       'request_form_link': {
                                                'title': 'Faire une demande au Bureau Des Membres',
                                                'type': 'text',
                                                'value': '',
                                                'class': 'link',
                                                'sort_order': 7
                       },
                       'late_service_form_link': {
                                                'title': 'Retard à mon service ou oubli validation',
                                                'type': 'text',
                                                'value': '',
                                                'class': 'link',
                                                'sort_order': 8
                       },
                       'change_template_form_link': {
                                                'title': 'Demande de changement de créneau',
                                                'type': 'text',
                                                'value': '',
                                                'class': 'link',
                                                'sort_order': 9
                       },
                       #TODO vérifier le nom d'un "binome"
                       'associated_subscribe_form_link': {
                                                'title': 'Demande de création de binôme',
                                                'type': 'text',
                                                'value': '',
                                                'class': 'link',
                                                'sort_order': 10
                       },
                       #TODO vérifier le nom d'un "binome"
                       'associated_unsubscribe_form_link': {
                                                'title': 'Se désolidariser de son binôme',
                                                'type': 'text',
                                                'value': '',
                                                'class': 'link',
                                                'sort_order': 11
                       },
                       'template_unsubscribe_form_link': {
                                                'title': 'Se désinscrire de son créneau',
                                                'type': 'text',
                                                'value': '',
                                                'class': 'link',
                                                'sort_order': 12
                       },
                       'change_email_form_link': {
                                                'title': 'Changer d\'adresse mail',
                                                'type': 'text',
                                                'value': '',
                                                'class': 'link',
                                                'sort_order': 13
                       },
                       'coop_unsubscribe_form_link': {
                                                'title': 'Demande de démission de la coopérative et/ou de remboursement de mes parts sociales',
                                                'type': 'text',
                                                'value': '',
                                                'class': 'link',
                                                'sort_order': 14
                       },
                       'sick_leave_form_link': {
                                                'title': 'Demande de congé maladie ou parental',
                                                'type': 'text',
                                                'value': '',
                                                'class': 'link',
                                                'sort_order': 15
                       },
                       'underage_subscribe_form_link': {
                                                'title': 'Demande de création d’un compte mineur rattaché',
                                                'type': 'text',
                                                'value': '',
                                                'class': 'link',
                                                'sort_order': 16
                       },
123
                       'member_cant_have_delay_form_link': {
124
                                                'title': 'Lien vers le formulaire pour les membres n\'ayant pas rattrapé leur service après la durée de l\'extension',
125
                                                'type': 'text',
126 127
                                                'value': '',
                                                'class': 'link',
Etienne Freiss committed
128
                                                'sort_order': 21
129 130 131 132 133 134 135
                       },
                       'on_picking_shift_template_msg': {
                         'title': 'Afficher une info à la sélection d\'un créneau (jour,heure) en respectant la syntaxe en exemple : lundi 14:00 Ici votre message',
                         'type': 'text',
                         'value': '',
                         'sort_order': 22
                       },
Etienne Freiss committed
136

Administrator committed
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
                    }

def config(request):
    """Page de configuration."""
    template = loader.get_template('outils/config.html')
    context = {'title': 'Configuration module Membres',
               'module': 'Membres'}
    return HttpResponse(template.render(context, request))



def get_settings(request):
    result = {}
    is_connected_user = CagetteUser.are_credentials_ok(request)
    if is_connected_user is True:
        try:
            msettings = MConfig.get_settings('members')
            if len(msettings) == 0:
                msettings = default_msettings
            # take care that every params will be shown (consider newly added params)
            for k, v in default_msettings.items():
                if not (k in msettings):
                    msettings[k] = v
160 161 162
            for k,v in msettings.items():
                if 'sort_order' not in v:
                    msettings[k]['sort_order'] = 1
163
            result['settings'] = dict(sorted(msettings.items(), key=lambda k_v: k_v[1]['sort_order']))
164
            # on preprod server, dict order (through JsonResponse ??) is not respected !!
Administrator committed
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
        except Exception as e:
            result['error'] = str(e)
    else:
        result['error'] = "Forbidden"

    return JsonResponse({"res": result}, safe=False)

def save_settings(request):
    result = {}
    is_connected_user = CagetteUser.are_credentials_ok(request)
    if is_connected_user is True:
        try:
            params = json.loads(request.POST.get('params'))
            result['save'] = MConfig.save_settings('members', params)
        except Exception as e:
            result['error'] = str(e)
    else:
        result['error'] = "Forbidden"

    return JsonResponse({"res": result}, safe=False)

def module_settings(request):
    if request.method == 'GET':
        return get_settings(request)
    else:
        return save_settings(request)

def add_pts_to_everybody(request, pts, reason):
    result = {}
    is_connected_user = CagetteUser.are_credentials_ok(request)
    if is_connected_user is True:
        try:
197
            fields = ['shift_type']
Administrator committed
198 199 200 201 202 203
            cond = [['is_member', '=', True]]
            all_members = CagetteMembers.get(cond, fields)
            if all_members and len(all_members) > 0:
                ftop_ids = []
                standard_ids = []
                for m in all_members:
204
                    if m['shift_type'] == 'ftop':
Administrator committed
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
                        ftop_ids.append(m['id'])
                    else:
                        standard_ids.append(m['id'])
                if len(standard_ids) > 0:
                    result['standard'] = CagetteMembers.add_pts_to_everyone('standard', standard_ids, pts, reason)
                else:
                    result['standard'] = 'No standard found ! '
                if len(ftop_ids) > 0:
                    result['ftop'] = CagetteMembers.add_pts_to_everyone('ftop', ftop_ids, pts, reason)
                else:
                    result['ftop'] = 'No FTOP found !'
                # result['ftop'] = ftop_ids
                # result['standard'] = standard_ids
        except Exception as e:
            result['error'] = str(e)
    else:
        result['error'] = "Forbidden"
    return JsonResponse({'res': result})

def manage_mess(request):
    """Admin part to manage mess - uncomplete subscription"""
    is_connected_user = CagetteUser.are_credentials_ok(request)
    template = loader.get_template('members/manage_mess.html')

    context = {'title': 'Gestion des inscriptions problématiques',
               'couchdb_server': settings.COUCHDB['url'],
               'db': settings.COUCHDB['dbs']['member_mess'],
               'is_connected_user': is_connected_user}
    return HttpResponse(template.render(context, request))
    # JsonResponse({'error' : str(e)}, status=500)

def raw_search(request):
    res = {}
    is_connected_user = CagetteUser.are_credentials_ok(request)
    if is_connected_user is True:
        try:
            needle = str(request.GET.get('needle'))
            members = CagetteMembers.raw_search(needle)
            res = {'members': members}
        except Exception as e:
            res['error'] = str(e)
        response = JsonResponse(res)
    else:
        response = JsonResponse(res, status=403)
    return response

def problematic_members(request):
    res = {}
    is_connected_user = CagetteUser.are_credentials_ok(request)
    if is_connected_user is True:
        try:
            members = CagetteMembers.get_problematic_members()
            res = {'members': members}
        except Exception as e:
            res['error'] = str(e)
        response = JsonResponse(res)
    else:
        response = JsonResponse(res, status=403)
    return response

def remove_member_from_mess_list(request):
    res = {}
    is_connected_user = CagetteUser.are_credentials_ok(request)
    if is_connected_user is True:
        try:
            res = CagetteMember.remove_from_mess_list(request)
        except Exception as e:
            res['error'] = str(e)
        response = JsonResponse(res)
    else:
        response = JsonResponse(res, status=403)
    return response

def generate_barcode(request, member_id):
    res = {}
    is_connected_user = CagetteUser.are_credentials_ok(request)
    if is_connected_user is True:
        try:
            res['done'] = CagetteMember(member_id).generate_barcode()
        except Exception as e:
            res['error'] = str(e)
        response = JsonResponse(res, safe=False)
    else:
        response = JsonResponse(res, status=403)
    return response

def generate_base_and_barcode(request, member_id):
    res = {}
    is_connected_user = CagetteUser.are_credentials_ok(request)
    if is_connected_user is True:
        try:
            res['done'] = CagetteMember(member_id).generate_base_and_barcode()
        except Exception as e:
            res['error'] = str(e)
        response = JsonResponse(res, safe=False)
    else:
        response = JsonResponse(res, status=403)
    return response

def create_envelops(request):
305
    """Only used from manage_mess, which was a tempory fonctionality"""
Administrator committed
306 307 308 309 310 311 312 313 314 315
    res = {}
    is_connected_user = CagetteUser.are_credentials_ok(request)
    if is_connected_user is True:
        try:
            res['result'] = CagetteMember.standalone_create_envelops(request)
        except Exception as e:
            res['error'] = str(e)
        response = JsonResponse(res, safe=False)
    else:
        response = JsonResponse(res, status=403)
316 317 318 319 320 321 322 323
    return response

# # # ADMIN / BDM # # #

def admin(request):
    """ Administration des membres """
    template = loader.get_template('members/admin/index.html')
    context = {'title': 'BDM',
324 325
               'module': 'Membres',
               'admin_binome_active': getattr(settings, 'ADMIN_BINOME_ACTIVE', True),}
326
    return HttpResponse(template.render(context, request))
327 328 329 330

def manage_makeups(request):
    """ Administration des membres """
    template = loader.get_template('members/admin/manage_makeups.html')
331
    committees_shift_id = CagetteServices.get_committees_shift_id()
332
    m = CagetteMembersSpace()
333
    context = {'title': 'BDM - Rattrapages',
334
               'module': 'Membres',
335
               'has_committe_shift': committees_shift_id is not None,
336
               'extension_duration': m.get_months_extension_duration()
337
              }
338
    return HttpResponse(template.render(context, request))
339

340 341 342 343 344 345 346
def manage_shift_registrations(request):
    """ Administration des services des membres """
    template = loader.get_template('members/admin/manage_shift_registrations.html')
    context = {'title': 'BDM - Services',
               'module': 'Membres'}
    return HttpResponse(template.render(context, request))

Félicie committed
347
def manage_attached(request):
Félicie committed
348
    """ Administration des binômes membres """
Félicie committed
349 350 351 352 353
    template = loader.get_template('members/admin/manage_attached.html')
    context = {'title': 'BDM - Binômes',
               'module': 'Membres'}
    return HttpResponse(template.render(context, request))

Félicie committed
354 355 356
def manage_regular_shifts(request):
    """ Administration des créneaux des membres """
    template = loader.get_template('members/admin/manage_regular_shifts.html')
Damien Moulard committed
357
    committees_shift_id = CagetteServices.get_committees_shift_id()
358
    committees_shift_name = getattr(settings, 'COMMITTEES_SHIFT_NAME', "service des Comités")
359 360 361 362
    if getattr(settings, 'USE_EXEMPTIONS_SHIFT_TEMPLATE', False) is True:
        exemptions_shift_id = CagetteServices.get_exemptions_shift_id()
    else:
        exemptions_shift_id = 0
363 364 365 366 367 368 369 370
    context = {
        'title': 'BDM - Créneaux',
        'module': 'Membres',
        'couchdb_server': settings.COUCHDB['url'],
        'db': settings.COUCHDB['dbs']['member'],
        'max_begin_hour': settings.MAX_BEGIN_HOUR,
        'mag_place_string': settings.MAG_NAME,
        'open_on_sunday': getattr(settings, 'OPEN_ON_SUNDAY', False),
371
        'show_ftop_button': getattr(settings, 'BDM_SHOW_FTOP_BUTTON', True),
Damien Moulard committed
372
        'has_committe_shift': committees_shift_id is not None,
373
        'committees_shift_id': committees_shift_id,
374
        'committees_shift_name': committees_shift_name,
375 376
        'ASSOCIATE_MEMBER_SHIFT' : getattr(settings, 'ASSOCIATE_MEMBER_SHIFT', ''),
        'exemptions_shift_id': exemptions_shift_id,
377
    }
Félicie committed
378 379
    return HttpResponse(template.render(context, request))

380 381 382 383 384
def get_makeups_members(request):
    """ Récupération des membres qui doivent faire des rattrapages """
    res = CagetteMembers.get_makeups_members()
    return JsonResponse({ 'res' : res })

Félicie committed
385 386 387 388 389
def get_attached_members(request):
    """ Récupération des membres en binôme """
    res = CagetteMembers.get_attached_members()
    return JsonResponse({ 'res' : res })

390
def update_members_makeups(request):
391
    """ Met à jour les rattrapages des membres passés dans la requête """
392 393 394 395
    res = {}
    is_connected_user = CagetteUser.are_credentials_ok(request)
    if is_connected_user is True:
        members_data = json.loads(request.body.decode())
396

397
        # Perform checks and compute some data in a loop on members.
398 399 400 401 402 403 404 405 406 407 408 409 410
        cs = CagetteShift()
        unsubscription_limit = api.get_system_param('lacagette_membership.points_limit_to_get_unsubscribed')
        for member_data in members_data:
            cm = CagetteMember(int(member_data["member_id"]))
            target_makeups_nb = int(member_data["target_makeups_nb"])
            if target_makeups_nb < 0:
                # Prevent setting a negative number of makeups_to_do https://redmine.coopdev.fr/issues/6090
                # This could happen when bdm has two screens open and clicks on minus btn on a coop line
                # with exactly 1 makeup_to_do on both screens
                error = []
                error["error"] = "L'opération de rattrapage a été annulée car le nombre de rattrapage cible est négatif."
                return JsonResponse(error, status=400)
            makeup_change_count = target_makeups_nb - cs.get_member_makeups_to_do(cm.id)
411 412 413 414 415 416
            points_target = cm.get_member_points("standard") - makeup_change_count
            if points_target <= unsubscription_limit:
                error = []
                error["error"]\
                    = "L'ajout de rattrapage a été annulé car il provoquerait la désinscription d'un membre."
                return JsonResponse(error, status=400)
417 418
            # Save computed data in member_data. It will be useful to update makeups and points.
            members_data[member_data["member_id"]]["points_diff"] = - makeup_change_count
419

420
        # Update makeups and points
421
        res["res"] = []
422 423 424 425 426 427
        update_members_makeups_core(members_data, res)
        response = JsonResponse(res)
    else:
        res["message"] = "Unauthorized"
        response = JsonResponse(res, status=403)
    return response
428

Thibault Grandjean committed
429

430 431 432
def update_members_makeups_core(members_data, res):
    for member_data in members_data:
        cm = CagetteMember(int(member_data["member_id"]))
433

434 435 436 437
        # If points are added, we need to manage makeups count change here,
        # otherwise this is handled automatically by odoo when points are changed
        if member_data["points_diff"] > 0:
            res["res"].append(cm.update_member_makeups(member_data))
438

439 440 441 442 443 444 445 446
        data = {
            'name': "Admin BDM - " + member_data["description"],
            'shift_id': False,
            'type': member_data["member_shift_type"],
            'partner_id': int(member_data["member_id"]),
            'point_qty': member_data['points_diff']
        }
        cm.update_member_points(data)
447

448
        res["res"][-1]['standard_points'] = cm.get_member_points("standard")
449

450 451 452 453
def regenerate_member_delay(request):
    """ From BDM admin, close existing extension if exists & recreate for 6 months """
    res = {}
    is_connected_user = CagetteUser.are_credentials_ok(request)
454
    
455
    if is_connected_user is True:
456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473
        raw_data = json.loads(request.body.decode())

        # Close extension if has one
        member_id = int(raw_data["member_id"])
        cm = CagetteMember(member_id)
        cm.close_extension()

        # Recreate starting now
        cs = CagetteShift()
        data = {
            "idPartner": member_id,
            "start_date": date.today().isoformat()
        }

        duration = raw_data["duration"]
        ext_name = "Délai étendue depuis l'admin BDM"
        res["create_delay"] = cs.create_delay(data=data, duration=duration, ext_name=ext_name)

474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501
        if (res["create_delay"]):
            try:
                # Add 0 pt to counter so odoo updates member status
                data = {
                    'name': "Forcer l'entrée en délai",
                    'shift_id': False,
                    'type': "standard",
                    'partner_id': member_id,
                    'point_qty': 0
                }
                cm.update_member_points(data)

                data = {
                    'name': "Forcer l'entrée en délai",
                    'shift_id': False,
                    'type': "ftop",
                    'partner_id': member_id,
                    'point_qty': 0
                }
                cm.update_member_points(data)

                res["force_entry_delay"] = True
            except Exception as e:
                print(str(e))
        else:
            coop_logger.error("regenerate_member_delay: %s, %s", str(res["create_delay"]), str(data))
            return HttpResponseServerError()

502
        res["member_data"] = CagetteMembers.get_makeups_members([member_id])[0]
503 504 505 506 507 508

        response = JsonResponse(res, safe=False)
    else:
        res["message"] = "Unauthorized"
        response = JsonResponse(res, status=403)
    return response
509 510 511

# --- Gestion des créneaux

512 513 514 515 516 517 518
def delete_shift_registration(request):
    """ From BDM admin, delete (cancel) a member shift registration """
    res = {}
    is_connected_user = CagetteUser.are_credentials_ok(request)
    if is_connected_user is True:
        data = json.loads(request.body.decode())
        member_id = int(data["member_id"])
519 520
        shift_registration_id = int(data["shift_registration_id"])
        shift_is_makeup = data["shift_is_makeup"]
521
        cancellation_description = data["cancellation_description"]
522 523 524

        # Note: 'upcoming_registration_count' in res.partner won't change because the _compute method
        #       in odoo counts canceled shift registrations.
525
        m = CagetteShift()
526
        res["cancel_shift"] = m.cancel_shift([shift_registration_id], origin='bdm', description=cancellation_description)
527 528 529

        if shift_is_makeup is True:
            fields = {
530
                'name': "Admin BDM (annulation de rattrapage par une annulation de présence) - " + cancellation_description,
531 532 533 534 535 536
                'shift_id': False,
                'type': data["member_shift_type"],
                'partner_id': member_id,
                'point_qty': 1
            }
            res["update_counter"] = m.update_counter_event(fields)
537

538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553
        response = JsonResponse(res, safe=False)
    else:
        res["message"] = "Unauthorized"
        response = JsonResponse(res, status=403)
    return response

def delete_shift_template_registration(request):
    """ From BDM admin, delete a member shift template registration """
    res = {}
    is_connected_user = CagetteUser.are_credentials_ok(request)
    if is_connected_user is True:
        try:
            data = json.loads(request.body.decode())
            partner_id = int(data["partner_id"])
            permanent_unsuscribe = data["permanent_unsuscribe"]

554 555 556 557
            # Knowing that the total number of makeups will always be 2 or less,
            # old code here would only increment the number of makeups...
            # which is not needed as makeup increment is now handled
            # by odoo.
558

559
            # Delete all shift registrations & shift template registration
560
            cm = CagetteMember(partner_id)
561
            res["unsubscribe_member"] = cm.unsubscribe_member()
562

563 564
            if permanent_unsuscribe is True:
                res["set_done"] = cm.set_cooperative_state("gone")
565 566 567
                if res["set_done"]:
                    """ Delete pair(s?) of partner if it is a parent to improve statistics (#4810) """
                    api = OdooAPI()
568
                    associated_members = api.search_read('res.partner', [['parent_id', '=', partner_id]], ['id', 'suppleant_member_id'])
569
                    for am in associated_members:
570 571 572 573
                        # Do not call delete_pair_core if contact is not a binom
                        if am['suppleant_member_id']:
                            data = {"child": {"id": am["id"]}, "gone": ["parent", "child"]}
                            delete_pair_core(data)
574

575 576
        except Exception as e:
            res["error"] = str(e)
577 578 579 580 581

        response = JsonResponse(res, safe=False)
    else:
        res["message"] = "Unauthorized"
        response = JsonResponse(res, status=403)
582 583
    return response

584
def shift_subscription(request):
585 586 587 588 589
    """ 
        Register a member to a shift template. 
        If the member was already subscribed to a shift template, unsubscribe him.her first
            and delete all existing shifts EXCEPT makeups.
    """
590
    res = {}
591 592 593 594 595 596 597 598
    data = json.loads(request.body.decode())
    partner_id = int(data["partner_id"])
    is_allowed = CagetteUser.are_credentials_ok(request)
    if is_allowed is False:
        credentials = CagetteMember.get_credentials(request, with_id = True)
        if 'success' in credentials and credentials['success'] is True and credentials['id'] == partner_id:
            is_allowed = True
    if is_allowed is True:
599 600
        api = OdooAPI()

601 602
        partner_id = int(data["partner_id"])
        shift_type = data["shift_type"]
603
        moving_from_std_to_ftop = False
604 605
        if shift_type == 1:
            # 1 = standard
606
            shift_template_id = int(data["shift_template_id"])
607
        elif shift_type == 2:
608
            # 2 = ftop
Damien Moulard committed
609 610

            # First try to get committees shift
611
            shift_template_id = CagetteServices.get_committees_shift_id()
Damien Moulard committed
612 613 614
            # If None, no committees shift, get the first ftop shift
            if shift_template_id is None:
                shift_template_id = CagetteServices.get_first_ftop_shift_id()
615 616 617 618

            c = [['id', '=', partner_id]]
            f = ['shift_type']
            moving_from_std_to_ftop = api.search_read('res.partner', c, f)[0]['shift_type'] == 'standard'
619 620 621 622
        else:
            # 3 = exempté
            # Get exemptions shift
            shift_template_id = CagetteServices.get_exemptions_shift_id()
Damien Moulard committed
623

624
        m = CagetteMember(partner_id)
625 626 627 628 629 630 631 632 633 634 635 636 637 638

        unsubscribe_first = data["unsubscribe_first"]
        if unsubscribe_first is True:
            # If the member is registered to a shift on the shift template, registering to this shift template will fail.
            has_makeups_in_new_shift = m.is_member_registered_to_makeup_shift_template(shift_template_id)

            if has_makeups_in_new_shift is True:
                return JsonResponse(
                    {
                        "message": "A makeup is registered on this shift template",
                        "code": "makeup_found"
                    }, 
                    status=409
                )
639 640
            changing_shift = not moving_from_std_to_ftop
            res["unsubscribe_member"] = m.unsubscribe_member(changing_shift)
641

642 643
        if moving_from_std_to_ftop:
            lower_makeup_count_to_zero_if_moving_from_std_to_ftop(partner_id, res)
644
        reg_id = m.create_coop_shift_subscription(shift_template_id, shift_type)
645

646
        # Return necessary data
647 648 649 650 651 652 653 654 655 656 657 658
        if reg_id is not None:
            c = [['id', '=', shift_template_id]]
            f = ['id', 'name']
            res["shift_template"] = api.search_read('shift.template', c, f)[0]

            c = [['id', '=', partner_id]]
            f = ['cooperative_state']
            res["cooperative_state"] = api.search_read('res.partner', c, f)[0]['cooperative_state']
            coop_logger.info("Resultat shift_subscription : %s (données reçues = %s)", str(res), str(data))
            response = JsonResponse(res)
        else:
            response = JsonResponse({"message": "Subscription failed"}, status=500)
659 660 661 662 663
    else:
        response = JsonResponse({"message": "Unauthorized"}, status=403)

    return response

664

665
def lower_makeup_count_to_zero_if_moving_from_std_to_ftop(partner_id, res):
666 667
    cs = CagetteShift()
    cm = CagetteMember(partner_id)
668 669 670 671 672
    members_data = []
    members_data.append({
        'member_id': partner_id,
        'member_shift_type': 'standard',
        'target_makeups_nb': 0,
673 674
        'description': 'reset automatique du compteur rattrapages suite changement créneau standard vers non standard',
        'points_diff': cs.get_member_makeups_to_do(cm.id)
675 676 677 678 679
    })
    res["res"] = []
    update_members_makeups_core(members_data, res)
    if res["res"][0]["update"]:
        res["makeups_to_do"] = 0
680

681
# --- Gestion des binômes
682

683
def get_member_info(request, id):
684 685 686 687 688 689 690
    """Retrieve information about a member."""
    res = {}
    is_connected_user = CagetteUser.are_credentials_ok(request)
    if is_connected_user:
        api = OdooAPI()
        fields = [
            'id',
Thibault Grandjean committed
691
            'name',
692 693 694 695 696 697 698 699 700 701
            'sex',
            'cooperative_state',
            'email',
            'street',
            'street2',
            'zip',
            'city',
            'current_template_name',
            'shift_type',
            'parent_id',
702
            'suppleant_member_id',
703
            'is_associated_people',
Félicie committed
704
            'parent_name',
705 706
            "makeups_to_do",
            "barcode_base"
707
        ]
708 709
        cond = [['id', '=', id]]
        member = api.search_read('res.partner', cond, fields)
710
        if member:
711 712
            member = member[0]
            if member['parent_id']:
713 714
                if not member['suppleant_member_id']:
                    return JsonResponse({"message": "Accès non supporté (rattaché non suppléant)."}, status=404)
715 716 717 718 719
                res_parent = api.search_read('res.partner', [['id', '=', int(member['parent_id'][0])]], ['barcode_base', 'email'])
                if res_parent:
                    parent = res_parent[0]
                    member['parent_barcode_base'] = parent['barcode_base']
                    member['parent_email'] = parent['email']
720
            res['member'] = member
721 722 723 724 725 726 727
            response = JsonResponse(res)
        else:
            response = JsonResponse({"message": "Not found"}, status=404)
    else:
        res['message'] = "Unauthorized"
        response = JsonResponse(res, status=403)
    return response
728 729 730 731 732 733 734 735 736 737

def create_pair(request):
    """Create pair

    payload example:
    {
        "parent": {"id": 3075},
        "child": {"id": 3067}
    }
    """
738 739 740 741 742 743 744 745 746 747 748 749 750 751
    if request.method == 'GET':
        template = loader.get_template('members/admin/manage_attached_create_pair.html')
        context = {'title': 'BDM - Binômes',
                   'module': 'Membres'}
        return HttpResponse(template.render(context, request))

    if request.method == 'POST':
        if CagetteUser.are_credentials_ok(request):
            api = OdooAPI()
            data = json.loads(request.body.decode())
            parent_id = data['parent']['id']
            child_id = data['child']['id']
            # create attached account for child
            fields = [
752
                "birthdate",
753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773
                "city",
                "commercial_partner_id",
                "company_id",
                "company_type",
                "cooperative_state",
                "barcode_rule_id",
                "country_id",
                "customer",
                "department_id",
                "email",
                "employee",
                "image",
                "image_medium",
                "image_small",
                "mobile",
                "name",
                "phone",
                "sex",
                "street",
                "street2",
                "zip",
774
                "nb_associated_people",
Thibault Grandjean committed
775 776
                "current_template_name",
                "parent_id",
777
                "suppleant_member_id",
Thibault Grandjean committed
778
                "is_associated_people",
779 780 781 782
                "makeups_to_do",
                "final_standard_points",
                "final_ftop_points",
                "shift_type"
783 784
            ]
            child = api.search_read('res.partner', [['id', '=', child_id]], fields)[0]
785 786 787
            parent = api.search_read('res.partner', [['id', '=', parent_id]],
                                                    ['commercial_partner_id',
                                                     'nb_associated_people',
Thibault Grandjean committed
788
                                                     'current_template_name',
789
                                                     'makeups_to_do',
790 791 792
                                                     "final_standard_points",
                                                     "final_ftop_points",
                                                     'shift_type'
793 794 795 796 797
                                                     'parent_id',
                                                     'suppleant_member_id'])[0]

            case = isPartnerInvolvedInBinom(api, child)

798
            errors = []
799 800 801 802 803 804 805
            if case == "MEMBER_SUPPLEANT_OF_BINOM":
                errors.append("Le suppléant sélectionné est déjà suppléant d'un binôme.")
            elif case == "CONTACT_OF_MEMBER_SUPPLEANT_OF_BINOM":
                errors.append("Le suppléant sélectionné est déjà (le contact d'un membre) suppléant d'un binôme.")
            elif case == "TITULAIRE_OF_BINOM":
                errors.append("Le suppléant sélectionné est déjà titulaire d'un binôme.")

806 807 808
            # le membre suppléant fait parti du commité?
            if child['current_template_name'] == "Services des comités":
                errors.append("Le membre suppléant séléctionné fait parti du comité")
809 810 811 812 813 814 815 816 817 818 819

            case = isPartnerInvolvedInBinom(api, parent)

            errors = []
            if case == "MEMBER_SUPPLEANT_OF_BINOM":
                errors.append("Le titulaire sélectionné est déjà suppléant d'un binôme.")
            elif case == "CONTACT_OF_MEMBER_SUPPLEANT_OF_BINOM":
                errors.append("Le titulaire sélectionné est déjà (le contact d'un membre) suppléant d'un binôme.")
            elif case == "TITULAIRE_OF_BINOM":
                errors.append("Le titulaire sélectionné est déjà titulaire d'un binôme.")

820 821 822
            if errors:
                return JsonResponse({"errors": errors}, status=409)

823 824 825
            del child["id"]
            for field in child.keys():
                if field.endswith("_id"):
826 827 828 829
                    try:
                        child[field] = child[field][0]
                    except TypeError:
                        child[field] = False
830
            child['is_associated_people'] = True
Thibault Grandjean committed
831
            child['parent_id'] = parent['id']
832 833
            #Link the newly created attached suppleant contact and the suppleant member
            child['suppleant_member_id'] = child_id
834 835 836 837 838
            # Following lines are useful if parent or child is unsubscribed
            if not 'shift_type' in parent:
                parent['shift_type'] = 'standard'
            if not 'shift_type' in child:
                child['shift_type'] = 'standard'
839
            # fusion des rattrapages
840 841 842
            child_makeups = child['makeups_to_do']
            parent_makeups = parent['makeups_to_do']

843
            child_scheduled_makeups = api.search_read('shift.registration', [['partner_id', '=', child_id],
844
                                                                             ['is_makeup', '=', True],
845 846 847
                                                                             ['state', '=', 'open'],
                                                                             ['date_begin', '>', datetime.now().isoformat()]])
            parent_scheduled_makeups = api.search_read('shift.registration', [['partner_id', '=', parent_id],
848
                                                                             ['is_makeup', '=', True],
849 850
                                                                             ['state', '=', 'open'],
                                                                             ['date_begin', '>', datetime.now().isoformat()]])
851 852 853 854 855 856 857
            child_makeups += len(child_scheduled_makeups)
            parent_makeups += len(parent_scheduled_makeups)

            if child_makeups:
                # le suppléant a des rattrapages
                if child_makeups + parent_makeups <=2:
                    # on transfert les rattrapages sur le parent
858

859 860
                    # Comme on annule les services du suppléant, les makeup_to_do du titulaire seront affectés par la modification du compteur
    
861 862
                    # On annule les rattrapages du child
                    api.update('res.partner', [child_id], {"makeups_to_do": 0})
863
                    
864 865 866 867 868
                    for makeup in range(child_makeups):
                        # reset du compteur du suppléant
                        api.create('shift.counter.event', {"name": 'passage en binôme',
                                                           "shift_id": False,
                                                           "type": child['shift_type'],
869
                                                           "partner_id": child_id,
870
                                                           "point_qty": 1})
871
                        # on retire les points au titulaire, les rattrages sont automatiquement ajoutés
872 873 874
                        api.create('shift.counter.event', {"name": 'passage en binôme',
                                                           "shift_id": False,
                                                           "type": parent['shift_type'],
875
                                                           "partner_id": parent_id,
876 877
                                                           "point_qty": -1})
                elif child_makeups + parent_makeups > 2:
878
                    # on annule les rattrapages du suppléant et les rattrapages du titulaire seront ajustés par les modifications de compteur
879 880 881 882 883 884
                    api.update('res.partner', [child_id], {"makeups_to_do": 0})
                    for makeup in range(child_makeups):
                        # reset du compteur du suppléant
                        api.create('shift.counter.event', {"name": 'passage en binôme',
                                                           "shift_id": False,
                                                           "type": child['shift_type'],
885
                                                           "partner_id": child_id,
886
                                                           "point_qty": 1})
887
                    for i in range(abs(parent_makeups - 2)):
888 889 890 891
                        # màj du compteur du titulaire
                        api.create('shift.counter.event', {"name": "passage en binôme",
                                                           "shift_id": False,
                                                           "type": parent['shift_type'],
892
                                                           "partner_id": parent_id,
893
                                                           "point_qty": -1})
894 895
                # No more useful since status is fully managed by lacagette_addons modules
                # api.execute('res.partner', 'run_process_target_status', [])
896

897
            
898
            m = CagetteMember(child_id).unsubscribe_member()
899 900
            # update child base account state
            api.update("res.partner", [child_id], {'cooperative_state': "associated"})
901

902 903 904
            # get barcode rule id
            bbcode_rule = api.search_read("barcode.rule", [['for_associated_people', "=", True]], ['id'])[0]
            child['barcode_rule_id'] = bbcode_rule["id"]
905 906 907 908 909 910 911 912 913 914 915
            child['cooperative_state'] = 'associated'
            for field in ["nb_associated_people",
                          "current_template_name",
                          "makeups_to_do",
                          "final_standard_points",
                          "final_ftop_points",
                          "shift_type"]:
                try:
                    del child[field]
                except KeyError:
                    pass
916 917 918 919 920 921 922
            attached_account = api.create('res.partner', child)
            # generate_base
            api.execute('res.partner', 'generate_base', [attached_account])
            response = JsonResponse({"message": "Succesfuly paired members"}, status=200)
        else:
            response = JsonResponse({"message": "Unauthorized"}, status=403)
        return response
923
    else:
924
        return JsonResponse({"message": "Method Not Allowed"}, status=405)
925 926


927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947
def isPartnerInvolvedInBinom(api, partner):
    # First make sure the selected partner "is not already a suppleant"
    # Let's be prudent : cover both case of partner being suppleant member,
    # and case of partner being the contact of a suppleant member
    contact_of_partner_suppleant = api.search_read('res.partner', [['suppleant_member_id', '=', partner['id']]])
    if contact_of_partner_suppleant:
        # partner has a suppleant contact therefore it is a suppleant member
        return "MEMBER_SUPPLEANT_OF_BINOM"
    if partner['parent_id'] and partner['suppleant_member_id']:
        # partner is the contact of a suppleant member
        return "CONTACT_OF_MEMBER_SUPPLEANT_OF_BINOM"

    # Second make sure the selected partner is not a titulaire
    children_of_partner = api.search_read('res.partner', [['parent_id', '=', partner['id']]])
    suppleant_of_partner = [x for x in children_of_partner if x['suppleant_member_id']]
    if suppleant_of_partner:
        # partner is titulaire
        return "TITULAIRE_OF_BINOM"

    return None

948 949
def delete_pair(request):
    """
950 951 952 953 954 955 956
    Administration des binômes membres
        Delete pair
    GET:
        Return template
    POST:
        payload example:
        {
957 958 959 960 961 962 963
          "child": {
            "id": "1620"
          },
          "gone": [
            "parent",
            "child"
          ]
964 965 966 967 968 969 970 971 972 973
        }
    """
    if request.method == 'GET':
        template = loader.get_template('members/admin/manage_attached_delete_pair.html')
        context = {'title': 'BDM - Binômes',
                   'module': 'Membres'}
        return HttpResponse(template.render(context, request))
    elif request.method == 'POST':
        if CagetteUser.are_credentials_ok(request):
            data = json.loads(request.body.decode())
974
            delete_pair_core(data)
975 976 977 978
            response = JsonResponse({"message": "Succesfuly unpaired members"}, status=200)
        else:
            response = JsonResponse({"message": "Unauthorized"}, status=403)
        return response
979
    else:
980
        return JsonResponse({"message": "Method Not Allowed"}, status=405)
981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997


def delete_pair_core(data):
    """
        Core of delete_pair
            argument example :
            {
              "child": {
                "id": "1620"
              },
              "gone": [
                "parent",
                "child"
              ]
            }
    """
    api = OdooAPI()
998 999 1000 1001
    child_contact_id = int(data['child']['id'])
    child_contact = api.search_read('res.partner', [['id', '=', child_contact_id]], ['email', 'id', 'parent_id', 'suppleant_member_id'])[0]
    parent = api.search_read('res.partner', [['id', '=', child_contact['parent_id'][0]]], ['cooperative_state'])[0]
    api.update('res.partner', [child_contact_id], {"parent_id": False, "is_associated_people": False, "active": False,
1002
                                           "is_former_associated_people": True, "suppleant_member_id": None})
1003
    child_member_update_fields = {'cooperative_state': "unsubscribed", "is_former_associated_people": True}
1004
    if 'gone' in data and 'child' in data['gone']:
1005 1006
        child_member_update_fields['cooperative_state'] = "gone"
    api.update("res.partner", [child_contact['suppleant_member_id']], child_member_update_fields)
1007 1008
    if 'gone' in data and 'parent' in data['gone']:
        api.update("res.partner", [parent['id']], {'cooperative_state': "gone", "is_former_associated_people": True})